Skip to content

Commit 5a8a347

Browse files
committed
fix: address station layout review
1 parent 7a7cb20 commit 5a8a347

7 files changed

Lines changed: 101 additions & 44 deletions

File tree

data/station/CDT.json

Lines changed: 0 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -33,23 +33,6 @@
3333
"townId": "toa-payoh",
3434
"firstLastTrain": {
3535
"services": [
36-
{
37-
"serviceId": "CCL_MAIN_CW",
38-
"times": {
39-
"weekday": {
40-
"firstTrain": "05:34",
41-
"lastTrain": "23:30"
42-
},
43-
"saturday": {
44-
"firstTrain": "05:34",
45-
"lastTrain": "23:30"
46-
},
47-
"sunday_public_holiday": {
48-
"firstTrain": "06:02",
49-
"lastTrain": "23:30"
50-
}
51-
}
52-
},
5336
{
5437
"serviceId": "TEL_MAIN_N",
5538
"times": {
@@ -90,26 +73,6 @@
9073
"levels": [],
9174
"exits": [],
9275
"platforms": [
93-
{
94-
"id": "CCDT_A",
95-
"label": "A",
96-
"lineId": "CCL",
97-
"serviceIds": [
98-
"CCL_MAIN_CCW",
99-
"CCL_EXT_CCW"
100-
],
101-
"accessPoints": []
102-
},
103-
{
104-
"id": "CCDT_B",
105-
"label": "B",
106-
"lineId": "CCL",
107-
"serviceIds": [
108-
"CCL_MAIN_CW",
109-
"CCL_EXT_CW"
110-
],
111-
"accessPoints": []
112-
},
11376
{
11477
"id": "TCDT_B",
11578
"label": "B",

docs/plans/active/station-first-last-train-data.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -162,8 +162,8 @@ Calendar categories should be data categories first. Any mapping to GTFS
162162
The first narrow seeds are intentionally hand-reviewed station records rather
163163
than outputs from a reusable operator ingest script:
164164

165-
- `CDT` seeds one SMRT-operated interchange slice, including CCL/TEL platform
166-
relationship validation.
165+
- `CDT` seeds one SMRT-operated TEL platform relationship slice at an
166+
interchange station.
167167
- `LTI` seeds one SBS Transit-operated interchange slice from the public SBS
168168
Transit first/last train page, covering both NEL and DTL timing table shapes.
169169

docs/plans/active/station-layout-data.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -378,9 +378,10 @@ Exit criteria:
378378

379379
### Phase 4: Seed Real Data
380380

381-
- Seed `CDT` first as a narrow platform-to-service mapping slice because its
382-
reviewed platform direction data maps cleanly to current CCL and TEL service
383-
patterns.
381+
- Seed `CDT` first as a narrow TEL platform-to-service mapping slice because
382+
its reviewed platform direction data maps cleanly to current TEL service
383+
patterns without depending on the expiring pre-CCL6 Circle Line service
384+
catalog.
384385
- Keep `OTP` as a later complete-layout candidate because it covers three
385386
lines, multiple platform levels, door-anchored access points, and multiple
386387
transfer path classifications.

packages/core/src/schema/Station.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,4 +194,29 @@ describe('StationSchema', () => {
194194
}),
195195
).not.toThrow();
196196
});
197+
198+
it('rejects layout platforms without service ids', () => {
199+
const result = StationSchema.safeParse({
200+
...minimalStation(),
201+
layout: {
202+
levels: [],
203+
exits: [],
204+
platforms: [
205+
{
206+
id: 'KET_ISL_A',
207+
label: 'A',
208+
lineId: 'ISL',
209+
serviceIds: [],
210+
accessPoints: [],
211+
},
212+
],
213+
transferPaths: [],
214+
},
215+
});
216+
217+
expect(result.success).toBe(false);
218+
expect(result.error?.issues[0]?.path.join('.')).toBe(
219+
'layout.platforms.0.serviceIds',
220+
);
221+
});
197222
});

packages/core/src/schema/Station.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ export const StationLayoutPlatformSchema = z.object({
178178
label: z.string(),
179179
lineId: z.string(),
180180
levelId: z.string().optional(),
181-
serviceIds: z.array(z.string()),
181+
serviceIds: z.array(z.string()).nonempty(),
182182
doorCount: z.number().int().positive().optional(),
183183
accessPoints: z.array(StationLayoutAccessPointSchema),
184184
});

packages/fs/src/index.test.ts

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2750,7 +2750,7 @@ describe('@mrtdown/fs', () => {
27502750
id: 'KET_TWL_A',
27512751
label: 'A',
27522752
lineId: 'ISL',
2753-
serviceIds: ['TWL_MAIN_N'],
2753+
serviceIds: ['TWL_MAIN_N', 'ISL_SKIP_KET'],
27542754
accessPoints: [
27552755
{
27562756
id: 'KET_AP_DUP',
@@ -2785,6 +2785,48 @@ describe('@mrtdown/fs', () => {
27852785
2,
27862786
)}\n`,
27872787
);
2788+
await writeFile(
2789+
join(dataDir, 'service/ISL_SKIP_KET.json'),
2790+
`${JSON.stringify(
2791+
{
2792+
id: 'ISL_SKIP_KET',
2793+
name: {
2794+
'en-SG': 'Island Line Skip Kennedy Town',
2795+
'zh-Hans': null,
2796+
ms: null,
2797+
ta: null,
2798+
},
2799+
lineId: 'ISL',
2800+
revisions: [
2801+
{
2802+
id: 'r_current',
2803+
startAt: '1979-10-01',
2804+
endAt: null,
2805+
path: {
2806+
stations: [
2807+
{
2808+
stationId: 'HKU',
2809+
displayCode: 'ISL2',
2810+
},
2811+
],
2812+
},
2813+
operatingHours: {
2814+
weekdays: {
2815+
start: '05:30',
2816+
end: '00:30',
2817+
},
2818+
weekends: {
2819+
start: '05:30',
2820+
end: '00:30',
2821+
},
2822+
},
2823+
},
2824+
],
2825+
},
2826+
null,
2827+
2,
2828+
)}\n`,
2829+
);
27882830

27892831
const result = await validateDataRoot(dataDir, ['station']);
27902832

@@ -2799,6 +2841,7 @@ describe('@mrtdown/fs', () => {
27992841
'station/KET.json: layout.platforms.0.levelId MISSING does not exist in layout.levels',
28002842
'station/KET.json: layout.platforms.0.serviceIds.0 MISSING_SERVICE does not exist in service/',
28012843
'station/KET.json: layout.platforms.1.serviceIds.0 TWL_MAIN_N belongs to line TWL, not ISL',
2844+
'station/KET.json: layout.platforms.1.serviceIds.1 ISL_SKIP_KET revision r_current does not include station KET in its current service path',
28022845
'station/KET.json: layout.platforms.0.accessPoints.0.connectsToLevelId MISSING does not exist in layout.levels',
28032846
'station/KET.json: layout.platforms.0.accessPoints.0.nearestDoor 25 is outside doorCount 24',
28042847
'station/KET.json: layout.transferPaths.0.from platform MISSING_PLATFORM does not exist in layout',

packages/fs/src/validate.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,31 @@ async function validateStationReferences(
352352
errors.push(
353353
`${station.path}: layout.platforms.${platformIndex}.serviceIds.${serviceIndex} ${serviceId} belongs to line ${service.value.lineId}, not ${platform.lineId}`,
354354
);
355+
continue;
356+
}
357+
358+
const currentRevisions = service.value.revisions.filter((revision) =>
359+
revisionContainsTimestamp(revision, validationTimestamp),
360+
);
361+
if (currentRevisions.length === 0) {
362+
errors.push(
363+
`${station.path}: layout.platforms.${platformIndex}.serviceIds.${serviceIndex} ${serviceId} does not have a current service revision`,
364+
);
365+
continue;
366+
}
367+
368+
for (const revision of currentRevisions) {
369+
const stationIds = new Set(
370+
revision.path.stations.map(
371+
(serviceStation) => serviceStation.stationId,
372+
),
373+
);
374+
375+
if (!stationIds.has(station.value.id)) {
376+
errors.push(
377+
`${station.path}: layout.platforms.${platformIndex}.serviceIds.${serviceIndex} ${serviceId} revision ${revision.id} does not include station ${station.value.id} in its current service path`,
378+
);
379+
}
355380
}
356381
}
357382
}

0 commit comments

Comments
 (0)