Skip to content

v0.7.3

Choose a tag to compare

@leemr leemr released this 06 Sep 17:10

Added

  • npm run test:split builds a broken Temporal on disk and checks that daymath still tells the truth. 0.7.2 fixed one catch-all and shipped test:demo to watch for the next one, but that gate needs the network, reads only the ops the demo page offers, and can only see a build that is already published. This one copies temporal-polyfill out of node_modules once per subpath, rewrites the bare specifiers in index.js to point each import at its own copy, and imports the result. That is exactly what jsDelivr's +esm produces, so the brand check between the copies fails the same way, and it runs offline against the working tree instead of against npmjs. The law it checks is one sentence, and it holds for any export: a broken implementation must never make daymath answer wrongly or blame the caller. Every call in the roster either agrees with ../index.js or throws a TypeError. A RangeError is the failure it exists to catch, because that is daymath saying the input was bad when the input was fine, and a wrong answer is worse still. Proved to block by reverting index.js to 0.7.2 and running it: the run names each mislabel and each wrong answer, with the source answer beside it, and exits 1. It runs on every matrix version in ci, because the shim lane and the native lane word their faults differently, and prepublishOnly now runs it too.

    A second phase raises the same fault from one import at a time, and that is the half that reaches every catch. The CDN shape cannot reach them all, and it never will: InstantFns.fromString and ZonedFns.fromFields take no calendar resolver, so no record crosses between copies and a split leaves both alone however the bundles fall. Their narrowing would then be code no gate runs, which is exactly how a catch-all comes back in a later edit. So the second phase stops modelling one CDN and models the rule — any import can fault, and daymath owes the same answer either way. Nothing is hand-listed: the import roster is read out of index.js, so an import added later joins the phase with no edit here, and a namespace member counts only where the source actually calls it. Each phase was proved to block on its own by planting the defect it exists for. Deleting the narrowing at those two calls turns the run red and names both, day("2026-01-01T00:00:00Z") reporting neither a moment nor a time zone and day(0, "America/New_York") reporting an unknown time zone for one it knows. An import that refuses to load is a legal outcome, because it is loud and answers nothing, and the run reports how many loaded. Three things stop that legality becoming a quiet pass, and a review round added all three. The stub is loaded on its own first, so a probe the gate failed to BUILD is a gate defect rather than a silent skip — an aliased import wrote a stub that did not parse, and the run would have gone quiet on the one import it was watching. A { getAny as resolve } import now yields the export name rather than the local one. And which imports were checked is recorded in scripts/split-copy.baseline.json, so coverage cannot fall quietly — a count can say that something stopped being checked and never say what, which is the lesson examples.baseline.json already carries. declined is a real state in that roster rather than a failure: index.js calls getAny at module scope, so a faulting stub stops the whole module loading and no export can be judged. Drift fails in both directions and names the import. npm run test:split:write re-records. Every one of these was proved to block by planting it.

  • The cross-runtime battery never passed a third argument, and never passed a weekStartsOn it refuses. isSameWeek and areIntervalsOverlapping are the only exports that take three, so their whole options path was unprobed, and scripts/battery.mjs offered only 0, 1 and 7 — all accepted. That is why npm run test:runtimes passed straight through the isSameWeek message fix below. An export could change WHICH error it reports for a bad option and no hash would move. The battery now carries a WEEK_STARTS list holding both halves, passes it in the third slot as well as the second, and exercises inclusive on areIntervalsOverlapping. Proved by recording the baseline against 0.7.2 and running it against this branch: one export differs, and it is isSameWeek. That is also the independent evidence that this release is a patch. The baseline is re-recorded, and it agrees on Node, Deno and Bun, so the widened probes read the same on native Temporal and on the polyfill. The same gap as JUNK in that file, which exists because no probe ever passed a bad string.

Fixed

  • A broken Temporal was still being relabelled everywhere toPlainDate is not. 0.7.2 narrowed one catch and called the job done. It was one instance of a class, and a sweep of the perimeter found the rest, each proved reachable by loading a broken build and calling it. isValid was the worst of them: it answered false for a perfectly valid day and raised nothing at all, so a reader learned nothing and had nowhere to look. calendarRule reported unknown for a calendar the runtime names happily. guardRange relabelled every throw under it, across every add, sub, setYear, startOf and interval export. The zoned branch of day() said it could not read a zone that it had read correctly. All of them now go through one function, assertCallerFault, which re-throws anything that is not a RangeError — find them with grep -n 'assertCallerFault' index.js. The discriminant is a measurement, not a guess: a bad string, a bad field, an out-of-range result and an unknown calendar each report a RangeError on the shim lane and on the native lane, checked through parse, withFields, addDays, addYears, fromFields and withCalendar. isValid needed its own shape check first, because toPlainDate reports a non-string with a TypeError and isValid still owes false for one.
  • isSameWeek blamed the dates for a bad weekStartsOn. It read the option inside its own range guard, so isSameWeek(a, b, { weekStartsOn: 8 }) answered daymath: isSameWeek could not produce a valid date and never named the option. startOfWeek and endOfWeek already read it before the guard and say weekStartsOn must be an integer 0…7. isSameWeek now does the same, which is the same defect as the entry above wearing different clothes: a guard that catches more than it was written for.