Skip to content

feat(spec)!: the three kernel/plugin-lifecycle-advanced.zod.ts duration keys carry their unit in the key name - #17986

Merged
zhuangjianguo merged 1 commit into
mainfrom
claude/issue-17780-plugin-lifecycle-duration-units
Sep 13, 2026
Merged

zhuangjianguo merged 1 commit into
mainfrom
claude/issue-17780-plugin-lifecycle-duration-units

Conversation

@zhuangjianguo

@zhuangjianguo zhuangjianguo commented Sep 13, 2026

Copy link
Copy Markdown
Collaborator

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 17986 reads 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 (interval 1, timeout 1, debounceDelay 1; lit control z.number 12, dark control 0), and each JSDoc was read rather than inferred from the default:

key JSDoc, as read published .describe(), before new spelling
PluginHealthCheck.interval "Health check interval in milliseconds" "How often to perform health checks (default: 30s)" intervalMs
PluginHealthCheck.timeout "Timeout for health check in milliseconds" "Maximum time to wait for health check response" timeoutMs
HotReloadConfig.debounceDelay "Debounce delay before reloading (milliseconds)" "Wait time after change detection before reload" debounceDelayMs

All three are milliseconds. The check:duration-unit-keys census reads all three [name: -] [prose: -] on origin/main — no unit in the name and none the gate recognises in the published prose. interval is the sharpest: its one unit-shaped token was the parenthetical (default: 30s), naming SECONDS for a value the schema bounds at min(1000) and defaults to 30000 milliseconds.

Why these spellings

Counted on this tree, not assumed from the card: 100 key-position *Ms declarations across packages/spec/src, of which timeoutMs is 29 and intervalMs is 3 — both renames land on names the surface already uses.

The analogous question to the sibling round's Ttl-versus-TTL was asked for debounceDelay and 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; no debounceMs variant anywhere), while the Delay-plus-Ms pairing 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. Neither PluginHealthCheckSchema nor HotReloadConfigSchema is .strict(), so a bare deletion would be a silent strip — and here the stripped value lands on a setInterval period, a race deadline and a setTimeout delay.
  • ADR-0087 D3 semantic entry kernel-health-check-and-hot-reload-durations-unit-in-key and three RETIRED_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 to PluginHealthMonitor / HotReloadManager in TypeScript), the same disposition plugin-auto-restart-never-reinitialised and hot-reload-watch-placeholder-retired recorded for keys on these two defs. registry.ts is generatedgen:migration-registry, never hand-merged.
  • @objectstack/core moves 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-0112 VALIDATION_ERROR / 400 naming the rename instead of undefined where 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).
  • Generated artifacts regenerated, never hand-edited: authorable-surface/kernel.json, authorable-defaults/kernel.json, content/docs/references/kernel/plugin-lifecycle-advanced.mdx. pnpm --filter @objectstack/spec check:generated reports 15 of 15 up to date.
  • Hand-written content/docs/protocol/kernel/lifecycle.mdx updated where it names the keys (+8 / −6).

The authorable-surface ratchet MOVES here, and that is correct

authorable-surface/kernel.json records 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 matching authorable-defaults moves. 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 by gen: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 the min(1000) bound, a pin that can no longer fail. They now assert the too_small issue code on the suffixed keys plus an accepting control at the boundary. The default and fixture pins were re-pointed at the new spellings with not.toHaveProperty on 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 at registerPlugin before the plugin is stored, with an accepting control beside each.

Consumer sweep — @objectstack/core is the only reader

subject, outside packages/spec and packages/core occurrences
PluginHealthCheckParsed / HotReloadConfigParsed 0 / 0
PluginHealthCheck · HotReloadConfig · PluginHealthMonitor · HotReloadManager 4 · 6 · 2 · 7 — all prose: changesets, the generated upgrade guide, a // comment in a packages/rest pin test, and two CHANGELOG lines. Zero code readers.
the three keys in a health-check / hot-reload context 0

Lit controls on the same corpus: @objectstack/spec 14580, PluginContext 973, ObjectKernel 588. Dark control zzqqxx 0.

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 of plugin-lifecycle-advanced.zod.ts occur 0 times, and debounceDelay occurs 0 times. Lit controls on the same corpus: objectstack 10171, @objectstack/spec 3479, timeout 832, interval 156. Dark control zzqqxx 0. Console Pin Gate is unaffected.

Verification

