No description
- TypeScript 67%
- Svelte 22.9%
- HTML 6.4%
- CSS 2.3%
- JavaScript 0.7%
- Other 0.7%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
Some checks failed
Deploy / test (push) Has been skipped
Deploy / images (push) Has been skipped
Deploy / build-and-push (push) Has been skipped
Deploy / deploy-test (push) Has been skipped
Deploy / deploy-production (push) Has been skipped
Deploy / build-dev (push) Successful in 3m42s
Deploy / deploy-dev (push) Successful in 1m7s
Deploy / e2e (push) Failing after 1h32m21s
The report was that the search could not handle days and times. It already could:
parseQuery returned time 17:00 and day "sat" for "Route De Cobo to town for 5pm on
saturday" before any of this. The day was then thrown away one file later.
MEASURED, before: src/lib/search/journey.ts took the weekday column from
`parsed.day` and filtered `AND c.<col> = true`. The parser returns an ISO date and
leaves `day` undefined for every RELATIVE date — "next saturday", "next monday",
"tomorrow" — and getDayColumn(undefined) returned TODAY's weekday. So
"next saturday at 5pm" carried the correct date, 2026-10-03, and searched
Thursday's timetable against it. No error, no empty state, and both weekdays
returned buses (26 direct trips on the sat column, 28 on fri), which is why nobody
noticed. A bare day name was the only correct form, because it is the only one that
sets `day`.
It was worse in the API. api/src/routes/search.ts did `if (date) parsed.day = date`
— a DATE written into the day field — and api/src/routes/journey.ts did the same
with `day: date`. Both hit `map[value] ?? 'mon'`, so a client that sent an explicit
date got MONDAY's timetable. Removed rather than repaired: the weekday is derived
from the date now, in the one place search, the API and the route timetable all
route through.
A TEST HAD RECORDED THE BUG. tests/unit/matching.test.ts drove the Route 92
turnaround with `date: '2026-09-19'` — a Saturday — and no day word, so it asserted
11:18, the value from TODAY's Friday timetable, while its own comment on the line
above said 11:17 for that Saturday. The assertion now matches the comment.
The date also came from toISOString() in four places (nlp.ts's resolveDay,
resolveDateFromDayName and its "next" branch, and journey.ts's dateStr default),
which is UTC in a codebase that insists on Guernsey wall-clock everywhere else:
between 00:00 and 01:00 BST, "today" resolved to yesterday and selected yesterday's
column. All four use luxon in Europe/Guernsey now.
The parser's second flaw was structural. Origin/destination extraction carried its
own hand-written list of patterns whose only job was to guess what the time and day
resolvers had matched, and the two had drifted: ten of fourteen leak cases came back
with the framing word attached, which is how the example produced a destination of
"town for on". matchStop's substring fallback hid it, but a leaked word only has to
prefer another place once. resolveTime and resolveDay now report WHERE they matched
and the strip derives from that, so there is one vocabulary instead of two. Verified
against a 21,242-query corpus of framings, prepositions, times, days and garbage:
6,249 destination improvements, 0 regressions, 0 rewordings. "Destinations
byte-identical" was not achievable and is not claimed — the old list and the
resolvers were already inconsistent in ways that only surface once deletion is
positional.
That refactor exposed four defects of its own, three of them pre-existing:
resolveTimeSpan read "next" as a relative TIME, so "at noon next saturday" gave a
time of now and — once the span was deleted — shattered the day expression into
"week"; the destination regex terminated on a THIRD hand-written word list that knew
"morning" and "noon" but not "lunch", "midnight" or "next"; a preposition stranded
by a removal ("... to Cobo at") produced 531 regressions until it was trimmed; and
"tonight" was missing from the vocabulary because \bnight\b cannot match inside it,
so it leaked whenever a different time won.
Then the day and time the reader asked for had to become visible. The results card
offered only Now / In 30 min / In 1 hour and read its label from that shortcut
alone, so a Saturday-5pm search rendered Saturday 17:00 under a chip saying "Now",
and choosing an offset from there searched Saturday at now+30min. It now has a day
picker, a native date input for anything beyond the next week, and a native time
input, and the label is derived from the resolved values so it cannot disagree with
the results beneath it. The resolution order is stated day, then stated clock, then
the words, then the shortcut, then now.
Guernsey's holidays needed a name-to-date map and nothing more, because the scraper
already writes the real bus-holiday calendar into calendar_exceptions as
exception_type='removed' and searchDirect already filters on it. Good Friday and
Easter Monday come from the Gregorian computus; Boxing Day moves to the Monday when
26 December is a weekend. Nine of the ten removed dates match the published calendar
exactly. The tenth does not, and it is a data question for the operator rather than
a parser bug: Guernsey's Spring Bank Holiday 2027 is Monday 31 May, but the feed
removes service on 24 May instead and does not remove the 31st. The cross-check test
records both directions explicitly so a rescrape that changes it fails loudly. A
date with no service at all now says so — the old copy claimed "the first buses
tomorrow are on the next tab along", which was false on Christmas Day and pointed at
In 1 hour, not tomorrow.
A journey opened for another day no longer claims a live bus. Live positions are
today's, and trip ids are date-independent, so matching them against a Saturday plan
would name a different departure as "on the road now"; the journey page does not
even ask for a snapshot when the day is not today.
Also removed the unused `compromise` dependency, imported nowhere. It is not a size
win and is not claimed as one: client JS measures 1,418,961 -> 1,418,956 bytes, a
5-byte chunk-hash difference. It leaves four packages out of the install.
Verification: check, lint and 1,221 unit and contract tests green. The browser suite
is 256 passed / 5 failed / 29 skipped, and the five failures are environmental rather
than ours — each times out after 30s waiting for a live bus dot and reports
"Received: 0", because GET /api/live returns {"count":0,"stale":true} with its newest
record 20 minutes old. Confirmed by running the same specs against the stashed
original tree, which failed live-bus-marker identically while routes-page-layout
passed; two runs failing different specs is the signature of live-data dependence.
Plan and research brief: nlp-dates.todo.yaml, .research/nlp-dates.research-brief.yaml.
Two items are deliberately left open in the plan's `discovered` list.
|
||
| .forgejo | ||
| .plans | ||
| .research | ||
| api | ||
| config | ||
| docs | ||
| placeholder | ||
| routes-gg-helpful-state-when-a-timetable | ||
| scripts | ||
| src | ||
| static | ||
| tests | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| .prettierrc | ||
| api-split.todo.yaml | ||
| bus-location.sh | ||
| CLAUDE.md | ||
| delay-per-route.sql | ||
| docker-compose.dev.yml | ||
| docker-compose.prod.yml | ||
| docker-compose.test.yml | ||
| Dockerfile | ||
| drizzle.config.ts | ||
| eslint.config.js | ||
| full_project_spec.md | ||
| IMPLEMENTATION_SUMMARY.md | ||
| live-buses.todo.yaml | ||
| map-nav.todo.yaml | ||
| map.todo.yaml | ||
| nlp-dates.todo.yaml | ||
| package-lock.json | ||
| package.json | ||
| performance.todo.yaml | ||
| playwright.config.ts | ||
| polish-seo-branding.todo.yaml | ||
| postcodes.csv | ||
| roads.csv | ||
| routes-page.todo.yaml | ||
| small-location-sample.json | ||
| svelte.config.js | ||
| timetable-journey.todo.yaml | ||
| todo.yaml | ||
| total-delays.sql | ||
| tsconfig.json | ||
| ui-redesign.todo.yaml | ||
| ux-flows.todo.yaml | ||
| vite.config.ts | ||
| vitest.config.ts | ||