v0.7.1
Added
index.d.tsnow carries@exampleblocks, and it had none at all. Measured before starting: zero examples across 69 declarations, whileindex.jshad ten and every one was inside theday()block.package.jsondeclares exactly one types path —"types": "./index.d.ts", repeated in theexportsmap — so a TypeScript editor resolves documentation from the.d.tsand never parsesindex.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@exampledoes — 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, becausedayis overloaded and an editor shows the doc of the overload it RESOLVES: the whole contract block sat onday(tz?), the zone-only form, whileday(moment, tz?)— the one that takes aDate, epoch milliseconds, a day string or a SQLiteDATETIME— 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:mindocumented andmaxbare,startOfWeekdocumented andendOfWeekbare,addMonthsdocumented andsubMonthsbare,differenceInMonthsdocumented anddifferenceInYearsbare on the same counting rule,eachDayOfIntervaldocumented and the other two bare on the same inclusivity. Documenting one of a pair implies the other differs.isLeapYearwas bare while carrying the library's single most dangerous trap —isLeapYear('2567-01-01')isfalsewhile 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:getMonthis 1-12,getDayis 1=Monday…7=Sunday,addMonths/setMonth/setDate/setYearclamp instead of rolling,startOfWeekdefaults to Sunday,differenceInMonthscounts byaddMonths,mintakes an array, and the interval helpers are inclusive at both ends.formatandisValidshow what they refuse.npm run test:examplesexecutes every@exampleand asserts its stated answer. It readsREADME.mdtoo, which is the file that most needed it: it ships in the tarball, it is what a developer opens first, and three of itsday()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:tsctype-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@exampleoccurrences 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 inscripts/examples.baseline.json, besidecross-runtime.baseline.jsonandbundle-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, andnpm run test:examples:writere-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,isSameDayandareIntervalsOverlapping, three of them the sibling asymmetry this entry already names as a defect.getYearwas the worst of them, becausegetYear('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@examplefails 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 —collectjoins a bare// …continuation onto the expression above it andcollectFenceddid not, so aREADME.mdclaim 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.mdis read by the gate, and a released entry was already lying. Two blind spots, one cause. The gate readREADME.mdonly, and its fence regex was anchored at column zero — so every INDENTED ```js fence was invisible, which is howCHANGELOG.mdand `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 publishcannot skip the gate.prepublishOnlyrantest:coveragealone, 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'andaddDays(day(), 2)was'2026-08-10'— all true the day they were written and none true afterwards, becauseday()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 inREADME.md, and the seventh inCHANGELOG.mditself, inside the released 0.4.0 entry. Each set was found by a different means, and the order is the whole lesson. Theindex.jsthree came from the gate. TheREADME.mdthree came from sweeping the CLASS rather than the diff, and they matter more, becauseREADME.mdships 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. -
setYearnow says that date-fns disagrees with it, whichsetDatealready said. Measured on date-fns 4.4.0:setYear('2024-02-29', 2026)rolls to2026-03-01where daymath clamps to2026-02-28.scripts/differential.mjsalready recorded it.setDatenamed the divergence andsetYeardid not, so the silence implied agreement — and this is the one a caller reaches by accident, through a stored 29 February.