command verdict
pnpm --filter @objectstack/spec build VERDICT command-exit 0
pnpm --filter @objectstack/spec check:generated exit 0 — 15 of 15 artifacts current
pnpm --filter '@objectstack/core^...' build VERDICT command-exit 0
pnpm --filter @objectstack/core build VERDICT command-exit 0
pnpm --filter @objectstack/core test VERDICT command-exit 0 — 51 files, 1316 tests
pnpm --filter @objectstack/spec test VERDICT command-exit 0 — 475 files, 13517 tests
pnpm --filter @objectstack/spec --filter @objectstack/core typecheck VERDICT command-exit 0
dispatch-gates --commands → all 110, reconciled with --ran … :: exit N 104 exit 0 · 0 red · 6 exit 3

The six exit-3s are PREREQUISITE NOT MET on 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-examples initially refused with exit 1 on an unbuilt @objectstack/client-react — the refusal shape that a --ran reconciler counts as run; it was measured properly after building that closure and reads exit 0 (258 prose examples across 3 surfaces).

Acceptance notes

⛔ Draft on purpose: the in-seat clause-② review at tier is owed first, and landing is the PM's step.


Generated by Claude Code

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Sep 13, 2026
@github-actions

github-actions Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/core, @objectstack/spec, touching 18 documentable anchor(s). ⚠️ 5 changed file(s) yielded no anchor (packages/spec/authorable-defaults/kernel.json, packages/spec/authorable-surface/kernel.json, packages/spec/src/migrations/entries/retired-keys/18.kernel__HotReloadConfig__debounceDelay.ts, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/automation/flows.mdx (via timeoutMs (literal, a string literal in RETIRED_HEALTH_CHECK_KEYS))
  • content/docs/automation/hook-bodies.mdx (via timeoutMs (literal, a string literal in RETIRED_HEALTH_CHECK_KEYS))
  • content/docs/automation/jobs.mdx (via intervalMs (literal, a string literal in RETIRED_HEALTH_CHECK_KEYS), timeoutMs (literal, a string literal in RETIRED_HEALTH_CHECK_KEYS))
  • content/docs/automation/webhooks.mdx (via intervalMs (literal, a string literal in RETIRED_HEALTH_CHECK_KEYS), timeoutMs (literal, a string literal in RETIRED_HEALTH_CHECK_KEYS))
  • content/docs/deployment/environment-variables.mdx (via timeoutMs (literal, a string literal in RETIRED_HEALTH_CHECK_KEYS))
  • content/docs/protocol/kernel/lifecycle.mdx (via PluginHealthCheckSchema (symbol, a top-level const), PluginHealthMonitor (symbol, a top-level class), registerPlugin (symbol, a method of class PluginHealthMonitor), startMonitoring (symbol, a method of class PluginHealthMonitor), intervalMs (literal, a string literal in RETIRED_HEALTH_CHECK_KEYS), timeoutMs (literal, a string literal in RETIRED_HEALTH_CHECK_KEYS))

3 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v16.mdx (via timeoutMs (literal, a string literal in RETIRED_HEALTH_CHECK_KEYS))
  • content/docs/releases/v17/17-0.mdx (via timeoutMs (literal, a string literal in RETIRED_HEALTH_CHECK_KEYS))
  • content/docs/releases/v17/17-4.mdx (via timeoutMs (literal, a string literal in RETIRED_HEALTH_CHECK_KEYS))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 5 changed file(s) yielded no anchor (packages/spec/authorable-defaults/kernel.json, packages/spec/authorable-surface/kernel.json, packages/spec/src/migrations/entries/retired-keys/18.kernel__HotReloadConfig__debounceDelay.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 — 141 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 65767d2340dabce16953908699082dd97032da3bpackageMentionDocs.

Which tree this was computed on

This run read content/docs from ef2f3d1eec602eda5ee2ac7e37029a78ac1c5eac — the merge of head db55ea6dde75d3e9f888d66cbad84c78c9006f0c into base 65767d2340dabce16953908699082dd97032da3b, 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 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

⚠️ 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 65767d2340dabce16953908699082dd97032da3b → pass the list as
args.docs, on the commit named under Which tree this was computed on.

… 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

Copy link
Copy Markdown
Collaborator Author

Contract review

Head-sha: db55ea6dde75d3e9f888d66cbad84c78c9006f0c

Reviewed in an isolated worktree at the head sha (merge-base with origin/main = 884e8347d; origin/main at review time = fc28c1d38, which already carries sibling PR #17954). No file in the worktree was edited; git status --porcelain empty at 2026-09-13T10:48:38Z. Every reading below names its ref and UTC time.

① Derived judgments

  • PluginHealthCheck.intervalintervalMs — RIGHT. JSDoc at 884e8347d reads "Health check interval in milliseconds"; min(1000) / default(30000) corroborate ms; the old describe's only unit token was "(default: 30s)", naming seconds for a ms value. intervalMs is an attested key spelling on this tree (3 key-position declarations in packages/spec/src/**/*.zod.ts at 884e8347d).
  • PluginHealthCheck.timeouttimeoutMs — RIGHT. JSDoc "Timeout for health check in milliseconds"; min(100) / default(5000); timeoutMs is the family's most attested spelling (29 key-position declarations in *.zod.ts).
  • HotReloadConfig.debounceDelaydebounceDelayMs — RIGHT on the rule (append the unit token to the existing key, the [#14478 stack 3/6] kernel/: the 14 remaining duration keys carry their unit in the key name — ADR-0087 conversions with readers (runtime-emitted measurements included) #15678/[#14478 stack 4/6] system/: the 15 remaining duration keys carry their unit in the key name — ADR-0087 conversions with readers; metrics.zod.ts size needs an honest name, not the mechanical one #15679 shape; *DelayMs is attested 26 times in key position in packages/spec/src at 884e8347d). One prose imprecision, not a defect: the body, the changeset and both migration entries say "no debounceMs variant anywhere" — measured at 884e8347d, debounceMs occurs 6 times in the repo, including packages/spec/authorable-surface.base.json:4673 kernel/PluginHotReload:debounceMs (a key of a def retired in 18, registry.ts 'kernel/PluginHotReload' row present) and a private field in driver-sqlite-wasm. It is absent from packages/spec/src, so the choice stands; the sentence should have said so rather than "anywhere".
  • Tombstones actually refuse — RIGHT. retiredKey() is z.never({ error }).optional() (packages/spec/src/shared/retired-key.ts), so absence passes and any value raises the prescription. Neither PluginHealthCheckSchema nor HotReloadConfigSchema is strict: 0 code-line .strict( / strictObject( on the file at head (7 mentions, all in comment lines; lit control permission.zod.ts 4 code-line strictObject(), and both are plain lazySchema(() => z.object({...})). On a non-strict object a bare deletion would silently strip the key onto a setInterval period, a race deadline and a setTimeout delay, so the tombstone is required here, not optional. The spec pin asserts the issue code is not unrecognized_keys and the message carries the rename.
  • ADR-0087 kit — RIGHT. Semantic entry kernel-health-check-and-hot-reload-durations-unit-in-key plus three RETIRED_KEYS_BY_MAJOR[18] rows (kernel/PluginHealthCheck:interval, :timeout, kernel/HotReloadConfig:debounceDelay), same major and same wording form ("renamed … in @objectstack/spec 17") as the landed PluginHealthReport.metrics.responseTime row on this file. registry.ts is generated: tsx scripts/build-migration-registry.ts --self-test --check → "current (208 semantic, 171 retired-key, 178 retired-def)", exit 0 at 2026-09-13T10:43:00Z. Naming of the entry files matches the sibling files in the same directories.
  • D3-not-D2 — RIGHT. packages/spec/src/conversions/registry.ts at head names neither def (0 for PluginHealthCheck|HotReloadConfig; lit control datasource.config.timeout (turso) 1; dark 0), and no schema outside the file embeds PluginHealthCheckSchema / HotReloadConfigSchema (only comments). That is the disposition every prior retirement on these two defs took (autoRestart destroys the plugin and never re-initialises it, then reports it recovering — and, once successThreshold binds, healthy #12032, [finding] HotReloadConfig.stateStrategy 'disk'/'distributed' are silent memory fallbacks, and distributedConfig has zero readers — declared≠enforced inside the KEPT host-driven library #12340, [finding] HotReloadManager.startWatching watches nothing and logs "File watching started" at info; watchPatterns has no reader and watchHandles is never populated #12428, [#14478 stack 3/6] kernel/: the 14 remaining duration keys carry their unit in the key name — ADR-0087 conversions with readers (runtime-emitted measurements included) #15678). ADR-0087 D2 converts at the loader over a normalized stack; a def no document carries has no seam, so a conversion would be a transform that never runs. The upgrade guide and spec-changes.json are current at head (build-upgrade-guide.ts --check, build-spec-changes.ts --check both exit 0 at 2026-09-13T10:46:56Z).
  • packages/core runtime — RIGHT. health-monitor.ts reads config.intervalMs (setInterval period, log field) and config.timeoutMs (race deadline, message); hot-reload.ts reads config.debounceDelayMs. Bare readers left in packages/core/src: 0 (lit control suffixed reads 18; dark 0). Refusal is reachable: assertNoRetiredKeys is the first statement of PluginHealthMonitor.registerPlugin and runs before any set; in HotReloadManager.registerPlugin it runs before the enabled early-return and before reloadConfigs.set. Shape is ADR-0112: code = 'VALIDATION_ERROR', status = 400 on the thrown Error, the same envelope the sibling rows use; VALIDATION_ERROR is in error-code-ledger.zod.ts. Both core tests pin refusal-before-storage plus an accepting control.
  • Ratchet baselines MOVE, and that is correct — VERIFIED from the generator, not the claim. packages/spec/scripts/build-schemas.ts builds the ledger as for (const [defKey, schema] of generatedSchemas) for (const [name, prop] of Object.entries(schema.properties)) currentKeys.set(defKey + ':' + name, isRetired(prop)) — top-level properties of each generated def, tombstones marked [RETIRED], isRetired = the { not: {} } a z.never() emits. A key inside an inline sub-object is not a row. Empirical check at 884e8347d: schemaCacheTTL in authorable-surface/ = 0 (lit controls system/MigrationPlan: 4, kernel/PluginHealthCheck:interval 1; dark 0), so feat(spec)!: tenant schemaCacheTTL carries its unit in the key name (#17784) #17954 moving nothing and this PR moving three rows are the same rule read on nested vs top-level keys. I ran OS_EAGER_SCHEMAS=1 tsx scripts/build-schemas.ts --check at head: exit 0 at 2026-09-13T10:43:43Z, "authorable-defaults/ verified against upstream 884e834 — 1170 default(s) unchanged (spec 门禁盲区:可作者化 key 的「默认值 / 约束」变更不被任何 gate、tombstone 或 conversion 记录(#4650 / #4659 同族) #4666)". Check (b) (live → retired must be registered by exact key) is satisfied by the three registry rows. CI job "Type Check · source gates" (id 103707942775) step 24 "authorable key surface is recorded and nothing vanished" completed: success — a job step, not the rollup.
  • Hand-written content/docs/protocol/kernel/lifecycle.mdx — TRUE at head. Every sentence naming these keys (lines 696, 706–709, 718, 792) now spells intervalMs / timeoutMs; line 708's "old spellings are refused at registerPlugin" is true of the code. The two shutdownTimeout: 30_000 sites (104, 836) are new ObjectKernel({...}) options, not HotReloadConfig, and are untouched correctly.
  • Pin tests — nothing deleted, weakened or skipped: 0 .skip/.only/.todo in the four test files; it( count 19→22 (spec), 21→22 (health-monitor), 16→18 (hot-reload). The two min-bound replacements are STRICTLY STRONGER: the old parse({ interval: 500 }).toThrow() would now stay green off the tombstone (a value on interval throws regardless of magnitude), while the new pins assert too_small on intervalMs: 500 / timeoutMs: 50 AND accept at the boundary (1000 / 100). Spec file run locally at head: 29 passed, exit 0, 2026-09-13T10:45:49Z.
  • check:doc-authoring — no baseline entry added: the diff is exactly 16 files, none under scripts/; node scripts/check-doc-authoring.mjs at head exit 0 ("no growth, no burn-down unrecorded", 2026-09-13T10:47:03Z); the two new core prescription strings carry no #NNNN, anchor in an adjacent // comment.
  • Generated content/docs/references/kernel/plugin-lifecycle-advanced.mdx — consistent with the new describes and tombstones; CI step 25 of job 103707942775 ("generated reference docs are in sync with the spec") completed: success.

② Semver level

minor / minor is RIGHT and the text matches the diff. Measured on origin/main (fc28c1d38) packages/spec/CHANGELOG.md at 2026-09-13T10:40:17Z: the four landed siblings — #15677 (line 124), #15680 (675), #15678 (1804), #15679 (2764) — all sit under ## 17.4.0### Minor Changes (the ### Patch Changes heading of that release starts at line 2988), each as feat(spec)! with a **BREAKING** banner and an adr-0087: registered … comment. So Ruling A's "patch" and its "following the #15678 / #15679 shape" cannot both hold; the shape is what shipped, and the PM's correction to minor is right by measurement. The convention that breaking-ness rides on the banner plus the disposition is stated in scripts/check-changeset-no-major.mjs (launch window ships breaking as minor; "purely additive widening … at least minor"); this changeset carries both: node scripts/check-adr-0087-registration.mjs → "[BREAKING+bang] registered kernel-health-check-and-hot-reload-durations-unit-in-key", exit 0; node scripts/check-changeset-no-major.mjs --base 884e8347d → no major bump, exit 0 (both 2026-09-13T10:43:11Z / 10:45:26Z); CI "Check Changeset" completed: success. @objectstack/core at minor is also right: its registration-time input contract narrows (old spellings now throw) and its readers move, and the three core refusal-row precedents (#12032, #12340, #12428) all shipped under ### Minor Changes in packages/core/CHANGELOG.md. Changeset body vs diff: the before/after table, the unchanged defaults 30000/5000/1000, the unchanged min bounds 1000/100/0, the D3-only disposition, the core refusals and the shutdownTimeout exclusion each correspond to a line in the diff.

③ Boundary flags

  • HotReloadConfig.shutdownTimeout "noted, not filed" — RIGHT to leave out of this card. JSDoc "Graceful shutdown timeout", describe "Maximum time to wait for graceful shutdown": no unit in either channel. The batch Add comprehensive test coverage for ObjectStack spec protocols - 100% coverage achieved #65 ruling 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 keeps unit-nowhere keys "listed, not judged", it is not one of the 21 rows the card partitions, and the census at head reads it [name: -] [prose: -] (check-duration-unit-keys.ts --list, line 413, 2026-09-13T10:43:02Z). Putting a unit into its describe would make it an offender under the standing rule and force a rename outside this card's rows. ESCALATED to the epic PM as a candidate follow-up card (default 30000, ms by magnitude), not a defect of this PR.
  • check-widening-tells T1 on the tombstone lines — CLAIM VERIFIED. node scripts/pm/check-widening-tells.mjs --declaration no --diff pr17986.diff (three-dot diff from 884e8347d) fires exactly 3 T1 rows, at plugin-lifecycle-advanced.zod.ts:198/199/383, all three the retiredKey( lines; the three new suffixed key lines were absorbed by the replacement budget against the three removed bare lines. T1's sentence ("the accept set gains a spelling an author may now write") is false of a never-typed tombstone, so the tell is inverted ([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). With --declaration yes the gate exits 0 (both runs 2026-09-13T10:46:41Z). The declaration is not what makes the diff acceptable, though: the diff DOES widen the accept set by three suffixed spellings, and that is what yes records.
  • Docs Drift Check, six pages, five claimed false positives — CONFIRMED on all five. From the bot comment the anchors are the bare timeoutMs / intervalMs literals in RETIRED_HEALTH_CHECK_KEYS. Grepped at head: automation/jobs.mdx, automation/webhooks.mdx, automation/flows.mdx, deployment/environment-variables.mdx, automation/hook-bodies.mdx each contain 0 PluginHealth mentions (lit control: lifecycle.mdx 9) and 0 debounceDelay; hook-bodies.mdx's two hot-reload hits (lines 57, 329) are Studio in-browser editing. The sixth, lifecycle.mdx, is the real hit and is edited.
  • Tracker ids in the two packages/core refusal tables — pre-existing, not this PR's. health-monitor.ts's three autoRestart destroys the plugin and never re-initialises it, then reports it recovering — and, once successThreshold binds, healthy #12032 rows are pinned as adjudicated population in scripts/doc-authoring-prose-id.baseline.json ("packages/core/src/health-monitor.ts": { "#12032": 3 }); hot-reload.ts carries none. The new rows follow the id-free rule and the guard is green. Burning the baseline down is maintainer-only; noted for whoever does.
  • Clause ② — node scripts/pm/check-clause2-carriers.mjs --pair 17986 → exit 0 verbatim: "the clause-② declaration is readable in the fixed spelling and both carriers agree" (2026-09-13T10:40:24Z). Independently: yes is correct on the mechanical floor. kernel/PluginHealthCheck and kernel/HotReloadConfig are published payloads (exported from @objectstack/spec/kernel, rows in the authorable-surface ledger, tables on the reference page) and intervalMs / timeoutMs / debounceDelayMs are spellings no author could write on them before. The ruling's no fails that floor; the correction is right. The PR's "neither def is an authorable surface" (D2 sense: no document carries them) and the ledger moving (mechanical sense: every generated def's top-level keys) use "authorable" in two meanings; both statements are true in their own sense and the PR should not be read as contradicting itself.
  • Cross-file staleness, searched at db55ea6dd (2026-09-13T10:40:33Z) across the whole tree excluding release-owned pages, CHANGELOGs, the generated registry and .changeset/:

Readings I re-measured

  • Three-dot diff 884e8347d..db55ea6dd: 16 files, +683/−76; single commit (2026-09-13T10:38:44Z).
  • JSDoc / describe / bounds / defaults for all three keys: read from the diff against 884e8347d; the three bare keys occur exactly once each in key position in the file at 884e8347d (lit control z.number 12; dark zzqqxx 0).
  • Family spelling at 884e8347d, packages/spec/src/**/*.zod.ts: *Ms key-position 100, timeoutMs 29, intervalMs 3 — matches the body exactly. debounceDelay key-position whole repo 6 (body says 5; the sixth is PHASE2_IMPLEMENTATION.md:71). debounceMs whole repo 6 (body says none — see ①).
  • Pinned objectui: fetched 53ded82bf7a494f54e344e19099dbf00854b8694 (= .objectui-sha at head) into scratch, 6409 tracked files; all thirteen exports of the file, debounceDelay, debounceDelayMs, plugin-lifecycle-advanced = 0 each; lit controls objectstack 10171, @objectstack/spec 3479, timeout 832, interval 156; dark zzqqxx 0 (2026-09-13T10:46:47Z) — every number matches the body.
  • Gates run locally at head: check:migration-registry 0; check:duration-unit-keys --list 0 offenders, the three renamed keys read [name: ms] [prose: ms]; check:adr-0087-registration 0; check-changeset-no-major --base 884e8347d 0; check:upgrade-guide 0; check:spec-changes 0; build-schemas.ts --check 0; check-doc-authoring 0; check-clause2-carriers --pair 17986 0; check-widening-tells yes → 0, no → exit 4 with 3 T1 rows on lines 198/199/383.
  • CI, read per job (completed: success, not the rollup): Lint & Repo Gates 103707942719 (completed 10:39:10Z, step 173 duration-unit gate and step 59 doc-authoring guard success); Type Check · source gates 103707942775 (steps 21, 22, 24, 25 success); Type Check · consumer gates 103707942720; Test Core (1/6…6/6) each success. TypeScript Type Check (3-second run) is an aggregator and was not used as evidence.
  • Taken on the body's word, not re-measured: the [prose: -] census verdict on origin/main for the three old describes (the describe strings themselves are verified from the diff; the gate's parse of "(default: 30s)" was not re-run at the merge-base); the full-suite counts 475 files / 13517 tests and 51 / 1316 (CI shard jobs green; I ran only the spec pin file, 29 passed); the 104 / 6 dispatch-gates reconciliation; the "16 asserting pin citations" line (CI step 35 of 103707942775 success).

Implemented-by: branch claude/issue-17780-plugin-lifecycle-duration-units
Reviewed-by: session_015c5G6TmpMKgnusmTpD7Ntt

VERDICT: PASS


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

Carriers cleared — provenance

needs:contract-review stripped from both carriers at 2026-09-13T10:55Z: this PR and card #17780, each read back after the write.

review record #17986 (comment)
head judged db55ea6dde75d3e9f888d66cbad84c78c9006f0c — unchanged at strip time, so the PASS is not stale
verdict PASS, adopted verbatim
served tier verified from the review transcript's harness stamps: claude-fable-5-1 × 132, no other model value in the file, 0 harness fallback notices
check-clause2-carriers --pair 17986 exit 0
checks at strip time 42 runs, all success or skipped, 0 failure

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 5652827871.

Not enqueued yet. The label write above re-triggered a check run, so mergeable_state has gone unstable; ready and enqueue wait on that converging green rather than on a rollup. Ready → enqueue happens only after, and only in that order — on sibling PR #17954 I enqueued before stripping and the Governed Surface Queue Guard correctly dequeued it.

⛔ 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

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 size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

spec: 3 duration key(s) in kernel/plugin-lifecycle-advanced.zod.ts name their unit only in JSDoc — #15939 Ruling A remediation (3 of the 21-row delta)

2 participants