Skip to content

feat(spec)!: the four system/logging.zod.ts duration keys carry their unit in the key name (#17782, ruling A on #15939) - #17999

Merged
zhuangjianguo merged 4 commits into
mainfrom
claude/issue-17782-logging-duration-units
Sep 13, 2026
Merged

zhuangjianguo merged 4 commits into
mainfrom
claude/issue-17782-logging-duration-units

Conversation

@claude

@claude claude Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

Fixes #17782
Clause-②: yes

Ruling A on #15939 (director seat 2026-09-11, maintainer 「同意」, decision batch #115), per-file
remediation of the #14478 rule. This is the system/logging.zod.ts card — 4 rows of the 21-row
delta enumerated in PR #17635, which lands LAST.

What changed

def before after nesting
HttpDestinationConfig batch.flushInterval: 5000 batch.flushIntervalMs nested under batch
HttpDestinationConfig retry.initialDelay: 1000 retry.initialDelayMs nested under retry
HttpDestinationConfig timeout: 30000 timeoutMs top-level
LoggingConfig buffer.flushInterval: 1000 buffer.flushIntervalMs nested under buffer

Every value, default and bound is unchanged. Each old spelling stays as a retiredKey() tombstone;
none of the four enclosing objects is .strict(), so a bare deletion would have stripped the value
in silence.

The JSDoc I read on each key, and what the published channel said

All four named milliseconds in a source JSDoc — "Flush interval in milliseconds", "Initial retry
delay in milliseconds", "Timeout in milliseconds" — and none carried a .describe() at all.
The check:duration-unit-keys census on this tree read all four as [name: -] [prose: -] before
the change, so content/docs/references/system/logging.mdx published a bare 5000 / 1000 / 30000 /
1000 with nothing on the page to decide milliseconds from seconds. Each key is renamed and given
the describe it never had: under the #14478 rule, fixing only the describe would itself be a
violation.

The two traps this card carried

flushInterval is declared twice, in two different defs, with two different defaults — 5000 on
the HTTP destination's batch, 1000 on the logging buffer. They are two keys. Each gets its own
tombstone, its own RETIRED_KEYS_BY_MAJOR[18] row, and a prescription that names its def so an
author who lands on one is not sent to the other. Located by def, not by key name.

Mixed nesting, so the ratchet moves for exactly one key. authorable-surface records top-level
keys per def (build-schemas.ts reads schema.properties one level deep). HttpDestinationConfig.timeout
is the only top-level key of the four, so it is the only one whose rows move:
system/HttpDestinationConfig:timeout becomes [RETIRED] beside a new
system/HttpDestinationConfig:timeoutMs, and the authorable-defaults/ row is renamed with it. The
three nested keys move neither file — correct, not an omission. authorable-surface.base.json is
untouched (it is written only by gen:authorable-surface-base).

Naming

Ms, derived from the family on this tree rather than from the card: 272 key-position *Ms:
declarations in packages/spec/src against 75 *Seconds:. The only competing unit spellings are
3 *MS: and 9 *Millis:, and every one mirrors a name fixed outside this repo — MongoDB's
maxCommitTimeMS / connectTimeoutMS, node-postgres's idleTimeoutMillis /
connectionTimeoutMillis on PoolConfigSchema — so unlike the Ttl-versus-TTL question a
sibling round settled, there is no in-repo alternative to choose between. All three target spellings
were already attested as key-position *.zod.ts declarations: flushIntervalMs 1 (on
kernel/events/integrations.zod.ts, at the same 1000 default), initialDelayMs 5, timeoutMs 30.

Changeset level

.changeset/17782-logging-duration-units.md declares "@objectstack/spec": minor, a feat(spec)!
summary, a **BREAKING** banner and the adr-0087: registered logging-durations-unit-in-key
disposition. Ruling A wrote patch; that half of the clause is falsified by the precedent the same
sentence names — every #15678 / #15679 entry in packages/spec/CHANGELOG.md under ## 17.4.0
is feat(spec)! at minor — and the correction is recorded publicly on #15939 (5652120294). The
declaration is Clause-②: yes for the same mechanical reason: a rename puts a new authorable
spelling on a published payload, and the floor in references/contract-review.md makes that a
mandatory affirmative whatever the round's size.

Registration

One ADR-0087 D3 semantic entry logging-durations-unit-in-key plus four RETIRED_KEYS_BY_MAJOR[18]
rows, one per key — the shape settled for the four remaining rename cards on #15939. No D2
conversion: stack.zod.ts declares no logging collection and neither LoggingConfigSchema nor
HttpDestinationConfigSchema is referenced anywhere in packages/spec/src outside
system/logging.zod.ts, so the chain has no rehydration seam that runs on an authored logging
document.

Registry regenerated with gen:migration-registry, never hand-merged. Set-difference over every
registry row (every id: line plus every lone quoted array element), measured three ways:

comparison rows lost gained
my regeneration, before → after 1624 → 1629 0 my 1 semantic id + 4 retired-key rows
the two os-regen-merge.sh syncs of main, pre → post regeneration 1631 → 1631 and 1631 → 1633 0 only what main brought (#17784, #17781)
merge base ae1b258889 → head 1628 → 1633 0 exactly my 5 rows

Exact-name occurrence counts on the regenerated registry after the second sync, guarding the silent
drop the os-regen driver can produce: sibling #17781's kernel-runtime-config-timeout-unit-in-key
1 and its row kernel/RuntimeConfig:resourceLimits.timeout 1; sibling #17784's
tenant-schema-cache-ttl-unit-in-key 1 and system/SchemaLevelIsolationStrategy:performance.schemaCacheTTL
1; my own id and four rows 1 each; dark control 0. Both siblings' implementation bodies survive too
(timeoutMs 7 in kernel/plugin-security-advanced.zod.ts, schemaCacheTtlSeconds 3 in
system/tenant.zod.ts).

Cross-repo

The pinned objectui checkout at .objectui-sha pin 53ded82bf7a494f54e344e19099dbf00854b8694
spells flushInterval 0 times, initialDelay 0, HttpDestinationConfig 0 and LoggingConfig 0
across its 6409 tracked files, against lit controls useState 2304 and timeout 702 on the same
corpus. Nothing in the sibling reads these keys.

Tests

All readings below are from head a5976809c2, the head this PR now carries, re-run after both
os-regen-merge.sh syncs of main.

  • pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 src/system/logging.test.ts
    46 passed. Three existing pins asserted the retired spellings (config.timeout 30000,
    batch.flushInterval 10000, buffer.flushInterval 2000); each was moved to the new key, its
    comment rewritten to say why, and none was deleted or weakened.
  • pnpm --filter @objectstack/spec typecheck && pnpm --filter @objectstack/spec test — VERDICT
    command-exit 0, 476 files / 13533 tests passed.
  • pnpm --filter @objectstack/spec check:generated15 of 15 artifacts up to date on this
    head, with the working tree clean, after gen:migration-registry, the package build and
    gen:docs.
  • node scripts/pm/dispatch-gates.mjs --commands derives 109 families for these 12 paths;
    --ran with a recorded exit code per command reconciles 109 accounted, 107 run green, 2
    NOT MEASURED, 0 unrun
    . The two are check:dual-build-cjs-loads and check:type-check-debt,
    both exit 3 PREREQUISITE NOT MET — they need every workspace package built, which CI's
    Build Core supplies and a spec-only worktree does not. ⛔ Neither is a pass.
  • pnpm --filter @objectstack/spec check:duration-unit-keys — 0 offenders; the four rows leave the
    census's unit-nowhere population.
  • pnpm --filter @objectstack/spec check:objectui-pin-citationsexit 0: 16 asserting
    citations match .objectui-sha (53ded82bf), 26 historical recorded and not checked. The asserting citation sits on ONE physical source line in both its sites (the
    semantic entry and its generated registry mirror), which is the failure three sibling PRs hit.
  • node scripts/pm/check-widening-tells.mjs --declaration yesexit 0. Under a hypothetical
    --declaration no it exits 4 with 4 T1 tells, one per retiredKey() line (:326 :351 :371
    :759) — the inversion already filed as [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, and the four newly writable *Ms keys are not what
    it flags. ⛔ The diff is not reshaped and the checker is not weakened.
  • Ablation, from the committed state, with an EXIT INT TERM restore trap. All four
    retiredKey(...) calls replaced by a marker comment: occurrence counts 1/1/1/1 → 0/0/0/0 and the
    injected marker 0 → 4, git hash-object 9768a4bb…40095a54… (the mutation proven on disk,
    not inferred from an editor exit code). The suite then read 4 failed | 42 passed — exactly the
    four REFUSES the retired spelling pins, no others. Restored with git checkout HEAD -- on an
    absolute path: git hash-object back to 9768a4bb… and git diff HEAD empty. That blob is still
    the one at this head, so the reading stands for the PR as it is.

Landing order, for whoever lands after me

This diff edits packages/spec/authorable-surface/system.json (+2/−1) and
packages/spec/authorable-defaults/system.json (±1), and card #17783 (system/metrics.zod.ts,
two top-level rows) is in flight against the same two shards. Both paths are routed
merge=os-regen in .gitattributes, so the local driver defers them and pre-commit collects the
debt — the merge queue rebuilds server-side where no driver runs, so the shard is textually shared
and a conflict there is expected, not a defect. The only correct resolution is
bash scripts/pm/os-regen-merge.sh (commit the merge FIRST, then regenerate on the merged base),
⛔ never a hand-merge of the JSON: these rows are a sorted set, a dropped line is silent, and
gen:schema recomputes the whole shard from the merged Zod graph anyway. One thing measured here
that the later lander can rely on: the ratchet moved for exactly one of my four keys, so the
regenerated shard differs from main by three lines and nothing else on this file is mine.

Acceptance notes

  • packages/spec/src/system/logging.zod.ts:298 backoffMultiplier also reads [name: -] [prose: -] in the census, but it is a ratio, not a duration, and is not one of this card's four
    rows. Noted, not filed.
  • Six gate families in this diff's derived set refuse with PREREQUISITE NOT MET on a
    worktree that has not built the whole repo; they are named in the report comment as NOT MEASURED
    rather than passed. CI's Build Core supplies what they need.

⛔ Not ready for review by this seat: the in-seat clause-② review at tier is the PM's step. Draft,
not enqueued, no auto-merge.

🤖 Generated with Claude Code

https://claude.ai/code/session_015c5G6TmpMKgnusmTpD7Ntt


Generated by Claude Code

… unit in the key name

Renames HttpDestinationConfig batch.flushInterval / retry.initialDelay /
timeout and LoggingConfig buffer.flushInterval to their *Ms spellings, each
with a retiredKey() tombstone, one ADR-0087 semantic entry and four
RETIRED_KEYS_BY_MAJOR rows. Values and defaults are unchanged.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015c5G6TmpMKgnusmTpD7Ntt
@github-actions

github-actions Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

8 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. ⚠️ 6 changed file(s) yielded no anchor (packages/spec/authorable-defaults/system.json, packages/spec/authorable-surface/system.json, packages/spec/src/migrations/entries/retired-keys/18.system__HttpDestinationConfig__batch.flushInterval.ts, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

What this run could not see
  • 6 changed file(s) yielded no anchor (packages/spec/authorable-defaults/system.json, packages/spec/authorable-surface/system.json, packages/spec/src/migrations/entries/retired-keys/18.system__HttpDestinationConfig__batch.flushInterval.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 — 136 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 c45ea8c3964f2d9b6d414d9ee7b7fd6c5ba4cf78packageMentionDocs.

Which tree this was computed on

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

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

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

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

Copy link
Copy Markdown
Collaborator

Docs Drift Check answered by hand — the gap it declares is real, and it is empty here

Epic PM for #15939 · readings on origin/main @ 611795e40 (this PR's base), 2026-09-13T10:51Z.

Docs Drift reads 8 anchors, no hand-written page naming any of them. Left there, that would be over-read: the run's own caveats say so in two places, and both apply to this diff.

  1. 6 of the 12 changed files yielded no anchor at all — including both ratchet JSONs and the retired-key entries — so "pages documenting them are NOT COVERED by this run".
  2. The structural one: "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." That is the same class this epic tracks as its residual landing risk 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 (5652790672): a green gate cannot see a stale sentence.

So the check was done by hand rather than inferred. Hand-written docs only — content/docs with the generated references/ and the release-owned releases/ excluded:

subject occurrences
flushInterval 0
initialDelay 0
files naming HttpDestinationConfig, LoggingConfig or "log destination" 1

Lit controls on that same corpus: logging 21, timeout 51, destinations 2. Dark control zzqqxx 0 — so the two zeros are readings, not an empty grep.

The single file hit is content/docs/getting-started/quick-reference.mdx:100:

| **[Logging](/docs/references/system/logging)** | logging.zod.ts | LoggingConfig | Structured logging |

An index row naming the def and the source file, and no key name on it (flushInterval / initialDelay / timeout all read 0 in that file). It stays true after the rename.

no hand-written page goes stale on this diff. The one page that renders these keys is content/docs/references/system/logging.mdx, which is generated and is regenerated in this PR (+36/−7).

Recorded rather than left implicit because this PR is the first in the #15939 epic where Docs Drift named nothing — sibling PR #17986 named six pages, five of them false positives off a generic timeoutMs string anchor and one a genuine hand-written protocol page. A quiet run and a clean surface are different facts, and only the second one is established above.

⛔ No change requested. Nothing here is a defect and nothing in this PR moves because of it.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator

The red TypeScript Type Check is on a superseded head — no fix owed, and nothing to re-run

Epic PM for #15939 · 2026-09-13T11:00Z. Recorded because the PR's check history now shows a red, and the next reader (the contract review included) should not spend a cycle on it.

What failed: the aggregator job TypeScript Type Check, run 34752812282, on head 360d095dbd. Its log is unambiguous and it is not a type error — three of its four lanes passed and the fourth was cancelled:

ok       typecheck-consumers
ok       typecheck-debt
ok       typecheck-source-gates
##[error]type-check lane `typecheck-workspace` concluded `cancelled` -- expected `success`.
check-typecheck-lanes: FAILED (1 problem(s) above).

Why the lane was cancelled: the branch head moved from 360d095dbd to 359cad806b — the dev pushed Merge remote-tracking branch 'origin/main', and that push cancelled the run in flight. The signature is visible across the whole dead head, not just this one job: of its 46 check runs, 8 are cancelled — all six Test Core shards, Lint & Repo Gates, and Type Check · workspace — with exactly one failure, the aggregator that refuses to report green when a lane it depends on has no verdict.

⇒ the aggregator behaved correctly. A cancelled lane's result is unknown, and riding green on unknown is precisely what it exists to prevent. ⛔ This is not a flake and it is not being called one.

The live head is healthy. On 359cad806b at 11:00Z: 32 check runs — 12 success, 2 skipped, 18 still in progress, 0 failures.

⇒ No fix is owed, nothing is ported, and ⛔ no job is re-run — the correct head is already under test and a re-run on a superseded commit would measure nothing. This PR stays watched until its live head is green.

⚠️ One thing the base merge does not yet include: it merged main at 611795e405, which lands just before cbcae14bca — sibling PR #17983 (#17781), merged 10:59:33Z. Not a problem to solve now (the merge queue merges against the latest main), but worth knowing when reading check:generated and the migration registry on this branch.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator

Contract review

Head-sha: a5976809c2b11a90c68c5ad9f66436cba7366590

Isolated at-tier review from a fresh worktree at the head sha (git worktree add /tmp/review-17999 a5976809c2…), origin/main at b2c51cb881, merge-base ae1b258889. Every number below was re-measured unless listed under "taken on the body's word". No dispatch order or PM verdict was read.

① Derived judgments

  • HttpDestinationConfig.batch.flushInterval → flushIntervalMs (default 5000) — RIGHT. On origin/main logging.zod.ts:276-278 the JSDoc reads "Flush interval in milliseconds" and the key carries no .describe(); the chain int().positive().optional().default(5000) is byte-identical after the rename; the tombstone refuses (pin passes; ablation A below).
  • HttpDestinationConfig.retry.initialDelay → initialDelayMs (default 1000) — RIGHT. origin/main :291-293, same JSDoc-only unit, same unchanged chain, tombstone refuses.
  • HttpDestinationConfig.timeout → timeoutMs (default 30000) — RIGHT. origin/main :302-304, JSDoc-only unit, unchanged chain, tombstone refuses. It is the one top-level key of the four (declared at the def's own indent; system/HttpDestinationConfig:batch / :retry are the surface rows, their children are not).
  • LoggingConfig.buffer.flushInterval → flushIntervalMs (default 1000) — RIGHT. origin/main :670-672 on LoggingConfigSchema (declared :575 on main), JSDoc-only unit, unchanged chain, tombstone refuses.
  • Ms is the family spelling — RIGHT. Key-position counts on origin/main packages/spec/src/**/*.zod.ts: *Ms: 104, *Seconds: 40, *MS: 1, *Millis: 6 (all .ts: 293 / 77 / 3 / 9; dark control *Qqzv: 0). Every MS/Millis site listed is an external-API mirror (maxCommitTimeMS on driver-nosql, idleTimeoutMillis / connectionTimeoutMillis on datasource / driver / postgres). The body's 272 / 75 is not reproducible under any corpus definition I tried (281 / 293 / 110 depending on tests and ?), but the ordering is the same under all of them, so the conclusion stands; the number in the registered prose is a claim, not a reading.
  • Tombstones on non-strict objects (ADR-0049) — RIGHT and it matters. grep -E 'strictObject|\.strict\(' on the file's code returns nothing (the four .strict() hits grep -c finds at :320/:345/:365/:753 are JSDoc prose); on a non-strict shape a bare deletion is a silent strip, so retiredKey() is exactly the route AGENTS.md prescribes.
  • Two-flushInterval handling — RIGHT, no cross-wiring. The batch prescription names HttpDestinationConfig.batch.flushInterval, targets flushIntervalMs, and says LoggingConfig.buffer.flushInterval "is a different key with its own rename"; the buffer one is the mirror image. The two RETIRED_KEYS_BY_MAJOR[18] rows are system/HttpDestinationConfig:batch.flushInterval and system/LoggingConfig:buffer.flushInterval, distinct and correctly pathed. Proof beyond reading: ablation B swapped the two prescription constants between the two retiredKey() calls (:326 ↔ :759) — exactly the two flushInterval REFUSES pins failed (2 failed / 44 passed), so the pins discriminate by def, not by leaf; and the defaults are pinned separately (5000 on batch, 1000 on buffer) in the "accepts" pins. Restored byte-identical (9768a4bb…).
  • Describe channel — RIGHT, inside ruling A, not an unflagged widening. Verified on origin/main: none of the four lines carries .describe( (lit control: 60 .describe( occurrences elsewhere in the same file; dark control wqxzplomb 0). Ruling A's own predicate (batch Add comprehensive test coverage for ObjectStack spec protocols - 100% coverage achieved #65, restated in the ruling) is "the gate refuses a duration key whose JSDoc names a unit its describe does not", and feat(spec): refuse a duration key whose JSDoc names a unit its describe does not #17635 must land last "into a tree it already reads as clean" — an absent describe names no unit, so adding one is required for that tree to read clean. The landed siblings fixed an existing describe (spec: 1 duration key(s) in system/tenant.zod.ts name their unit only in JSDoc — #15939 Ruling A remediation (1 of the 21-row delta) #17784 'Schema cache TTL''… in seconds', spec: 1 duration key(s) in kernel/plugin-security-advanced.zod.ts name their unit only in JSDoc — #15939 Ruling A remediation (1 of the 21-row delta) #17781 'Maximum execution time''… in milliseconds'); this card adds where none existed — same predicate, one more step. The resulting mdx rows are flagged: the changeset "Why" says "given the describe it never had", and the declaration is Clause-②: yes.
  • ADR-0087 kit — RIGHT. One semantic entry logging-durations-unit-in-key (id equals the changeset marker; check-adr-0087-registration.mjs --base ae1b258889 exit 0) plus four retired-key rows. registry.ts is generated: pnpm --filter @objectstack/spec gen:migration-registry on the head tree rewrote it to the identical blob 4d7e7d2e99…. D3-not-D2, both halves verified: stack.zod.ts contains logging 0 times (lit control objects 107 lines); LoggingConfigSchema|HttpDestinationConfigSchema appear in packages/spec/src only in logging.zod.ts, logging.test.ts, registry.ts and the semantic entry — no rehydration seam.
  • Ratchet — RIGHT. build-schemas.ts:850-855 reads schema.properties one level per def, so the surface is top-level only; measured: 0 dotted rows in authorable-surface/*.json (lit 7384 rows) and 0 in authorable-defaults/*.json (lit 1170). Hence exactly one key moves: system/HttpDestinationConfig:timeouttimeout [RETIRED] + timeoutMs (surface +2/−1), :timeout = 30000:timeoutMs = 30000 (defaults +1/−1). authorable-surface.base.json is not in the diff and still carries system/HttpDestinationConfig:timeout at :5910; only --update-base writes it (build-schemas.ts:136-140). Check (b) registration is exact-row membership across all majors (registeredRetiredKeys()), satisfied.
  • Pin tests — RIGHT. it( count 34 on main → 46 at head; the diff deletes exactly four lines, all replaced in place (config.timeouttimeoutMs, batch.flushInterval input + assertion → flushIntervalMs, buffer.flushIntervalflushIntervalMs); .skip|.only|.todo 0. New pins sit under their own top-level describe('logging duration keys → *Ms …') at :514; every pre-existing describe header in the file is a bare schema name, none makes a counted claim, so none was falsified.
  • Generated docs — RIGHT. content/docs/references/system/logging.mdx is the generator's output: CI job Type Check · source gates (check-run 103714730003, completed: success, 11:12:22–11:15:13Z) runs check:authorable-surface then check:docs; check:spec-changes and check:upgrade-guide exit 0 locally on the head tree.

② Semver level

minor for @objectstack/spec is RIGHT, and the changeset text matches the diff. The launch-window guard lives at scripts/check-changeset-no-major.mjs (not scripts/pm/): its header states that breaking changes ship as minor until GA and that the carriers are the **BREAKING** banner plus the ADR-0087 disposition — the changeset carries both (feat(spec)! title, **BREAKING** line, adr-0087: registered logging-durations-unit-in-key); node scripts/check-changeset-no-major.mjs --base ae1b258889 exit 0 (level axis N/A offline; CI Check Changeset success). Precedent measured in packages/spec/CHANGELOG.md under ## 17.4.0: :1804 #15678 and :2764 #15679 are both feat(spec)! at a minor, and the landed siblings' changesets (17781-…, 17784-…) are "@objectstack/spec": minor. The corrections on #15939 (5652120294) are therefore right on both halves: patch was falsified by the shape ruling A itself names, and Clause-②: yes is the mechanical floor — the rename puts new authorable spellings on a published payload and check-widening-tells --declaration no refuses the diff (exit 4). The body's table, defaults, kit and migration diff match the source. No @objectstack/core (or any consumer) changeset is owed: outside packages/spec and the generated docs, flushInterval / initialDelay / HttpDestinationConfig / LoggingConfig occur 0 times in .ts/.tsx/.mjs/.md/.mdx/.json/.yml (node_modules/dist/.turbo excluded).

③ Boundary flags

Cross-file staleness sweep (whole tree at head, including packages/spec, skills/, .claude/, examples/, docs/, content/): no prose, tombstone, migration entry, test comment or doc sentence is made false by this diff. The only hit on either old key outside the diff's own files is packages/spec/scripts/check-duration-unit-keys.ts:56, which uses flushInterval as a generic example of a duration-shaped name — still true. The def names outside the diff appear only in index listings that name no key (content/docs/references/index.mdx:339, content/docs/getting-started/quick-reference.mdx:100, packages/spec/PROTOCOL_MAP.md:127). packages/spec/liveness/ has 0 rows for either def (lit: 70 system/ rows).

Clause ②: node scripts/pm/check-clause2-carriers.mjs --pair 17999exit 0 (2026-09-13T11:52:55Z, "the clause-② declaration is readable in the fixed spelling and both carriers agree"). Independently, yes is correct on the mechanical floor: new authorable spellings on a published payload plus the exit-4 tells above.

Ablation: reproduced and it proves what it claims. From the committed state, under an EXIT INT TERM restore trap, all four retiredKey( calls replaced by a marker (occurrences 4 → 0, marker 0 → 4, blob 9768a4bb…4f10556b…): 4 failed / 42 passed, exactly the four "REFUSES the retired spelling" pins; restored with git checkout HEAD -- to blob 9768a4bb…, git status --porcelain empty. Four is the right number: each tombstone has exactly one refusal pin, the "accepts" pins feed only the new key and so cannot see a missing tombstone, and the describe pins read .description — so four is the whole population that ablation can reach. Ablation B (above) adds the def-discrimination proof the removal ablation alone does not give.

Readings I re-measured

All in /tmp/review-17999 at a5976809c2 unless stated; origin/main = b2c51cb881 after git fetch.

  • 11:49:03Z worktree created; merge-base git merge-base origin/main HEAD = ae1b258889.
  • 11:50:03Z git show origin/main:packages/spec/src/system/logging.zod.ts | grep -n -B6 -A2 -E '^\s+(flushInterval|initialDelay|timeout|backoffMultiplier):' — the four keys at :278 :293 :304 :672, JSDoc "in milliseconds", no .describe(; .describe( count in file 60 (main) → 72 (head); dark control 0.
  • 11:50:03Z .strict() / strictObject in the file's code: none (4 comment hits only).
  • 11:50:41Z / 11:52Z *Ms: / *Seconds: / *MS: / *Millis: key-position counts (grep -rhoE '^\s*[a-zA-Z_]+SUFFIX\??:') on head and git grep … origin/main on *.zod.ts and all *.ts; the 7 MS/Millis sites listed; flushIntervalMs / initialDelayMs / timeoutMs attested 1 / 5 / 31 on main; integrations.zod.ts:149 flushIntervalMs … default(1000).
  • 11:52:02Z staleness sweep, grep -rn -w over .ts .tsx .mjs .js .md .mdx .json .yml .yaml excluding node_modules/dist/.turbo/.git, both old keys and both def names; 11:56Z packages/spec with no extension filter, liveness/, skills/, .claude/, examples/.
  • 11:52:55Z node scripts/pm/check-clause2-carriers.mjs --pair 17999 → exit 0.
  • 11:52:59Z git diff ae1b258889 HEAD | node scripts/pm/check-widening-tells.mjs --declaration no --diff - → exit 4, tells at :326 :351 :371 :759; --declaration yes → exit 0.
  • 11:53:00Z pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 src/system/logging.test.ts → 46 passed.
  • 11:53:57–59Z gen:migration-registryregistry.ts blob unchanged 4d7e7d2e99….
  • 11:54:00Z check:duration-unit-keys → exit 0, 211 keys, zero offenders.
  • 11:54:10Z node scripts/check-changeset-no-major.mjs --base ae1b258889 → exit 0.
  • 11:55:08–12Z ablation A (4 failed / 42 passed, restore to 9768a4bb…); 11:55:14–17Z ablation B (2 failed / 44 passed, restore to 9768a4bb…). Both under a trap, both in my own worktree, working tree clean afterwards; nothing in the shared checkout touched.
  • 11:55:20Z node scripts/check-adr-0087-registration.mjs --base ae1b258889 → exit 0.
  • 11:56:07Z objectui pin grep in /home/user/objectstack-ai/objectui at HEAD 53ded82bf7a4… = .objectui-sha, 6409 tracked files: flushInterval 0, initialDelay 0, HttpDestinationConfig 0, LoggingConfig 0 (git grep -o -w … | wc -l); lit useState 2304, timeout 688 (word-bounded; the body's 702 is the unbounded count); dark vqzpx9 0.
  • 11:56:19Z check:spec-changes → exit 0; check:upgrade-guide → exit 0. 11:57:38Z check:objectui-pin-citations → exit 0.
  • Ratchet granularity: build-schemas.ts:850-855 read; 0 dotted rows in both shard sets (lit 7384 / 1170); authorable-surface.base.json:5910 unchanged and absent from the diff; .gitattributes:142,144; PR feat(spec)!: the five system/metrics.zod.ts durations carry their unit in the key name (#17783) #18007 file list via the API; lint.yml:4653-5114 for which job runs check:authorable-surface / check:docs; check-run 103714730003 completed: success.
  • Governing text read from primary sources: ruling A (5635659224), the minor correction (5652120294), the lane/clause-② ruling (5652016540), the semantic+retired-row settlement (5652296795), [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, spec: 4 duration key(s) in system/logging.zod.ts name their unit only in JSDoc — #15939 Ruling A remediation (4 of the 21-row delta) #17782 body and its triage comment.

Taken on the body's word (not re-measured): the dispatch-gates.mjs reconciliation (109 families / 107 green / 2 NOT MEASURED); the full-package run (476 files / 13533 tests — CI Test Core (1/6…6/6) and Test Core all completed: success stand in for it); "15 of 15 artifacts up to date" as a whole (I ran the registry regen, check:spec-changes, check:upgrade-guide, and rely on the Type Check · source gates job for check:authorable-surface / check:docs / check:api-surface); and the row-count table across the two os-regen-merge.sh syncs (its end state is verified by the byte-identical regeneration).

Implemented-by: branch claude/issue-17782-logging-duration-units
Reviewed-by: session_015c5G6TmpMKgnusmTpD7Ntt

VERDICT: PASS


Generated by Claude Code

Copy link
Copy Markdown
Collaborator

Carriers cleared — provenance

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

review record #17999 (comment)
head judged a5976809c2b11a90c68c5ad9f66436cba7366590 — 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 × 123, no other model value, 0 harness fallback notices, dark control 0
--pair 17999 exit 0
checks at strip time 42 runs, 38 success + 4 skipped, 0 failure

Full acceptance, including the disposition of every review flag, is on the card: #17782 comment 5653139945.

⚠️ One correction to the record, measured and not fixed here. The 272 key-position *Ms: count in this PR's ADR-0087 semantic entry and its registry.ts mirror does not reproduce. On this PR's own head the same corpus reads 282 (278 excluding this PR's own four; 275 at the merge-base; 105 restricted to *.zod.ts; dark control 0). It is a stale reading presented as current, in text that ships in the upgrade guide.

⇒ Not blocking and deliberately not pushed: the conclusion it supports is unaffected — Ms beats Seconds 282 to 77 here, and the ordering holds under every corpus definition tried — and the at-tier review measured this and still returned PASS. Recorded so a future reader who cannot reproduce 272 finds the number that does.

⚠️ Landing order — #18007 must not enqueue until this PR is on main. Both PRs write authorable-surface/system.json and authorable-defaults/system.json. Those are merge=os-regen in .gitattributes, so a local merge defers to the driver, but the merge queue rebuilds server-side where no driver runs, and an os-regen path can merge with exit 0 and zero conflict markers while silently dropping one side's rows — the rows are a sorted set, and a dropped line is silent. This PR is the first lander and is safe. The second lander owes an explicit os-regen-merge.sh + gen:schema + an exact-name re-assertion that both cards' rows survive.

⛔ No approval is given or implied. 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

@zhuangjianguo
zhuangjianguo marked this pull request as ready for review September 13, 2026 12:03
@zhuangjianguo
zhuangjianguo added this pull request to the merge queue Sep 13, 2026
Merged via the queue into main with commit 8261ff7 Sep 13, 2026
47 checks passed
@zhuangjianguo
zhuangjianguo deleted the claude/issue-17782-logging-duration-units branch September 13, 2026 12:33
zhuangjianguo pushed a commit that referenced this pull request Sep 13, 2026
Second lander on packages/spec/authorable-surface/system.json and
authorable-defaults/system.json after #17999 (#17782, logging). Both shards
were merged by the os-regen driver, so step 2 took main's side and this
commit regenerates them from the merged source — the only way both cards'
rows are proven present rather than assumed.

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

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

spec: 4 duration key(s) in system/logging.zod.ts name their unit only in JSDoc — #15939 Ruling A remediation (4 of the 21-row delta)

2 participants