Skip to content

fix(spec): refuse a diverging retention + ttl + archive lifecycle triple at parse time - #10643

Merged
os-elon merged 1 commit into
mainfrom
claude/issue-10527-lifecycle-triple-refuse
Aug 21, 2026
Merged

fix(spec): refuse a diverging retention + ttl + archive lifecycle triple at parse time#10643
os-elon merged 1 commit into
mainfrom
claude/issue-10527-lifecycle-triple-refuse

Conversation

@os-elon

Copy link
Copy Markdown
Collaborator

Fixes#10527

What

LifecycleSchema now refuses the retention + ttl + archive triple at parse time unless the ttl restates the age bound exactly — ttl.field: 'created_at' with ttl.expireAfter === retention.maxAge (disposition 2 of the card, per triage direction: sibling-ruling inheritance from the in-file archive.after === retention.maxAge alignment refine).

Why

Since #10347 (PR #10541) the Archiver selects the rows it moves by the declared ttl cutoff (ttl.field older than ttl.expireAfter) whenever ttl is declared, and by created_at/archive.after only when it is not (packages/objectql/src/lifecycle/lifecycle-service.ts, the [#10347] WHICH ROWS ARE DUE block — whose own comment defers exactly this triple to #10527). On a diverging triple, the age bound (retention.maxAge, pinned equal to archive.after by the existing refine) no longer separately bounds the hot store: a row whose ttl.field sits in the future stays hot past retention.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-dated expires_at still outlives maxAge), 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)

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) and sys_metadata_audit (packages/metadata-core) are the only archive-declaring objects, both retention + archive pairs. objectql's #10347 fixture is a ttl + archive pair typed as any, never parsed through LifecycleSchema. All LifecycleSchema test references live inside packages/spec.

Verification

(readout pinned to final HEAD in the report comment on #10527)

  • @objectstack/spec build + full test suite (416 files / 11102 tests green) + typecheck
  • Owning packages of the two pair-declarers: @objectstack/plugin-audit, @objectstack/metadata-core tests
  • Reverse verification from committed state: with object.zod.ts reverted 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.zod source-relatively
  • Gate families re-derived with node scripts/pm/dispatch-gates.mjs against the actual diff

Changeset: 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

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. ✅

What this run could not see
  • 1 anchor(s) matched too much of the corpus to be a work list: created_at (literal, 34 pages)
  • the SDK route bridge reached 45 of 221 client-bound route-ledger rows — the other 176 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run: node scripts/docs-audit/affected-docs.mjs --bridge-coverage

Coarse fallback — 126 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 6b0be02209751420425e9194db87dfedfe6e3e38packageMentionDocs.

Which tree this was computed on

This run read content/docs from 00edec6ebe1fee05dc73072b2012f1833f7f90ac — the merge of head fe2f316a4be48864fafbeb66c5b5e20bf0d3a435 into base 6b0be02209751420425e9194db87dfedfe6e3e38, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling protocol:data labels Aug 21, 2026
@os-elon
os-elon marked this pull request as ready for review August 21, 2026 07:58
@os-elon
os-elon added this pull request to the merge queueAug 21, 2026
Merged via the queue into main with commit 5649efbAug 21, 2026
33 checks passed
@os-elon
os-elon deleted the claude/issue-10527-lifecycle-triple-refuse branch August 21, 2026 08:14
os-elon pushed a commit that referenced this pull request Aug 21, 2026
…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
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationprotocol:datasize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding(objectql): with retention + ttl + archive all declared, only the ttl cutoff bounds the hot store

2 participants

@os-elon@claude