Repository navigation
fix(metadata-protocol): seed replay compares time, datetime and date fields in their stored form - #24617
Conversation
… stored form isNoOpReplay compared a seed's time / datetime / date value with the driver's stored spelling literally, so '09:15' against stored '09:15:00' and '...T14:30:00Z' against '...T14:30:00.000Z' read as changed and the row was rewritten on every boot (updated_at bumped, lifecycle validation re-run, a phantom history edit). Both sides of a field the object declares temporal now go through temporalStorageForm, the one rule every driver applies on write; a value that rule cannot read still reads as changed. Claude-Session: https://claude.ai/code/session_01AHeUBbe6S3JRawcpbLejXD Co-authored-by: Claude <noreply@anthropic.com>
…y comparison Claude-Session: https://claude.ai/code/session_01AHeUBbe6S3JRawcpbLejXD Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AHeUBbe6S3JRawcpbLejXD Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 11 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 3608a4206f43dfe4f69dc97534762d55a1e22311 && git checkout 3608a4206f43dfe4f69dc97534762d55a1e22311
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 5a871e4e41b110d5d4b0cdcc325a75723740bb31 7cad49c762782a6f61554117884e93c7c0618b4b && git checkout -B drift-repro 5a871e4e41b110d5d4b0cdcc325a75723740bb31 && git merge --no-ff 7cad49c762782a6f61554117884e93c7c0618b4b
node scripts/docs-audit/affected-docs.mjs --json 5a871e4e41b110d5d4b0cdcc325a75723740bb31
|
席位核对 · ACCEPT ·
|
Fixes #24562
Clause-②: no
What changed
SeedLoaderService.isNoOpReplay(packages/metadata-protocol/src/seed-loader.ts) now compares a field the object declarestime,datetimeordatein its stored form. Both sides go throughtemporalStorageForm(@objectstack/core), the ruleSqlDriverandInMemoryDriveralready apply on write (ADR-0053). Every other field keeps the comparison it had.loadDatasetalready resolves (temporalFieldKinds, next toseedValuesInStoredForm). They are passed explicitly to both write paths that callisNoOpReplay: the batcheddecideWriteAction(all three call sites) and the sequentialwriteRecordused by self-referencing objects. The new parameter is required, so no call site can leave it out.temporalStorageFormreturns a value it cannot read unchanged, so an unparseable value equals only its own identical spelling. A non-text result (an Invalid Date, a structure) never compares equal.Measured: mechanism before and after, on a real driver
This used a scratch harness (not committed): a real
ObjectQLandSqlDriver(better-sqlite3) on one SQLite file DB. Each boot is a new driver, engine and loader. Results:The
datekind has the same spelling gap (an instant written to adatefield), so it is covered too.What this PR does not fix: Specimen — Full still gets a new
updated_aton every boot. The cause is a second, independent mechanism. The loader reads existing rows throughengine.find, which masks apasswordfield to••••••••in every context, so the seed's plaintexthunter2never equals the stored value. It is not a temporal-spelling issue. Fixing it means deciding how a replay compares a credential it cannot read, so it is reported to the seat as a separate finding and left out of this PR. So of the card's 5 churning showcase rows, 4 stop churning here; the fifth keeps churning until that finding has its own fix.The full
objectstack devtwo-boot on the stock showcase is NOT MEASURED: the CLI's build closure is 63 packages, which is too much for this container's shared verify lock. The harness above loads the showcase's own object definitions and seed data through the sameSeedLoaderService,ObjectQLandSqlDriver, and on base it reproduces the card's exact 5 rows.Tests
packages/metadata-protocol/src/seed-loader-replay.test.ts, reusing the file's pinned faithful engine double. Itsinsertandupdateare overridden to write temporal fields in the driver's stored form and to stampupdated_at. Each case asserts which write path ran (awriteRecordspy):'09:15','…T14:30:00Z', zone-naive'2026-06-17 14:30', an instant on adatefield, plus a stored-spelling control) issues no update and leaves the store,updated_atincluded, deep-equal. 4 cases: batched and sequential paths, modesupsertandupdate;pnpm --filter @objectstack/metadata-protocol exec vitest run --maxWorkers=2 src/seed-loader-replay.test.ts: 13 passed (5 existing + 8 new).pnpm --filter @objectstack/metadata-protocol typecheck && pnpm --filter @objectstack/metadata-protocol exec vitest run --maxWorkers=2at7cad49c76(the final head, after mergingorigin/main5a871e4e4and rebuilding the package closure): typecheck exit 0; 248 files passed, 3 skipped; 20731 tests passed, 19 skipped. The packagetsconfigincludessrc/**/*;--listFilescounts the test file.09b324c5b, throughscripts/ablation-replace.mjs; tests import./seed-loader.jsfrom source, so nodist/is involved). Theif (kind)stored-form block was deleted: anchor 1 to 0, blobee3eb38f87deto206fd67db529. Predicted direction: all 8 new cases red, 5 existing green. Observed: 8 failed, 5 passed. The replay negatives failed with "expected not to be called, called 4 times" (the 4 short-spelled rows); the control and unparseable cases failed by rewriting extra rows. Restored: blob equals HEADee3eb38f87de,git diff HEADempty.Gates
Locally, the derived set from
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, run at the final head7cad49c76.--ranreconciliation: 64 derived, 64 run, 0 NOT-MEASURED, 0 UNRUN, every one exit 0.check:engine-double-contract(OK, 1031 pinned),check:durability-log-level,check:nul-bytes,check:type-check-debt,check:dual-build-cjs-loads(111 require entry points load),check:test-source-alias,check:cross-package-test-inputs,check:published-filesandcheck-adr-0087-registration(1 non-breaking changeset).check-plugin-teardown-shape --self-testneeds a positive-control commit that lay outside this shallow clone. That commit was fetched with--depth=1, and the self-test then passed with 48 cases.test:repoand the whole-repolint: not run locally, declared to CI.Acceptance notes
.changeset/24562-seed-replay-time-equality.md,@objectstack/metadata-protocolpatch.seedValuesInStoredForm(aDateresolved by a CEL envelope, put in its stored form before the write) is untouched. The new helper reads the definition's field types the same way.@objectstack/metadata-protocolhas no dependency on a driver package, so the pin uses the existing double with the drivers' own storage rule, not a real driver. The real-driver reading is the measurement above. It was a one-shot and is not a permanent test, by the dispatch file surface.Generated by Claude Code