feat(spec)!: retire the inert outer keys of MetadataManagerConfig.cache — enabled, ttlSeconds, maxSize; cache.databaseLoader is the only live half (#15624, ADR-0049) - #16636
Conversation
…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
📓 Docs Drift CheckThis PR changes 2 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 132 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 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
|
…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
Fixes #15624
Clause-②: yes
Summary
MetadataManagerConfig.cachedeclared three outer knobs —enabled(defaulttrue),ttlSeconds(default 3600; spelledttluntil #14478) andmaxSize("bytes") — beside the nesteddatabaseLoaderblock, 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 thespec-property-retirementplaybook:retiredKey()tombstones (the nested object is not strict), no D2 conversion (a manager config is no stack collection member — thepersistence.overlayWritableprecedent), 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)git grep -nE "cache\??\.(ttlSeconds|enabled|maxSize)" HEAD -- packages ':!*.test.ts' ':!*CHANGELOG*'RegistryConfig.cache.maxSize(a different schema). Zero runtime readers.git grep -nE "cache\??\.databaseLoader" HEAD -- packages ':!*.test.ts' ':!*CHANGELOG*'packages/metadata/src/metadata-manager.ts:619and:649—cache: this.config.cache?.databaseLoader, the only runtime consumer of the blockReader-shape sweep beyond the exact spelling:
git grep -nE "config\??\.cache\b|cacheConfig|cache\s*=\s*this\.config"overpackages ':!*.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
packages/spec/src/kernel/metadata-loader.zod.ts:cache.enabled,cache.ttlSeconds,cache.maxSizebecomeretiredKey()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 fromMetadataManagerConfigin @objectstack/spec 17 (ADR-0049 enforce-or-remove)"); noos migrate metasentence because no conversion covers this surface (same aspersistence.overlayWritable).ttlkeys with different units in one block, baretimeoutkeys, unit-less tenant timeouts #14478 rename is absorbed (playbook §0 「同 major 记账」):ttl → ttlSecondswas registered under the same unreleased major 18 and never shipped, socache.ttl's tombstone now prescribes deletion (namingcache.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 nesteddatabaseLoader.ttl → ttlMshalf is untouched. The D3 entrymetadata-manager-config-cache-ttl-unit-in-keyand the unreleased changeset.changeset/duration-unit-in-key-name.mdare re-worded for the outer half only (they would otherwise ship a prescription pointing at a dead key).entries/retired-keys/18.kernel__MetadataManagerConfig__cache.{enabled,ttlSeconds,maxSize,ttl}.ts(dotted spelling, thepersistence.overlayWritableprecedent;cache.ttlregistered as the deletion it now is), one new semantic entry18.metadata-manager-config-inert-cache-keys-retired.ts, step-18rationaleextended;gen:migration-registry/gen:spec-changes/gen:upgrade-guiderun.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-errorper key).metadata-persistence.test.tsstops authoringcache.enabled.content/docs/references/{kernel/metadata-loader, kernel/metadata-plugin, system/metadata-persistence}.mdxregenerated ([REMOVED]rows replace the three knob rows); hand-writtencontent/docs/protocol/kernel/metadata-service.mdxandpackages/metadata/README.md(ships infiles[], hence@objectstack/metadata: patch) stop authoring the outerenabled: trueand say why..changeset/metadata-manager-inert-cache-keys-retired.md:@objectstack/spec: minor+**BREAKING**banner + theadr-0087: registered metadata-manager-config-inert-cache-keys-retireddisposition, FROM → TO block, the retirement-kit paragraph.Verification (head
0663ee68a; every build/test throughscripts/pm/os-verify-lock.sh, verdict lines quoted)pnpm --filter @objectstack/spec build—VERDICT 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=2—Test 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.MetadataManagerOptions extends MetadataManagerConfig):turbo run build --filter="@objectstack/metadata^..."(12 packages) thenpnpm --filter @objectstack/metadata typecheck—VERDICT command-exit 0. Direction: downstream consumer of@objectstack/spec. No other package constructsMetadataManagerwith acacheblock (grep overpackages examples apps).node scripts/pm/dispatch-gates.mjs --commandsat 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 fullpnpm build) andcheck: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-testneeded its pinned control commit on a shallow clone (git fetch --depth=1 origin 621a4876…, declared) and then passed 47/47.check:skill-examplesneeded@objectstack/client-reactbuilt (turbo, 34 packages) and then passed (257 prose examples type-check).check-adr-0087-registration✓ (registered metadata-manager-config-inert-cache-keys-retired (new here));check-changeset-no-major✓ nomajor(its LEVEL AXIS is NOT MEASURED locally — nopull_requestpayload; the declaration is this body'sClause-②: yesline);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.zodrelatively (src-resolved — nodist/on the path, so no rebuild leg is owed). Trap-guarded restore fromHEAD, proven by bytes: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; theRETIRED_KEYS_BY_MAJOR[18]entries follow the house convention (36 nested entries are already registered that way,persistence.overlayWritablein this very def).authorable-surface.base.jsonis 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:
.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 outercache.ttlis deleted, not renamed). The sentence now states that one exception and keeps the nestedcache.databaseLoader.ttl → ttlMsrename as listed — the CHANGELOG an upgrading agent greps no longer ships a self-contradictory instruction.packages/metadata/ROADMAP.md:32no longer listspersistence.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 --commandswith 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) andcheck: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)
enabled/maxSizeare 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.storageinput types areneveron 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.packages/qa/dogfoodexpression-conformance suite not run. Its ledger has noMetadataManagerConfig.cacheentry (the onemetadata-loadermention is a comment aboutMetadataLoadOptions.filter, a different def), so nocoversentry can strand; building@objectstack/verify's 35-package closure for a suite that cannot fire was not worth the shared box.ttlkeys with different units in one block, baretimeoutkeys, unit-less tenant timeouts #14478 semantic entry, its unreleased changeset andcache.ttl's tombstone — because the absorption rule requires that no shipped prescription point at a retired key.MetadataManagerConfigis not a governed liveness type (nopackages/spec/liveness/*.jsonnames it), as the card says.验收备注
packages/metadata/ROADMAP.md:32listedpersistence.overlayWritableas a live write gate (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) — a retired-key doc line in a package this PR already edits, so it had a taker here.cache.databaseLoader.ttl(the spec: duration-shaped number keys carry their unit in describe prose only — twottlkeys with different units in one block, baretimeoutkeys, unit-less tenant timeouts #14478 nested rename) has noRETIRED_KEYS_BY_MAJORentry, unlike thepersistence.overlayWritableprecedent; nested keys are invisible to the surface walk so gate (b) never asks — a registry-convention omission on a key belonging to spec: duration-shaped number keys carry their unit in describe prose only — twottlkeys with different units in one block, baretimeoutkeys, unit-less tenant timeouts #14478's nested half, and the D3 semantic entry carries the prescription.RETIRED_KEYS_BY_MAJORentries), so nested-key retirements are registered by convention rather than by gate; consistent across the tree, recorded as a boundary, not a defect; 承接者:无.维护者速读(草稿)
改了什么:
MetadataManagerConfig.cache外层的三个键enabled/ttlSeconds(原ttl)/maxSize退役为retiredKey()墓碑;cache.databaseLoader是唯一活着的那一半,保持不变。同批重生成了 published 参考页,改了手写文档与@objectstack/metadataREADME,登记了 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).