Skip to content

feat(spec)!: tenant schemaCacheTTL carries its unit in the key name (#17784) - #17954

Draft
claude[bot] wants to merge 3 commits into
mainfrom
claude/issue-17784-tenant-schema-cache-ttl-unit
Draft

feat(spec)!: tenant schemaCacheTTL carries its unit in the key name (#17784)#17954
claude[bot] wants to merge 3 commits into
mainfrom
claude/issue-17784-tenant-schema-cache-ttl-unit

Conversation

@claude

@claude claude Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

Fixes #17784

Clause-②: yes

Executes director-seat ruling A on #15939 (2026-09-11, maintainer 「同意」, decision batch #115) — the per-file remediation of the #14478 duration-unit rule. This card owns exactly one row, in packages/spec/src/system/tenant.zod.ts. The gate PR (#17635) is sequenced to land LAST and is untouched here.

⚠️ The declaration reads yes, not the no ruling A wrote: the PM corrected it on the card (comment 5652103109) under the mechanical floor in references/contract-review.md — a new key on a published payload is always yes — and a rename is a removal plus a new key. needs:contract-review is hung on card and PR; the changeset is graded minor accordingly, matching the four sibling duration-rename changesets of this same rule (#15677 · #15678 · #15679 · #15680), every one of which shipped as feat(spec)! under ## 17.4.0.

What changed

SchemaLevelIsolationStrategy.performance.schemaCacheTTL named its unit in a source JSDoc — "Schema cache TTL in seconds" — and nowhere else. The .describe() that content/docs/references/system/tenant.mdx renders said "Schema cache TTL" and named no unit at all, so the reader of the published reference page could not tell 3600 seconds from 3600 milliseconds.

before after
authored key performance.schemaCacheTTL: 3600 performance.schemaCacheTtlSeconds: 3600
published describe Schema cache TTL Schema cache TTL in seconds
value + default seconds, 3600 unchanged

The new spelling is Ttl, not TTL, derived from how the suffixed family already spells itself on this tree rather than from the dispatch: cacheTtlSeconds 11, ttlSeconds 3, defaultCacheTtlSeconds 1, and no key-position TTLSeconds variant anywhere.

The kit, following the #15678 / #15679 shape the ruling names:

  • a retiredKey() tombstone on the old spelling — tsc types it never and a value reaching the parse raises the rename prescription instead of being silently stripped (the nested performance object is not .strict())
  • ADR-0087 D3 semantic entry tenant-schema-cache-ttl-unit-in-key and the RETIRED_KEYS_BY_MAJOR[18] row system/SchemaLevelIsolationStrategy:performance.schemaCacheTTL, both as migrations/entries/ files with registry.ts regenerated by gen:migration-registry (never hand-merged)
  • no D2 conversion: stack.zod.ts declares no tenancy collection and a tenant isolation strategy is not a stored metadata row, so the chain has no seam that runs on it — the same reading tenant-timeouts-unit-in-key recorded for the two sibling keys on this same file
  • pin tests on SchemaLevelIsolationStrategySchema: the refusal carries the rename prescription, the suffixed key parses at the magnitude the retired one carried with the same 3600 default, and the describe publishes the unit
  • content/docs/references/system/tenant.mdx regenerated by gen:docs — all four rows move and the tombstone prescription renders in place of the old describe
  • a minor changeset carrying the FROM to TO mapping and the ADR-0087 disposition

The measurement this card was dispatched to produce

This is the first of six renames, so the mechanical clause-② reading matters for the other five.

node scripts/pm/check-widening-tells.mjs --declaration no over this PR's own diff — exit 4, exactly one tell:

✗ T1 packages/spec/src/system/tenant.zod.ts:454 — a new key on a Zod object schema — the accept set gains a spelling an author may now write
    + schemaCacheTTL: retiredKey(

The tell is not on the rename. It is on the tombstone. Two synthetic probes separate the two halves on the same instrument:

probe exit tells
the rename alone (one key line removed, one added, one change block) 0 none — the #16943 REPLACEMENT budget absorbs it
an added retiredKey( key line with no paired removal 4 T1 on the tombstone line

So the accounting is exact: the removed schemaCacheTTL: line buys one T1 unit, the added schemaCacheTtlSeconds: line spends it, and the added schemaCacheTTL: retiredKey( line is the surplus. Every retiredKey() retirement raises this, and the line it raises on is the one that makes the accept set strictly narrower — the key becomes never. That is the same inversion this file's own header records for #17300 (T2 on the retirement ledger's rows) and #17618 (T1 on a narrowed in-shape key), one door further along. Filed as #17955; the diff was not reshaped to silence it and no gate was weakened.

⚠️ Read the two readings apart, because they agree on the verdict for different reasons: the mechanical tell is a false positive on the tombstone line, while the yes this PR now declares rests on the contract-review floor (the rename really does add a key an author may write). check-clause2-carriers --pair 17954 read exit 4 / C5 against the original no and reads exit 0 against the corrected yes.

Verification

Heavy runs through scripts/pm/os-verify-lock.sh, verdicts read from its own VERDICT line; every gate's exit code captured before any pipe.

  • pnpm --filter '@objectstack/spec^...' build — exit 0, empty closure (No projects matched): packages/spec has no workspace dependencies, so step ① is a documented no-op
  • pnpm --filter @objectstack/spec buildVERDICT command-exit 0, run before every dist-reading gate and re-run after the last source edit
  • pnpm --filter @objectstack/spec test475 test files / 13507 tests passed
  • pnpm --filter @objectstack/spec typecheck — exit 0 (tsc --noEmit + check:scripts-typecheck + check:test-typecheck)
  • pnpm --filter @objectstack/spec check:generatedall 15 generated artifacts up to date after gen:docs; check:authorable-surface, check:api-surface, check:migration-registry, check:spec-changes and check:upgrade-guide among them
  • check:authorable-surface does not move for this key and that is correct: the ratchet records top-level keys per def and this one is nested under performance (0 hits for the key across authorable-surface/ and authorable-surface.base.json, against 4 for the system/MigrationPlan: lit control)
  • check:duration-unit-keys (the gate as it stands on main, not feat(spec): refuse a duration key whose JSDoc names a unit its describe does not #17635's) — exit 0
  • check:objectui-pin-citations — exit 0, 16 asserting pin citations match .objectui-sha
  • cross-repo, per the ruling's execution note and AGENTS.md post-task step 4: git grep of the pinned objectui checkout at .objectui-sha = 53ded82bf7a494f54e344e19099dbf00854b8694 (re-read from this tree) — schemaCacheTTL 0 occurrences across 6409 tracked files, beside lit controls TTL 112, Ttl 11, tenant 819, cacheTTL 1 and a dark control at 0, so the zero is a reading

Gate set derived at the actual change set with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack; full per-gate exit codes, including the ones that could not run in this container, are in the round report on #17784.

Acceptance notes

⛔ Draft on purpose: the in-seat clause-② review this PR now declares is owed before it may turn ready or enqueue. That is the PM's step, not this round's.

Round report, with every reading above and its exit code: the os-dev-report comment on #17784.


Generated by Claude Code

`SchemaLevelIsolationStrategy.performance.schemaCacheTTL` named seconds in a
source JSDoc only; the published `.describe()` said "Schema cache TTL" and
named no unit, so a reference-page reader could not tell 3600 seconds from
3600 milliseconds. Renamed to `schemaCacheTtlSeconds` with the unit in the
describe too, tombstoned with `retiredKey()` (the nested object is not
strict), and registered as an ADR-0087 D3 semantic entry plus a
RETIRED_KEYS_BY_MAJOR[18] row.

Claude-Session: https://claude.ai/code/session_015c5G6TmpMKgnusmTpD7Ntt
Co-authored-by: Claude <noreply@anthropic.com>
`gen:docs` moves the four `schemaCacheTTL` rows in
content/docs/references/system/tenant.mdx onto `schemaCacheTtlSeconds` and
renders the tombstone prescription in place of the old describe.
check:authorable-surface and check:api-surface are unchanged — the ratchet
records top-level keys per def and this one is nested under `performance`.

Claude-Session: https://claude.ai/code/session_015c5G6TmpMKgnusmTpD7Ntt
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation protocol:system tests tooling labels Sep 13, 2026
@github-actions

github-actions Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

3 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. ⚠️ 1 changed file(s) yielded no anchor (packages/spec/src/migrations/entries/retired-keys/18.system__SchemaLevelIsolationStrategy__performance.schemaCacheTTL.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/spec/src/migrations/entries/retired-keys/18.system__SchemaLevelIsolationStrategy__performance.schemaCacheTTL.ts) — pages documenting those are invisible to this run
  • 4 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 100 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 136 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 2c87a48f0d553224e9a899d29562a172a36dc5cepackageMentionDocs.

Which tree this was computed on

This run read content/docs from 847b65f75d015b5d3360a3ec844ca9de26b1df7b — the merge of head 0183e54fc1add770986077b260f44715c02eeae5 into base 2c87a48f0d553224e9a899d29562a172a36dc5ce, 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 847b65f75d015b5d3360a3ec844ca9de26b1df7b && git checkout 847b65f75d015b5d3360a3ec844ca9de26b1df7b
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 2c87a48f0d553224e9a899d29562a172a36dc5ce 0183e54fc1add770986077b260f44715c02eeae5 && git checkout -B drift-repro 2c87a48f0d553224e9a899d29562a172a36dc5ce && git merge --no-ff 0183e54fc1add770986077b260f44715c02eeae5

node scripts/docs-audit/affected-docs.mjs --json 2c87a48f0d553224e9a899d29562a172a36dc5ce

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

Copy link
Copy Markdown
Collaborator

PM status on the two red checks — both diagnosed, both this PR's, fixes in flight

Epic PM for the #15939 subtree, session_015c5G6TmpMKgnusmTpD7Ntt, 2026-09-13T08:20Z. Read from the job logs, ⛔ not from the round's self-report.

1. Check Changeset⚠️ triggered by a PM action, and it exposed a real error

I hung needs:contract-review on this PR at ~08:01 after correcting the card's clause-② declaration to the affirmative (correction comment 5652103109, reasoning in 5652094166). The gate read the carrier and said:

⛔ This PR declares clause-② YES, and it grades NO package whose packages/**/src/** it moves at minor or above. … @objectstack/spec: patch

This is not "a PM label broke a good PR". The patch grading was already wrong, and nothing was going to say so while the declaration read the other way. Measured by me on origin/main in packages/spec/CHANGELOG.md, every sibling of this card under ## 17.4.0 (a minor bump):

line entry
:1804 feat(spec)!: the fourteen kernel/ duration keys carry their unit in the key name (#15678, ruling B on #14478)
:2764 feat(spec)!: the fifteen system/ duration keys carry their unit in the key name (#15679, …)
:124 · :675 · :912 same shape, feat(spec)!, each with an adr-0087: registered disposition

Ruling A prescribes "patchfollowing the #15678 / #15679 shape" — and that shape is feat(spec)! at minor, four times over, never once a patch. The two halves of the ruling's own clause disagree; the measurable half wins. Corroborated independently by the maintainer ruling the gate quotes at itself (2026-09-04, decision batch #35, on #15294: "a purely additive widening … takes AT LEAST minor").

Fix: the changeset rises to minor / feat(spec)!, keeping the [BREAKING] marker and the ADR-0087 disposition that check-adr-0087-registration already passed. Recorded for the whole epic on #15939 — it applies to all six rename cards, ⛔ not to #17786 / PR #17953, which adds no key and stays at patch.

2. Type Check · source gates — entirely this PR's, unrelated to the above

@objectstack/spec check:objectui-pin-citations✗ 2 pin-citation / anchor problem(s) in packages/spec/src. The gate accepts exactly two spellings and says "an unrecognised spelling is not a pass: it leaves the citation outside every check, which is the silent state this gate exists to remove." The cross-repo pin citation in this PR's prose uses a third. ⇒ Fix: use the spelling that is true of what was done (this round checked against the pin). ⛔ Not by weakening the gate, ⛔ not by dropping the citation.

3. PR body declaration line

check-changeset-no-major reads the PR-body line, not my correction comment, and it printed the disagreement: "· carrier: needs:contract-review IS on this PR · declaration line: (negative)". The body line moves to the affirmative to match the card. ⇒ Note that check-clause2-carriers --pair 17954 already reads exit 0 via the correction comment; this is the human-legibility and changeset-gate half of the same fact.

Also read this wake, and judged as needing no action

Docs Drift Check — advisory, names no page. It declares its own blind spot honestly: the retired-key entry 18.system__SchemaLevelIsolationStrategy__performance.schemaCacheTTL.ts yields no anchor, so pages documenting it are not covered by that run. That is a limitation of the drift tool, ⛔ not a defect in this diff, and the generated reference page for this key is regenerated in this PR by gen:docs. No action.

Posture

All three fixes are with the round now. ⛔ Draft stays draft; no ready, no enqueue, no auto-merge. An in-seat clause-② contract review at CONTRACT_REVIEW_TIER is owed on this PR before any of that, and it is this seat's to supply — per the maintainer's direct instruction today, quoted verbatim: 「你自己直接负责契约复审」.

epic PM for #15939 · session_015c5G6TmpMKgnusmTpD7Ntt · 2026-09-13T08:20Z


Generated by Claude Code

…ecked spelling

Three corrections after the PM flipped card #17784's clause-② declaration to
`yes` under the contract-review floor (a rename adds a new key on a published
payload):

- the changeset grades `@objectstack/spec` `minor`, matching the four sibling
  duration-rename changesets of this same rule, and its summary takes the
  `feat(spec)!` shape they use
- the semantic entry cites the pinned objectui checkout as
  `.objectui-sha` = `<sha>`, one of the two spellings
  `check:objectui-pin-citations` recognises — it was in a third spelling, which
  the gate reports as a citation outside every check
- registry.ts regenerated so its mirror of that prose matches

Claude-Session: https://claude.ai/code/session_015c5G6TmpMKgnusmTpD7Ntt
Co-authored-by: Claude <noreply@anthropic.com>
@zhuangjianguo zhuangjianguo changed the title fix(spec): tenant schemaCacheTTL carries its unit in the key name (#17784) feat(spec)!: tenant schemaCacheTTL carries its unit in the key name (#17784) Sep 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

spec: 1 duration key(s) in system/tenant.zod.ts name their unit only in JSDoc — #15939 Ruling A remediation (1 of the 21-row delta)

2 participants