Observation filed from stack card 4/6 of #14478 (PR #15938). Not fixed there — it is outside that card's population and touches the gate's own reading surface, which card 6/6 owns.
The shape
check:duration-unit-keys reads a key's unit prose from its .describe() (and from .meta({ description })), and its own self-test pins that behaviour: "a describe declared through .meta({ description }) is READ — no exemption by blindness". It does not read the JSDoc block immediately above the key.
So a duration-shaped z.number() whose name carries no unit, and whose unit is stated only in its JSDoc, is invisible to the population. It is not exempt by either declared structural exemption (EpochMs, externalVocabulary) — it simply never enters the count. The rule's own premise is that the unit must not live in prose alone; here it lives in a less discoverable prose channel than the one the rule already rejects.
Three verified instances, read rather than pattern-matched
Measured on claude/issue-15679-system-duration-unit-in-key-name @ 45a35896c:
| site |
JSDoc says |
.describe() says |
packages/spec/src/kernel/plugin-security-advanced.zod.ts:294 timeout |
"Execution timeout in milliseconds" |
'Maximum execution time' — no unit |
packages/spec/src/system/metrics.zod.ts:552 window |
"Window size in seconds" |
'Window size' — no unit |
packages/spec/src/system/metrics.zod.ts:626 interval |
"Export interval in seconds" |
no .describe() at all |
Why the first one is the sharp end
RuntimeConfig.resourceLimits.timeout is not a hypothetical. Card 3/6 (#15678) reasoned about it explicitly and pinned it as out of population, recording in its changeset that the key "names no unit anywhere in its prose". That reading is measurably imprecise — the JSDoc two lines above it names milliseconds. The conclusion (leave it alone, it is outside the gate's population) is still correct as a statement about the gate, but the reason recorded for a future author is wrong, and a pin test now holds it in place.
That is the cost of the blindness: it does not just miss keys, it produces confident wrong prose about why they were missed.
Not a duplicate of #15642
#15642 reports that the gate scans packages/spec/src/** only, so the same offender shape survives in other workspace packages. That is about the scanned subtree. This is about which prose channel is read inside a file the gate already scans. Both could be true and fixed independently.
Sizing — deliberately not asserted
A crude heuristic scan of packages/spec/src/**/*.zod.ts (duration-shaped numeric key, no unit token in the name, no unit in its describe, unit present in the JSDoc above) returns 60 candidate sites. That number is not a population count: reading a sample shows it includes clear false positives — byte and percentage measurements, counts, and keys that already carry a unit token. The three rows above are the ones actually read and confirmed. The real count needs the gate's own predicate applied to the JSDoc channel, which is the fix, not the triage.
Options, for whoever picks this up
- Read the JSDoc as a prose channel too. Most faithful to the rule's premise; expands the population, so it wants its own card and its own before/after count.
- Refuse a duration-shaped key whose JSDoc names a unit its describe does not. Narrower: it treats the divergence itself as the defect, and pushes authors to move the unit into the describe, where the gate and the reference pages both see it.
- Leave it and correct the prose. Fix card 3/6's pin note so the recorded reason is true, and accept the blindness as declared behaviour.
I have no recommendation between these — the choice is a rule change and belongs with the maintainer ruling that shaped #14478.
Generated by Claude Code
Observation filed from stack card 4/6 of #14478 (PR #15938). Not fixed there — it is outside that card's population and touches the gate's own reading surface, which card 6/6 owns.
The shape
check:duration-unit-keysreads a key's unit prose from its.describe()(and from.meta({ description })), and its own self-test pins that behaviour: "a describe declared through.meta({ description })is READ — no exemption by blindness". It does not read the JSDoc block immediately above the key.So a duration-shaped
z.number()whose name carries no unit, and whose unit is stated only in its JSDoc, is invisible to the population. It is not exempt by either declared structural exemption (EpochMs,externalVocabulary) — it simply never enters the count. The rule's own premise is that the unit must not live in prose alone; here it lives in a less discoverable prose channel than the one the rule already rejects.Three verified instances, read rather than pattern-matched
Measured on
claude/issue-15679-system-duration-unit-in-key-name@45a35896c:.describe()sayspackages/spec/src/kernel/plugin-security-advanced.zod.ts:294timeout'Maximum execution time'— no unitpackages/spec/src/system/metrics.zod.ts:552window'Window size'— no unitpackages/spec/src/system/metrics.zod.ts:626interval.describe()at allWhy the first one is the sharp end
RuntimeConfig.resourceLimits.timeoutis not a hypothetical. Card 3/6 (#15678) reasoned about it explicitly and pinned it as out of population, recording in its changeset that the key "names no unit anywhere in its prose". That reading is measurably imprecise — the JSDoc two lines above it names milliseconds. The conclusion (leave it alone, it is outside the gate's population) is still correct as a statement about the gate, but the reason recorded for a future author is wrong, and a pin test now holds it in place.That is the cost of the blindness: it does not just miss keys, it produces confident wrong prose about why they were missed.
Not a duplicate of #15642
#15642 reports that the gate scans
packages/spec/src/**only, so the same offender shape survives in other workspace packages. That is about the scanned subtree. This is about which prose channel is read inside a file the gate already scans. Both could be true and fixed independently.Sizing — deliberately not asserted
A crude heuristic scan of
packages/spec/src/**/*.zod.ts(duration-shaped numeric key, no unit token in the name, no unit in its describe, unit present in the JSDoc above) returns 60 candidate sites. That number is not a population count: reading a sample shows it includes clear false positives — byte and percentage measurements, counts, and keys that already carry a unit token. The three rows above are the ones actually read and confirmed. The real count needs the gate's own predicate applied to the JSDoc channel, which is the fix, not the triage.Options, for whoever picks this up
I have no recommendation between these — the choice is a rule change and belongs with the maintainer ruling that shaped #14478.
Generated by Claude Code