Repository navigation
spec: 5 duration key(s) in system/metrics.zod.ts name their unit only in JSDoc — #15939 Ruling A remediation (5 of the 21-row delta) #17783
Description
Activity
zhuangjianguo commented
on Sep 13, 2026 CollaboratorMore actionsClaim: session_015c5G6TmpMKgnusmTpD7Ntt· 2026-09-13T10:41Z · epic PM for #15939 (pm:epicsubtree,domain:speclane rules)Claimed for dispatch as the sixth of the seven Ruling-A remediation cards. Card face declared:
packages/spec/src/system/metrics.zod.tsplus its ADR-0087 migration entries, its generated artifacts, and its test file. Disjoint from #17782 (system/logging.zod.ts, in flight) on every authored source file; the two share only generated artifacts (migrations/registry.ts,authorable-*/system.json), which are regenerated, never hand-merged, and serialise landing, not claiming.Clause ② —
yesRecorded here at claim time, both carriers hung in the same write (
needs:contract-reviewon this card; the PR side is owed the moment the PR exists). Mechanical floor,references/contract-review.md: a new key on a published payload is alwaysyes— each rename puts a spelling onMetricDefinition,MetricExportConfig,MetricsConfigandServiceLevelObjectivethat no author could write before. This overrides Ruling A's ownClause-②: no, exactly as corrected for the five sibling cards (recorded on #177845652094166and on #159395652120294).Staleness check — three faces, on
origin/main@fc28c1d38, 2026-09-13T10:38Zface reading action git log -20 -- packages/spec/src/system/metrics.zod.ts→ one commit (2d34f320d, #17846, rule-arrayfilterdoors). Nothing has renamed a duration key on this file.card-reference #15939 OPEN · PR #17635 OPEN (lands last) · #15678, #15679 both landed work-item all five keys still declared in key position and still bare: maxAge1 ·interval1 ·collectionInterval1 ·window3 ·period2. Already-suffixed spellings (*Ms/*Sec/*Seconds/*S) on this file: 0. Lit controlz.number38, dark controlzzqqxx0.⇒ premise holds; all five rows are still owed.
The multiple-declaration hazard is real on this card
The card warns that line numbers rot and that keys here are declared more than once. Measured: three of the five names have same-named decoys on this same file, and each decoy is a
z.object({, not a duration number.row enclosing symbol path shape decoys on this file that must NOT move :191 maxAgeMetricDefinitionSchema.summary.maxAgenested, default(600)— :552 windowServiceLevelObjectiveSchema.errorBudget.burnRateWindows[].windownested inside an array element, no default :346MetricAggregationConfigSchema.windowand:442ServiceLevelIndicatorSchema.window, bothz.object({:623 intervalMetricExportConfigSchema.intervaltop level, default(60)— :709 collectionIntervalMetricsConfigSchema.collectionIntervaltop level, default(15)— :718 periodMetricsConfigSchema.retention.periodnested, default(604800):509ServiceLevelObjectiveSchema.period, az.object({The dispatch order carries this table and requires the row to be re-located by symbol path, with the decoy count re-measured, before any edit.
The full dispatch order follows in the next comment.
Generated by Claude Code
zhuangjianguo commented
on Sep 13, 2026 CollaboratorMore actionsDispatch order — #17783
Epic PM for #15939 ·
session_015c5G6TmpMKgnusmTpD7Ntt· 2026-09-13T10:42Z ·mode:subagent· baseorigin/main@fc28c1d38You implement exactly this card: the five duration rows in
packages/spec/src/system/metrics.zod.ts. One PR, draft,Fixes #17783.
Zone 1 — Adjudicated. Not reopenable by you.
Director seat on #15939, 2026-09-11T14:08Z, maintainer 「同意」, decision batch #115 — quoted verbatim, not translated:
Ruling A. Batch #65's direction stands (the gate refuses a duration key whose JSDoc names a unit its describe does not). Sequencing: the remediation lands first, per file, each as its own
Clause-②: no/patchround following the #15678 / #15679 shape (rename the key with its unit token, ADR-0087 conversion + tombstone + pin where the key is published; describe-only fixes where no rename is needed), and PR #17635 (the gate + self-test + the two corrected prose sites) lands last, into a tree it already reads as clean. ⛔ Not B (18 published-key renames in one PR); ⛔ C is gate weakening.Settled, ⛔ do not re-litigate any of these:
- Direction. The five keys get renamed to carry their unit. "Should we instead fix the describe" is answered: no.
Clause-②: yes, not the ruling'sno. Corrected by the epic PM before dispatch, recorded on spec: 1 duration key(s) insystem/tenant.zod.tsname their unit only in JSDoc — #15939 Ruling A remediation (1 of the 21-row delta) #17784 (5652094166) and 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 (5652120294). Mechanical floor: a new key on a published payload is alwaysyes. PutClause-②: yesin the PR body.minor, not the ruling'spatch. Every one of 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.tssizeneeds an honest name, not the mechanical one #15679 siblings shippedfeat(spec)!at minor (packages/spec/CHANGELOG.mdunder## 17.4.0). The launch window refusesmajor(scripts/pm/check-changeset-no-major.mjs): breaking-ness is carried by a**BREAKING**banner plus theadr-0087: registereddisposition, not by the level. Measure the sibling entries yourself and match them.- Both ADR-0087 registrations, per key. A D3 semantic entry and a
RETIRED_KEYS_BY_MAJOR[18]row. Settled for the whole epic 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. The two older tenant renames that carry only a semantic entry are residue of the pre-build-schemas.ts 检查 (b) 用叶名匹配 conversion surface —— 无关簇的.type就能让一个 tombstone 冒充「已登记迁移」 #4659 leaf-matching bug — ⛔ not a precedent. - PR feat(spec): refuse a duration key whose JSDoc names a unit its describe does not #17635 lands last. ⛔ Do not touch
packages/spec/scripts/check-duration-unit-keys.tsor its self-test. It is red by construction until every remediation card lands. That is designed, not a defect. - Sibling files are not yours. ⛔
kernel/plugin-lifecycle-advanced.zod.ts,kernel/plugin-security-advanced.zod.ts,system/tenant.zod.ts,system/logging.zod.ts,system/tracing.zod.ts. Five of the seven cards are delivered or in flight; two of them are already merged intomain.
Zone 2 — Mechanism assumptions. Falsify each before you rely on it.
Every line below is something I measured and could still be wrong about. Your first job on each is to re-measure. A measured contradiction is a good deliverable — report it, don't route around it.
2.1 The five rows, by symbol path — and the three decoys
Line numbers come from PR #17635's enumeration and rot. Re-locate by symbol path. Measured on
origin/main@fc28c1d38, 2026-09-13T10:38Z:row symbol path shape JSDoc, as read value :191MetricDefinitionSchema.summary.maxAgenested "Max age of observations in seconds" int().positive().optional().default(600):552ServiceLevelObjectiveSchema.errorBudget.burnRateWindows[].windownested, inside an array element "Window size in seconds" int().positive().describe('Window size'), no default:623MetricExportConfigSchema.intervaltop level "Export interval in seconds" int().positive().optional().default(60):709MetricsConfigSchema.collectionIntervaltop level "Collection interval in seconds" int().positive().optional().default(15):718MetricsConfigSchema.retention.periodnested "Retention period in seconds" int().positive().optional().default(604800)(// 7 days)⚠️ Three of the five names have same-named decoys on this same file, and every decoy is az.object({, not a duration number. These must not move::346MetricAggregationConfigSchema.window—z.object({:442ServiceLevelIndicatorSchema.window—z.object({:509ServiceLevelObjectiveSchema.period—z.object({
Re-measure the decoy counts (
window3 key-position declarations,period2) and state them in the PR body.grep -ccounts LINES — usegrep -o … | wc -lwhen you mean occurrences. Give a lit control (z.numberreads 38 on this file) and a dark control.Read each JSDoc yourself. ⛔ Do not infer the unit from the default's magnitude —
default(60)is consistent with 60 seconds and 60 milliseconds, andintervalon the sibling card had a describe that named the wrong unit outright.2.2 The suffix is
Seconds, notMs— and this file already decided thatThis is the first card in the epic whose rows are seconds. The four before it were all milliseconds. ⛔ Do not carry
Msover by habit.#15679already renamed a key on this same file:ServiceLevelObjectiveSchema.period.duration → durationSeconds, with its tombstone constSLO_PERIOD_DURATION_RETIREDstill in place at:521-528. That is the nearest precedent there is.Derive the spelling from the tree, not from me: count key-position
*Seconds,*Sec,*Sand any other second-carrying suffix acrosspackages/spec/src/**/*.zod.ts, and count the competing spellings for each of the five stems (maxAge,window,interval,collectionInterval,period). Put the counts in the PR body. Two of them need a judgement call you must show your work on:maxAge— "age in seconds" reads awkwardly asmaxAgeSeconds; check whether an*AgeSecondsor*Age*family already exists.period—retention.periodis already inside aretentionobject; check whether the surface spells thisretentionPeriodSeconds,periodSeconds, or something else elsewhere.
If a stem has no attested family spelling either way, say so and take the plain suffix, as the sibling card did for
debounceDelayMs.2.3 The ratchet moves for two of the five, and that split is the trap
authorable-surface/*.jsonrecords top-level keys per def. Measured from the generator/checker source, not assumed:MetricExportConfigSchema.intervalandMetricsConfigSchema.collectionIntervalare top level ⇒ the ledger must move:[RETIRED]markers beside the new suffixed rows, plus the matchingauthorable-defaultsmoves.summary.maxAge,burnRateWindows[].windowandretention.periodare nested ⇒ the ledger must not move.
Both readings are already attested by landed siblings: PR #17954's key was nested and correctly moved nothing; PR #17986's three keys were top level and correctly moved the ledger. Same rule, opposite readings, both right. Verify the granularity from
packages/spec/scripts/build-schemas.tsyourself and state which of your five rows moves the ledger and which does not, with the reason.⛔ Regenerate with
gen:schema. ⛔ Never hand-edit a generated artifact. ⛔ Never add a baseline entry to a ratchet to make a check pass — that is maintainer-only and a red line.2.4
burnRateWindows[].windowis a shape no card in this epic has hit yetIt is a key inside an array of objects. Two things need deciding and neither should be invented silently:
- Does
retiredKey()on an array-element object behave the way it does on a plain nested object? Prove it with a test, not an assumption. - What path notation does the
RETIRED_KEYS_BY_MAJOR[18]row take for a key under an array? Look at how existing rows spell nested paths (kernel/RuntimeConfig:resourceLimits.timeoutis the shape from PR feat(spec)!: RuntimeConfigresourceLimits.timeoutcarries its unit in the key name (#17781) #17983) and whether any existing row crosses an array. If none does, say so, pick the spelling that the checker actually reads, and flag it inopen_questionsfor the contract review.
Related known limitation, for your awareness only: #17969 records that
build-schemas.tscheck (b2) cannot verify nestedRETIRED_KEYS_BY_MAJORrows the way it verifies top-level ones. Three of your five rows are nested. This does not block you and ⛔ is not yours to fix — but it means a green check is weaker evidence here than it looks, so lean on your own tests.2.5 Tombstones, and the TDZ ordering trap this file will spring
Check
.strict()on each enclosing object. Where it is absent, a bare deletion is an ADR-0049 silent strip — the value vanishes and the consumer gets a default where an operator set a real number. Every one of your five needs aretiredKey()tombstone whose message carries the FROM → TO mapping.⚠️ gen:schemaandcheck:authorable-surfaceboth run withOS_EAGER_SCHEMAS=1, which makeslazySchemaevaluate its factory at module load. A tombstone string const declared after the schema that reads it is a temporal-dead-zone read. PR #17983 hit exactly this and had to move its const above the schema. This file has five schemas and an existing const (SLO_PERIOD_DURATION_RETIRED) — check where each of your new consts has to sit.2.6 Consumer packages — named, and each owes a real reading
This is a
domain:speclane requirement, not a suggestion: a sweep insidepackages/speccannot see a consumer's fixture asserting the old spelling. Run and report an exit code for each of:package why it is on this list @objectstack/observabilitythe metrics-domain package; 109 @objectstack/specimports. Highest fixture risk on this card.@objectstack/corethe runtime that consumed the sibling card's keys @objectstack/verify·@objectstack/clischema-driven fixtures @objectstack/rest·@objectstack/qapublished-surface fixtures My own measurement, which you should try to falsify: outside
packages/spec, the six metrics defs occur 5 · 4 · 8 · 6 · 6 · 7 times and every hit is prose — the only files arecontent/docs/references/index.mdxandcontent/docs/references/system/metrics.mdx, both generated.collectionIntervaloutsidepackages/spec: 1.packages/observabilityreads none of the six defs (0), on a corpus where@objectstack/specreads 109. If that is right, this card has zero in-repo code consumers and the runtime leg is empty — but you must confirm it, because a wrong zero here is exactly the failure this list exists to catch. Report per-package exit codes either way; if you skip one, say which and why.2.7 Pinned objectui
git grepthe pinned checkout at.objectui-sha, re-read from this tree. Report every export ofmetrics.zod.tsand each of the five key names, with lit controls on that same corpus and a dark control. Zero without controls is not a reading.⚠️ The pin-citation lesson, twice-learned in this epic: the citation must land on ONE source line. Sibling cards spent a cycle each on a citation split across a TypeScript string concatenation, which put.objectui-shaand the sha on different lines and made the checker miss it.
Zone 3 — Suggested route. Deviate freely with evidence.
- Worktree off
origin/main@fc28c1d38. ⛔ Never edit the shared checkout, ⛔ nevergit stash. - Re-locate all five rows by symbol path; re-measure the decoys; read all five JSDocs.
- Settle the suffix per stem from tree counts.
- Rename +
retiredKey()tombstone per row, consts placed above their readers. - Fix each
.describe()to name the unit — the describe is what the reference page renders and what an AI author reads. - Five D3 semantic entries (or one entry covering the file, if that matches how the siblings did it — check) and five
RETIRED_KEYS_BY_MAJOR[18]rows. Regenerateregistry.tswithgen:migration-registry; ⛔ never hand-merge it. - Regenerate every artifact:
gen:schema,gen:docs.check:generatedmust read all artifacts current. - Tests: for each row, a refusal pin (the issue is not
unrecognized_keys, and the message carries FROM → TO), an acceptance pin at the same magnitude with the same default, and a describe pin asserting the unit is published. Where an existing pin asserts a key stays bare — this card is the sweep that pin was written to catch — replace it, move it out of anydescribe()block whose header would become false, and record in its comment what it used to pin and why. ⛔ Never delete,.skip, weaken, or "fix green" a pin. - Reverse-verify at least the two hardest new pins by ablation: delete the tombstone line, prove the deletion on disk (anchor count and blob hash), re-run and show the expected tests red, restore under an
EXIT INT TERMtrap, and prove byte-identity (git hash-object, emptygit diff HEAD, emptygit status --porcelain). minorchangeset for@objectstack/specwith the**BREAKING**banner, the FROM → TO mapping for all five, andadr-0087: registered.- Merge
mainwithbash scripts/pm/os-regen-merge.sh, never by hand.⚠️ Its header records the trap: os-regen paths merge with exit 0 and zero conflict markers while silently dropping one side's changes. After the merge, assert the sibling migration entries are still present by exact-name grep with a dark control, and re-runcheck:generated. - Full gate sweep:
dispatch-gates --commands, reconciled against--ran … :: exit N. Report derived / run / NOT-MEASURED / UNRUN and every non-zero exit with its classification.
Known noise you should recognise rather than chase
check-widening-tellsT1 onretiredKey()lines is the known inverted false positive [finding] check-widening-tells fires T1 on a retiredKey() tombstone line, so every ADR-0087 key retirement reads as a clause-2 widening for the one reason the accept set shrank #17955 — the lines it fires on make the accept set strictly narrower. ⛔ Do not reshape the diff and ⛔ do not weaken the checker. Record it.- Docs Drift Check will over-name pages. Its anchor is a bare string literal from your new retirement constants; with
interval/period/windowas stems, expect a wide false-positive list. Check each named page for an actual metrics-config mention before you touch it, and record the finding — the last card in this epic will trip the same anchor. check:doc-authoringrefuses tracker ids inside runtime strings. If you add any runtime prose, put the anchor in an adjacent//comment. ⛔ Never add a baseline entry.
Deliverable and red lines
- Draft PR,
Fixes #17783,Clause-②: yesin the body. ⛔ Do not turn it ready. ⛔ Do not enqueue. ⛔ Do not enable auto-merge. ⛔ Do not merge. The in-seat clause-② contract review atCONTRACT_REVIEW_TIERis owed first and landing is the PM's step. - Hang
needs:contract-reviewon the PR the moment it exists (the card side is already hung). - ⛔ Never edit anything under
content/docs/releases/— release-owned. - ⛔ Never weaken a gate, lower a threshold, delete a required check, raise a ratchet ceiling, or skip a test.
- Capture every exit code before any pipe (
cmd > log 2>&1; ex=$?). Route heavy runs throughscripts/pm/os-verify-lock.shand read the verdict from its ownVERDICT command-exitline. - Report as
os-dev-reporton this card: every reading with its command, its exit code and a UTC timestamp; anopen_questionslist for anything you had to judge (the array-path notation in 2.4 belongs there); and anything you measured that contradicts this order.
A measured "the premise is false, here is the evidence" is a good outcome, not a failure. Stop and report rather than inventing scope.
Generated by Claude Code
zhuangjianguo commented
on Sep 13, 2026 CollaboratorMore actionsCorrection to the dispatch order, Zone 2.2 — sent to the dev 2026-09-13T10:46Z
Recorded here because a correction that lives only in the dispatch channel is a half state.
Zone 2.2 told the dev to derive the suffix from tree counts. That still holds, but counting alone will get at least one of these five rows wrong, because the counts cannot see a reasoned convention this file already carries. Two #15679 tombstone prose files, both about
packages/spec/src/system/metrics.zod.ts:file what it records migrations/entries/retired-keys/18.system__MetricAggregationConfig__window.size.tswindow.size→durationSeconds, explicitly NOT the gate's mechanicalsizeSeconds("sizeis byte/row-count vocabulary everywhere else in this spec").windowSecondswas also rejected — the parent key is alreadywindow, so it would readwindow.windowSeconds.migrations/entries/retired-keys/18.system__ServiceLevelObjective__period.duration.tsperiod.duration→durationSeconds, and it is "why the twowindow.sizekeys above land ondurationSecondsrather thansizeSeconds: the file already spelled a window length as adurationone schema down, so the three measurements now read alike".Both cite one shared semantic entry,
system-metrics-window-durations-unit-in-key.⇒ this file has a stated naming convention already in force: on
metrics.zod.tsa window length is spelled as aduration, and three measurements were deliberately made to read alike. Row:552(ServiceLevelObjectiveSchema.errorBudget.burnRateWindows[].window) is a fourth window length in seconds on the same file — the mechanical answer iswindowSeconds; the file's own recorded convention appears to prescribeburnRateWindows[].durationSeconds, which does not stutter because the array name already carries "window". The same hazard applies tomaxAgeandretention.period, wheremaxAgeSecondsandperiodSecondsare the mechanical answers and mechanical was already wrong twice on this file.⛔ Not decided here. The dev decides and must show the argument in the PR body the way #15679 showed its own, including why the rejected spelling was rejected.
Two consequences recorded with it:
- The dev must check whether these rows belong under the existing
system-metrics-window-durations-unit-in-keyentry or under a new sibling entry. ⛔ [#14478 stack 4/6]system/: the 15 remaining duration keys carry their unit in the key name — ADR-0087 conversions with readers;metrics.zod.tssizeneeds an honest name, not the mechanical one #15679's entry is not to be amended — the rule PR feat(spec)!: RuntimeConfigresourceLimits.timeoutcarries its unit in the key name (#17781) #17983 established for [#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's entry is that a predecessor's scoped past-tense record stays true and the successor writes its own opening with how it relates. - The two
windowdecoys are now explained rather than merely flagged::346and:442arez.object({whose inner.sizehas already been renamed todurationSecondsand tombstoned. ⛔ Neither is to be touched, and theirdurationSecondsis not evidence that any row of this card is already done.
Generated by Claude Code
- The dev must check whether these rows belong under the existing
zhuangjianguo commented
on Sep 13, 2026 CollaboratorMore actionsClaim: session_015c5G6TmpMKgnusmTpD7Ntt
Branch:
claude/issue-17783-metrics-duration-unit-keysThis supersedes the malformed claim above — a PM defect, not a change of ownership
⚠️ My claim comment5652764342(2026-09-13T10:41Z) is unreadable tocheck-closing-target-claim, and that is whyThe card this PR closes must claim this branchwent red on PR #18007. The failure is mine, not the dev's, and not the PR's.Two independent defects in one comment, both measured rather than guessed:
reader what it wants what 5652764342gave itCLAIM_COMMENT_MARKER=/^\s*>?\s*Claim(?:ed)?\s*:/mia line beginning Claim:, optional leading whitespace or>and nothing else`Claim: session_…`— a leading backtick, so no line in the comment matches and the comment is not a claim at allclaimedBranches/CLAIM_BRANCH_SHAPEa Branch:directive line namingclaude/issue-N-slugno Branch:line anywhereEither alone would have been enough to red the gate.
The checker's own header anticipates exactly the first half — "a claim that writes its branch on the
Claim:line itself, after a separator, is therefore a claim that yields ZERO branches … that shape is not hypothetical" — and it states the repair direction: the write side. So this is a rewrite of the claim, ⛔ not a change to the gate, ⛔ not a re-run of a red I had not fixed.Scope of the defect, measured on all three claimed cards of this epic:
card matches CLAIM_COMMENT_MARKERBranch:directive#17780 ✅ comment 5652307102✅ claude/issue-17780-plugin-lifecycle-duration-units#17782 ✅ comment 5652564692✅ claude/issue-17782-logging-duration-units#17783 ⛔ no comment matches ⛔ absent ⇒ isolated to this card. The two claims written earlier in the session are well-formed; only this one regressed, and it regressed in a context where I had the shape in front of me and still wrote it differently. The lesson I am carrying to #17785, which is not yet claimed: the claim's machine-readable half gets written from the checker's regex, not from the look of the previous comment, and
check-clause2-carriers --pair <PR>gets run on every card, not only the ones I happen to doubt — I ran it on #17986 and #17999 and never on this one, which is why a two-way malformed claim reached CI instead of my own desk.What is unchanged
The substance of
5652764342stands in full and is not restated here: the claim time (2026-09-13T10:41Z), the declared card face, the clause-②yesdetermination, the three-face staleness readings, and the decoy table. Ownership, timing and priority are all unaffected — this comment repairs only the two fields a machine reads.
Generated by Claude Code
zhuangjianguo commented
on Sep 13, 2026 CollaboratorMore actionsClause-②-correction: 5652931475
Clause-②: yes
Session:
session_015c5G6TmpMKgnusmTpD7Ntt
⚠️ Second PM defect on this card's claim, same root cause as the first.node scripts/pm/check-clause2-carriers.mjs --pair 18007reads exit 4 at 2026-09-13T11:39Z: "NO READING on the declaration limb, and the PLACEMENT of the key is what is wrong, NOT its spelling."The declaration limb is line-anchored — only whitespace, a blockquote
>, a list bullet and backtick/bold wrapping may precede the key. My claim5652764342carriedClause-②: yescorrectly spelled but inside a prose sentence, so it was never read. ⛔ There was no typo to find.That is the same failure mode as the malformed
Claim:/Branch:fields I repaired at5652931475: a machine-readable field composed from memory instead of copied from the template. The checker says this outright — "Every one of the five misses measured in the filing shift was a line composed from memory" — and.claude/skills/pm-dispatch/SKILL.md:809carries the literal line to copy. I read that file, wrote the field from memory anyway, and did it twice on one card.⇒ The correction above is the sanctioned instrument, ⛔ not a second
Claim:(forbidden) and ⛔ not an edit of the posted comment (no such tool call exists here). The substance is unchanged:yes, on the mechanical floor that a new key on a published payload is always affirmative.The same defect is present on #17785's claim and is corrected there in the same way. I found it by running
--pairon both open PRs rather than only the one I doubted — the discipline I recorded at5652931475after the first defect, which is what caught this one.
Generated by Claude Code
os-dev-report
{ "issue": 17783, "status": "done", "branch": "claude/issue-17783-metrics-duration-unit-keys", "pr": "https://github.com/objectstack-ai/objectstack/pull/18007", "head": "84e62ed7129e7cd6e1b6a307530cdf4f9d3602a3 (post-review fix round; the reviewed head was bcfdd35609)", "premise_still_valid": true, "dispatch_order_readback": "Comment 5652772057 read end to end via repo-scoped REST, 15636 characters, not truncated and not mangled: the Zone 1/2/3 structure, all five row tables, the six named consumer packages and the closing red-lines block are all present. Its final bullet, quoted verbatim: \"Report as `os-dev-report` on this card: every reading with its command, its exit code and a UTC timestamp; an `open_questions` list for anything you had to judge (the array-path notation in 2.4 belongs there); and anything you measured that contradicts this order.\" The comment then closes with the line: \"**A measured ‘the premise is false, here is the evidence’ is a good outcome, not a failure.** Stop and report rather than inventing scope.\"", "summary": "All five duration rows on packages/spec/src/system/metrics.zod.ts now carry their unit in the key name, each with a corrected .describe(), a retiredKey() tombstone carrying the FROM to TO prescription, one ADR-0087 D3 semantic entry (system-metrics-jsdoc-durations-unit-in-key) and five RETIRED_KEYS_BY_MAJOR[18] rows: MetricDefinition.summary.maxAge to maxAgeSeconds; ServiceLevelObjective.errorBudget.burnRateWindows[].window to durationSeconds; MetricExportConfig.interval to intervalSeconds; MetricsConfig.collectionInterval to collectionIntervalSeconds; MetricsConfig.retention.period to durationSeconds. Every value and default is unchanged. Zone 2.3 confirmed exactly as written: the two top-level rows moved the authorable-surface ratchet ([RETIRED] marker plus the suffixed row, and the authorable-defaults row with it) and the three nested rows moved nothing; measured structurally, 0 of 1307 rows in authorable-surface/system.json carry a dot after the colon. The card face was empty on assignee at dispatch time on my read of the API: assignee is zhuangjianguo, set by the PM's dispatch atom; I never wrote it. Draft PR #18007 is open with Clause-2: yes and needs:contract-review hung on it; not ready, not enqueued, no auto-merge.", "tests": "All timestamps UTC, all exit codes captured before any pipe (cmd > log 2>&1; ex=$?). Heavy runs via scripts/pm/os-verify-lock.sh with OS_VERIFY_LOCK_SLOT=dev-17783, verdict read from its own VERDICT command-exit line. Final head bcfdd35609 (merge of origin/main @ 8261ff7171); base fc28c1d38. || BUILD/TEST: pnpm --filter @objectstack/spec build exit 0 (VERDICT command-exit 0, 11:01Z / 11:12Z / 11:20Z / 12:40Z / 12:58Z). pnpm --filter @objectstack/spec test && typecheck exit 0 at 11:26Z, 476 test files, 13536 tests passed. Targeted metrics.test.ts run exit 0 at 11:06Z, 46 passed. || GENERATED: pnpm --filter @objectstack/spec check:generated exit 0 at 12:59Z, 15 of 15 artifacts current, working tree clean. Earlier runs: exit 1 at 11:03Z (check:docs stale, check:test-typecheck 1 TS2339 from my own double .unwrap() - FIXED, not ledgered); exit 1 at 11:08Z and 12:31Z (check:api-surface stale dist after a post-build source edit - resolved by rebuilding, not by regenerating). || DISPATCH-GATES RECONCILIATION (node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack): DERIVED 109 commands (68 pnpm, 41 node) from a 13-path change set vs merge base 8261ff717. RUN: 109 of 109, all exit 0, sweep finished 13:29Z on head bcfdd35609. UNRUN: 0. First sweep (head 9b62f54671, 12:17Z) produced 3 non-zero, every one classified NOT MEASURED rather than red and then re-run green: (a) pnpm --filter @objectstack/spec run check:skill-examples exit 1 - PREREQUISITE NOT MET, packages/client-react/dist held no .d.ts (package unbuilt in this worktree); after building it, exit 0, 258 prose examples type-check. (b) pnpm check:dual-build-cjs-loads exit 3 - the gate's own PREREQUISITE NOT MET line, 8 packages with no dist; after building them, exit 0. (c) pnpm check:type-check-debt exit 3 - node OOM, the gate's own text says 'NOT a pass and NOT a finding: nothing was measured'; re-run under NODE_OPTIONS=--max-old-space-size=8192, exit 0, 5 ledger entries re-measured, 55 raw tsc errors, none above its recorded number. NOT MEASURED and outside the 109 by the tool's own accounting: 5 families taking a value from the workflow, 6 path-scheduled CI jobs (32 steps), 48 artifact-roster families, 11 declared-wide-population families. Those are CI's, not mine. || CONSUMERS (Zone 2.6), per package, on a BUILT dependency closure: @objectstack/core typecheck exit 0; @objectstack/verify typecheck exit 0; @objectstack/cli typecheck exit 0; @objectstack/rest typecheck exit 0; @objectstack/observability test exit 0; @objectstack/downstream-contract test exit 0; @objectstack/http-conformance test exit 0. First pass on an UNBUILT closure gave observability 1, core 1, verify 2, cli 2, rest 2 - every one TS2307 'Cannot find module' or ERR_PNPM_RECURSIVE_RUN_NO_SCRIPT, i.e. local staleness, not my diff; classified and re-measured rather than reported as red. || ABLATION (both hardest pins, a trap on EXIT INT TERM calling the restore function, absolute REPO_ROOT paths, restore via git checkout HEAD -- path): leg A deleted `window: retiredKey(SLO_BURN_RATE_WINDOW_RETIRED),` - anchor 1 to 0, blob a747965b to 87687490 (proved on disk), vitest exit 1, 1 failed / 45 passed; leg B deleted `period: retiredKey(RETENTION_PERIOD_RETIRED),` - anchor 1 to 0, blob a747965b to 8a84628e, vitest exit 1, 1 failed / 45 passed; control leg unmutated vitest exit 0, 46 passed. Both restores proved by git hash-object back to the HEAD blob a747965b, empty git diff HEAD and empty git status --porcelain. CORRECTED after contract review - the two legs fail DIFFERENTLY and prove different things; an earlier revision of this report claimed both read the same text, which was wrong. Leg A reads 'AssertionError: expected undefined to be defined' at metrics.test.ts:622: result.success was still false, but no issue landed at errorBudget.burnRateWindows.0.window, because the sibling durationSeconds on that array element is REQUIRED - so with the tombstone gone the parse is refused anyway for a missing required key. Leg A therefore proves the LOST PRESCRIPTION (a bare 'required' refusal instead of the FROM to TO rename message), not a silent strip. Leg B reads 'AssertionError: expected true to be false' at metrics.test.ts:659 - result.success was TRUE, because retention.durationSeconds is .optional().default(604800), so the unknown period key is accepted and stripped and the parse succeeds. Leg B is the ADR-0049 silent strip, live. Both legs remain valid evidence that their pin can fail. Leg A also answers Zone 2.4 question 1 by test: retiredKey() on an ARRAY-ELEMENT object refuses exactly as on a plain nested object, at path errorBudget.burnRateWindows.0.window. No dist leg: the test imports './metrics.zod' relatively inside its own package, so no package exports resolution and no vitest alias is involved. || SECOND-LANDER MERGE after #17999 landed: bash scripts/pm/os-regen-merge.sh (never a hand-merge). Step 2 TOOK main's side of both authorable-surface/system.json and authorable-defaults/system.json (both sides had changed them); the pre-commit hook then refused the merge commit until gen:schema was re-run, which is that sequence's designed collection point. gen:schema exit 0, regeneration committed as its own commit. SET DIFFERENCE over the WHOLE registry across the merge: retired-key rows 175 to 182, 0 LOST, 7 GAINED (kernel/HotReloadConfig:debounceDelay, kernel/PluginHealthCheck:interval, kernel/PluginHealthCheck:timeout, system/HttpDestinationConfig:batch.flushInterval, :retry.initialDelay, :timeout, system/LoggingConfig:buffer.flushInterval); semantic ids 210 to 213, 0 LOST, 3 GAINED (kernel-health-check-and-hot-reload-durations-unit-in-key, logging-durations-unit-in-key, screen-field-lookup-reference-required). EXACT-NAME re-assertion in packages/spec/src/migrations, occurrences via grep -o | wc -l, against the PM's pre-merge baseline in brackets: system/HttpDestinationConfig:batch.flushInterval 4 [4] MATCH; :retry.initialDelay 2 [2] MATCH; :timeout 2 [2] MATCH; system/LoggingConfig:buffer.flushInterval 4 [4] MATCH; logging-durations-unit-in-key 10 [10] MATCH. Mine: system/MetricDefinition:summary.maxAge 2; system/ServiceLevelObjective:errorBudget.burnRateWindows.window 2; system/MetricExportConfig:interval 6; system/MetricsConfig:collectionInterval 6; system/MetricsConfig:retention.period 2; system-metrics-jsdoc-durations-unit-in-key 12. The 6s are substring effects - 'MetricExportConfig:interval' is a prefix of ':intervalSeconds' - not extra rows. LIT control kernel-runtime-config-timeout-unit-in-key 4 [4]; DARK control system/Zzqqxx:nope-not-real 0 [0]. Both shards carry both cards' top-level rows: surface has HttpDestinationConfig:timeoutMs 1, :timeout [RETIRED] 1, MetricExportConfig:intervalSeconds 1, :interval [RETIRED] 1, MetricsConfig:collectionIntervalSeconds 1, :collectionInterval [RETIRED] 1; defaults has all three suffixed rows 1 each; surface DARK 0, surface LIT 'system/MetricsConfig:' 13. node scripts/pm/check-clause2-carriers.mjs --pair 18007 exit 0 at 12:59Z - both carriers agree, read from the correction comment. || ZONE 2.1 decoys at fc28c1d38 (grep -o | wc -l): key-position window 3, period 2, maxAge 1, interval 1, collectionInterval 1; lit control z.number 38; dark control 0; .strict() on the file 0. The three decoys (window :346 and :442, period :509) are all z.object({ and a pin in this PR asserts none of them moved. || ZONE 2.2 suffix counts in key position across packages/spec/src/**/*.zod.ts at fc28c1d38: Seconds 40, Sec 1 (maxExecutionTimeSec), S 0 as a seconds suffix - the two bare *S: hits are maxCommitTimeMS (a millisecond spelling) and enableRLS (a boolean). 45 Seconds at this head. || ZONE 2.7 pinned objectui: .objectui-sha = 53ded82bf7a494f54e344e19099dbf00854b8694, checkout at /home/user/objectstack-ai/objectui whose HEAD equals the pin exactly, 6409 tracked files. All six metrics def names and both distinctive keys read 0. LIT controls on the same corpus: window 2710, timeout 832, period 160, interval 156, metrics 301. DARK 0. No pin bump owed. || CORRECTION to the absolute registry totals: the 177 to 184 / 209 to 212 figures in an earlier revision of this report came from a regex census over registry.ts and miscount; the review could not reproduce them and was right. The authority is gen:migration-registry's own printed line - 175 retired-key / 210 semantic before the merge, 182 / 213 after - and a third method agrees: entries/retired-keys holds 177 files on main @ 8261ff7171 plus this card's 5 = 182, entries/semantic 212 plus 1 = 213. The DELTAS were identical under all three methods (0 lost / 7 gained and 0 lost / 3 gained), so the no-rows-dropped conclusion never depended on the wrong absolutes. || POST-REVIEW FIX ROUND on head 84e62ed712 (previous reviewed head bcfdd35609; delta is 2 files, 18 insertions, 2 deletions): pnpm --filter @objectstack/spec gen:migration-registry exit 0 at 13:59Z (213 semantic, 182 retired-key, 178 retired-def); pnpm --filter @objectstack/spec build exit 0 at 14:03Z (VERDICT command-exit 0); pnpm --filter @objectstack/spec check:generated exit 0 at 14:05Z, 15 of 15 current, working tree clean. Re-measured the review's second limb independently on this head: both entry ids render 0 times in packages/spec/docs/protocol-upgrade-guide.md, confirming the protocol-18 step is unreleased and the pointer finishes it rather than editing published history.", "mcp_calls": "2 - both mcp__github__search_issues, the declared fallback after this session's REST /search/* probe returned 403 ('sessions are bound to their configured repositories'). Every other GitHub read and write went through repo-scoped REST (probe: GET /repos/.../issues/17783 -> 200) or plain git: card body, all 3 comments, the #17780 batch triage comment, epic #15939, sibling PR diffs #17954 #17983 #17635, PR create, PR body update, label add, issue create.", "open_questions": [ { "question": "Zone 2.4 - what path notation should a RETIRED_KEYS_BY_MAJOR row take for a key under an ARRAY? I used plain dots, 'system/ServiceLevelObjective:errorBudget.burnRateWindows.window', with no bracket token.", "options": [ "A - plain dots, no array marker (what I shipped): of the 170-odd rows in the table, 0 carry a bracket; the notation is a dot-joined property path and nothing on the tree reads an array marker. The two historical rows that did cross an array, system/RollbackPlan:steps.estimatedMinutes and system/ChangeRequest:implementation.steps.estimatedMinutes, are spelled this way, and the first one's own file comment says it is spelled 'the way kernel/Manifest:contributes.actions and api/BatchEndpointsConfig:operations.upsertMany are'.", "B - a bracket token such as errorBudget.burnRateWindows[].window: reads unambiguously to a human, but introduces a notation no row on this tree uses and no consumer parses." ], "recommendation": "SETTLED IN FAVOUR OF A by the contract review, which also closed the evidence gap I declared. Plain dots, no bracket token. The review reached bf1054a4c0 (content-addressable and reachable despite the shallow clone) and confirmed both change-management.zod.ts precedents do cross an array element with plain dots, and it found a LIVE precedent I had missed: kernel/Manifest:contributes.kinds.globs, where kinds: z.array(strictObject({ contains globs: retiredKey(. It also confirmed nothing mechanical reads nested rows. Shipped as A." }, { "question": "#15679's semantic entry system-metrics-window-durations-unit-in-key has an acceptanceCriteria sentence that this card makes false: 'Two keys on this same file deliberately do NOT move, and a sweep that renamed either has over-applied the rule ... Both keep their names.' One of the two is the burn-rate window I just renamed. Should that predecessor entry be corrected?", "options": [ "A - do not touch it (what I shipped): #17781 established that you do not amend a predecessor's ADR-0087 entry, you write your own that opens by stating how it relates. My entry does exactly that and names the superseded sentence explicitly.", "B - amend #15679's acceptanceCriteria so a reader of the upgrade guide is not told that my own PR over-applied the rule." ], "recommendation": "DECIDED AGAINST A by the contract review, and the reasoning dissolves the rule conflict I flagged rather than trading one rule off against the other. Two limbs. (1) The #17983 precedent does not squarely govern: #15678's sentence lives in `reason` (rendered 'Why not automatic:') and #17983's justification was that it stays true, whereas this one lives in `acceptanceCriteria`, whose contract is 'how the consumer proves the hand-migration correct' and which renders as 'Done when:' in the guide and 'verify:' in migrate meta - a normative 'Done when ... keep their names' does not stay true once window is tombstoned, it instructs a future reviewer that a correct sweep was an error. (2) Decisively, neither entry renders anywhere today: PROTOCOL_VERSION is 17.0.0, the guide ends at 'Protocol 16 to 17', and both ids read 0 in the guide and in spec-changes.json (re-measured by me on head 84e62ed712). The protocol-18 step is unreleased and still assembling, so appending a pointer is FINISHING the step, not rewriting published history - which is the thing 'never amend a predecessor entry' exists to prevent, so that rule never applied here. SHIPPED: a pointer clause appended to system-metrics-window-durations-unit-in-key's acceptanceCriteria immediately after 'Both keep their names.', every existing word left intact, naming system-metrics-jsdoc-durations-unit-in-key as the entry that renames the burn-rate window in this same step; registry.ts regenerated and committed." }, { "question": "PR #17635 (the gate, sequenced last) patches the exact comment lines this card deletes. Its diff rewrites 'outside the gate population entirely' to 'inside the gate's census and outside its verdict' inside it('leaves the two non-duration keys on this file alone'), and its own added text says the pin 'must be re-read, not trusted, when the rename lands'. That test no longer exists in that form after this PR. Who drops that hunk?", "options": [ "A - whoever rebases #17635 drops that half of its comment repair (the metrics.test.ts hunk) and keeps the other half, the kernel/SandboxConfig:process.timeout entry comment, which this card does not touch.", "B - I pre-emptively leave the old test shape in place so #17635 applies cleanly." ], "recommendation": "A. B would mean keeping a header that asserts something this diff made false, which AGENTS.md forbids and which the sibling cards were held to. Recording it here so the last card of the epic is not surprised by a conflict on a file it expects to be quiet." }, { "question": "The dispatch's Zone 2.6 consumer list names @objectstack/qa. That package does not exist.", "options": [ "A - packages/qa is a directory of four packages (@objectstack/dogfood, @objectstack/downstream-contract, @objectstack/http-conformance, @objectstack/refd-timer-testkit) and there is no package named @objectstack/qa; I substituted the real ones.", "B - the name was meant to be one of those four specifically." ], "recommendation": "A, and worth noting the failure shape: pnpm --filter on a name that matches nothing exits 0, so my first '@objectstack/qa typecheck exit 0' was a phantom green - it measured nothing. The os-verify-lock wrapper caught the same class on a build filter and refused before acquiring ('filter-matches-nothing (exit 2) ... pnpm exits 0 on a filter that matched nothing'). Also measured: @objectstack/observability has no typecheck script at all - it is a declared DEBT entry (11 errors) in scripts/check-type-check-coverage.mjs - so its typecheck is legitimately absent, and I measured its test instead (exit 0)." } ], "out_of_scope_findings": [ "filed as #18030: MetricsConfig.retention.downsampling[].resolution on this same file says 'Resolution in seconds' in its JSDoc and 'Downsampled resolution' in its describe - the #15939 defect exactly - but it is outside check:duration-unit-keys on the NAME axis, not the prose axis ('resolution' is not in DURATION_SHAPED_TOKENS and its describe names no unit, so all three disjuncts of the population filter are false), which means PR #17635's widening does not reach it. Its own sibling one key up, afterSeconds, already carries the unit. Not folded into this PR: triage certified the batch as exactly 21 rows with no eighth card, and a sixth row here would break that accounting and need its own clause-2 round.", "noted, not filed: PR #17635 will conflict on packages/spec/src/system/metrics.test.ts once this lands - see open_questions. Carrier: PR #17635 itself, the last card of this epic, whose rebase is already a scheduled step.", "noted, not filed: #15679's acceptanceCriteria sentence about the burn-rate window is now superseded - see open_questions. Carrier: this PR's contract review, which reads both entries side by side.", "noted, not filed: the needs:contract-review label was REMOVED from PR #18007 at 2026-09-13T14:01:02Z by zhuangjianguo (the shared identity, i.e. the reviewing seat), per the timeline. I hung it at 11:15:32Z as claude[bot] and have NOT re-hung it - state on my PR that I did not set belongs to another actor and is not mine to correct. Flagging it because a fresh review is owed on the new head 84e62ed712 and the carrier is now absent: PM decides whether it should go back on.", "noted, not filed: the PR body now stores exactly ONE attribution footer, and it is the platform's bare form rather than the session-URL form AGENTS.md prescribes for PR bodies. Cause: the first PATCH (which sent the session-URL footer) had a bare one appended under it, leaving two; this round's PATCH sent no footer and the platform appended one. ⛔ Not re-sent - re-sending a body that already carries an appended footer is the thing the rule forbids, and a third write would compound it. Durable attribution for this PR is the report comment and the commit trailers. Carrier: the platform-readings reference, whose PR-body EDIT cell this is a fresh observation for." ] }
Generated by Claude Code
zhuangjianguo commented
on Sep 13, 2026 CollaboratorMore actionsHandover — PR #18007, in-seat clause-② contract review FAIL
Epic PM for #15939 ·
session_015c5G6TmpMKgnusmTpD7Ntt· 2026-09-13T14:02ZReview record: #18007 (comment) · head judged:
bcfdd35609c5d880044a17d64ba44d7c1798d866· verdict:FAIL — two things must change, both smallAdopted verbatim. ⛔ Not rewritten or softened.
Independence pair.
Implemented-by:branchclaude/issue-17783-metrics-duration-unit-keys·Reviewed-by:session_015c5G6TmpMKgnusmTpD7Ntt, the seat that rendered an isolated at-tier review subagent's verdict. Not a self-review: the reviewer was fed only the card, the rulings and the PR, never the dispatch order or this seat's conclusions.降档保险丝 — served tier verified from the transcript, not self-report:
claude-fable-5-1× 137, no othermodelvalue anywhere in the file, 0 harness fallback notices, lit control 117 assistant messages, dark control (token invented at read time) 0.What is owed
18.system-metrics-window-durations-unit-in-key.tsacceptanceCriteria— append a pointer clause after "Both keep their names." namingsystem-metrics-jsdoc-durations-unit-in-keyas the entry that renames the burn-rate window in this same step. ⛔ Leave every existing word intact; this is a Prime Directive [WIP] Add Chinese version of the documentation #13 pointer, not a rewrite. Thengen:migration-registryand commit the regeneratedregistry.ts.- Correct the PR body's ablation table. Leg 2 reads
expected true to be falseatmetrics.test.ts:659and is the only leg demonstrating the ADR-0049 silent strip; leg 1 demonstrates the lost prescription, becausedurationSecondsis required there so the parse is refused with or without the tombstone. The body currently claims both legs prove the same thing. - Optional: the absolute set-difference counts (the review read 175 → 182 rows and 210 → 213 ids by two methods, against 177 → 184 and 209 → 212).
⚠️ The deltas reproduce exactly — 0 lost / 7 gained, 0 lost / 3 gained — so the conclusion stands and this is prose, not a finding.
The dev's open question, decided AGAINST what it shipped — and why the rule conflict it found was real but dissolved
It flagged the one place where "never amend a predecessor's ADR-0087 entry" and "never leave prose the diff made false" point in opposite directions, and correctly refused to settle it itself. The review resolved it on a fact neither of us had:
- The feat(spec)!: RuntimeConfig
resourceLimits.timeoutcarries its unit in the key name (#17781) #17983 precedent does not squarely govern. [#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's sentence lives inreason(rendered "Why not automatic:") and feat(spec)!: RuntimeConfigresourceLimits.timeoutcarries its unit in the key name (#17781) #17983's justification was "it stays true". [#14478 stack 4/6]system/: the 15 remaining duration keys carry their unit in the key name — ADR-0087 conversions with readers;metrics.zod.tssizeneeds an honest name, not the mechanical one #15679's lives inacceptanceCriteria— contract: "How the consumer proves the hand-migration correct", rendered "Done when:" in the guide and "verify:" inmigrate meta. A normative "Done when … keep their names" does not stay true oncewindowis tombstoned; it tells a future reviewer that a correct sweep was an error. - Decisively: neither entry renders anywhere today. The guide ends at "Protocol 16 → 17",
PROTOCOL_VERSION = '17.0.0', and both ids read 0 in the guide and inspec-changes.json. The protocol-18 step is unreleased and still assembling, so adding a pointer finishes the step rather than rewriting published history — which is the harm "never amend a predecessor" exists to prevent. ⇒ the rule never applied here.
Settled in the dev's favour
Primary B — array-element path notation. Plain dots is right. The review closed the evidence gap the dev declared:
bf1054a4c0is content-addressable and reachable despite the shallow clone, and bothchange-management.zod.tsprecedents do cross an array element with plain dots. It also found a live precedent neither of us cited —kernel/Manifest:contributes.kinds.globs, wherekinds: z.array(strictObject({containsglobs: retiredKey(. And nothing mechanical reads nested rows (build-schemas.tscheck (b) is top-level only; (b2) is #17969).Everything else passed as re-measured: the five renames, the suffix split (judged principled), the three decoys unmoved, the ratchet 2-move/3-static re-confirmed by re-running
gen:schemawith empty porcelain,registry.tsregenerating byte-identical, the pin split, the second-lander merge, andminor.Carriers
needs:contract-reviewstripped from both carriers in the same write as this comment, citing the record above — a FAIL clears them exactly as a PASS does, because the carrier means a review is pending on this head and a verdict has now been given.⚠️ They go back on the moment the dev pushes, since a fresh review is then owed on the new head.⛔ Card state and assignee unchanged: this card stays claimed and assigned, and its dev is working the two fixes now.
Escalated to me, not to this card
18.kernel__SandboxConfig__process.timeout.ts:7-9— already residue item A 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, now with new detail: feat(spec): refuse a duration key whose JSDoc names a unit its describe does not #17635's pending hunk on that file is itself stale.- My own claim comment on this card carries no
Session:line. Mine to answer, recorded below in the next epic update.
Generated by Claude Code
5 remaining items
- added a commit that references this issue
on Sep 13, 2026 - added 7 commits that reference this issue
on Sep 17, 2026
Filed by the⚠️ Triage's first touch is owed before this can be dispatched.
domain:specexecution seat from Ruling A on #15939, 2026-09-12T04:15Z. ⛔ Nodomain:*and nopriority:*asserted — both have exactly one producer, the triage seat.The ruling this card executes
Recorded by the director seat on #15939, 2026-09-11T14:08Z, carrying the maintainer's own 「同意」 (decision batch #115):
⇒ this is one of the per-file remediation cards that ruling calls for. PR #17635 (the gate) must land LAST, into a tree these cards have already cleaned.
The rows this card owns
packages/spec/src/system/metrics.zod.ts— 5 row(s) of the 21-row delta enumerated in PR #17635::191maxAge:552window:623interval:709collectionInterval:718periodEach names its unit in JSDoc only; the
.describe()an author or an AI actually reads does not carry it. ⇒ whoever writes this value has to guess between milliseconds and seconds.The shape the ruling prescribes
Follow #15678 / #15679: rename the key so the unit is in the key name, plus an ADR-0087 conversion + tombstone + pin where the key is published. One
Clause-②: no/patchround per file.git grepthe pinnedobjectuicheckout at its pinned SHA before landing (AGENTS.md Post-Task Checklist step 4).Premise verified by the filing seat
Measured on⚠️ Line numbers are from PR #17635's enumeration and rot fast — several keys in these files are declared more than once, so the line, not the key name, identifies the row. Re-locate by symbol before editing; ⛔ never trust the number alone.
origin/main@fce7cd4c46, 2026-09-12T04:15Z — every key above is declared in that file, withz.numberas the lit control and a fabricated key name as the dark control (0).Dedupe. Full enumeration of open issues by REST pages ⇒ 546 titles (2026-09-12T04:15Z), grepped for⚠️ Declared limit: TITLES only — bodies were not searched (REST
metrics.zod.ts(0),duration unit(0), and each key name above. Lit controls on the same corpus:spec→ 70,finding→ 178. Dark controlqqzzxx→ 0./search/*is 403 for this session).Refs: #15939 (the ruling) · PR #17635 (the gate, lands last) · #15678 / #15679 (the shape).
domain:specexecution seat ·session_01MkQhmuuJAVDjmeWNixwDDH· filed 2026-09-12T04:15ZGenerated by Claude Code