Uh oh!
There was an error while loading. Please reload this page.
fix(spec): refuse a diverging retention + ttl + archive lifecycle triple at parse time - #10643
Conversation
…ple at parse time (#10527)
📓 Docs Drift Check1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. ✅ What this run could not see
Coarse fallback — 126 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 00edec6ebe1fee05dc73072b2012f1833f7f90ac && git checkout 00edec6ebe1fee05dc73072b2012f1833f7f90ac
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 6b0be02209751420425e9194db87dfedfe6e3e38 fe2f316a4be48864fafbeb66c5b5e20bf0d3a435 && git checkout -B drift-repro 6b0be02209751420425e9194db87dfedfe6e3e38 && git merge --no-ff fe2f316a4be48864fafbeb66c5b5e20bf0d3a435
node scripts/docs-audit/affected-docs.mjs --json 6b0be02209751420425e9194db87dfedfe6e3e38 |
Uh oh!
There was an error while loading. Please reload this page.
…rom ttl.onlyWhen's own describe string (#10526) Bounded in-place extension of this PR's #10526 member, declared in the PR body. The refine comment and rejection message were not the only places the pre-#10347 rationale was written down: `ttl.onlyWhen`'s `.describe()` closed with "Incompatible with rotation storage and archive, which act on whole shards / age alone" — and by construction that sentence only ever applies when `ttl` IS declared, which is exactly the case where the Archiver now selects by the ttl cutoff rather than by age. Same defect class, same file, same fix already derived for the message; the correct wording is pinned by the merged Archiver read this PR's first commit cites. Leaving it would have shipped a PR whose refine message and whose describe string give contradictory reasons for one refusal. `retention.onlyWhen`'s matching clause is NOT touched and is still accurate: with `retention` and no `ttl` the Archiver moves rows by `created_at` age, and when the triple is declared #10643's refine pins `ttl` to restate `created_at`/`retention.maxAge`, so the ttl cutoff coincides with the age bound in every shape that parses. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B4h3medzvhB9rpfoja9jcw
Fixes#10527
What
LifecycleSchemanow refuses theretention+ttl+archivetriple at parse time unless the ttl restates the age bound exactly —ttl.field: 'created_at'withttl.expireAfter === retention.maxAge(disposition 2 of the card, per triage direction: sibling-ruling inheritance from the in-filearchive.after === retention.maxAgealignment refine).Why
Since #10347 (PR #10541) the Archiver selects the rows it moves by the declared ttl cutoff (
ttl.fieldolder thanttl.expireAfter) wheneverttlis declared, and bycreated_at/archive.afteronly when it is not (packages/objectql/src/lifecycle/lifecycle-service.ts, the[#10347] WHICH ROWS ARE DUEblock — whose own comment defers exactly this triple to #10527). On a diverging triple, the age bound (retention.maxAge, pinned equal toarchive.afterby the existing refine) no longer separately bounds the hot store: a row whosettl.fieldsits in the future stays hot pastretention.maxAge, silently. A declared bound nothing enforces is the class this refine block already refuses loudly (ADR-0049 declared ≠ enforced), so the triple now folds into the same loud-refusal family.The new message states the post-#10347 runtime truth ("the Archiver moves rows by the ttl cutoff when ttl is declared") — deliberately NOT the neighbouring message's pre-#10347 "moves rows by age alone" phrasing. That neighbouring message's own staleness is #10526 (hard-serialized behind this card, remains open) and its text is untouched here.
Consistency predicate derived
The ttl bound is the pair (
field,expireAfter); the age bound is (created_at,retention.maxAge). Equal durations alone do NOT make the age bound enforced (a future-datedexpires_atstill outlivesmaxAge), so consistency requires both components:ttl.field === 'created_at' && ttl.expireAfter === retention.maxAge. An aligned triple (ttl restating retention) parses — mirroring the precedent refine, which requires alignment rather than refusing the pair.Ruled-legal shapes unchanged (positive pins)
retention+archivealigned pair (the shape of both shipped declarers,sys_audit_log/sys_metadata_audit) — existing pin kept.ttl+archivepair (the finding(objectql): a lifecycle declaring bothttlandarchiveparses, but the ttl policy is silently inert —reapObjectreturns intoarchiveObjectbefore the ttl reap runs #10347-ruled shape) — new positive pin.New refusal pins assert the named message content (both bounds' values + the runtime truth), not just parse failure.
Census
Zero triple declarers anywhere (re-verified on this merge base):
sys_audit_log(packages/plugins/plugin-audit) andsys_metadata_audit(packages/metadata-core) are the only archive-declaring objects, bothretention+archivepairs. objectql's #10347 fixture is attl+archivepair typedas any, never parsed throughLifecycleSchema. AllLifecycleSchematest references live insidepackages/spec.Verification
(readout pinned to final HEAD in the report comment on #10527)
@objectstack/specbuild + full test suite (416 files / 11102 tests green) + typecheck@objectstack/plugin-audit,@objectstack/metadata-coretestsobject.zod.tsreverted to origin/main (mutation confirmed on disk: 19-line diff vs HEAD, marker grep-count 0), exactly the 3 new refusal pins went red (parse succeeded) and 176 others including the new positive pins stayed green; restored (marker count 1, tree clean) and re-run 179/179 green — no rebuild needed on either leg, the co-located tests import./object.zodsource-relativelynode scripts/pm/dispatch-gates.mjsagainst the actual diffChangeset: patch on
@objectstack/spec(grading precedent: #10541's behaviour-change patch; zero shipped objects affected). ADR-0087 registration gate green — a parse-time refusal of a never-declared shape needs no removal-registry entry, so no race with #10485's registry write.Generated by Claude Code
Generated by Claude Code