feat(spec)!: the three kernel/plugin-lifecycle-advanced.zod.ts duration keys carry their unit in the key name - #17986
Conversation
📓 Docs Drift CheckThis PR changes 2 package(s): 6 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 3 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 141 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 ef2f3d1eec602eda5ee2ac7e37029a78ac1c5eac && git checkout ef2f3d1eec602eda5ee2ac7e37029a78ac1c5eac
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 65767d2340dabce16953908699082dd97032da3b db55ea6dde75d3e9f888d66cbad84c78c9006f0c && git checkout -B drift-repro 65767d2340dabce16953908699082dd97032da3b && git merge --no-ff db55ea6dde75d3e9f888d66cbad84c78c9006f0c
node scripts/docs-audit/affected-docs.mjs --json 65767d2340dabce16953908699082dd97032da3b
|
8159cfd to
7c68204
Compare
… the key name `PluginHealthCheck.interval` / `.timeout` and `HotReloadConfig.debounceDelay` each named milliseconds in a source JSDoc only. `.describe()` is what the reference pages publish and the JSDoc above a key is not, so the reader who most needs the unit was the only one who never saw it — and `interval`'s describe carried a "(default: 30s)" parenthetical naming SECONDS for a value the schema bounds and defaults in milliseconds. Renamed to `intervalMs`, `timeoutMs` and `debounceDelayMs`, with a `retiredKey()` tombstone on each old spelling (neither object is `.strict()`, so a bare deletion would silently strip the value onto a `setInterval` period, a race deadline and a `setTimeout` delay), an ADR-0087 D3 semantic entry and three `RETIRED_KEYS_BY_MAJOR[18]` rows. `@objectstack/core` moves with it: both lifecycle classes read the suffixed keys and refuse an old spelling at registration with the rename. Executes director-seat ruling A on #15939 (2026-09-11, maintainer 「同意」, decision batch #115), the per-file remediation of the #14478 rule. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015c5G6TmpMKgnusmTpD7Ntt
7c68204 to
db55ea6
Compare
Contract review
Reviewed in an isolated worktree at the head sha (merge-base with ① Derived judgments
② Semver level
③ Boundary flags
Readings I re-measured
Implemented-by: branch claude/issue-17780-plugin-lifecycle-duration-units VERDICT: PASS Generated by Claude Code |
Carriers cleared — provenance
Full acceptance, including the disposition of all four review escalations and one dark control of my own that I had to throw out and redo, is on the card: #17780 comment ⏳ Not enqueued yet. The label write above re-triggered a check run, so ⛔ No approval is given or implied here. This is a governed surface: the clause-② review clears the queue gate, it is not a review approval, and this seat neither approves nor merges. Generated by Claude Code |
Fixes #17780
Clause-②: yes
Executes director-seat ruling A on #15939 (2026-09-11, maintainer 「同意」, decision batch #115) for
packages/spec/src/kernel/plugin-lifecycle-advanced.zod.ts— 3 rows of the 21-row delta, following the #15678 / #15679 shape. The declaration above reads the affirmative: the rename puts three spellings on a published payload no author could write before.check-clause2-carriers --pair 17986reads exit 0 — declaration readable in the fixed spelling, both carriers agree.The three rows, re-located by symbol
Each key was confirmed to be the only key-position declaration of that name in the file (
interval1,timeout1,debounceDelay1; lit controlz.number12, dark control 0), and each JSDoc was read rather than inferred from the default:.describe(), beforePluginHealthCheck.intervalintervalMsPluginHealthCheck.timeouttimeoutMsHotReloadConfig.debounceDelaydebounceDelayMsAll three are milliseconds. The
check:duration-unit-keyscensus reads all three[name: -] [prose: -]onorigin/main— no unit in the name and none the gate recognises in the published prose.intervalis the sharpest: its one unit-shaped token was the parenthetical(default: 30s), naming SECONDS for a value the schema bounds atmin(1000)and defaults to30000milliseconds.Why these spellings
Counted on this tree, not assumed from the card: 100 key-position
*Msdeclarations acrosspackages/spec/src, of whichtimeoutMsis 29 andintervalMsis 3 — both renames land on names the surface already uses.The analogous question to the sibling round's
Ttl-versus-TTLwas asked fordebounceDelayand answered the other way: it is the only debounce-shaped key spelling in the repo (5 key-position occurrences, all this key and its fixtures; nodebounceMsvariant anywhere), while the Delay-plus-Mspairing is already attested (maxDelayMs,initialDelayMs,retryDelayMs,delayMs). There was no competing family spelling to choose between, so it takes the plain suffix.The kit
retiredKey()tombstone on each old spelling. NeitherPluginHealthCheckSchemanorHotReloadConfigSchemais.strict(), so a bare deletion would be a silent strip — and here the stripped value lands on asetIntervalperiod, a race deadline and asetTimeoutdelay.kernel-health-check-and-hot-reload-durations-unit-in-keyand threeRETIRED_KEYS_BY_MAJOR[18]rows — both, per the PM's settlement on finding: check:duration-unit-keys reads .describe() but not JSDoc — a duration key documenting its unit only in JSDoc never enters the population, and one card already recorded a wrong reason because of it #15939. No D2 conversion: neither def is an authorable surface (both are library parameters a host passes toPluginHealthMonitor/HotReloadManagerin TypeScript), the same dispositionplugin-auto-restart-never-reinitialisedandhot-reload-watch-placeholder-retiredrecorded for keys on these two defs.registry.tsis generated —gen:migration-registry, never hand-merged.@objectstack/coremoves with the rename (it is the only in-repo reader): both classes read the suffixed keys, and each registration-time refusal table gains a row so a host still passing an old spelling gets an ADR-0112VALIDATION_ERROR/ 400 naming the rename instead ofundefinedwhere a duration belongs. Those two runtime strings carry no tracker id — a runtime string reaches operators who cannot resolve one; the anchor sits in an adjacent//comment (check:doc-authoring).authorable-surface/kernel.json,authorable-defaults/kernel.json,content/docs/references/kernel/plugin-lifecycle-advanced.mdx.pnpm --filter @objectstack/spec check:generatedreports 15 of 15 up to date.content/docs/protocol/kernel/lifecycle.mdxupdated where it names the keys (+8 / −6).The authorable-surface ratchet MOVES here, and that is correct
authorable-surface/kernel.jsonrecords top-level keys per def. All three of these keys are top level, so the rename must move the ledger — three[RETIRED]markers added beside three new suffixed rows, plus the matchingauthorable-defaultsmoves. This is the opposite reading from sibling PR #17954, whose key was nested (performance.schemaCacheTTL) and correctly moved nothing: different readings, same rule, both right. Regenerated bygen:schema, never hand-edited.Pin tests replaced, not deleted
Two minimum-bound pins are the ones worth calling out. Spelled through the bare keys,
PluginHealthCheckSchema.parse({ interval: 500 })would have kept throwing — off the tombstone's refusal, not themin(1000)bound, a pin that can no longer fail. They now assert thetoo_smallissue code on the suffixed keys plus an accepting control at the boundary. The default and fixture pins were re-pointed at the new spellings withnot.toHavePropertyon the old ones; nothing was deleted, weakened or skipped.New pins: the refusal carries the rename prescription (asserting the issue is not
unrecognized_keys), the suffixed keys parse at the magnitude the retired ones carried with the same defaults, the describes publish the unit, and both core classes refuse an old spelling atregisterPluginbefore the plugin is stored, with an accepting control beside each.Consumer sweep —
@objectstack/coreis the only readerpackages/specandpackages/corePluginHealthCheckParsed/HotReloadConfigParsedPluginHealthCheck·HotReloadConfig·PluginHealthMonitor·HotReloadManager//comment in apackages/restpin test, and two CHANGELOG lines. Zero code readers.Lit controls on the same corpus:
@objectstack/spec14580,PluginContext973,ObjectKernel588. Dark controlzzqqxx0.Pinned objectui: 0, with controls
.objectui-sha=53ded82bf7a494f54e344e19099dbf00854b8694— this IS the pin we build against (checked here). Grepped at that sha across its 6409 tracked files: all thirteen exports ofplugin-lifecycle-advanced.zod.tsoccur 0 times, anddebounceDelayoccurs 0 times. Lit controls on the same corpus:objectstack10171,@objectstack/spec3479,timeout832,interval156. Dark controlzzqqxx0. Console Pin Gate is unaffected.Verification
pnpm --filter @objectstack/spec buildVERDICT command-exit 0pnpm --filter @objectstack/spec check:generatedpnpm --filter '@objectstack/core^...' buildVERDICT command-exit 0pnpm --filter @objectstack/core buildVERDICT command-exit 0pnpm --filter @objectstack/core testVERDICT command-exit 0— 51 files, 1316 testspnpm --filter @objectstack/spec testVERDICT command-exit 0— 475 files, 13517 testspnpm --filter @objectstack/spec --filter @objectstack/core typecheckVERDICT command-exit 0dispatch-gates --commands→ all 110, reconciled with--ran … :: exit NThe six exit-3s are
PREREQUISITE NOT METon unbuilt sibling workspace packages outside this change's build closure (@objectstack/lint,@objectstack/formula) — NOT MEASURED by the gates' own classification, and built by CI.check:skill-examplesinitially refused with exit 1 on an unbuilt@objectstack/client-react— the refusal shape that a--ranreconciler counts as run; it was measured properly after building that closure and reads exit 0 (258 prose examples across 3 surfaces).Acceptance notes
HotReloadConfig.shutdownTimeoutis deliberately not renamed with these three. Its JSDoc reads "Graceful shutdown timeout" and names no unit anywhere — it is the unit-nowhere shape 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 gate leaves outside its verdict by measurement, and it is not one of the 21 rows PR feat(spec): refuse a duration key whose JSDoc names a unit its describe does not #17635 enumerates. Noted, not filed: the gate's own--listcensus already keeps it visible, and its header records that closing that class is a describe-by-describe decision rather than a defect.check-widening-tellsraising T1 on theretiredKey()tombstone lines is the known inverted false positive ([finding] check-widening-tells fires T1 on a retiredKey() tombstone line, so every ADR-0087 key retirement reads as a clause-2 widening for the one reason the accept set shrank #17955) — the lines it fires on make the accept set strictly narrower. The diff is not reshaped and the checker is not weakened.timeoutMsstring literal inside the new retirement constant, so it listed every page using that generic word:automation/jobs.mdx,automation/webhooks.mdx,automation/flows.mdx,deployment/environment-variables.mdxandautomation/hook-bodies.mdx— job scheduling, webhook retry backoff and the ADR-0102 sandbox CPU budget, none of themPluginHealthCheckorHotReloadConfig(0 plugin-health mentions each;hook-bodies.mdx's two "hot-reload" hits are Studio in-browser editing). The sixth,protocol/kernel/lifecycle.mdx, is the genuine hit and is edited here. The threecontent/docs/releases/pages are release-owned and untouched. Recorded because the four remaining rename cards in this epic will trip the same anchor.⛔ Draft on purpose: the in-seat clause-② review at tier is owed first, and landing is the PM's step.
Generated by Claude Code