feat(transport) TRAN-8 transitTime etaAtZone dwellTime #189 #192 - #279
Conversation
Checked every remaining realm spec against its primary source before building on Core. Record with citations: research/realm-gap-spike-2026-09.md, per-realm verification under research/spike-2026-09/. - 22 new stories with specs, tracker rows, painpoint evidence and GitHub issues #256..#277: CORE-54..56, TRAN-57, INT-58, MAR-59..60, ROAD-61..62, RAI-63, AV-64..65, IOT-66, HLTH-67..70, FIN-71..73, SPA-74..75. - 33 existing specs refined, e.g. FIN-42 adopts the FpML day-count codes, ROAD-20 anchors the duty day on home-terminal time (49 CFR 395.2), ROAD-21 uses the fixed week of Reg 561/2006 Art 4(i), MAR-19 restates every term against BIMCO 2013, MAR-16 becomes GNSS time scales. - The port-dwell draft is absorbed into TRAN-8 and TRAN-9 with a Corrections note: free time is contractual, not per-port. - SPA-47 and SPA-49 no longer depend on each other: TDB and TCB move to SPA-74. - Definition of Done: a story that adds public functions ships its docs-site guide, scenarios and mistakes page in the same PR (docs-site.md). TRAN-8 scope lists its pages; its June example is corrected to -04:00; the tracker marks it done. - `scripts/deps.mjs` orders shared primitives by tracker row, not ID number, so stories inserted earlier in build order render as blockers.
Adds the `transport/` namespace, the first Phase 2 story of the Realms epic. - `transitTime(departure, duration)` adds a leg as exact elapsed time in the departure's own zone. A `D` component is 24 hours; years, months and weeks return the sentinel. `Z` stays `Z`, an offset stays an offset, and a bracketed zone that contradicts its offset is rejected, not reinterpreted. - `etaAtZone(instant, zone)` renders an arrival in the zone where it lands. No disambiguation arises; the offset in the result tells the two passes of a fall-back hour apart. - `dwellTime(entry, exit, targetZone?)` returns the exact duration, zone-local entry and exit, and `calendarDays`: the distinct local dates the half-open interval touches, from `floorToZone`. Bare instants without a zone return `null`. This is the one local-days-crossed primitive INT-12, MAR-19 and HLTH-69 will consume. Subpaths `@northguild/gmt/transport`, `transport/calculate` and `transport/convert` join the package exports, with the `Dwell` result type. Minor changeset; both READMEs gain the transport subpath and a Transport legs and dwell section; `gmt-timezone` skill covers the three functions and every skill moves to 1.17.0; dox module registry, reference corpus and published test stats regenerated. Docs site: an industries guide group with the transport guide, three scenarios (container dwell counted in days, a 26-hour rail leg across spring-forward, two arrivals on the fall-back night) and a transport mistakes page, every result checked against dist. The reference generator refuses two exports whose page paths differ only by case, which on macOS overwrote one page with the other; a manifest test guards it.
- New `/tools/dwell-ledger/` page and `showDwellLedger` chat tool with a starter pill. It draws a dwell on a zone's real local-day grid and shows the real `dwellTime` result, with an optional compare zone. - Playground: `object | null` returns get their own kind, so an invalid-input `null` shows NO SIGNAL instead of a live value. Interval functions whose `null` means no overlap show the empty state. - Gate scripts report a page with no baseline instead of crashing. - Docs-site rule, TRAN-8 spec and Dox notes cover the widget; the changeset spells out the elided Amsterdam result.
…TRAN-8, #189) - Reference pages whose export is not in the newest `@northguild/gmt` tag get an Unreleased caution note and a sidebar badge. With no tags, nothing is badged. - `deploy-dox.yml` runs after the Release workflow, so a publishing push has its tag before the site builds and shows no badge. - The tracker gains an owner-maintained `Release` column, plain issue numbers and wrapped deliverables. PUBLISHING.md, the finalizer and the docs-site rules agree on it. - Restores the TRAN-8 changeset (minor); the package stays at 1.16.0.
- `dwellTime.calendarDays` counts distinct local dates across the zone's real transitions. It returned 3 for Apia's deleted day, 0 or 1 across Goose Bay's midnight fall-back, and 4 across Manila 1844. - `transitTime` and `dwellTime` parse through `zonedDateTimeFrom`, so zoned values at the range maximum no longer fail. - `transitTime` accepts elective and critical `u-ca` and unknown key annotations as RFC 9557 does; a new matrix keeps all three consistent. - `transitTime` keeps a sub-minute offset such as `+05:30:15` as written. - JSDoc cites Temporal for days as 24 hours and documents bracket reading and fixed-offset zones; docs, skill and changeset no longer say the count comes from `floorToZone`. Tracker lists plain issue numbers and wraps long deliverables.
Every top-level group and every autogenerated subgroup starts collapsed. Starlight still opens any group that contains the current page, so a link lands with only its own path expanded.
baldurpan
left a comment
There was a problem hiding this comment.
Reviewed against context/code-review-checklist.md and context/coding-standards.md, in a clean worktree at 7753381. One defect, then suggestions. The library work here is unusually careful — the elapsed-vs-wall-clock and half-open day-count decisions are both right, documented, and tested at the range limits.
Verified, not just read
vitest packages/gmt/src/transport— 211 pass.tsc -p tsconfig.build.jsonclean.node scripts/test-markers.mjs checkclean.- All 30 JSDoc
@exampleresults, and 55 documented results acrosspackages/gmt/README.md, the changeset, the industries guide, the three scenarios and the mistakes page, reproduce exactly against this branch. The "every result was run against the built package" claim holds. - Dox:
dwell-ledger,dwell-ledger-mount,released-exportsandbuild-referencetests pass (48 + 51) with the workspace package built from this branch. - The crux is right:
round({ largestUnit: "hours" })beforeZonedDateTime.addis what makesP1D24 exact hours rather than calendar-day arithmetic, and it is covered in both DST directions. calendarDaysagrees with coding-standards § 8. Probed independently on Apia (deleted date), Goose Bay (re-entered date), Havana, Santiago, Lord Howe and Chatham, and the widget's own day grid agrees withdwellTimeon all of them.- Sentinels per the table, try-catch on every Temporal call, bounded walk returning the sentinel,
minorchangeset, skills pre-bumped to 1.17.0 (matching the precedent set in 7846ecf for 1.16.0), and "thirteen namespace subpaths" is the right count.
Defect: transitTime drops the departure's offset for a comma fraction
packages/gmt/src/transport/calculate/transitTime.ts:7
const trailingOffset = /(Z|[+-]\d{2}:\d{2}(?::\d{2}(?:\.\d{1,9})?)?)(?:\[[^\]]*\])*$/;GMT's canonical instant grammar accepts either decimal separator — internal/isoStringBody.ts:34, where EXTENDED_UTC_OFFSET ends (?::[0-5]\d(?:[.,]\d{1,9})?)?. This regex allows only ., so the comma spelling misses and the ?? "Z" default takes over:
transitTime("2024-06-15T10:00:00+05:30:00,5", "PT1H"); // "2024-06-15T05:29:59.5Z" <- offset dropped
transitTime("2024-06-15T10:00:00+05:30:00.5", "PT1H"); // "2024-06-15T11:00:00+05:30:00.5"The instant is correct either way, so no moment is computed wrongly. But isValidInstant accepts both spellings, so both are inside the documented contract, and the JSDoc promises that an offset-only instant "keeps its offset, written exactly as the departure wrote it". Two equivalent departures returning two different forms breaks that. The offset-preserving it.each table has the +05:30:00.5 row but not the ,5 twin.
Suggested fix: accept [.,], or better, build the capture from the canonical EXTENDED_UTC_OFFSET instead of restating the grammar, and add the comma row beside the existing one.
Suggestions, none blocking
- The range-limit idiom is now duplicated. The offset branch (
transitTime.ts:104-110) is the same three-call chain, with the same rationale, as the no-zone branch ofinstant/convert/fromOffsetInstant.ts. Have you considered oneinternal/helper? The reasoning aboutPlainDateTimereaching a day past the instant range would then live in one place — and the defect above would have been impossible by construction. - A third zone-annotation regex.
transitTime.ts:14has/\[!?[^\]=]+\]/,toOffsetInstant.tshas/^\[!?[^\]=]+\]/, andapps/dox/src/lib/dwell-ledger.ts:118has/\[!?(?!u-ca=)([^\]]+)\]/. They agree on everything tested today, but three spellings of one RFC 9557 rule will drift. scripts/deps.mjsorder trap.buildOrderis module-level state filled as a side effect ofdominationTracker(), andorderOfsilently degrades to100000 + idNumberwhen a caller reads it first. All four commands are correctly ordered today, and the comments say why, but nothing enforces it. Passing the map, or memoisingdominationTracker()insidedeclaredDeps(), would make it structural.- The namespace lists still omit
transport/.context/coding-standards.md:186andcontext/code-review-checklist.md:18enumerate the namespaces. Since this is the first realm namespace, the next reviewer applying the checklist literally would flagtransport/as not matching the existing structure. - Scope. The
deploy-dox.ymltrigger change (push toworkflow_runon Release) checks out sound —name: Release, it runs on every push tomain, andfetch-depth: 0brings the tagsreleased-exports.tsneeds. But a delivery-pipeline change whose failure mode is "the docs site quietly stops deploying" is doing a lot of work inside a feature PR, and might be easier to reason about on its own.
Happy to approve as soon as the comma-offset row is in. Requesting changes only because of Core Rule 12, not because anything about the design worries me.
|
Thanks @baldurpan. Everything is addressed; the fixes are in the next push on this branch.
|
- `transitTime` keeps a comma-fraction offset (`+05:30:00,5`) as written; the capture is built from the canonical `EXTENDED_UTC_OFFSET`. - New `internal/wallClockAtOffset`, shared by `transitTime` and `fromOffsetInstant`, holds the range-limit wall-clock shift once. - One `TIME_ZONE_ANNOTATION` rule (RFC 9557) feeds `transitTime`, `zonedWallClockDifference` and `toOffsetInstant`; the Dox widget's stale copy is removed. - `deps.mjs`: `orderOf` fills the build order on first use. - Namespace lists gain `interval/` and `transport/`. - The deploy workflow change moves to #280; this branch restores `deploy-dox.yml` to main and points its comments there.
…/189-tran-8-transittime-etaatzone-dwelltime
TRAN-8:
transitTime,etaAtZone,dwellTime, and the realm gap spikeCloses #189.
Two commits:
into true build order.
transport/namespace and its pages on the docs site.The
transport/namespaceEvery transport mode shares three operations, and every mode gets the same two things wrong. A
leg that crosses a DST change takes a fixed number of elapsed hours. Dwell is charged in
local calendar days, not hours.
transitTime(departure, duration)adds a leg as exact elapsed time, in the departure'sown zone.
P1Dis 24 hours. Years, months and weeks return"", because they need areference point.
ZstaysZand an offset stays an offset. A zone whose offset contradictsit, such as
-05:00[America/New_York]in June, is rejected, not reinterpreted.etaAtZone(instant, zone)shows an arrival in the zone where it lands. Nodisambiguation is needed: a moment has one wall time in a zone. On the night clocks go back,
the offset in the result tells the two 01:30 arrivals apart. The caller supplies the zone.
GMT does not look up port, airport or station codes.
dwellTime(entry, exit, targetZone?)returns{ duration, enter, exit, calendarDays }.calendarDayscounts the local dates that[entry, exit)touches, using the zone's real dayboundaries from
floorToZone. So a 23 or 25 hour day counts as one day, and an exit exactlyat midnight does not touch the next day. Two bare instants with no zone return
null,because a day count needs a place. INT-12, MAR-19 and HLTH-69 will all count days with this
function.
The tests cover the
battleTestTimeZonesmatrix, DST transition rows in the probe zones, exitsexactly at midnight, invalid inputs, and Temporal throwing. The package now exports
@northguild/gmt/transport,…/transport/calculateand…/transport/convert, plus theDwellresult type. This PR also includes a minor changeset, a Transport section in both READMEs, and
updated
gmt-timezoneskill coverage, with every skill moved to 1.17.0.Docs site
section.
DwellTime. Its reference page and thedwellTimepage differ only in letter case, so on macOS they are the same file: one pageoverwrote the other, and the sidebar linked to a page that did not exist. The type is now
Dwell.build-reference.tsrefuses such a pair before writing anything, andreference-corpus.test.tschecks the route list.Realm gap spike
Before building the remaining realm stories on Core, we checked every spec against its primary
source. The record, with citations, is
context/domination/research/realm-gap-spike-2026-09.md.Each realm's verification notes are under
research/spike-2026-09/.painpoints.mdand a GitHubissue (CORE-54: Holiday rule engine: nth weekday, Easter, observance shifts #256 to SPA-75: Solar events: sunrise, sunset, twilight, solar position #277):
timestamps (CORE-56);
is set by contract, not by port. Its old issue, INT-11: containerLeg + terminalDwell #192, can be closed.
TCB move to SPA-74.
says a story that adds public functions also ships a guide, scenarios and a mistakes page,
with every result checked against the built package. The rules are in
context/domination/docs-site.md. TRAN-8 is the first story under this rule.the first row not marked Done is the next story. That is INT-12, then INT-58. Each Core
primitive sits just before the first story that uses it: CORE-55 before Maritime and
Aviation, CORE-54 before Aviation, and CORE-56 before IoT, since no story needs it.
scripts/deps.mjstakes build order from the tracker row, not the ID number. That keepsprimitives added later showing as blockers.
Checks
pnpm run validatepassesnode scripts/deps.mjs check: 75 Domination rows and 15 Dox rows OKpnpm exec intent validate packages/gmt/skills: 9 of 9 skills pass