go-transit-mcp 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +68 -0
  3. package/dist/entry/http.js +78 -0
  4. package/dist/entry/http.js.map +1 -0
  5. package/dist/entry/stdio.js +16 -0
  6. package/dist/entry/stdio.js.map +1 -0
  7. package/dist/errors.js +30 -0
  8. package/dist/errors.js.map +1 -0
  9. package/dist/metrolinx/cache.js +29 -0
  10. package/dist/metrolinx/cache.js.map +1 -0
  11. package/dist/metrolinx/client.js +180 -0
  12. package/dist/metrolinx/client.js.map +1 -0
  13. package/dist/metrolinx/types.js +5 -0
  14. package/dist/metrolinx/types.js.map +1 -0
  15. package/dist/normalize/fares.js +44 -0
  16. package/dist/normalize/fares.js.map +1 -0
  17. package/dist/normalize/fleet-consist.js +31 -0
  18. package/dist/normalize/fleet-consist.js.map +1 -0
  19. package/dist/normalize/journey.js +184 -0
  20. package/dist/normalize/journey.js.map +1 -0
  21. package/dist/normalize/line-schedule.js +56 -0
  22. package/dist/normalize/line-schedule.js.map +1 -0
  23. package/dist/normalize/list-lines.js +29 -0
  24. package/dist/normalize/list-lines.js.map +1 -0
  25. package/dist/normalize/next-service.js +59 -0
  26. package/dist/normalize/next-service.js.map +1 -0
  27. package/dist/normalize/search-stops.js +113 -0
  28. package/dist/normalize/search-stops.js.map +1 -0
  29. package/dist/normalize/service-alerts.js +61 -0
  30. package/dist/normalize/service-alerts.js.map +1 -0
  31. package/dist/normalize/service-exceptions.js +51 -0
  32. package/dist/normalize/service-exceptions.js.map +1 -0
  33. package/dist/normalize/service-guarantee.js +23 -0
  34. package/dist/normalize/service-guarantee.js.map +1 -0
  35. package/dist/normalize/stop-destinations.js +54 -0
  36. package/dist/normalize/stop-destinations.js.map +1 -0
  37. package/dist/normalize/stop-details.js +44 -0
  38. package/dist/normalize/stop-details.js.map +1 -0
  39. package/dist/normalize/trip-status.js +88 -0
  40. package/dist/normalize/trip-status.js.map +1 -0
  41. package/dist/normalize/trip-updates.js +113 -0
  42. package/dist/normalize/trip-updates.js.map +1 -0
  43. package/dist/normalize/union-departures.js +52 -0
  44. package/dist/normalize/union-departures.js.map +1 -0
  45. package/dist/normalize/vehicle-positions.js +67 -0
  46. package/dist/normalize/vehicle-positions.js.map +1 -0
  47. package/dist/prompts.js +96 -0
  48. package/dist/prompts.js.map +1 -0
  49. package/dist/resources.js +61 -0
  50. package/dist/resources.js.map +1 -0
  51. package/dist/schemas/fares.js +30 -0
  52. package/dist/schemas/fares.js.map +1 -0
  53. package/dist/schemas/fleet-consist.js +32 -0
  54. package/dist/schemas/fleet-consist.js.map +1 -0
  55. package/dist/schemas/journey.js +127 -0
  56. package/dist/schemas/journey.js.map +1 -0
  57. package/dist/schemas/line-schedule.js +43 -0
  58. package/dist/schemas/line-schedule.js.map +1 -0
  59. package/dist/schemas/list-lines.js +29 -0
  60. package/dist/schemas/list-lines.js.map +1 -0
  61. package/dist/schemas/next-service.js +29 -0
  62. package/dist/schemas/next-service.js.map +1 -0
  63. package/dist/schemas/search-stops.js +35 -0
  64. package/dist/schemas/search-stops.js.map +1 -0
  65. package/dist/schemas/service-alerts.js +55 -0
  66. package/dist/schemas/service-alerts.js.map +1 -0
  67. package/dist/schemas/service-exceptions.js +27 -0
  68. package/dist/schemas/service-exceptions.js.map +1 -0
  69. package/dist/schemas/service-guarantee.js +32 -0
  70. package/dist/schemas/service-guarantee.js.map +1 -0
  71. package/dist/schemas/stop-destinations.js +32 -0
  72. package/dist/schemas/stop-destinations.js.map +1 -0
  73. package/dist/schemas/stop-details.js +27 -0
  74. package/dist/schemas/stop-details.js.map +1 -0
  75. package/dist/schemas/trip-status.js +39 -0
  76. package/dist/schemas/trip-status.js.map +1 -0
  77. package/dist/schemas/trip-updates.js +51 -0
  78. package/dist/schemas/trip-updates.js.map +1 -0
  79. package/dist/schemas/union-departures.js +26 -0
  80. package/dist/schemas/union-departures.js.map +1 -0
  81. package/dist/schemas/vehicle-positions.js +48 -0
  82. package/dist/schemas/vehicle-positions.js.map +1 -0
  83. package/dist/server.js +58 -0
  84. package/dist/server.js.map +1 -0
  85. package/dist/time.js +159 -0
  86. package/dist/time.js.map +1 -0
  87. package/dist/tools/get-fares.js +26 -0
  88. package/dist/tools/get-fares.js.map +1 -0
  89. package/dist/tools/get-fleet-consist.js +34 -0
  90. package/dist/tools/get-fleet-consist.js.map +1 -0
  91. package/dist/tools/get-line-schedule.js +27 -0
  92. package/dist/tools/get-line-schedule.js.map +1 -0
  93. package/dist/tools/get-next-service.js +25 -0
  94. package/dist/tools/get-next-service.js.map +1 -0
  95. package/dist/tools/get-service-alerts.js +45 -0
  96. package/dist/tools/get-service-alerts.js.map +1 -0
  97. package/dist/tools/get-service-exceptions.js +25 -0
  98. package/dist/tools/get-service-exceptions.js.map +1 -0
  99. package/dist/tools/get-service-guarantee.js +26 -0
  100. package/dist/tools/get-service-guarantee.js.map +1 -0
  101. package/dist/tools/get-stop-destinations.js +28 -0
  102. package/dist/tools/get-stop-destinations.js.map +1 -0
  103. package/dist/tools/get-stop-details.js +25 -0
  104. package/dist/tools/get-stop-details.js.map +1 -0
  105. package/dist/tools/get-trip-status.js +31 -0
  106. package/dist/tools/get-trip-status.js.map +1 -0
  107. package/dist/tools/get-trip-updates.js +26 -0
  108. package/dist/tools/get-trip-updates.js.map +1 -0
  109. package/dist/tools/get-union-departures.js +29 -0
  110. package/dist/tools/get-union-departures.js.map +1 -0
  111. package/dist/tools/get-vehicle-positions.js +30 -0
  112. package/dist/tools/get-vehicle-positions.js.map +1 -0
  113. package/dist/tools/list-lines.js +27 -0
  114. package/dist/tools/list-lines.js.map +1 -0
  115. package/dist/tools/plan-journey.js +46 -0
  116. package/dist/tools/plan-journey.js.map +1 -0
  117. package/dist/tools/plan-trip.js +81 -0
  118. package/dist/tools/plan-trip.js.map +1 -0
  119. package/dist/tools/search-stops.js +25 -0
  120. package/dist/tools/search-stops.js.map +1 -0
  121. package/dist/tools/test-support.js +93 -0
  122. package/dist/tools/test-support.js.map +1 -0
  123. package/package.json +57 -0
