fix(metadata-protocol): order the ADR-0067 commit timeline by instant, so rollbackToPackageCommit stops planning off the weekday name - #14036
Conversation
…, not by the weekday name
`created_at` is an engine-injected audit column: not in `datetimeFields`, and
`SqlDriver#formatOutput` repairs it only inside `if (this.isSqlite)`. The live
SQL dialects therefore hand it out of the record read door as a JS `Date` while
the SQLite family hands out canonical ISO-Z text.
Both ADR-0067 commit-timeline consumers compared `String(created_at)`, and
`String(aDate)` is `"Sun Aug 30 2026 18:19:25 GMT+0800 (China Standard Time)"` —
the LEADING token is the weekday NAME, so lexicographic order over those strings
is `Fri < Mon < Sat < Sun < Thu < Tue < Wed`. Unrelated to chronology, and
stable across the whole set, so it is wrong on every run and wrong the same way.
- `listCommits` returned the timeline in weekday-name order while claiming
newest-first; its own comment stated the assumption ("sort by the ISO
timestamp") and it was false on the production default driver.
- `rollbackToPackageCommit` both consumed that ordering and re-derived the same
comparison itself, so neither site could correct the other: it reverted
`apply` commits OLDER than the target and skipped the newer ones it exists to
undo.
Both sites now compare canonical absolute instants through `compareAuditInstants`,
a sibling of the `canonicalVersionInstant` helper #13382 landed one seam over in
this same file. The canonicalisation is reused; the ordering is new, because
`versionTokensAgree` answers equality between client-supplied version tokens and
an ordering question needs `<`/`>`. When either side does not denote an instant
the two are compared verbatim exactly as before, so only instant-bearing pairs
change verdict.
The pin drives a hand-made `Date` — `@objectstack/metadata-protocol` has no
driver dependency and must not grow one — over four consecutive days, the
smallest fixture for which no timezone alignment can make the old weekday
comparison agree with chronology.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F3jdziLbAPGeceVNmSox5L
…w engine double
Both are the gates' own sanctioned repairs for the line/ledger movement the fix
caused, applied with their own tooling and inspected:
- `check-system-context-census --fix` RE-POINTED row 21's anchor
`metadata-protocol/src/protocol.ts:1664` -> `:1736`, the 72-line shift the new
`compareAuditInstants` helper block introduced above it. No row was deleted and
no needle changed; the gate then reports 109 elevation read sites, 145 anchors
resolving.
- `check-engine-double-contract --write` ADDED one row recording that the new pin
file pins 1 `findOne` double ("1 added or grown, 0 lost"). The shrink-only
baseline is untouched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F3jdziLbAPGeceVNmSox5L
📓 Docs Drift Check6 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 8 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin c2a06fd34a42a36790102bab5bd5aad86913256e && git checkout c2a06fd34a42a36790102bab5bd5aad86913256e
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin a7002ce5ac9acf6a4d556713c587a770f1848c26 df231b4ece19b7f27067eb0ec488c8b596d94a8c && git checkout -B drift-repro a7002ce5ac9acf6a4d556713c587a770f1848c26 && git merge --no-ff df231b4ece19b7f27067eb0ec488c8b596d94a8c
node scripts/docs-audit/affected-docs.mjs --json a7002ce5ac9acf6a4d556713c587a770f1848c26 |
The merge of origin/main routed content/docs/permissions/system-context.mdx through the os-regen driver, which exits 0 without text-merging and leaves git's pre-filled OURS side in place. That silently dropped the 16 anchor re-points main had landed (#13829, #13934, #13910, #13857) while keeping this branch's single re-point. This commit takes main's side of the page and re-derives every anchor from the merged tree with `pnpm gen:system-context-census`, which re-pointed row 21's metadata-protocol/src/protocol.ts anchor to 1736. Prose is byte-identical on both sides once line numbers are normalised, so nothing but line numbers moved.
Fixes #13995
created_atis an engine-injected audit column: it is not indatetimeFields, andSqlDriver#formatOutputrepairs it only insideif (this.isSqlite). So the live SQL dialects hand it out of the record read door as a JSDatewhile the SQLite family hands out canonical ISO-Z text — pinned one layer down bypackages/drivers/driver-sql/src/sql-driver-13567-audit-stamp-materialisation.test.ts.Both ADR-0067 commit-timeline consumers in
packages/metadata-protocol/src/protocol.tscomparedString(created_at).String(aDate)is"Sun Aug 30 2026 18:19:25 GMT+0800 (China Standard Time)", whose leading token is the weekday NAME, so lexicographic order over those strings runsFri,Mon,Sat,Sun,Thu,Tue,Wed— unrelated to chronology, and stable across the whole set, so the failure is systematic rather than intermittent. There is never an "it worked once" to warn anyone.listCommitsreturned the package timeline in weekday-name order while claiming newest-first. Its own comment stated the assumption in as many words — "sort by the ISO timestamp" — and that assumption was false on the production default driver.rollbackToPackageCommitboth consumed that ordering and re-derived the same comparison itself, so neither site could correct the other. It revertedapplycommits older than the target and skipped the newer ones it exists to undo: a destructive operation planning off a wrong predicate.The repair
Route A from the card — compare instants at the consumer. Both sites now go through
compareAuditInstants, a sibling of thecanonicalVersionInstanthelper #13382 landed one seam over in this same file for the OCCupdated_atcomparison.The canonicalisation is reused (
canonicalVersionInstant's measured input domain —Date, epochnumber, canonical ISO text, opaque — is exactly the domain acreated_atarrives in). The ordering is new, becauseversionTokensAgreeanswers equality between two client-supplied version tokens and an ordering question needs a comparison, not===; it also takes the pre-tidied pair an HTTPIf-Matchheader produces, which no driver-stamped column has. When either side does not denote an instant the two are compared verbatim exactly as before, so only instant-bearing pairs change verdict. No tolerant??alias fallback was added anywhere (#13973's standing prohibition); no driver dependency was added to@objectstack/metadata-protocol.Option B (normalising at the driver read door) is deliberately not taken here: it reverses
withPostgresCalendarDayAsTextand belongs to the whole census, not this site.The pin, and the ablation that proves it bites
packages/metadata-protocol/src/protocol.commit-timeline-instant-order.test.tsdrives a hand-madeDateon the read side —@objectstack/metadata-protocolhas no driver dependency and must not grow one, the same split the driver-side test documents for the OCC seam.The fixture is four consecutive days, and that number is load-bearing:
String(aDate)renders the weekday in the process timezone, so rather than pinTZ, the fixture is chosen so the old comparison is wrong in every alignment. Ranking the weekday names lexicographically (Fri=0,Mon=1,Sat=2,Sun=3,Thu=4,Tue=5,Wed=6), not one of the seven windows of four consecutive weekdays is monotonic — three days is not enough, sinceMon Tue WedandFri Sat Sunboth are. The first case in the file asserts that discriminating property mechanically rather than trusting the comment.Ablation on the committed tree at
ce47ff58f6, each leg mutated with anchored greps proving the edit landed in both directions and a changedgit hash-objectblob, restored undertrap ... EXIT INT TERMwith absolute paths and proved restored by an emptygit diff HEADplus a blob match. No rebuild leg is needed and none was fabricated: the pin imports./protocol.js, a package-relative specifier vitest resolves tosrc/protocol.ts, and the first green run happened beforepackages/metadata-protocol/distexisted at all.listCommitssortlistCommits returns the timeline newest-first, which receivedcmt_d4, cmt_d3, cmt_d5, cmt_d1, cmt_d2(weekday orderWed, Tue, Thu, Sun, Mon)rollbackToPackageCommitpredicatecmt_d1, cmt_d3, cmt_d4: it planned a revert ofcmt_d1, a commit older than the targetThe two legs are disjoint by construction: the planner case asserts a set, whose membership depends on site 2's predicate alone, so it stays green when only site 1 is reverted. The ISO-text half stays green in both legs — which is what shows the repair is about the
Dateshape and not a blanket rewrite.The priority question the triage explicitly left unmeasured
Triage set p1 rather than p0 on one mitigating factor — "rollback is an operator-initiated action, no automatic path triggers it" — and said it had not measured that. Measured now, and the mitigation holds: across
objectstackandobjectuithe only non-test caller ofrollbackToPackageCommitis the REST routePOST /packages/:id/rollback(packages/runtime/src/domains/packages.ts:750-756), behindrequireManageMetadata.rollbackOnFailureinpackages/core/src/kernel.ts:379is plugin-startup rollback (rollbackStartedPlugins) and never reaches it;os rollbackwas removed from the CLI;client.packages.rollbackhas no non-test caller. Full search expressions, controls and the one residual are in the seat report.One amplifier is worth recording even though it does not flip the verdict: objectui's
packages/app-shell/src/preview/CommitTimeline.tsxrendersGET /packages/:id/commits— the mis-ordered list — as the operator's selection surface, so the human in the loop was choosing a target from the wrong ordering.Verification
Gate union re-run at
b0db873a78, the final commit. All green, with two recorded non-greens that are not findings:check-test-completenessexits 3 = PREREQUISITE NOT MET when run with no saved turbo log (its own text says the local reading is NOT MEASURED), andcheck-half-statesneeds--use-env-proxyto reach GitHub, after which it exits 0.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstackat the first commit, then 31 more after the census repair movedcontent/**andscripts/**— re-derived rather than reused, and all 67 run.pnpm lint(repo-wideeslint . --no-inline-config) — exit 0, no narrowing claimed.pnpm --filter @objectstack/metadata-protocol exec vitest run— 149 files, 2065 passed.tsc --noEmit --listFileson the package — 63 errors, the exact count itscheck-type-check-coverageledger entry records, and--listFilesconfirms both edited files were in the program while none of the 63 is in either.pnpm check:type-check-debt(--re-measure, on a fully built workspace) exits 0.dist: objectqlprotocol-commit-history.test.ts32 passed; runtimepackage-list-commits-org-scope+package-revert-commit-org-scope+package-revert-commit-attribution-org-scope+http-dispatcher+packages-capability-gate— 341 passed.dist/index.d.ts;listCommitsandrollbackToPackageCommitkeep their signatures.The two gate repairs in the second commit, both applied with the gates' own tooling
check-system-context-census --fixre-pointed row 21's anchormetadata-protocol/src/protocol.ts:1664to:1736— the shift the new helper block introduced above it. It re-pointed, it did not delete a row; the gate then reports 109 elevation read sites and 145 anchors resolving.check-engine-double-contract --writeadded one row recording that the new pin file pins 1findOnedouble: "1 added or grown, 0 lost". The shrink-only baseline is untouched.Not addressed here
#13973 (the census) and #13382 (the OCC seam) are backlinks only and stay open. The sibling census cards #13996, #13997, #13998 and #13999 are each a different package and a different repair — none of them is touched by this branch.
One new class-(c) site found while working and not repaired here, because the correct shape is a judgement call rather than mechanical:
listCommitsdeclarescreatedAt?: stringin its return type but assigns the raw driver value, so on Postgres and MySQL it hands aDateto every consumer of a field typedstring. Filed separately; see the seat report.Generated by Claude Code
Generated by Claude Code