Skip to content

feat(spec)!: the fourteen kernel/ duration keys carry their unit in the key name (#15678, stack 3/6) - #15906

Merged
os-sales merged 21 commits into
claude/issue-14478-duration-unit-in-key-namefrom
claude/issue-15678-kernel-duration-unit-in-key-name
Sep 5, 2026
Merged

feat(spec)!: the fourteen kernel/ duration keys carry their unit in the key name (#15678, stack 3/6)#15906
os-sales merged 21 commits into
claude/issue-14478-duration-unit-in-key-namefrom
claude/issue-15678-kernel-duration-unit-in-key-name

Conversation

@claude

@claude claude Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Part of #14478
Stack card 3/6 — implements #15678, kernel/ (no closing keyword: this card is one of six and the stack lands as a unit).

Stacked on card 2/6. Base = claude/issue-15677-api-duration-unit-in-key-name, cut at b4633903b282cbb1e343c1a6e6ec99bde5dc4d59.

Maintainer ruling B on #14478 (2026-09-02, decision batch #43): a duration-shaped z.number() carries its unit in the key NAME, never only in its .describe() prose, and no existing offender is grandfathered.

The number

check:duration-unit-keys on this head (8f2b8f389), exit code captured before any pipe:

verdict line
before (b4633903b) ✗ check:duration-unit-keys — 36 offender(s) among 215 duration-shaped numeric key(s) in 786 source file(s) (6 declared EpochMsinstant(s), 11 declaredexternalVocabulary mirror(s)):
after (8f2b8f389) ✗ check:duration-unit-keys — 22 offender(s) among 215 duration-shaped numeric key(s) in 805 source file(s) (6 declared EpochMsinstant(s), 11 declaredexternalVocabulary mirror(s)):

36 → 22, and src/kernel/** reads 0. The gate still exits 1, as it must until card 6/6 closes the stack — the 22 that remain are system/ (15, card 4/6) and data/ui/ai/integration (7, card 5/6). The source-file count moves 786 → 805 because this card adds 19 ADR-0087 registry entry files under packages/spec/src/; the 215 duration-shaped key population is unchanged, which is what a pure rename should do.

The 14 keys

key replacement unit
EventPersistence.retention retentionDays days
EventSourcingConfig.retention retentionDays days
UpgradePlan.estimatedDuration estimatedDurationSeconds seconds
PluginHealthReport.metrics.uptime uptimeMs milliseconds
PluginHealthReport.metrics.responseTime responseTimeMs milliseconds
SandboxConfig.process.timeout timeoutMs milliseconds
KernelSecurityPolicy.authentication.tokenExpiration tokenExpirationSeconds seconds
KernelSecurityPolicy.auditLog.retention retentionDays days
PluginSecurityManifest.vulnerabilityDisclosure.responseTime responseTimeHours hours
PackageDependencyResolutionResult.resolvedIn resolvedInMs milliseconds
MultiVersionSupport.rollout.duration durationMs milliseconds
StartupOptions.timeout timeoutMs milliseconds
PluginStartupResult.duration durationMs milliseconds
StartupOrchestrationResult.totalDuration totalDurationMs milliseconds

Every value is unchanged and every default moves with its key. Each old spelling is a retiredKey() tombstone, so authoring it fails tsc (input type never) and fails the parse with the rename prescription rather than a bare unrecognized-key error.

Population re-derived from the gate on this branch's own head before the first edit, per ruling (1) — the gate's predicate is the population's definition. The card's table listed the right file+key pairs; its line numbers were stale from e68ae2b58 (startup-orchestrator.zod.ts had shifted 37 → 39, 149 → 161, 196 → 208 under cards 1/6 and 2/6).

Two unit collisions this removes

responseTime meant two different units on two kernel shapes: HOURS on the security manifest's disclosure block, MILLISECONDS on the health report. responseTime: 24 was a day on one and a fortieth of a second on the other. They land on responseTimeHours and responseTimeMs; a pin asserts the manifest prescription does not name responseTimeMs, so one find-and-replace cannot rewrite both.

uptime was already costing prose: content/docs/protocol/kernel/lifecycle.mdx carried a standing paragraph whose only job was distinguishing the milliseconds-valued metrics.uptime from the seconds-valued uptime of GET /health. metrics.uptimeMs makes the paragraph a statement about the fix rather than a warning; the HTTP body's uptime is a separate surface and is untouched.

Dispositions — five semantic entries, no D2 conversion

Decided per key from the schema's producer, not from the card's class column, and the answer here is uniform: none of the fourteen gets a D2 conversion. A D2 conversion runs over a stack document, and stack.zod.ts declares no eventBus, startup, upgrade or plugin-security root — none of these twelve defs is a stack collection member (mapCollection targets: actions agents analyticsCubes apis apps books connectors dashboards datasets datasources flows hooks jobs mappings objects permissions sharingRules skills themes tools translations) and none is a registered metadata kind stored as a sys_metadata row. So the chain has no seam that would see one, and each key carries a semantic entry instead — the disposition kernel/HealthStatus:timestamp already holds on one of these very files from card 1/6.

All fourteen are registered by exact key in RETIRED_KEYS_BY_MAJOR under major 18, including the seven nested ones the authorable-surface walk does not reach (the RestApiPluginConfig:performance.defaultCacheTtl precedent from card 2/6). Gate (b) named exactly the seven top-level keys and now passes.

Keys deliberately left bare — with a pin each

EventSourcingConfig.snapshotRetention is a COUNT of snapshots and MultiVersionSupport.rollout.percentage is a proportion; neither is a duration. RuntimeConfig.resourceLimits.timeout names no unit anywhere in its prose, so it is outside the gate's population — a pin asserts it still parses bare, so a later sweep cannot read the four security renames as "every timeout on that file".

Readers were found by TYPE, not by name, and each hit was read rather than rewritten. Two near-misses that a name grep would have renamed: HealthStatus.details = { uptime: 1000 } is a free-form z.record entry, and packages/core/src/plugin-loader.ts declares its OWN local PluginStartupResult interface — a different type carrying startTime, no duration key at all, not a reader of this schema. Both untouched.

Verification

Local, because heavy CI does not report on a stacked PR — verified on this head: lint.yml and ci.yml both declare pull_request: branches: [main], and this PR's base is card 2/6's branch, so none of the 7 required contexts runs.

  • pnpm --filter @objectstack/spec test473 files / 12786 tests passed
  • pnpm --filter @objectstack/spec typecheck — clean (tsc --noEmit, scripts project, and the test layer at its unchanged 54 files / 261 errors / 145 pinned signatures)
  • pnpm --filter @objectstack/core typecheck + full core suite — 49 files / 1190 tests passed
  • check:generatedall 15 generated artifacts up to date
  • all 14 spec source audits, the four doc gates (check:doc-authoring 0 findings across 14971 customer-facing strings, check:skill-examples 257 prose examples type-checked, check:doc-formula-expressions, check:doc-security-posture), and ~70 root-level gates derived from the diff by scripts/pm/dispatch-gates.mjs — green.

Prerequisite non-readings were treated as non-readings, never as passes: check:skill-examples, check:doc-formula-expressions and check:doc-security-posture first exited 3 / needed builds, and were re-run green after building @objectstack/formula, @objectstack/lint, @objectstack/client-react and @objectstack/client. check:i18n and check:published-readme-exports remain PREREQUISITE-NOT-MET (exit 3) — reported as not measured, not as passes.

Reverse verification

Prediction written before running: reverting the StartupOptions.timeoutMs rename and dropping its tombstone should move the gate 22 → 23 with exactly one new kernel/ row, and red the two StartupOptions pins while the other two startup refusal pins stay green.

Observed: gate 23 offender(s), kernel/ rows 0 → 1, the single new row src/kernel/startup-orchestrator.zod.ts:39 timeout``; startup-orchestrator.test.ts 5 failed / 11 passed — the two predicted pins plus the three pre-existing `StartupOptionsSchema` tests this PR moved onto `timeoutMs`, which the prediction under-counted. Direction and localisation held exactly; the magnitude did not, and is reported as measured.

The mutation was proved on disk before measuring — injected marker grep -c = 1, removed marker = 0, blob 3d952c6ca...370a6101b... — and the restore was proved by git rev-parse HEAD:PATH matching git hash-object PATH at 3d952c6ca... with git diff HEAD and git status --porcelain both empty, never by an exit code. The script carried an absolute-path trap ... EXIT INT TERM. packages/spec was rebuilt afterwards, because restoring the source made dist older than src and five gates correctly refused to read a stale build.

Two reds that are NOT this card's

check:llms-txt and check:quick-reference-counts are red on this head and were already red on the base. One cause: 3f9544447 (card 1/6) added packages/spec/src/shared/epoch.zod.ts and content/docs/references/shared/epoch.mdx without moving the two hand-kept count claims. This branch changes 0 files under packages/spec/src/shared and 0 under content/docs/references/shared, and adds no schema file at all. Filed as #15904 rather than repaired here — that is card 1/6's diff.

Scope

No api/, no system/, no data/ui/ai/integration, no gate-population change, no content/docs/releases/. No skills/** file names any of these 14 keys (every retention hit under skills/ is data.lifecycle.retention, a different key that is an object rather than a duration number), so nothing was dropped to avoid governance — there was nothing to keep.

Clause-②: yesneeds:contract-review applied to this PR and to #15678.


🤖 Generated with Claude Code

https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4


Generated by Claude Code

…15676)

Ruling B on #14478 exempts two structural classes from the duration-unit rule,
and is explicit that both are declared ON THE SCHEMA, never in a gate ledger.
This commit lands the declaration channels themselves:

- `EpochMs` (`packages/spec/src/shared/epoch.zod.ts`) — the shared
  epoch-milliseconds instant. A key whose value IS this schema is an instant,
  not a duration, and `check:duration-unit-keys` recognises that structurally.
- `.meta({ externalVocabulary: '<the standard>' })` — the marker a key carries
  when it mirrors a name fixed outside this repo. It rides `z.toJSONSchema`
  verbatim, the same channel `xRef` / `xExpression` already use.

Neither exemption is a pass on lying: a marked key still fails
`name-unit-contradicts-prose`, and an `EpochMs` key whose describe names a unit
other than milliseconds fails the new `instant-unit-contradicts-schema`. Both
classes stay visible in the census — `--list` marks them and the verdict line
counts them.

The gate also now reads `description` out of `.meta()`. Without it, moving a
describe into `.meta({ description })` would take a key out of the population
silently — an exemption by blindness. Measured: one numeric key declares its
description that way today (`data/Field.precision`), naming no time unit, so
the reading adds no offender.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4
…ternal-vocabulary keys (#15676)

The two exemption classes ruling B declares, applied to the keys the gate lists.

Instants (exemption i) — all six now typed `EpochMs`; the four whose name was
bare are renamed to the `*At` instant convention, tombstoned with `retiredKey()`
and registered in `RETIRED_KEYS_BY_MAJOR[18]` plus one D3 semantic entry:

  api/WebSocketEvent.timestamp        -> occurredAt
  api/SimplePresenceState.lastSeen    -> lastSeenAt
  kernel/KernelContext.startTime      -> startedAt  (+ TenantRuntimeContext)
  kernel/HealthStatus.timestamp       -> checkedAt
  kernel/ServiceMetadata.registeredAt    (already `*At`, schema only)
  kernel/ScopeInfo.createdAt             (already `*At`, schema only)

`*At` and not `*Ms`, measured rather than chosen: on this package's own
authorable surface all 51 distinct `*Ms` keys are durations and all 51 distinct
`*At` keys are instants, so spelling an instant `*Ms` would move it into the
family the rule exists to separate it from.

Semantic entries rather than D2 conversions because all four are
runtime-emitted — wire payloads, a host-constructed kernel context, an emitted
health report — so no conversion seam ever sees one. That is the disposition
`kernel/KernelContext:previewMode` already carries on one of these defs, and
what ruling B prescribes for a runtime-emitted key.

External-standard mirrors (exemption ii) — eleven keys marked, not thirteen.
Two of the thirteen the card attributed do not survive verification against
their own schema and are left for their directory cards; the PR body records the
evidence.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4
…page (#15676)

The published half of exemption (ii). A marked key keeps its bare name BECAUSE
an external standard fixes it, and that argument only reaches the reference-page
reader if the page names the standard — so the description cell now carries
"(unit per <the standard>)". Without it the exemption would leave exactly the
reader `check:duration-unit-keys` was filed for where the gate found them.

Also: `EpochMs` gains its type alias (the docs import-surface ratchet demands
one for every documented schema) and its ADR-0122 isomorphism pin.

Regenerated: json-schema.manifest/, authorable-surface/, api-surface/,
export-origins/, declaration-map/, content/docs/references/**.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4
…he tombstones caught (#15676)

- `src/contracts/startup-orchestrator.test.ts` built a `HealthStatus` with the
  old `timestamp` key. The `retiredKey()` tombstone refused it at compile time
  (`Type 'number' is not assignable to type 'undefined'`, 7 errors) — the
  audible-removal property the tombstone exists for, working on the first
  consumer it met.
- `type-alias-convention.pin.test.ts`: the pin count assertion follows the new
  `Iso868`.
- Regenerated: export-origins/, declaration-map/, api-surface/ and the
  `objectstack-api` skill reference index (one generated line, naming the new
  `shared/epoch.zod.ts` module).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4
…ellings (#15677)

The schema half of stack card 2/6. Gate reads 48 -> 36 with src/api/ at 0.
Readers, registry entries and regenerated artifacts follow.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4
…enames (#15677)

12 retired-key entries, one D2 conversion
(api-endpoint-cache-ttl-to-cache-ttl-seconds — apis: is a stack collection) and
five semantic entries for the eleven runtime-emitted / construction-argument
keys. Readers moved in runtime, metadata, rest-adjacent tests, dogfood fixtures
and the showcase example; liveness/api.json carries the renamed row plus the
dead tombstone row.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4
…acts (#15677)

Per-key refusal tests assert the prescription (code + rename text), not a bare
throw. Two readers the key-name grep missed and tsc/the tombstones caught: the
ApiEndpoint alias table (cacheTTL/ttl/cache retargeted onto cacheTtlSeconds —
an alias must point at a key the schema accepts) and the showcase endpoint
fixture in metadata-type-api-registration.test.ts.

Regenerated: authorable surface + defaults, reference docs, liveness
state-counts. skills/objectstack-api/SKILL.md carries the rename (governed;
net 0 lines, file and package both).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4
@objectstack/spec minor with the BREAKING banner naming every renamed key, the
six adr-0087 ids registered, the retryAfter wire note, and the disposition
split (one D2 conversion, five semantic entries).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4
…ied on incidentally (#15677)

An ADR-0087 conversion fixture copies a routable metadata kind verbatim, so an
`apis:` fixture carries `method:` beside `path:` — because that is what an
ApiEndpoint IS. Ruling A named conversions/registry.ts as the guard's target but
enforced it with requireMethodSignal, a content proxy that held only while no
conversion fixture carried a verb. This card's apis: conversion is the first that
does, and the live pin red exactly as designed.

The fixture is correct and stays. The exclusion moves to CONVERSION_REPLAY_FILE_RE,
which states the structural fact instead of testing a symptom, and three cases pin
the new guard as load-bearing rather than incidental in its turn.

NOT restricting kind (b) to packages/spec/src/api/**: that is the invariant the
live pin asserts, and enforcing it in the walk would make that pin true by
construction — a check that cannot fail.

Measured tail-neutral: the scan census is byte-identical to the base (17 route
sources, 12 call sites, 5 contract declarations, 78 tails, 61 reachable).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4
…e issue ids (#15677)

Lap 1 regenerated content/docs/references/** but left the HAND-WRITTEN pages
teaching the old spellings. Three of them carried `os:check` blocks authoring
`cacheTtl`, so check:skill-examples was RED and lap 1 never ran it — it sits in
check:generated's "not run here" list and I did not run it separately.

cacheTtl -> cacheTtlSeconds: 14 occurrences on 13 lines, all the ApiEndpoint key.
retryAfter -> retryAfterSeconds: 14 occurrences, the ADR-0112 envelope field only.

Deliberately NOT swept, each verified rather than assumed:
  - the HTTP `Retry-After` response header (6 locals over 4 sites) — RFC 9110,
    a separate surface, and the thing the tombstone prose exists to protect;
  - `retry_after` as a RetryStrategy ENUM VALUE (errors.zod.ts z.enum);
  - `details.retry_after` on the wire, and the pre-existing
    `details.retryAfterSeconds` the runtime really emits (endpoint-policy.ts).

Also strips `(#14478 ruling B)` from the twelve tombstone prescriptions THIS card
wrote: check:doc-authoring forbids an internal issue id in customer-facing spec
text (maintainer ruling 2026-08-12), and the campaign's own earlier tombstones
already comply. The version and the FROM -> TO mapping stay — those are the
durable references AGENTS.md requires. Measured: the gate read 4 findings on the
base and 16 on my head; it now reads the base's 4 again, so this PR adds none.
Those 4 are card 1/6's (PR #15814) and are not mine to touch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4
…mbstone prescriptions (#15676)

`check:doc-authoring` rule 3 forbids an internal issue id in customer-facing
spec text (maintainer ruling 2026-08-12). The four `retiredKey()` prescriptions
this stack card added each opened with a `(#14478 ruling B)` parenthetical — a
citation-shaped token that resolves to nothing for the author who meets it the
moment their key is refused, and which the generated reference pages publish
verbatim.

The parenthetical goes; the sentence keeps everything actionable it carried —
the FROM to TO rename, that the value is unchanged, and the
`SimplePresenceState.lastSeen` neighbour caveat — matching the shape the
campaign's already-compliant tombstones use (`hook.timeout`, `job.timeout`,
`DriverOptions.timeout`). The internal anchor is untouched in the adjacent `//`
and `/** */` comments, which are not customer-facing and were never findings.

`content/docs/references/**` regenerated with `pnpm --filter @objectstack/spec
gen:docs` — no generated artifact was hand-edited.

check:doc-authoring: 4 findings before, exit 0 after.
check:duration-unit-keys: unmoved — 48 offender(s) among 215 duration-shaped
numeric key(s), (6 declared `EpochMs` instant(s), 11 declared
`externalVocabulary` mirror(s)).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4
…external-vocabulary-exemptions' into claude/issue-15677-api-duration-unit-in-key-name
The merge with card 1/6's advanced tip (9605808) touched
api/websocket.zod.ts on both sides. The schema source auto-merged; the
generated content/docs/references/api/websocket.mdx is routed to merge=os-regen,
so the driver deferred it and the merge kept OUR side — silently dropping card
1/6's half. Regenerating from the merged tree is what repairs it, and it carries
both sides: their two stripped prescriptions land (issue-id occurrences 2 -> 0)
while my four renamed keys stay (6 -> 6).

Not hand-edited and not resolved by taking a side: the bytes come from
`pnpm --filter @objectstack/spec check:generated --fix` on the merged tree, and
the staged diff was read before committing (`git diff` reads clean over this
trap; only the staged diff shows it).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4
… spellings (#15678)

The schema half of stack card 3/6. Gate reads 36 -> 22 with src/kernel/ at 0.
Readers, registrations and regenerated artifacts follow.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4
…#15678)

14 retired-key entries and five semantic entries. No D2 conversion on this
card: none of the twelve defs is a stack collection member or a stored
sys_metadata row (stack.zod.ts declares no eventBus / startup / plugin-security
root), so the conversion chain has no seam that would see one. Readers moved in
core's health monitor, the kernel and contracts test suites, and the
hand-written lifecycle protocol page.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4
…15678)

Per-key refusal tests assert the prescription (issue code + rename text), not a
bare throw, plus acceptance pins at the same magnitudes and defaults. Two
deliberate NEGATIVE controls: RuntimeConfig.resourceLimits.timeout names no unit
so it stays bare, and EventSourcingConfig.snapshotRetention is a count.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4
@objectstack/spec minor + @objectstack/core patch, BREAKING banner naming every
renamed key, adr-0087: registered with the five semantic ids. Documents the two
unit collisions the rename removes (responseTime hours vs ms; uptime ms vs the
seconds-valued GET /health) and the three keys deliberately left bare.

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

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/core, @objectstack/spec, touching 27 documentable anchor(s). ⚠️ 16 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__EventPersistence__retention.ts, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/automation/flows.mdx (via durationMs (literal, a string literal on a changed line), timeoutMs (literal, a string literal on a changed line))
  • content/docs/automation/hook-bodies.mdx (via timeoutMs (literal, a string literal on a changed line))
  • content/docs/automation/jobs.mdx (via timeoutMs (literal, a string literal on a changed line))
  • content/docs/automation/webhooks.mdx (via timeoutMs (literal, a string literal on a changed line))
  • content/docs/deployment/environment-variables.mdx (via timeoutMs (literal, a string literal on a changed line))

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

  • content/docs/releases/v14.mdx (via retentionDays (literal, a string literal on a changed line))
  • content/docs/releases/v16.mdx (via timeoutMs (literal, a string literal on a changed line))
  • content/docs/releases/v17.mdx (via durationMs (literal, a string literal on a changed line), timeoutMs (literal, a string literal on a changed line))

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
  • 16 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__EventPersistence__retention.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 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 — 135 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 43cc4659ac5de8f0c4c5b8d2bb4a44354c4c0fe2packageMentionDocs.

Which tree this was computed on

This run read content/docs from 93d8885ae8115a3537a9ca7d28b1bb44a769339c — the merge of head 7abb2ff4b945e4b4685ff632956a0a75833e9393 into base 43cc4659ac5de8f0c4c5b8d2bb4a44354c4c0fe2, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older claude/issue-14478-duration-unit-in-key-name 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 93d8885ae8115a3537a9ca7d28b1bb44a769339c && git checkout 93d8885ae8115a3537a9ca7d28b1bb44a769339c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 43cc4659ac5de8f0c4c5b8d2bb4a44354c4c0fe2 7abb2ff4b945e4b4685ff632956a0a75833e9393 && git checkout -B drift-repro 43cc4659ac5de8f0c4c5b8d2bb4a44354c4c0fe2 && git merge --no-ff 7abb2ff4b945e4b4685ff632956a0a75833e9393

node scripts/docs-audit/affected-docs.mjs --json 43cc4659ac5de8f0c4c5b8d2bb4a44354c4c0fe2

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

os-sales commented Sep 5, 2026

Copy link
Copy Markdown
Collaborator

Seat verification — independently reproduced, clean on every axis

Separate pnpm install --frozen-lockfile + build + gate run in a dedicated detached worktree at 8f2b8f389, exit codes captured before any pipe. The gate's own 37-case self-test passed first, so this is a real reading rather than a prerequisite refusal.

✗ check:duration-unit-keys — 22 offender(s) among 215 duration-shaped numeric key(s) in 805 source file(s)
  (6 declared `EpochMs` instant(s), 11 declared `externalVocabulary` mirror(s)):

kernel/ = 0, tree = 22. grep 'src/kernel/' over the offender list returns no match. 36 − 14 = 22 exactly.

It moved nothing it does not own

dir base head
api/ 0 0
system/ 15 15
data/ 3 3
integration/ 2 2
ui/ 1 1
ai/ 1 1
shared/ 0 0

Every non-kernel/ bucket byte-for-byte unchanged; the entire delta is the 14 rows this card owns.

The gate family — the check that was skipped on #15814 and cost four hidden findings

gate exit
check:doc-authoring 0 0 findings — 14971 customer-facing strings across 804 spec sources clean, 46 published skill files clean
check:skill-examples 0 257 prose examples type-check across 3 surfaces
check:generated 0 all 15 artifacts current
test 0 473 files / 12786 tests passed, 0 failures
typecheck 0 clean; check:test-typecheck debt unchanged at its pinned 54/261/145

check-governed-merges --test over the 52-path list → exit 0, NOT governed (0 of 52 paths hit the register). Worth noting the contrast with cards 1/6 and 2/6, which were governed: this card edits no generator and names no skills/** file, so the #11705 generated-surface exception applies cleanly. content/docs/releases/0 files.

The two inherited reds — confirmed, with one correction to the PR's framing

check:llms-txt and check:quick-reference-counts are red on this head and byte-identically red on the basediff of the finding blocks between the two commits returns exit 0 for both gates. Same files, same line numbers, same numbers. Traceable to card 1/6's commits; this card's diff touches neither packages/spec/llms.txt nor content/docs/getting-started/quick-reference.mdx.

⚠️ Correction: it is two gates but three findings — llms.txt carries two count rows (domain shared 13→14 and tree total 207→208), plus the one quick-reference.mdx row. Filing them as "two reds" undercounts by one.

They are already fixed on card 1/6's branch (head 5b1b5308e): the counts were corrected together with the prose beside them — the shared domain row gained Epoch Instant (EpochMs) alongside 13→14, rather than a bare number bump that would have left a row claiming 14 while listing 8 names. #15904 can close once the stack integrates.

⚠️ A correction to my own dispatch briefs, not to this card

check:doc-authoring and check:quick-reference-counts are root-package scripts. The pnpm --filter @objectstack/spec check:… spelling I put in earlier briefs returns ERR_PNPM_RECURSIVE_RUN_NO_SCRIPT — a non-reading that a hurried run could easily record as a failure. The root form is correct. Also, check:skill-examples needs @objectstack/client-react built; the spec... build alone is not sufficient. Both are now fixed in card 4/6's dispatch.

⛔ Draft, not enqueued, no auto-merge, not approved. Card 4/6 (#15679) dispatched against this head at 2026-09-05T14:19Z.


Generated by Claude Code

…s left bare (#15678)

The changeset stated the key "names no unit anywhere in its prose". That is
false and it would have published into the CHANGELOG: the JSDoc directly above
the key (plugin-security-advanced.zod.ts:291-293) says "Execution timeout in
milliseconds". Its `.describe('Maximum execution time')` names none.

The accurate reason is stronger: `check:duration-unit-keys` reads `.describe()`
and `.meta({ description })` and does NOT read JSDoc, so this key's unit sits in
a channel the gate cannot see. The gate therefore lists it among the
duration-shaped keys but deliberately does not judge it — neither an offender
nor an exemption. That channel gap is tracked in #15939.

The same false clause appeared in three more places on this branch and is
corrected in all of them: the ADR-0087 semantic migration entry (its `reason`
publishes into the generated upgrade guide), its generated mirror in
registry.ts (via gen:migration-registry, never hand-edited), and the comment
above the pin test.

Prose only. No key renamed, no schema, `.describe()`, JSDoc, tombstone or pin
assertion touched; the key stays bare and stays out of the rename.
`check:duration-unit-keys` still reads 22 offender(s) among 215 duration-shaped
numeric key(s) in 805 source file(s), `kernel/` at 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4
@claude

claude Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor Author

Corrected one false claim in the changeset — prose only

Pushed 8f2b8f389..adad660a1. No key renamed; no schema, .describe(), JSDoc, tombstone or pin assertion touched. RuntimeConfig.resourceLimits.timeout stays bare and stays out of the rename — only the recorded reason changes.

The defect

.changeset/kernel-duration-keys-unit-in-key-name.md would have published this into the CHANGELOG:

RuntimeConfig.resourceLimits.timeout names no unit anywhere in its prose, so it is outside the gate's population and outside this rename; a pin test asserts that, so a later sweep cannot read the four security renames as "every timeout on that file".

Measurably false. packages/spec/src/kernel/plugin-security-advanced.zod.ts:291-295:

    /**
     * Execution timeout in milliseconds
     */
    timeout: z.number().int().min(0).optional()
      .describe('Maximum execution time'),

The JSDoc names milliseconds. The .describe() names no unit.

The corrected sentence

RuntimeConfig.resourceLimits.timeout names its unit only in the JSDoc above the key ("Execution timeout in milliseconds"), a channel check:duration-unit-keys does not read: it reads .describe() and .meta({ description }), and this key's describe ("Maximum execution time") names none. The gate therefore lists it among the duration-shaped keys but deliberately does not judge it — neither an offender nor an exemption — so it is outside this rename; that JSDoc-channel gap is filed as #15939. A pin test asserts the key still parses bare, so a later sweep cannot read the four security renames as "every timeout on that file".

Two things in the old sentence were wrong, not one. The second is the gate's own vocabulary: the key is in the population — --list prints it as one of the 215 census rows (src/kernel/plugin-security-advanced.zod.ts:294 timeout [name: -] [prose: -] "Maximum execution time"), and the gate's self-test pins that reading: "listed, not judged: duration-shaped name with no unit anywhere (the #14519 shape) is a census row". It is outside the verdict, not outside the population. The new wording says only what is measured.

The same clause was in three more places on this branch

file what it is why in scope
.changeset/kernel-duration-keys-unit-in-key-name.md the changeset publishes into the CHANGELOG
packages/spec/src/migrations/entries/semantic/18.kernel-plugin-security-durations-unit-in-key.ts ADR-0087 semantic entry reason publishes — build-upgrade-guide.ts renders reason into docs/protocol-upgrade-guide.md as "Why not automatic"
packages/spec/src/migrations/registry.ts generated mirror of that entry regenerated with pnpm --filter @objectstack/spec gen:migration-registry, never hand-edited between the markers
packages/spec/src/kernel/plugin-security-advanced.test.ts:396 comment above the pin test the same false claim where the next auditor reads it; the it(...) title, body and assertions are byte-identical

Not touched, and reported rather than edited: packages/spec/src/migrations/entries/retired-keys/18.kernel__SandboxConfig__process.timeout.ts:7-9 says the neighbouring key's "describe names no unit at all" — that clause is true, so it does not carry this defect. It does repeat "outside the gate's population", the second imprecision above; correcting that is a separate call.

Readings — all at adad660a1, exit codes captured before any pipe

  • pnpm --filter @objectstack/spec check:duration-unit-keys — unmoved, byte-identical verdict line and offender list vs 8f2b8f389:
    ✗ check:duration-unit-keys — 22 offender(s) among 215 duration-shaped numeric key(s) in 805 source file(s) (6 declared EpochMs instant(s), 11 declared externalVocabulary mirror(s)):
    kernel/ offenders: 0. Exit 1 is this stack's expected state, not a regression.
  • Pin test — src/kernel/plugin-security-advanced.test.ts > Plugin security durations carry their unit (#15678) > leaves \RuntimeConfig.resourceLimits.timeout` bare — its describe names no unit` — passed (file: 21 passed).
  • pnpm --filter @objectstack/spec test472 files passed, 12784 tests passed, 1 skipped.
  • pnpm --filter @objectstack/spec typecheck — exit 0 (tsc, check:scripts-typecheck, check:test-typecheck; ledger held at 54 files / 261 errors / 145 pinned signatures).
  • check:empty-changeset · check:changeset-no-major · check:adr-0087-registration · check:doc-authoring · check:nul-bytes · check:migration-registry · check:generated — all exit 0. check:doc-authoring output is byte-identical to its pre-edit run, so the added #15939 reference entered no customer-facing sink.

check:generated first read api-surface/ as stale — that was a non-reading: the generator refuses on an unbuilt packages/spec/dist. After pnpm --filter @objectstack/spec build it reads ✓ All 15 generated artifacts are up to date.

Unrelated red already on this branch — reported, not touched

pnpm check:quick-reference-counts fails at branch head, and it failed identically at 8f2b8f389 before this lap (verified in a throwaway worktree at that commit):

content/docs/getting-started/quick-reference.mdx:209  [total] section "Shared Protocol" declares "of 8 schemas" but content/docs/references/shared/ publishes 9 page(s)

Cause: this stack adds content/docs/references/shared/epoch.mdx (card 1/6), taking the published shared pages from 8 to 9, while line 209 still reads ## Shared Protocol (5 of 8 schemas). On origin/main (a2051fa4e) the same page declares 8 against 8 and is green, so this is a branch regression that will red Lint & Repo Gates on this PR. The gate's own prescription for a [total] finding is to update M. One token, but it belongs to card 1/6's file and outside this lap's dispatched scope — flagging for the PM rather than editing it.


Generated by Claude Code

os-sales commented Sep 5, 2026

Copy link
Copy Markdown
Collaborator

Seat ruling on the two flagged deviations, and the quick-reference-counts question closed

Verified independently on adad660a1: 4 files, all prose; the stale clause reads 0 in each of the four and the JSDoc explanation is present in each; zero changed lines touch a key, .describe(), .meta(), schema, tombstone or assertion. 8/8 checks green.

⚠️ One correction to my own verification method, stated because I nearly reported it as a finding: my exclusion grep flagged 6 lines as touching .describe( / .meta(. All six are prose that quotes those method names as text. A pattern that matches code and the discussion of code is not an exclusion check — the same class of false positive I hit earlier on this stack with a non-comment-line grep.

Deviation 1 — refusing my prescribed wording: correct, and I was wrong twice

My dispatch prescribed saying the key "never enters the population". The dev refused it and measured why: --list prints src/kernel/plugin-security-advanced.zod.ts:294 timeout [name: -] [prose: -] "Maximum execution time" as one of the 215 census rows, and the gate's own self-test pins "listed, not judged: duration-shaped name with no unit anywhere (the #14519 shape) is a census row".

So the key is outside the verdict, not outside the population. Writing my phrasing would have replaced one false claim with another — which is precisely the failure this lap existed to fix. ⇒ The dev's wording stands; my prescription was wrong.

That is the second imprecision of mine this card has corrected, both in the same direction. Recorded rather than smoothed over.

Deviation 2 — editing the comment above the pin test: correct reading, and my dispatch was ambiguous

My dispatch said both "⛔ do not touch … its pin" and "⚠️ a source comment would be in scope". Those pull against each other and the dev flagged it instead of choosing silently.

Ruling: the dev read it right. "The pin stays as it is" meant the assertion — the it(...) title, body and assertions are byte-identical, which is verified. The comment above a pin test is exactly where the next auditor reads the rationale, so a false claim there is worth more than one in the changeset, not less. ⛔ Do not revert that hunk.

The ambiguity is mine: a ⛔ constraint and a ⚠️ scope-widening in the same dispatch can contradict, and the dispatch should have said which wins.

The quick-reference-counts red — no card wanted, it is already fixed upstream

The dev asked the seat to overrule if a card was wanted. No card. This is the known card-1/6 finding (#15904) and its fix is already in the merged stack base, measured:

quick-reference.mdx:209 llms.txt
stack base c09fd1023 ## Shared Protocol (5 of **9** schemas) 208 schemas; shared | 14 | … Epoch Instant (EpochMs)
this branch adad660a1 ## Shared Protocol (5 of **8** schemas)

This branch was cut from card 1/6's head before that count lap, so its ancestry does not carry the fix. It flows forward as each card merges the advanced base — card 2/6 is doing exactly that right now. ⛔ Nothing to repair here, and ⛔ nothing to file: a defect already fixed on a branch that is now merged does not need a second record.

⚠️ One nuance on the dev's phrasing: it will not actually red Lint & Repo Gates on this PR, because heavy CI never runs on a stacked head (lint.yml and ci.yml both declare pull_request: branches: [main]). It would red at integration — which is the same reason it must be, and is, fixed before then.

The residual imprecision left in place — agreed

Two sites still say "outside the gate's population" and were deliberately not touched because they carry the imprecise phrase without the false claim (18.kernel__SandboxConfig__process.timeout.ts, whose neighbouring clause "describe names no unit at all" is true, and #15939's own body). ⛔ Agreed — do not widen. Recording the distinction on #15939 is the right home.


Generated by Claude Code

Copy link
Copy Markdown
Contributor

Contract review (clause ②) PASS at head 8f2b8f38 — director seat, session_01TezFG8ZMrNH6n5VTNpPpdH, 2026-09-05T16:1xZ. Verdict with the derived judgments, the semver reading (spec minor + BREAKING; core patch accepted because both are in the same fixed changeset group) and the landing note: objectstack#15678 comment 5553081426. needs:contract-review removed from this PR and from the card in the same stroke (legitimate clear: two removals seconds apart, both citing that comment).

Landing stays with the domain:spec seat's stack sequencing: this PR is based on card 2/6's branch, and PR #15837 is dirty since PR #15814 merged card 1/6 into the stack base at 15:51Z — conflict lap on 2/6 first, then the stack lands as a unit. This PASS covers 8f2b8f38; merge-only head movement can be re-extended on request, a hand edit re-hangs the gate.

Implemented-by: session_01G4138K1EG7kQ81FNba5Kp4
Reviewed-by: session_01TezFG8ZMrNH6n5VTNpPpdH


Generated by Claude Code

The trunk landed cards 1/6 and 2/6 as a SQUASH commit, so their content has
no shared ancestry with this branch and the merge replays it. One real text
conflict, packages/spec/src/migrations/registry.ts, resolved as the UNION of
both sides' RETIRED_KEYS_BY_MAJOR[18] rows: 76 base + 17 from the trunk + 14
from this card = 107, nothing dropped.

The two os-regen driver-managed artifacts both sides moved
(packages/spec/authorable-surface/kernel.json,
content/docs/references/kernel/startup-orchestrator.mdx) take the trunk's
side here; the regeneration follows as its own commit on the merged tree.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4
Discharges the deferral the merge commit recorded. `check:generated` proved
exactly one artifact set stale (content/docs/references/**) after the spec
build re-derived authorable-surface/kernel.json, so only that one was
regenerated. Both files re-derive this card's [RETIRED] rows on top of the
trunk's side; the trunk's own renames (checkedAt, startedAt) survive in both.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4
@os-sales
os-sales merged commit 74ddc0d into claude/issue-14478-duration-unit-in-key-name Sep 5, 2026
9 checks passed
@os-sales
os-sales deleted the claude/issue-15678-kernel-duration-unit-in-key-name branch September 5, 2026 23:15
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/xl tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants