Skip to content

feat(spec)!: retire the inert outer keys of MetadataManagerConfig.cache — enabled, ttlSeconds, maxSize; cache.databaseLoader is the only live half (#15624, ADR-0049) - #16636

Draft
os-trump wants to merge 3 commits into
mainfrom
claude/issue-15624-retire-inert-metadata-cache-keys
Draft

feat(spec)!: retire the inert outer keys of MetadataManagerConfig.cache — enabled, ttlSeconds, maxSize; cache.databaseLoader is the only live half (#15624, ADR-0049)#16636
os-trump wants to merge 3 commits into
mainfrom
claude/issue-15624-retire-inert-metadata-cache-keys

Conversation

@os-trump

@os-trump os-trump commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator

Fixes #15624

Clause-②: yes

Summary

MetadataManagerConfig.cache declared three outer knobs — enabled (default true), ttlSeconds (default 3600; spelled ttl until #14478) and maxSize ("bytes") — beside the nested databaseLoader block, and nothing read the outer three. ADR-0049 enforce-or-remove ⇒ retire (the PM's ruling on the card, conditioned on the measurement below; option ② "wire readers" was not taken: 「只有在证明存在第二个缓存层的消费者时才成立;⛔ 别把它当成对称的两个选项——今天的证据只支持一边。」). Route per the spec-property-retirement playbook: retiredKey() tombstones (the nested object is not strict), no D2 conversion (a manager config is no stack collection member — the persistence.overlayWritable precedent), one D3 semantic entry, RETIRED_KEYS_BY_MAJOR[18] entries, regenerated reference docs, hand-written docs and README moved in the same PR. Runtime behaviour is byte-identical.

Premise re-verified on the base (554a1603, the same ref the PM measured)

grep (same shape, same ref, same path filter) hits reading
git grep -nE "cache\??\.(ttlSeconds|enabled|maxSize)" HEAD -- packages ':!*.test.ts' ':!*CHANGELOG*' 12 all prose: migration-entry / registry strings describing the #14478 rename, plus three comments about RegistryConfig.cache.maxSize (a different schema). Zero runtime readers.
positive control git grep -nE "cache\??\.databaseLoader" HEAD -- packages ':!*.test.ts' ':!*CHANGELOG*' 19 includes packages/metadata/src/metadata-manager.ts:619 and :649cache: this.config.cache?.databaseLoader, the only runtime consumer of the block

Reader-shape sweep beyond the exact spelling: git grep -nE "config\??\.cache\b|cacheConfig|cache\s*=\s*this\.config" over packages ':!*.test.ts' returns only those two lines. premise_still_valid: true; the ruling's part ③ (stop on a real reader) did not trigger. Line numbers on the card were stale exactly as the PM's A2.2 table says (ttlSeconds :89, maxSize :96, databaseLoader :106); the #14478 rename is on the base, and the file had no other writer in this worktree.

What changed

  • Schema packages/spec/src/kernel/metadata-loader.zod.ts: cache.enabled, cache.ttlSeconds, cache.maxSize become retiredKey() tombstones whose prescription says "Delete the key" and names the live nested knob — cache.databaseLoader.enabled / .ttlMs (milliseconds, default 60000) / .maxSize (an entry count, default 500). Wording follows the sibling tombstones in the same file ("was removed from MetadataManagerConfig in @objectstack/spec 17 (ADR-0049 enforce-or-remove)"); no os migrate meta sentence because no conversion covers this surface (same as persistence.overlayWritable).
  • The spec: duration-shaped number keys carry their unit in describe prose only — two ttl keys with different units in one block, bare timeout keys, unit-less tenant timeouts #14478 rename is absorbed (playbook §0 「同 major 记账」): ttl → ttlSeconds was registered under the same unreleased major 18 and never shipped, so cache.ttl's tombstone now prescribes deletion (naming cache.databaseLoader.ttlMs) instead of a rename to a key that is itself retired — an author upgrading from a published 17.x sees one hop. The nested databaseLoader.ttl → ttlMs half is untouched. The D3 entry metadata-manager-config-cache-ttl-unit-in-key and the unreleased changeset .changeset/duration-unit-in-key-name.md are re-worded for the outer half only (they would otherwise ship a prescription pointing at a dead key).
  • Registration (ADR-0087 D3): four entries/retired-keys/18.kernel__MetadataManagerConfig__cache.{enabled,ttlSeconds,maxSize,ttl}.ts (dotted spelling, the persistence.overlayWritable precedent; cache.ttl registered as the deletion it now is), one new semantic entry 18.metadata-manager-config-inert-cache-keys-retired.ts, step-18 rationale extended; gen:migration-registry / gen:spec-changes / gen:upgrade-guide run.
  • Pins packages/spec/src/kernel/metadata-loader.test.ts: a negative pin per key asserting the prescription (removed · ADR-0049 · Delete the key · the live nested knob named), a positive pin that the parse output no longer materializes the two former defaults while the nested half stays byte-identical, an empty-cache-block pin, and the tsc channel (@ts-expect-error per key). metadata-persistence.test.ts stops authoring cache.enabled.
  • Docs: content/docs/references/{kernel/metadata-loader, kernel/metadata-plugin, system/metadata-persistence}.mdx regenerated ([REMOVED] rows replace the three knob rows); hand-written content/docs/protocol/kernel/metadata-service.mdx and packages/metadata/README.md (ships in files[], hence @objectstack/metadata: patch) stop authoring the outer enabled: true and say why.
  • Changeset .changeset/metadata-manager-inert-cache-keys-retired.md: @objectstack/spec: minor + **BREAKING** banner + the adr-0087: registered metadata-manager-config-inert-cache-keys-retired disposition, FROM → TO block, the retirement-kit paragraph.

Verification (head 0663ee68a; every build/test through scripts/pm/os-verify-lock.sh, verdict lines quoted)

  • pnpm --filter @objectstack/spec buildVERDICT command-exit 0 (gen:schema's gates (a)/(b)/(b2)/(c) ran inside it).
  • pnpm --filter @objectstack/spec check:generated✓ All 15 generated artifacts are up to date. · VERDICT command-exit 0.
  • pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2Test Files 461 passed (461) · Tests 12809 passed (12809) · VERDICT command-exit 0.
  • pnpm --filter @objectstack/spec typecheck (tsc + scripts + test-typecheck) — VERDICT command-exit 0.
  • Consumer of the narrowed input type (MetadataManagerOptions extends MetadataManagerConfig): turbo run build --filter="@objectstack/metadata^..." (12 packages) then pnpm --filter @objectstack/metadata typecheckVERDICT command-exit 0. Direction: downstream consumer of @objectstack/spec. No other package constructs MetadataManager with a cache block (grep over packages examples apps).
  • node scripts/pm/dispatch-gates.mjs --commands at that head derived 100 families; every one run (exit codes in the report); reconciliation: ✓ dispatch-gates --ran: 100 derived famil(ies) accounted for — 100 run, 0 NOT-MEASURED. Two are CI's and read NOT MEASURED here: check:dual-build-cjs-loads (needs every package's dist, a full pnpm build) and check:type-check-debt (needs the whole ./packages/* closure built) — both exit 3 = PREREQUISITE NOT MET, neither a pass nor a finding. check-plugin-teardown-shape --self-test needed its pinned control commit on a shallow clone (git fetch --depth=1 origin 621a4876…, declared) and then passed 47/47. check:skill-examples needed @objectstack/client-react built (turbo, 34 packages) and then passed (257 prose examples type-check).
  • Changeset gates: check-adr-0087-registration ✓ (registered metadata-manager-config-inert-cache-keys-retired (new here)); check-changeset-no-major ✓ no major (its LEVEL AXIS is NOT MEASURED locally — no pull_request payload; the declaration is this body's Clause-②: yes line); check:nul-bytes ✓ plus a control-character self-scan of every touched file (0 hits).

Reverse verification (ablation)

Committed first; mutation = put the live enabled: z.boolean().default(true) back in place of its tombstone; the pin file imports ./metadata-loader.zod relatively (src-resolved — no dist/ on the path, so no rebuild leg is owed). Trap-guarded restore from HEAD, proven by bytes:

mutation on disk: injected=1 removed-tombstone=0
Tests  3 failed | 15 passed (18)    MUTATED-RUN-EXIT=1
  (the cache.enabled prescription pin, the no-materialized-defaults pin, the empty-block pin)
restore: git diff HEAD is EMPTY · hash-object 1ba58456… == HEAD blob · injected=0 tombstone=1
Tests  18 passed (18)               RESTORED-RUN-EXIT=0

Ratchet reading (route first, then the expectation)

authorable-surface/, authorable-defaults/, json-schema.manifest/, api-surface/: zero change, expected. The surface walk lists top-level keys only — 0 dotted rows across all 14 shards — so a nested-key tombstone never produces a [RETIRED] row and gates (b)/(c) never ask for it; the RETIRED_KEYS_BY_MAJOR[18] entries follow the house convention (36 nested entries are already registered that way, persistence.overlayWritable in this very def). authorable-surface.base.json is untouched.

Patch round (after the contract review — VERDICT: PASS; head 03bf78c44)

Two one-line follow-ups the review folded back, both in files this PR already owned; the design is unchanged:

  1. .changeset/duration-unit-in-key-name.md — the absorbed changeset's "Migration. Rename each key; no value changes." sentence contradicted its own table row (the outer cache.ttl is deleted, not renamed). The sentence now states that one exception and keeps the nested cache.databaseLoader.ttl → ttlMs rename as listed — the CHANGELOG an upgrading agent greps no longer ships a self-contradictory instruction.
  2. packages/metadata/ROADMAP.md:32 no longer lists persistence.overlayWritable (retired in Retire the paper metadata-customization protocol with its full coupling set — spec module, authorable customizationPolicies/mergeStrategy keys, metadata API §5, optional IMetadataService overlay methods, unreachable packages/metadata limb (re-charter of #12057) #13135) as a live write gate.

Gates derived for exactly those two paths (dispatch-gates.mjs --commands with the two paths as arguments): 46 families, all run, reconciliation ✓ dispatch-gates --ran: 46 derived famil(ies) accounted for — 46 run, 0 NOT-MEASURED. Three read exit 3 = PREREQUISITE NOT MET in the re-created (dist-less) worktree — check:dts-closure, check:sourcemap-no-sources-content (both measured 0 in the first round on the identical built inputs; this commit touches no package source) and check:dual-build-cjs-loads (CI's full build, as before). The changeset gates re-ran green: check-adr-0087-registration, check-changeset-no-major, check-empty-changeset, check:changeset-gate-self-tests, check:nul-bytes.

Deviations (declared)

  1. No tree-scoped text absence pin. The retired spellings enabled / maxSize are also the live nested half's spellings (databaseLoader.enabled / .maxSize), so a text grep cannot tell the retired outer key from the honoured nested one and is not an admissible instrument here. The tree-scoped guard is the tsc channel — MetadataManagerConfig / MetadataManagerOptions / MetadataPluginConfig.storage input types are never on the keys, measured by the spec and metadata typechecks — plus the prescription pins; the two prose authoring sites (docs page, README) were fixed by hand.
  2. packages/qa/dogfood expression-conformance suite not run. Its ledger has no MetadataManagerConfig.cache entry (the one metadata-loader mention is a comment about MetadataLoadOptions.filter, a different def), so no covers entry can strand; building @objectstack/verify's 35-package closure for a suite that cannot fire was not worth the shared box.
  3. Files outside the card's three keys were edited — the spec: duration-shaped number keys carry their unit in describe prose only — two ttl keys with different units in one block, bare timeout keys, unit-less tenant timeouts #14478 semantic entry, its unreleased changeset and cache.ttl's tombstone — because the absorption rule requires that no shipped prescription point at a retired key.
  4. Liveness ledger: none touched — MetadataManagerConfig is not a governed liveness type (no packages/spec/liveness/*.json names it), as the card says.

验收备注

维护者速读(草稿)

改了什么: MetadataManagerConfig.cache 外层的三个键 enabled / ttlSeconds(原 ttl)/ maxSize 退役为 retiredKey() 墓碑;cache.databaseLoader 是唯一活着的那一半,保持不变。同批重生成了 published 参考页,改了手写文档与 @objectstack/metadata README,登记了 ADR-0087 D3 条目与 RETIRED_KEYS_BY_MAJOR[18],并把 #14478 尚未发布的 ttl → ttlSeconds 改名折进本次删除(cache.ttl 现在直接提示删除)。契约复审 PASS 后的补丁轮只改了两行文档:改名 changeset 的迁移句子与 ROADMAP 里一个已退役键的提法。

为什么改: 这三个键声明了、给了默认值、写进了参考文档,但运行时没有任何读者——MetadataManager 只把 cache.databaseLoader 交给 loader。作者写 cache: { enabled: false } 得到干净的 parse 和完全不变的缓存,文档在教一件假的事(ADR-0049 的 declared-but-not-enforced)。测量在 554a1603 上复核:外层三键 0 个运行时读者,阳性对照命中 metadata-manager.ts:619 / :649

风险与代价(含回滚): 运行时零变化;唯一收窄的是 accept 面——之前写了这三个键的配置从「静默无效」变为编译错误 + parse 错误,错误信息直接给出处方。按 launch-window 惯例以 minor + BREAKING 横幅发布。回滚 = revert 本 PR(schema、entries、生成物、文档在同一批提交),无数据迁移。

席位意见: (留空)

你要做的: 确认「退役而非接线」这一处置(卡片测量与分诊只支持这一边);合并后无需人工动作。

Authored in Claude Code session session_01F8SRGcf2eKTK7RRpWCGxwf (https://claude.ai/code/session_01F8SRGcf2eKTK7RRpWCGxwf).

…Size} tombstones + kit (pre-build)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F8SRGcf2eKTK7RRpWCGxwf
…onfig.cache keys

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F8SRGcf2eKTK7RRpWCGxwf
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation protocol:system tests tooling labels Sep 7, 2026
@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/metadata, @objectstack/spec, touching 3 documentable anchor(s). ⚠️ 6 changed file(s) yielded no anchor (packages/metadata/README.md, packages/metadata/ROADMAP.md, packages/spec/src/migrations/entries/retired-keys/18.kernel__MetadataManagerConfig__cache.enabled.ts, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/protocol/kernel/metadata-service.mdx (via MetadataManagerConfigSchema (symbol, a top-level const))
What this run could not see
  • 6 changed file(s) yielded no anchor (packages/metadata/README.md, packages/metadata/ROADMAP.md, packages/spec/src/migrations/entries/retired-keys/18.kernel__MetadataManagerConfig__cache.enabled.ts, …) — pages documenting those are invisible to this run
  • 5 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 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 — 132 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 bc0ac1d1f5775659e72ad35452c463443a71f1bepackageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json bc0ac1d1f5775659e72ad35452c463443a71f1be

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

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs bc0ac1d1f5775659e72ad35452c463443a71f1be → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…he outer deletion; ROADMAP drops the retired overlayWritable gate

Patch round on #15624 after the contract review (VERDICT: PASS): the
duration-unit changeset's "Rename each key" sentence now states the one
exception its own table already carried (the outer cache.ttl is deleted,
not renamed), and packages/metadata/ROADMAP.md stops listing
persistence.overlayWritable, retired in #13135, as a live write gate.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F8SRGcf2eKTK7RRpWCGxwf
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:system size/l tests tooling

Projects

None yet

2 participants