@@ -0,0 +1,184 @@
1
+ import { addHoursToTime, combineDateAndHhmm, dateToWire, diffMinutes, hhmmToWire, toIsoWithTorontoOffset, } from "../time.js";
2
+ // GO Transit's known line roster. Schedule/Journey's Trip object carries no
3
+ // line-name field (only Line/LineVariant codes and a Display string that
4
+ // mixes the code with the destination, e.g. "LW - Aldershot GO") — same gap
5
+ // and same best-effort static map already used for get_stop_destinations.
6
+ const LINE_NAMES = {
7
+ LW: "Lakeshore West",
8
+ LE: "Lakeshore East",
9
+ MI: "Milton",
10
+ GT: "Kitchener",
11
+ RH: "Richmond Hill",
12
+ BR: "Barrie",
13
+ ST: "Stouffville",
14
+ UP: "UP Express",
15
+ };
16
+ // Mirrors Stop/NextService's ServiceType (T/B) single-letter vocabulary
17
+ // (tool-schemas spec §1.1) — not yet independently confirmed for this
18
+ // endpoint's Trip.Type field, same best-effort default as normalize/next-service.ts.
19
+ function normalizeLegMode(type) {
20
+ return type === "B" ? "bus" : "train";
21
+ }
22
+ function firstAndLastByOrder(stops) {
23
+ let first = stops[0];
24
+ let last = stops[0];
25
+ for (const stop of stops) {
26
+ if (stop.Order < first.Order)
27
+ first = stop;
28
+ if (stop.Order > last.Order)
29
+ last = stop;
30
+ }
31
+ return [first, last];
32
+ }
33
+ // Leg boundaries come from the trip's own Stops list, not from
34
+ // departFromCode/destinationStopCode. The captured fixture shows upstream
35
+ // trims each trip's Stops to the ridden portion (trip 1961's Display says
36
+ // "Niagara Falls GO" yet its Stops end at the journey destination OA) and
37
+ // stamps departFromCode/destinationStopCode with journey-level endpoints,
38
+ // so those codes can't be trusted as per-leg boundaries on any multi-leg
39
+ // journey. Note: no multi-leg journey has ever been observed live —
40
+ // Schedule/Journey returned SchJourneys: [] for a cross-line pair the
41
+ // official site routes via a transfer (tool-schemas spec §5) — this is
42
+ // defensive correctness from the fixture evidence, not a confirmed shape.
43
+ function normalizeLeg(trip, journeyDate, stopNames) {
44
+ const stops = trip.Stops?.Stop ?? [];
45
+ if (stops.length === 0)
46
+ return undefined;
47
+ const [first, last] = firstAndLastByOrder(stops);
48
+ return {
49
+ mode: normalizeLegMode(trip.Type),
50
+ line_code: trip.Line,
51
+ line_name: LINE_NAMES[trip.Line] ?? trip.Line,
52
+ direction: trip.Direction,
53
+ from: {
54
+ stop_code: first.Code,
55
+ stop_name: stopNames.get(first.Code) ?? first.Code,
56
+ time: combineDateAndHhmm(journeyDate, first.Time),
57
+ },
58
+ to: {
59
+ stop_code: last.Code,
60
+ stop_name: stopNames.get(last.Code) ?? last.Code,
61
+ time: combineDateAndHhmm(journeyDate, last.Time),
62
+ },
63
+ trip_number: trip.Number,
64
+ };
65
+ }
66
+ function normalizeService(service, journeyDate, stopNames) {
67
+ const legs = (service.Trips?.Trip ?? [])
68
+ .map((trip) => normalizeLeg(trip, journeyDate, stopNames))
69
+ .filter((leg) => leg !== undefined);
70
+ if (legs.length === 0)
71
+ return undefined;
72
+ return {
73
+ departure_time: toIsoWithTorontoOffset(service.StartTime),
74
+ arrival_time: toIsoWithTorontoOffset(service.EndTime),
75
+ duration_minutes: diffMinutes(service.EndTime, service.StartTime),
76
+ transfers: legs.length - 1,
77
+ // "" / "R" / "B" / "RB" (tool-schemas spec §5 pattern, unconfirmed
78
+ // positive case) — any non-empty code collapses to accessible: true.
79
+ accessible: service.Accessible !== "",
80
+ legs,
81
+ };
82
+ }
83
+ /**
84
+ * Raw journey response -> itinerary DTOs. Journey trips carry no stop names
85
+ * of their own (same gap as Schedule/Trip) — resolved via a stop_code ->
86
+ * stop_name index built from the cached Stop/All dataset (get_trip_status
87
+ * precedent), passed in by the caller.
88
+ */
89
+ export function normalizeJourney(raw, stopNames) {
90
+ const entry = raw.SchJourneys?.[0];
91
+ if (!entry)
92
+ return [];
93
+ const itineraries = [];
94
+ for (const service of entry.Services ?? []) {
95
+ const itinerary = normalizeService(service, entry.Date, stopNames);
96
+ if (itinerary)
97
+ itineraries.push(itinerary);
98
+ }
99
+ return itineraries;
100
+ }
101
+ async function fetchItineraries(client, fromStopCode, toStopCode, date, time, maxResults, stopNames) {
102
+ const raw = await client.getJourney(dateToWire(date), fromStopCode, toStopCode, hhmmToWire(time), maxResults);
103
+ return normalizeJourney(raw, stopNames);
104
+ }
105
+ // Via-Union transfer composition (ADR 0002): Schedule/Journey only returns
106
+ // single-service journeys — both documented endpoint variants confirmed live
107
+ // (2026-07-18) to return SchJourneys: [] for a cross-line pair the official
108
+ // planner routes via a transfer. Union is GO's hub, so when a direct query
109
+ // comes back empty, plan_trip composes one transfer there from two journey
110
+ // calls. Each leg is upstream-attested; only the pairing is ours. The
111
+ // transfer buffer is a starting heuristic (same spirit as the arrive_by
112
+ // window, spec §5); bus-hub composition is a separate grilled decision.
113
+ const UNION_STOP_CODE = "UN";
114
+ const MIN_TRANSFER_MINUTES = 10;
115
+ function isoToMs(iso) {
116
+ return new Date(iso).getTime();
117
+ }
118
+ // Toronto-local clock of one of our own ISO outputs
119
+ // ("2026-07-20T08:50:00-04:00" -> "08:50") — safe to slice because every
120
+ // itinerary timestamp is produced by combineDateAndHhmm/toIsoWithTorontoOffset.
121
+ function hhmmOfIso(iso) {
122
+ return iso.slice(11, 16);
123
+ }
124
+ function combine(first, second) {
125
+ const legs = [...first.legs, ...second.legs];
126
+ return {
127
+ departure_time: first.departure_time,
128
+ arrival_time: second.arrival_time,
129
+ duration_minutes: Math.round((isoToMs(second.arrival_time) - isoToMs(first.departure_time)) / 60_000),
130
+ transfers: legs.length - 1,
131
+ accessible: first.accessible && second.accessible,
132
+ legs,
133
+ };
134
+ }
135
+ async function composeViaUnion(client, fromStopCode, toStopCode, date, time, maxResults, stopNames) {
136
+ const inbound = await fetchItineraries(client, fromStopCode, UNION_STOP_CODE, date, time, maxResults, stopNames);
137
+ if (inbound.length === 0)
138
+ return [];
139
+ const earliest = inbound.reduce((a, b) => isoToMs(a.arrival_time) <= isoToMs(b.arrival_time) ? a : b);
140
+ const onward = await fetchItineraries(client, UNION_STOP_CODE, toStopCode, date, hhmmOfIso(earliest.arrival_time), maxResults, stopNames);
141
+ const combined = [];
142
+ for (const first of inbound) {
143
+ const readyMs = isoToMs(first.arrival_time) + MIN_TRANSFER_MINUTES * 60_000;
144
+ const second = onward.find((candidate) => isoToMs(candidate.departure_time) >= readyMs);
145
+ if (second)
146
+ combined.push(combine(first, second));
147
+ }
148
+ return combined.slice(0, maxResults);
149
+ }
150
+ // Direct query first; the via-Union fallback only runs when it comes back
151
+ // empty and neither endpoint already is Union, so every currently-working
152
+ // direct journey is untouched. Worst case 3 upstream calls (6 under
153
+ // arrive_by's widening retry).
154
+ async function fetchDirectOrComposed(client, query, time, stopNames) {
155
+ const direct = await fetchItineraries(client, query.from, query.to, query.date, time, query.maxResults, stopNames);
156
+ if (direct.length > 0 || !query.viaUnionFallback)
157
+ return direct;
158
+ if (query.from === UNION_STOP_CODE || query.to === UNION_STOP_CODE) {
159
+ return direct;
160
+ }
161
+ return composeViaUnion(client, query.from, query.to, query.date, time, query.maxResults, stopNames);
162
+ }
163
+ // arrive_by is emulated (tool-schemas spec §2.1): one Schedule/Journey call
164
+ // with StartTime back-shifted, filtered on arrival <= target, one retry with
165
+ // a wider window if empty. Both windows are starting heuristics to tune
166
+ // against real journey data (spec §5).
167
+ const ARRIVE_BY_BACKSHIFT_HOURS = 2;
168
+ const ARRIVE_BY_WIDE_BACKSHIFT_HOURS = 4;
169
+ async function itinerariesArrivingBy(client, query, stopNames, backshiftHours) {
170
+ const startTime = addHoursToTime(query.time, -backshiftHours);
171
+ const itineraries = await fetchDirectOrComposed(client, query, startTime, stopNames);
172
+ const targetMs = new Date(combineDateAndHhmm(query.date, query.time)).getTime();
173
+ return itineraries.filter((itinerary) => new Date(itinerary.arrival_time).getTime() <= targetMs);
174
+ }
175
+ export async function planItineraries(client, query, stopNames) {
176
+ if (query.timeMode === "depart_after") {
177
+ return fetchDirectOrComposed(client, query, query.time, stopNames);
178
+ }
179
+ const narrow = await itinerariesArrivingBy(client, query, stopNames, ARRIVE_BY_BACKSHIFT_HOURS);
180
+ if (narrow.length > 0)
181
+ return narrow;
182
+ return itinerariesArrivingBy(client, query, stopNames, ARRIVE_BY_WIDE_BACKSHIFT_HOURS);
183
+ }
184
+ //# sourceMappingURL=journey.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"journey.js","sourceRoot":"","sources":["../../src/normalize/journey.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,56 @@
1
+ import { MetrolinxError } from "../errors.js";
2
+ import { toIsoWithTorontoOffset } from "../time.js";
3
+ function firstAndLastStop(stops) {
4
+ let first = stops[0];
5
+ let last = stops[0];
6
+ for (const stop of stops) {
7
+ if (stop.Order < first.Order)
8
+ first = stop;
9
+ if (stop.Order > last.Order)
10
+ last = stop;
11
+ }
12
+ return [first, last];
13
+ }
14
+ function summaryTrip(trip) {
15
+ const stops = trip.Stops ?? [];
16
+ if (stops.length === 0)
17
+ return undefined;
18
+ const [first, last] = firstAndLastStop(stops);
19
+ return {
20
+ trip_number: trip.Number,
21
+ display: trip.Display,
22
+ departs_first_stop: toIsoWithTorontoOffset(first.Time),
23
+ arrives_last_stop: toIsoWithTorontoOffset(last.Time),
24
+ };
25
+ }
26
+ function stopTimeTrip(trip, stopCode) {
27
+ const stop = (trip.Stops ?? []).find((s) => s.Code === stopCode);
28
+ if (!stop)
29
+ return undefined;
30
+ return {
31
+ trip_number: trip.Number,
32
+ display: trip.Display,
33
+ time: toIsoWithTorontoOffset(stop.Time),
34
+ };
35
+ }
36
+ // Schedule/Line/{Date}/{LineCode}/{LineDirection} is already scoped by the
37
+ // request URL, so the response carries at most one Lines.Line entry for that
38
+ // line/direction (confirmed live, issue #3 fixture capture).
39
+ export function normalizeLineSchedule(raw, stopCode) {
40
+ const entry = raw.Lines?.Line?.[0];
41
+ if (!entry) {
42
+ throw new MetrolinxError("not_found", "No schedule for that line/direction. Verify both via list_lines.", false);
43
+ }
44
+ const trips = [];
45
+ for (const trip of entry.Trip ?? []) {
46
+ const mapped = stopCode ? stopTimeTrip(trip, stopCode) : summaryTrip(trip);
47
+ if (mapped)
48
+ trips.push(mapped);
49
+ }
50
+ return {
51
+ trips,
52
+ truncated: false,
53
+ total_matched: trips.length,
54
+ };
55
+ }
56
+ //# sourceMappingURL=line-schedule.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"line-schedule.js","sourceRoot":"","sources":["../../src/normalize/line-schedule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAU9C,OAAO,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAEpD,SAAS,gBAAgB,CACvB,KAA4B;IAE5B,IAAI,KAAK,GAAG,KAAK,CAAC,CAAC,CAAE,CAAC;IACtB,IAAI,IAAI,GAAG,KAAK,CAAC,CAAC,CAAE,CAAC;IACrB,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC,KAAK;YAAE,KAAK,GAAG,IAAI,CAAC;QAC3C,IAAI,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK;YAAE,IAAI,GAAG,IAAI,CAAC;IAC3C,CAAC;IACD,OAAO,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;AACvB,CAAC;AAED,SAAS,WAAW,CAAC,IAAyB;IAC5C,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC;IAC/B,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,SAAS,CAAC;IACzC,MAAM,CAAC,KAAK,EAAE,IAAI,CAAC,GAAG,gBAAgB,CAAC,KAAK,CAAC,CAAC;IAC9C,OAAO;QACL,WAAW,EAAE,IAAI,CAAC,MAAM;QACxB,OAAO,EAAE,IAAI,CAAC,OAAO;QACrB,kBAAkB,EAAE,sBAAsB,CAAC,KAAK,CAAC,IAAI,CAAC;QACtD,iBAAiB,EAAE,sBAAsB,CAAC,IAAI,CAAC,IAAI,CAAC;KACrD,CAAC;AACJ,CAAC;AAED,SAAS,YAAY,CACnB,IAAyB,EACzB,QAAgB;IAEhB,MAAM,IAAI,GAAG,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC;IACjE,IAAI,CAAC,IAAI;QAAE,OAAO,SAAS,CAAC;IAC5B,OAAO;QACL,WAAW,EAAE,IAAI,CAAC,MAAM;QACxB,OAAO,EAAE,IAAI,CAAC,OAAO;QACrB,IAAI,EAAE,sBAAsB,CAAC,IAAI,CAAC,IAAI,CAAC;KACxC,CAAC;AACJ,CAAC;AAED,2EAA2E;AAC3E,6EAA6E;AAC7E,6DAA6D;AAC7D,MAAM,UAAU,qBAAqB,CACnC,GAA4B,EAC5B,QAAiB;IAEjB,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;IACnC,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,MAAM,IAAI,cAAc,CACtB,WAAW,EACX,kEAAkE,EAClE,KAAK,CACN,CAAC;IACJ,CAAC;IAED,MAAM,KAAK,GAAmB,EAAE,CAAC;IACjC,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,IAAI,IAAI,EAAE,EAAE,CAAC;QACpC,MAAM,MAAM,GAAG,QAAQ,CAAC,CAAC,CAAC,YAAY,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;QAC3E,IAAI,MAAM;YAAE,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACjC,CAAC;IAED,OAAO;QACL,KAAK;QACL,SAAS,EAAE,KAAK;QAChB,aAAa,EAAE,KAAK,CAAC,MAAM;KAC5B,CAAC;AACJ,CAAC"}
@@ -0,0 +1,29 @@
1
+ function modesOf(entry) {
2
+ const modes = [];
3
+ if (entry.IsTrain)
4
+ modes.push("train");
5
+ if (entry.IsBus)
6
+ modes.push("bus");
7
+ return modes;
8
+ }
9
+ function normalizeLine(entry) {
10
+ return {
11
+ line_code: entry.Code,
12
+ line_name: entry.Name,
13
+ modes: modesOf(entry),
14
+ variants: (entry.Variant ?? []).map((variant) => ({
15
+ code: variant.Code,
16
+ direction: variant.Direction,
17
+ display: variant.Display,
18
+ })),
19
+ };
20
+ }
21
+ export function normalizeListLines(raw) {
22
+ const lines = (raw.AllLines?.Line ?? []).map(normalizeLine);
23
+ return {
24
+ lines,
25
+ truncated: false,
26
+ total_matched: lines.length,
27
+ };
28
+ }
29
+ //# sourceMappingURL=list-lines.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"list-lines.js","sourceRoot":"","sources":["../../src/normalize/list-lines.ts"],"names":[],"mappings":"AAMA,SAAS,OAAO,CAAC,KAAsB;IACrC,MAAM,KAAK,GAAwB,EAAE,CAAC;IACtC,IAAI,KAAK,CAAC,OAAO;QAAE,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACvC,IAAI,KAAK,CAAC,KAAK;QAAE,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACnC,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,aAAa,CAAC,KAAsB;IAC3C,OAAO;QACL,SAAS,EAAE,KAAK,CAAC,IAAI;QACrB,SAAS,EAAE,KAAK,CAAC,IAAI;QACrB,KAAK,EAAE,OAAO,CAAC,KAAK,CAAC;QACrB,QAAQ,EAAE,CAAC,KAAK,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;YAChD,IAAI,EAAE,OAAO,CAAC,IAAI;YAClB,SAAS,EAAE,OAAO,CAAC,SAAS;YAC5B,OAAO,EAAE,OAAO,CAAC,OAAO;SACzB,CAAC,CAAC;KACJ,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,GAAuB;IACxD,MAAM,KAAK,GAAG,CAAC,GAAG,CAAC,QAAQ,EAAE,IAAI,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;IAC5D,OAAO;QACL,KAAK;QACL,SAAS,EAAE,KAAK;QAChB,aAAa,EAAE,KAAK,CAAC,MAAM;KAC5B,CAAC;AACJ,CAAC"}
@@ -0,0 +1,59 @@
1
+ import { MetrolinxError } from "../errors.js";
2
+ import { diffMinutes, toIsoWithTorontoOffset } from "../time.js";
3
+ function normalizeMode(serviceType) {
4
+ if (serviceType === "T")
5
+ return "train";
6
+ if (serviceType === "B")
7
+ return "bus";
8
+ // Stop/NextService captures have only shown "T" so far (issue #3); this is
9
+ // a best-effort default per the tool-schemas spec §1.1 single-letter
10
+ // enum-expansion pattern — revisit if another code is ever observed.
11
+ return "bus";
12
+ }
13
+ function normalizeStatus(departureStatus) {
14
+ // Confirmed empirically (tool-schemas spec §5): E = expected (not yet
15
+ // departed), A = actual (has departed). C never appeared live; "cancelled"
16
+ // is the best-effort inference carried over from ticket 006/#3.
17
+ if (departureStatus === "E")
18
+ return "expected";
19
+ if (departureStatus === "A")
20
+ return "actual";
21
+ if (departureStatus === "C")
22
+ return "cancelled";
23
+ return departureStatus;
24
+ }
25
+ function normalizeDeparture(line) {
26
+ const platform = {};
27
+ if (line.ScheduledPlatform)
28
+ platform.scheduled = line.ScheduledPlatform;
29
+ if (line.ActualPlatform)
30
+ platform.actual = line.ActualPlatform;
31
+ return {
32
+ line_code: line.LineCode,
33
+ line_name: line.LineName,
34
+ direction: line.DirectionName.trim(),
35
+ mode: normalizeMode(line.ServiceType),
36
+ scheduled_time: toIsoWithTorontoOffset(line.ScheduledDepartureTime),
37
+ expected_time: toIsoWithTorontoOffset(line.ComputedDepartureTime),
38
+ delay_minutes: diffMinutes(line.ComputedDepartureTime, line.ScheduledDepartureTime),
39
+ status: normalizeStatus(line.DepartureStatus),
40
+ platform,
41
+ trip_number: line.TripNumber,
42
+ };
43
+ }
44
+ export function normalizeNextService(raw) {
45
+ // An absent NextService block (Metadata.ErrorCode "204"/"No Content",
46
+ // confirmed live, issue #7) means the stop code itself is unknown — a
47
+ // present block with an empty/absent Lines array is a valid, just-quiet
48
+ // stop, which is not an error.
49
+ if (!raw.NextService) {
50
+ throw new MetrolinxError("not_found", "No stop matches that code. Verify the code via search_stops.", false);
51
+ }
52
+ const departures = (raw.NextService.Lines ?? []).map(normalizeDeparture);
53
+ return {
54
+ departures,
55
+ truncated: false,
56
+ total_matched: departures.length,
57
+ };
58
+ }
59
+ //# sourceMappingURL=next-service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"next-service.js","sourceRoot":"","sources":["../../src/normalize/next-service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAM9C,OAAO,EAAE,WAAW,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAEjE,SAAS,aAAa,CAAC,WAAmB;IACxC,IAAI,WAAW,KAAK,GAAG;QAAE,OAAO,OAAO,CAAC;IACxC,IAAI,WAAW,KAAK,GAAG;QAAE,OAAO,KAAK,CAAC;IACtC,2EAA2E;IAC3E,qEAAqE;IACrE,qEAAqE;IACrE,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,eAAe,CAAC,eAAuB;IAC9C,sEAAsE;IACtE,2EAA2E;IAC3E,gEAAgE;IAChE,IAAI,eAAe,KAAK,GAAG;QAAE,OAAO,UAAU,CAAC;IAC/C,IAAI,eAAe,KAAK,GAAG;QAAE,OAAO,QAAQ,CAAC;IAC7C,IAAI,eAAe,KAAK,GAAG;QAAE,OAAO,WAAW,CAAC;IAChD,OAAO,eAAe,CAAC;AACzB,CAAC;AAED,SAAS,kBAAkB,CAAC,IAAwB;IAClD,MAAM,QAAQ,GAA0B,EAAE,CAAC;IAC3C,IAAI,IAAI,CAAC,iBAAiB;QAAE,QAAQ,CAAC,SAAS,GAAG,IAAI,CAAC,iBAAiB,CAAC;IACxE,IAAI,IAAI,CAAC,cAAc;QAAE,QAAQ,CAAC,MAAM,GAAG,IAAI,CAAC,cAAc,CAAC;IAE/D,OAAO;QACL,SAAS,EAAE,IAAI,CAAC,QAAQ;QACxB,SAAS,EAAE,IAAI,CAAC,QAAQ;QACxB,SAAS,EAAE,IAAI,CAAC,aAAa,CAAC,IAAI,EAAE;QACpC,IAAI,EAAE,aAAa,CAAC,IAAI,CAAC,WAAW,CAAC;QACrC,cAAc,EAAE,sBAAsB,CAAC,IAAI,CAAC,sBAAsB,CAAC;QACnE,aAAa,EAAE,sBAAsB,CAAC,IAAI,CAAC,qBAAqB,CAAC;QACjE,aAAa,EAAE,WAAW,CACxB,IAAI,CAAC,qBAAqB,EAC1B,IAAI,CAAC,sBAAsB,CAC5B;QACD,MAAM,EAAE,eAAe,CAAC,IAAI,CAAC,eAAe,CAAC;QAC7C,QAAQ;QACR,WAAW,EAAE,IAAI,CAAC,UAAU;KAC7B,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,oBAAoB,CAClC,GAA2B;IAE3B,sEAAsE;IACtE,sEAAsE;IACtE,wEAAwE;IACxE,+BAA+B;IAC/B,IAAI,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC;QACrB,MAAM,IAAI,cAAc,CACtB,WAAW,EACX,8DAA8D,EAC9D,KAAK,CACN,CAAC;IACJ,CAAC;IAED,MAAM,UAAU,GAAG,CAAC,GAAG,CAAC,WAAW,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC;IACzE,OAAO;QACL,UAAU;QACV,SAAS,EAAE,KAAK;QAChB,aAAa,EAAE,UAAU,CAAC,MAAM;KACjC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,113 @@
1
+ function normalizeText(text) {
2
+ return text
3
+ .toLowerCase()
4
+ .normalize("NFKD")
5
+ .replace(/[\u0300-\u036f]/g, "")
6
+ .replace(/[^a-z0-9]+/g, " ")
7
+ .trim();
8
+ }
9
+ // Match tiers, best first — exact, prefix, then substring anywhere. Both
10
+ // entries in an ambiguous name collision (e.g. "Oakville GO" as a Bus Stop
11
+ // and as a Train & Bus Station) land in the same tier, so both surface as
12
+ // candidates — required for plan_trip's later disambiguation reuse.
13
+ function matchTier(name, query) {
14
+ const n = normalizeText(name);
15
+ const q = normalizeText(query);
16
+ if (!q)
17
+ return null;
18
+ if (n === q)
19
+ return 0;
20
+ if (n.startsWith(q))
21
+ return 1;
22
+ if (n.includes(q))
23
+ return 2;
24
+ return null;
25
+ }
26
+ export function stopType(locationType) {
27
+ if (locationType === "Train & Bus Station")
28
+ return "both";
29
+ if (locationType === "Train Station")
30
+ return "train";
31
+ return "bus";
32
+ }
33
+ function matchesFilter(type, filter) {
34
+ return filter === "any" || type === filter || type === "both";
35
+ }
36
+ // Station LocationCodes ("UN", "OA") are what Stop/Details and
37
+ // Stop/NextService accept for train (and mixed) stops. Pure bus stops have
38
+ // no such alpha code — their unified stop_code is the 6-digit PublicStopId
39
+ // instead (tool-schemas spec §1.4). Unconfirmed against a live Stop/Details
40
+ // call on a bus-only stop; revisit if that ever surfaces a mismatch.
41
+ export function resolveStopCode(entry) {
42
+ return entry.LocationType.includes("Train")
43
+ ? entry.LocationCode
44
+ : entry.PublicStopId;
45
+ }
46
+ export function toStopMatch(entry) {
47
+ return {
48
+ stop_code: resolveStopCode(entry),
49
+ stop_name: entry.LocationName,
50
+ stop_type: stopType(entry.LocationType),
51
+ };
52
+ }
53
+ // Full stop dataset in the search_stops match shape (tool-schemas spec §3,
54
+ // gotransit://stops resource) — every stop, unfiltered, sharing the same
55
+ // per-entry mapping as normalizeSearchStops so the two paths cannot drift.
56
+ export function normalizeAllStops(raw) {
57
+ const matches = (raw.Stations?.Station ?? []).map(toStopMatch);
58
+ return { matches, truncated: false, total_matched: matches.length };
59
+ }
60
+ function scoredMatches(entries, query, filter) {
61
+ return entries
62
+ .map((entry) => ({ entry, tier: matchTier(entry.LocationName, query) }))
63
+ .filter((scored) => scored.tier !== null)
64
+ .filter((scored) => matchesFilter(stopType(scored.entry.LocationType), filter));
65
+ }
66
+ export function normalizeSearchStops(raw, query, filter = "any", limit = 10) {
67
+ const scored = scoredMatches(raw.Stations?.Station ?? [], query, filter);
68
+ scored.sort((a, b) => a.tier - b.tier);
69
+ const totalMatched = scored.length;
70
+ const matches = scored
71
+ .slice(0, limit)
72
+ .map(({ entry }) => toStopMatch(entry));
73
+ return {
74
+ matches,
75
+ truncated: totalMatched > limit,
76
+ total_matched: totalMatched,
77
+ };
78
+ }
79
+ /** code -> stop_name, keyed by the unified stop_code (tool-schemas spec §1.4). */
80
+ export function buildStopNameIndex(raw) {
81
+ const index = new Map();
82
+ for (const entry of raw.Stations?.Station ?? []) {
83
+ index.set(resolveStopCode(entry), entry.LocationName);
84
+ }
85
+ return index;
86
+ }
87
+ /**
88
+ * Resolves a `plan_trip` `from`/`to` input, which per tool-schemas spec §2.1
89
+ * accepts either a station/stop name (fuzzy-matched, same matcher as
90
+ * search_stops) or an exact stop code. An exact unified stop_code match
91
+ * short-circuits the fuzzy search. Otherwise, only the best-scoring tier is
92
+ * considered: a single best-tier match resolves; more than one (e.g. the
93
+ * real "Oakville GO" name collision, tool-schemas spec §5) is ambiguous,
94
+ * with candidates reproducible via search_stops for the same query.
95
+ */
96
+ export function resolveStopByName(raw, query) {
97
+ const entries = raw.Stations?.Station ?? [];
98
+ const trimmed = query.trim().toLowerCase();
99
+ const codeMatch = entries.find((entry) => resolveStopCode(entry).toLowerCase() === trimmed);
100
+ if (codeMatch)
101
+ return { status: "resolved", match: toStopMatch(codeMatch) };
102
+ const scored = scoredMatches(entries, query, "any");
103
+ if (scored.length === 0)
104
+ return { status: "not_found" };
105
+ const bestTier = Math.min(...scored.map((s) => s.tier));
106
+ const candidates = scored
107
+ .filter((s) => s.tier === bestTier)
108
+ .map(({ entry }) => toStopMatch(entry));
109
+ if (candidates.length === 1)
110
+ return { status: "resolved", match: candidates[0] };
111
+ return { status: "ambiguous", candidates };
112
+ }
113
+ //# sourceMappingURL=search-stops.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search-stops.js","sourceRoot":"","sources":["../../src/normalize/search-stops.ts"],"names":[],"mappings":"AAMA,SAAS,aAAa,CAAC,IAAY;IACjC,OAAO,IAAI;SACR,WAAW,EAAE;SACb,SAAS,CAAC,MAAM,CAAC;SACjB,OAAO,CAAC,kBAAkB,EAAE,EAAE,CAAC;SAC/B,OAAO,CAAC,aAAa,EAAE,GAAG,CAAC;SAC3B,IAAI,EAAE,CAAC;AACZ,CAAC;AAED,yEAAyE;AACzE,2EAA2E;AAC3E,0EAA0E;AAC1E,oEAAoE;AACpE,SAAS,SAAS,CAAC,IAAY,EAAE,KAAa;IAC5C,MAAM,CAAC,GAAG,aAAa,CAAC,IAAI,CAAC,CAAC;IAC9B,MAAM,CAAC,GAAG,aAAa,CAAC,KAAK,CAAC,CAAC;IAC/B,IAAI,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IACpB,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,CAAC,CAAC;IACtB,IAAI,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC;QAAE,OAAO,CAAC,CAAC;IAC9B,IAAI,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC;QAAE,OAAO,CAAC,CAAC;IAC5B,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,YAAoB;IAC3C,IAAI,YAAY,KAAK,qBAAqB;QAAE,OAAO,MAAM,CAAC;IAC1D,IAAI,YAAY,KAAK,eAAe;QAAE,OAAO,OAAO,CAAC;IACrD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,aAAa,CACpB,IAA8B,EAC9B,MAA+B;IAE/B,OAAO,MAAM,KAAK,KAAK,IAAI,IAAI,KAAK,MAAM,IAAI,IAAI,KAAK,MAAM,CAAC;AAChE,CAAC;AAED,+DAA+D;AAC/D,2EAA2E;AAC3E,2EAA2E;AAC3E,4EAA4E;AAC5E,qEAAqE;AACrE,MAAM,UAAU,eAAe,CAAC,KAAuB;IACrD,OAAO,KAAK,CAAC,YAAY,CAAC,QAAQ,CAAC,OAAO,CAAC;QACzC,CAAC,CAAC,KAAK,CAAC,YAAY;QACpB,CAAC,CAAC,KAAK,CAAC,YAAY,CAAC;AACzB,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,KAAuB;IACjD,OAAO;QACL,SAAS,EAAE,eAAe,CAAC,KAAK,CAAC;QACjC,SAAS,EAAE,KAAK,CAAC,YAAY;QAC7B,SAAS,EAAE,QAAQ,CAAC,KAAK,CAAC,YAAY,CAAC;KACxC,CAAC;AACJ,CAAC;AAED,2EAA2E;AAC3E,yEAAyE;AACzE,2EAA2E;AAC3E,MAAM,UAAU,iBAAiB,CAAC,GAAuB;IACvD,MAAM,OAAO,GAAG,CAAC,GAAG,CAAC,QAAQ,EAAE,OAAO,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;IAC/D,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,aAAa,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC;AACtE,CAAC;AAED,SAAS,aAAa,CACpB,OAA2B,EAC3B,KAAa,EACb,MAA+B;IAE/B,OAAO,OAAO;SACX,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,SAAS,CAAC,KAAK,CAAC,YAAY,EAAE,KAAK,CAAC,EAAE,CAAC,CAAC;SACvE,MAAM,CACL,CAAC,MAAM,EAAuD,EAAE,CAC9D,MAAM,CAAC,IAAI,KAAK,IAAI,CACvB;SACA,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CACjB,aAAa,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC,CAC3D,CAAC;AACN,CAAC;AAED,MAAM,UAAU,oBAAoB,CAClC,GAAuB,EACvB,KAAa,EACb,SAAkC,KAAK,EACvC,KAAK,GAAG,EAAE;IAEV,MAAM,MAAM,GAAG,aAAa,CAAC,GAAG,CAAC,QAAQ,EAAE,OAAO,IAAI,EAAE,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC;IACzE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC;IAEvC,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,CAAC;IACnC,MAAM,OAAO,GAAgB,MAAM;SAChC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC;SACf,GAAG,CAAC,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC;IAE1C,OAAO;QACL,OAAO;QACP,SAAS,EAAE,YAAY,GAAG,KAAK;QAC/B,aAAa,EAAE,YAAY;KAC5B,CAAC;AACJ,CAAC;AAED,kFAAkF;AAClF,MAAM,UAAU,kBAAkB,CAChC,GAAuB;IAEvB,MAAM,KAAK,GAAG,IAAI,GAAG,EAAkB,CAAC;IACxC,KAAK,MAAM,KAAK,IAAI,GAAG,CAAC,QAAQ,EAAE,OAAO,IAAI,EAAE,EAAE,CAAC;QAChD,KAAK,CAAC,GAAG,CAAC,eAAe,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC,YAAY,CAAC,CAAC;IACxD,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAOD;;;;;;;;GAQG;AACH,MAAM,UAAU,iBAAiB,CAC/B,GAAuB,EACvB,KAAa;IAEb,MAAM,OAAO,GAAG,GAAG,CAAC,QAAQ,EAAE,OAAO,IAAI,EAAE,CAAC;IAC5C,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAE3C,MAAM,SAAS,GAAG,OAAO,CAAC,IAAI,CAC5B,CAAC,KAAK,EAAE,EAAE,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC,WAAW,EAAE,KAAK,OAAO,CAC5D,CAAC;IACF,IAAI,SAAS;QAAE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,WAAW,CAAC,SAAS,CAAC,EAAE,CAAC;IAE5E,MAAM,MAAM,GAAG,aAAa,CAAC,OAAO,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC;IACpD,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC;IAExD,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;IACxD,MAAM,UAAU,GAAG,MAAM;SACtB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,QAAQ,CAAC;SAClC,GAAG,CAAC,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC;IAE1C,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC;QACzB,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,UAAU,CAAC,CAAC,CAAE,EAAE,CAAC;IACvD,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,UAAU,EAAE,CAAC;AAC7C,CAAC"}
@@ -0,0 +1,61 @@
1
+ import { toIsoWithTorontoOffset } from "../time.js";
2
+ // INIT/UPD/CORR/FINAL confirmed live across the ServiceAlert feed (issue #9:
3
+ // only INIT/UPD observed so far; CORR/FINAL carried over from ticket 006's
4
+ // documented mapping).
5
+ function expandStatus(code) {
6
+ if (code === "INIT")
7
+ return "new";
8
+ if (code === "UPD")
9
+ return "updated";
10
+ if (code === "CORR")
11
+ return "corrected";
12
+ if (code === "FINAL")
13
+ return "final";
14
+ return "new";
15
+ }
16
+ function pickLang(en, fr, lang) {
17
+ if (lang === "fr" && fr)
18
+ return fr;
19
+ return en;
20
+ }
21
+ function normalizeMessage(message, category, lang) {
22
+ return {
23
+ id: message.Code,
24
+ category,
25
+ status: expandStatus(message.Status),
26
+ posted_at: toIsoWithTorontoOffset(message.PostedDateTime),
27
+ subject: pickLang(message.SubjectEnglish, message.SubjectFrench, lang),
28
+ body: pickLang(message.BodyEnglish, message.BodyFrench, lang),
29
+ affected: {
30
+ lines: (message.Lines ?? []).map((l) => l.Code),
31
+ stops: (message.Stops ?? []).map((s) => ({
32
+ stop_code: s.Code,
33
+ stop_name: s.Name ?? s.Code,
34
+ })),
35
+ trips: (message.Trips ?? []).map((t) => t.TripNumber),
36
+ },
37
+ };
38
+ }
39
+ // The category filter selects which upstream feeds get fetched (tool layer)
40
+ // rather than being re-applied here — every message in `feeds` is already
41
+ // in-scope by category.
42
+ export function normalizeServiceAlerts(feeds, filters = {}) {
43
+ const lang = filters.lang ?? "en";
44
+ const limit = filters.limit ?? 20;
45
+ const alerts = feeds.flatMap((feed) => (feed.raw.Messages?.Message ?? []).map((m) => normalizeMessage(m, feed.category, lang)));
46
+ const filtered = alerts.filter((alert) => {
47
+ if (filters.line && !alert.affected.lines.includes(filters.line))
48
+ return false;
49
+ if (filters.stop &&
50
+ !alert.affected.stops.some((s) => s.stop_code === filters.stop))
51
+ return false;
52
+ return true;
53
+ });
54
+ const totalMatched = filtered.length;
55
+ return {
56
+ alerts: filtered.slice(0, limit),
57
+ truncated: totalMatched > limit,
58
+ total_matched: totalMatched,
59
+ };
60
+ }
61
+ //# sourceMappingURL=service-alerts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service-alerts.js","sourceRoot":"","sources":["../../src/normalize/service-alerts.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,51 @@
1
+ import { toIsoWithTorontoOffset } from "../time.js";
2
+ // Raw stops carry separate SchArrival/SchDeparture; the DTO wants one
3
+ // scheduled_time per affected stop. Departure preferred (matches the rest
4
+ // of the codebase's departure-first convention, e.g. Stop/NextService),
5
+ // falling back to arrival for a trip's final stop.
6
+ function scheduledTime(stop) {
7
+ const naive = stop.SchDeparture ?? stop.SchArrival;
8
+ return naive ? toIsoWithTorontoOffset(naive) : "";
9
+ }
10
+ // Confirmed live (issue #9 follow-up): IsCancelled/IsOverride arrive as
11
+ // JSON strings ("True"/"False"), not native booleans — contrary to the
12
+ // ticket 001 Help-page shape this endpoint's types were sourced from (never
13
+ // part of issue #3's capture batch). Coerced case-insensitively; anything
14
+ // that isn't exactly "true" is treated as false, same as a real `false`.
15
+ function toBool(value) {
16
+ return typeof value === "boolean"
17
+ ? value
18
+ : value.trim().toLowerCase() === "true";
19
+ }
20
+ function normalizeStop(stop) {
21
+ const dto = {
22
+ stop_code: stop.Code,
23
+ stop_name: stop.Name,
24
+ scheduled_time: scheduledTime(stop),
25
+ cancelled: toBool(stop.IsCancelled),
26
+ };
27
+ if (stop.ActualTime)
28
+ dto.actual_time = toIsoWithTorontoOffset(stop.ActualTime);
29
+ return dto;
30
+ }
31
+ function normalizeTrip(trip) {
32
+ // "affected_stops" per the field's own name — only stops actually flagged
33
+ // cancelled or overridden, not every stop on the trip (anti-dump, same
34
+ // spirit as get_line_schedule's two-mode design).
35
+ const affected = (trip.Stop ?? []).filter((s) => toBool(s.IsCancelled) || toBool(s.IsOverride));
36
+ return {
37
+ trip_number: trip.TripNumber,
38
+ trip_name: trip.TripName,
39
+ cancelled: toBool(trip.IsCancelled),
40
+ affected_stops: affected.map(normalizeStop),
41
+ };
42
+ }
43
+ export function normalizeServiceExceptions(raw) {
44
+ const exceptions = (raw.Trip ?? []).map(normalizeTrip);
45
+ return {
46
+ exceptions,
47
+ truncated: false,
48
+ total_matched: exceptions.length,
49
+ };
50
+ }
51
+ //# sourceMappingURL=service-exceptions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service-exceptions.js","sourceRoot":"","sources":["../../src/normalize/service-exceptions.ts"],"names":[],"mappings":"AAUA,OAAO,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAEpD,sEAAsE;AACtE,0EAA0E;AAC1E,wEAAwE;AACxE,mDAAmD;AACnD,SAAS,aAAa,CAAC,IAAsB;IAC3C,MAAM,KAAK,GAAG,IAAI,CAAC,YAAY,IAAI,IAAI,CAAC,UAAU,CAAC;IACnD,OAAO,KAAK,CAAC,CAAC,CAAC,sBAAsB,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;AACpD,CAAC;AAED,wEAAwE;AACxE,uEAAuE;AACvE,4EAA4E;AAC5E,0EAA0E;AAC1E,yEAAyE;AACzE,SAAS,MAAM,CAAC,KAAuB;IACrC,OAAO,OAAO,KAAK,KAAK,SAAS;QAC/B,CAAC,CAAC,KAAK;QACP,CAAC,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,KAAK,MAAM,CAAC;AAC5C,CAAC;AAED,SAAS,aAAa,CAAC,IAAsB;IAC3C,MAAM,GAAG,GAAiB;QACxB,SAAS,EAAE,IAAI,CAAC,IAAI;QACpB,SAAS,EAAE,IAAI,CAAC,IAAI;QACpB,cAAc,EAAE,aAAa,CAAC,IAAI,CAAC;QACnC,SAAS,EAAE,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC;KACpC,CAAC;IACF,IAAI,IAAI,CAAC,UAAU;QACjB,GAAG,CAAC,WAAW,GAAG,sBAAsB,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IAC5D,OAAO,GAAG,CAAC;AACb,CAAC;AAED,SAAS,aAAa,CAAC,IAAsB;IAC3C,0EAA0E;IAC1E,uEAAuE;IACvE,kDAAkD;IAClD,MAAM,QAAQ,GAAG,CAAC,IAAI,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,MAAM,CACvC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,WAAW,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,UAAU,CAAC,CACrD,CAAC;IACF,OAAO;QACL,WAAW,EAAE,IAAI,CAAC,UAAU;QAC5B,SAAS,EAAE,IAAI,CAAC,QAAQ;QACxB,SAAS,EAAE,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC;QACnC,cAAc,EAAE,QAAQ,CAAC,GAAG,CAAC,aAAa,CAAC;KAC5C,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,0BAA0B,CACxC,GAAiC;IAEjC,MAAM,UAAU,GAAG,CAAC,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;IACvD,OAAO;QACL,UAAU;QACV,SAAS,EAAE,KAAK;QAChB,aAAa,EAAE,UAAU,CAAC,MAAM;KACjC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,23 @@
1
+ function normalizeStop(stop, lang) {
2
+ return {
3
+ stop_code: stop.Code,
4
+ scope: stop.Scope,
5
+ reason: lang === "fr" && stop.ReasonFr ? stop.ReasonFr : stop.ReasonEn,
6
+ };
7
+ }
8
+ export function normalizeServiceGuarantee(raw, lang = "en") {
9
+ // Confirmed live (issue #9 follow-up): a real, unambiguously-valid
10
+ // trip_number/date returned an absent Stops container. The
11
+ // presence-vs-absence "not_found" signal confirmed for other endpoints
12
+ // (Stop/Details, Stop/NextService, Stop/Destinations) does NOT hold here
13
+ // — an absent container is the ordinary shape for "this trip wasn't
14
+ // delayed enough to be guarantee-eligible," not a signal that the
15
+ // trip/date itself is wrong. Previously this threw not_found for every
16
+ // non-eligible trip, which is most of them.
17
+ const stops = raw.Stops?.Stop ?? [];
18
+ return {
19
+ eligible: stops.length > 0,
20
+ stops: stops.map((s) => normalizeStop(s, lang)),
21
+ };
22
+ }
23
+ //# sourceMappingURL=service-guarantee.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service-guarantee.js","sourceRoot":"","sources":["../../src/normalize/service-guarantee.ts"],"names":[],"mappings":"AASA,SAAS,aAAa,CACpB,IAAsB,EACtB,IAAiB;IAEjB,OAAO;QACL,SAAS,EAAE,IAAI,CAAC,IAAI;QACpB,KAAK,EAAE,IAAI,CAAC,KAAK;QACjB,MAAM,EAAE,IAAI,KAAK,IAAI,IAAI,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ;KACvE,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,yBAAyB,CACvC,GAAgC,EAChC,OAAoB,IAAI;IAExB,mEAAmE;IACnE,2DAA2D;IAC3D,uEAAuE;IACvE,yEAAyE;IACzE,oEAAoE;IACpE,kEAAkE;IAClE,uEAAuE;IACvE,4CAA4C;IAC5C,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,EAAE,IAAI,IAAI,EAAE,CAAC;IACpC,OAAO;QACL,QAAQ,EAAE,KAAK,CAAC,MAAM,GAAG,CAAC;QAC1B,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,aAAa,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;KAChD,CAAC;AACJ,CAAC"}