Skip to content

v0.7.1

Choose a tag to compare

@leemr leemr released this 25 Aug 15:55
· 18 commits to master since this release

Added

  • index.d.ts now carries @example blocks, and it had none at all. Measured before starting: zero examples across 69 declarations, while index.js had ten and every one was inside the day() block. package.json declares exactly one types path — "types": "./index.d.ts", repeated in the exports map — so a TypeScript editor resolves documentation from the .d.ts and never parses index.js. Every example daymath had was invisible on hover, which is where a typed caller reads. Every declaration site now has a hover tooltip, and every one that can carry a runnable @example does — and the gate now enforces exactly that — before this, 55 of 69 exports showed nothing but their signature. The type aliases are exempt by name, because a type has no call to show. The count is by declaration SITE rather than by export name, and that difference is what found the gap, because day is overloaded and an editor shows the doc of the overload it RESOLVES: the whole contract block sat on day(tz?), the zone-only form, while day(moment, tz?) — the one that takes a Date, epoch milliseconds, a day string or a SQLite DATETIME — had none. The block now sits on the moment overload, where nearly all of it applies, and the clock-reading overload gets its own. The first pass covered only the traps, which left sibling asymmetries that are their own defect: min documented and max bare, startOfWeek documented and endOfWeek bare, addMonths documented and subMonths bare, differenceInMonths documented and differenceInYears bare on the same counting rule, eachDayOfInterval documented and the other two bare on the same inclusivity. Documenting one of a pair implies the other differs. isLeapYear was bare while carrying the library's single most dangerous trap — isLeapYear('2567-01-01') is false while Buddhist 2567 is ISO 2024 and IS a leap year, and the two rules disagree in 49 of the 101 Buddhist years from 2500 to 2600 with nothing thrown. Coverage is complete, but emphasis is not: where daymath surprises a date-fns user earns a worked example rather than a bare call, and that choice is made by surprise rather than by call frequency: getMonth is 1-12, getDay is 1=Monday…7=Sunday, addMonths/setMonth/setDate/setYear clamp instead of rolling, startOfWeek defaults to Sunday, differenceInMonths counts by addMonths, min takes an array, and the interval helpers are inclusive at both ends. format and isValid show what they refuse.
  • npm run test:examples executes every @example and asserts its stated answer. It reads README.md too, which is the file that most needed it: it ships in the tarball, it is what a developer opens first, and three of its day() lines had silently rotted. Fixing those three closed the instances; reading the file closes the class. Every claim in every file it reads is either asserted or listed with the reason it cannot be, and the accounting closes with none unattributed. It is a new gate for a real blind spot: tsc type-checks the declarations and ignores the comments, the differential harness compares daymath to date-fns and never opens a JSDoc block, and the cross-runtime battery enumerates exports rather than documentation. So an example could go stale on any behaviour change with every gate still green — and the example is the line a caller copies. Proved to block by planting a wrong answer: exit 1, naming the file, line, expected and actual.
  • The gate reconciles its own count against the source, because it needed to. A one-line /** … @example … */ block was invisible to the collector, so every example inside one went unchecked and the run stayed green — the exact failure the gate exists to prevent, inside the gate. Fixing the regex would have closed that instance and not the class, so the script now counts raw @example occurrences per file and fails when fewer were read than written. Proved by reverting the regex: the run prints the total written, the smaller total read, and each file's own written count, then exits 1. A recorded roster of unassertable claims catches the other direction, a checked claim being downgraded to prose — which the accounting cannot see, because the written total does not move. The roster is a roster and not a count, because a count can say that a claim stopped being checked and can never say WHICH. It lives in scripts/examples.baseline.json, beside cross-runtime.baseline.json and bundle-size.baseline.json, and it is keyed by file, expression and reason with no line number, so an edit anywhere above a claim does not move it. Dropping the line number means two claims can share a key, because the same expression is sometimes documented on two declarations, so the diff counts occurrences rather than testing membership. It has to: with a membership test, a key already in the roster once would let a SECOND claim with that key be downgraded in silence, which is the one direction the roster exists to catch. Measured at two edits from a clean tree, exit 0, reporting "roster unchanged" while a claim had stopped being checked. Only the diff is ever printed, so a green run is one line — a wall of skips on every pass is a wall nobody reads, and a check nobody reads is not a check. Drift fails both ways: an added line is a claim that stopped being checked, a missing line was asserted or deleted. Both are deliberate, and npm run test:examples:write re-records. Proved by planting each direction, and each names the exact claim.
  • The gate now proves every declaration carries an example, and stops dropping two-line claims in README.md. Two holes, both found by reviewing this PR's own fixes rather than its code. The release notes claimed every capable declaration had an @example, and five did not — getYear, getDate, getQuarter, isSameDay and areIntervalsOverlapping, three of them the sibling asymmetry this entry already names as a defect. getYear was the worst of them, because getYear('2026-01-31[u-ca=buddhist]') is 2569 and that is the library's headline trap. All five now carry a runnable example, and a declaration site with no @example fails the run by name. Proved by planting: strip an example, or add a bare export, and the run names it and exits 1. The second hole was an asymmetry between the two collectors — collect joins a bare // … continuation onto the expression above it and collectFenced did not, so a README.md claim written across two lines landed in NO bucket, invisible to the skip list AND to the accounting, because both of that check's totals come from the same function. A declaration line carrying an answer was dropped the same way. Four claims sat in those two gaps: three joined the roster and the fourth asserts. Those three are newly VISIBLE, not newly unchecked — nothing moved from checked to unchecked.
  • CHANGELOG.md is read by the gate, and a released entry was already lying. Two blind spots, one cause. The gate read README.md only, and its fence regex was anchored at column zero — so every INDENTED ```js fence was invisible, which is how CHANGELOG.md and `FUTURE.md` write all of theirs. Un-anchoring the regex and adding the file found `day() // '2026-08-08' now, UTC` in the shipped 0.4.0 notes, rotted since the day it was written and seen by no gate. A released entry is a record, but a wrong code example in it is still copied. `CHANGELOG.md` stays on the list, so every future release note carries the same duty as the README.
  • npm publish cannot skip the gate. prepublishOnly ran test:coverage alone, so the examples gate existed and the release path never consulted it — the same shape of gap as a claim nothing checks. It now runs both.

Fixed

  • Seven clock-dependent examples had silently rotted, across three files. They claimed day() was '2026-08-08', day('Asia/Tokyo') was '2026-08-09' and addDays(day(), 2) was '2026-08-10' — all true the day they were written and none true afterwards, because day() reads a clock. A literal that depends on today's date can only rot, so every one now reads as prose. No behaviour changed; the documentation stopped lying.

    Three were in index.js, three in README.md, and the seventh in CHANGELOG.md itself, inside the released 0.4.0 entry. Each set was found by a different means, and the order is the whole lesson. The index.js three came from the gate. The README.md three came from sweeping the CLASS rather than the diff, and they matter more, because README.md ships in the npm tarball. The seventh was found by nothing at all until the gate was pointed at the file — it had survived three releases and every check. So the class was closed twice by hand before the tool could see the last instance, which is the argument for widening what the tool reads rather than fixing what it reports.

  • setYear now says that date-fns disagrees with it, which setDate already said. Measured on date-fns 4.4.0: setYear('2024-02-29', 2026) rolls to 2026-03-01 where daymath clamps to 2026-02-28. scripts/differential.mjs already recorded it. setDate named the divergence and setYear did not, so the silence implied agreement — and this is the one a caller reaches by accident, through a stored 29 February.