You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
[finding] check:duration-unit-keys judges the new JSDoc-divergence rule on isDurationShaped() alone while its census also admits keyUnits — two rows of the ruled shape (latencyMs, frequencyHours) escape the zero #18075
Found by the context-isolated contract review of PR #17635 at head 605b7f60a4 (review record), and re-measured independently by the filing seat before filing. The PR itself is correct and is landing — this is a gap in the rule it introduces, not a defect in the PR, and it sits outside its landing scope under ruling A's sequencing (remediation first, gate last).
The gap
check:duration-unit-keys's census admits a key three ways — proseUnits > 0 || durationShaped || keyUnits > 0 — but the new unit-in-jsdoc-not-in-describeverdict is gated on site.durationShaped alone. isDurationShaped() (packages/spec/scripts/check-duration-unit-keys.ts:353) is a name-token list of 25 tokens (:269-273). So a key admitted to the census by keyUnits — its unit is in the name — is reported in --list but can never be judged by the new rule, even when it is exactly the shape #15939 ruled should be refused.
Two rows on the head tree are in precisely that state, both the same shape as the three #17786 rows that were remediated:
Taken at head 605b7f60a4, 2026-09-13T18:00Z. Both sites read directly out of the tree (git show), and the predicate measured by parsing the actual Set literal:
DURATION_SHAPED_TOKENS — set size 25
timeout YES (lit control) latency no
interval YES (lit control) frequency no
delay YES (lit control) dark control 'zzqv7x' no
window YES (lit control)
⚠️A methodology note worth keeping, because it nearly produced a false finding: a first attempt measured membership with grep -o "…new Set(\[[^]]*\]" | grep -qw "'$t'", which reported no for every token including timeout. The Set literal spans four lines, so the single-line grep -o matched nothing and every answer was a broken-instrument zero. It was caught only because timeout was included as a lit control and is visibly in the set. The reading above re-does it by parsing the multi-line literal.
The review also reports, from its own fixtures: latencyMs shape → durationShaped=false, not refused; durationMs shape → refused. Its --list on the head shows both rows carrying [jsdoc: X] [prose: -].
The related self-test hole, same fix
The review's mutation testing found the direction pin is partial. Ablating jsdocTextOf → undefined correctly turns 5 cases red, so the positive controls are live. But mutant M1 — adding && site.keyUnits.length === 0, i.e. letting a unit in the name make the JSDoc satisfy the rule without a describe — passes the entire self-test (exit 0) and reads 0 offenders on head. The two pins the PR body quotes cannot fail under M1; they are vacuous for that back door. The gate as written is correct, but that guarantee is unpinned.
Suggested fix shape — for the triaging seat, not a ruling
In ruling A's own order (remediation before rule widening):
Then widen the branch to (site.durationShaped || site.keyUnits.length > 0).
Add a positive control whose name carries the unit (e.g. timeoutMs + JSDoc ms + no describe → refused), so mutant M1 goes red.
⛔ Order matters: widening first turns the gate red on main, which is exactly the sequencing ruling A rejected as option B.
One knock-on, recorded rather than acted on
The changeset landing with #17635 says the gate "refuses the DIVERGENCE … (or there is no describe at all)", which over-claims by these two rows. The review judged this not blocking and the filing seat adopted that: pulling a queued PR to reword one changeset sentence costs a full re-run and re-review, and the honest place to tighten the wording is the changeset of whichever PR widens the predicate. Recording it here so it is not lost — ⛔ the CHANGELOG text is not to be edited in place afterwards.
Dedupe
Searched for an existing card on this predicate and these two sites: only #15939 (the parent finding, open) and #17783 (a remediation card, closed) match, neither covering the keyUnits-admitted rows. The gate's census/verdict split is not the subject of #15642 (scanned subtree) or #15682 (walk width) either.
Filed by the epic PM for #15939, session_015c5G6TmpMKgnusmTpD7Ntt, 2026-09-13T18:00Z. Surfaced by the at-tier contract review of #17635; every reading above re-taken by the filing seat.
Found by the context-isolated contract review of PR #17635 at head
605b7f60a4(review record), and re-measured independently by the filing seat before filing. The PR itself is correct and is landing — this is a gap in the rule it introduces, not a defect in the PR, and it sits outside its landing scope under ruling A's sequencing (remediation first, gate last).The gap
check:duration-unit-keys's census admits a key three ways —proseUnits > 0 || durationShaped || keyUnits > 0— but the newunit-in-jsdoc-not-in-describeverdict is gated onsite.durationShapedalone.isDurationShaped()(packages/spec/scripts/check-duration-unit-keys.ts:353) is a name-token list of 25 tokens (:269-273). So a key admitted to the census bykeyUnits— its unit is in the name — is reported in--listbut can never be judged by the new rule, even when it is exactly the shape #15939 ruled should be refused.Two rows on the head tree are in precisely that state, both the same shape as the three
#17786rows that were remediated:.describe()packages/spec/src/ai/usage.zod.ts:52latencyMs/** Wall-clock latency in milliseconds. */packages/spec/src/system/tenant.zod.ts:603frequencyHours/** Backup frequency in hours */'Backup frequency'— names no unitReadings
Taken at head
605b7f60a4, 2026-09-13T18:00Z. Both sites read directly out of the tree (git show), and the predicate measured by parsing the actualSetliteral:grep -o "…new Set(\[[^]]*\]" | grep -qw "'$t'", which reportednofor every token includingtimeout. TheSetliteral spans four lines, so the single-linegrep -omatched nothing and every answer was a broken-instrument zero. It was caught only becausetimeoutwas included as a lit control and is visibly in the set. The reading above re-does it by parsing the multi-line literal.The review also reports, from its own fixtures:
latencyMsshape →durationShaped=false, not refused;durationMsshape → refused. Its--liston the head shows both rows carrying[jsdoc: X] [prose: -].The related self-test hole, same fix
The review's mutation testing found the direction pin is partial. Ablating
jsdocTextOf→undefinedcorrectly turns 5 cases red, so the positive controls are live. But mutant M1 — adding&& site.keyUnits.length === 0, i.e. letting a unit in the name make the JSDoc satisfy the rule without a describe — passes the entire self-test (exit 0) and reads 0 offenders on head. The two pins the PR body quotes cannot fail under M1; they are vacuous for that back door. The gate as written is correct, but that guarantee is unpinned.Suggested fix shape — for the triaging seat, not a ruling
In ruling A's own order (remediation before rule widening):
.describe()is silent. Same shape as spec: three duration keys already name their unit in the key but not in.describe()— #15939 Ruling A remediation (the 3 describe-only rows of the 21-row delta) #17786.(site.durationShaped || site.keyUnits.length > 0).timeoutMs+ JSDoc ms + no describe → refused), so mutant M1 goes red.⛔ Order matters: widening first turns the gate red on
main, which is exactly the sequencing ruling A rejected as option B.One knock-on, recorded rather than acted on
The changeset landing with #17635 says the gate "refuses the DIVERGENCE … (or there is no describe at all)", which over-claims by these two rows. The review judged this not blocking and the filing seat adopted that: pulling a queued PR to reword one changeset sentence costs a full re-run and re-review, and the honest place to tighten the wording is the changeset of whichever PR widens the predicate. Recording it here so it is not lost — ⛔ the CHANGELOG text is not to be edited in place afterwards.
Dedupe
Searched for an existing card on this predicate and these two sites: only #15939 (the parent finding,
open) and #17783 (a remediation card,closed) match, neither covering thekeyUnits-admitted rows. The gate's census/verdict split is not the subject of #15642 (scanned subtree) or #15682 (walk width) either.Filed by the epic PM for #15939,
session_015c5G6TmpMKgnusmTpD7Ntt, 2026-09-13T18:00Z. Surfaced by the at-tier contract review of #17635; every reading above re-taken by the filing seat.Generated by Claude Code