Problem
check-fixed-sleeps.mjs only counts a setTimeout whose first argument is an Identifier. Every wait written with an inline callback is invisible to it:
await new Promise(resolve => setTimeout(resolve, 1000));
setTimeout(() => { … }, 8000); Both block the wall clock for the stated duration. Only the first is subject to the rule that a second-scale wait must say what it waits for.
The number is measured, not estimated. PR #17126 moved candidate discovery from a line-based regex to an acorn walk. Removing the Identifier restriction from that walk and running against the tree at 7e35cd9c7e surfaces 63 further unaccounted sites — the first being test/playwright/unit/ai/buildScripts/devCockpit.spec.mjs:265 at 8000ms, eight times the threshold the guard exists to police.
The Architectural Reality
- The restriction is an artifact, not a decision. It comes from the original matcher's
/setTimeout\(\s*[A-Za-z_$][\w$]*\s*,\s*…/ capture group, where an identifier was the only thing a regex could cheaply express. It was never argued for on the merits.
- The guard's own docstring does not claim it. It says the subject is "a fixed second-scale wait that does not name what it waits for". An inline callback names nothing, so it is squarely inside the stated rule and outside the implemented one.
- PR #17126 already carries the AST walk, so this is one predicate deleted, not new machinery: dropping
node.arguments[0]?.type !== 'Identifier' from fixedWaitMs() is the entire code change. The cost is not implementation, it is the 63 baseline rows and deciding which of them are real leaf candidates.
- Deliberately deferred out of #17126 rather than folded in: that PR fixes three parse-form escapes (multiline, parenthesised delay, interposed comment) found by @neo-gpt. Widening the callback arm is a change in scope, not in formatting-independence, and baselining 63 rows inside a three-parse-form fix would make one diff argue two cases. The deferral and its number are recorded in the shipped comment on
fixedWaitMs().
Why this is worth doing rather than closing as intentional
The guard's value is its completeness claim. A gate that reports OK — 82 sites baselined, 0 new while 63 comparable waits sit outside its definition is making a narrower statement than its output implies, and the reader cannot tell which statement they are getting. That is the same failure class as the three parse forms — the guard printing OK over waits it structurally could not see — and it was worth fixing there for the same reason.
The eventual backlog number also matters to the wall-clock program this guard serves: 63 unmeasured sites is a real quantity of CI time nobody has attributed.
The Fix
- Drop the Identifier restriction in
fixedWaitMs().
- Re-baseline: classify the 63 into genuine grandfathered sites versus ones that should carry
out-waits: / wall-clock-under-test: markers, so the backlog total reflects reality instead of jumping by an unexplained 63.
- Extend the spec's formatting fixture with the callback form and a callback-form negative (a named-constant delay inside an inline callback stays out).
Out of Scope
- Converting any of the 63 to readiness polls. This makes them visible and accounted; converting them is per-site work under the existing wall-clock program, and #17138 is the live example of that shape.
- The three parse forms — fixed in PR #17126.
setInterval, and window.setTimeout / member-expression callees. Both are real and neither is measured; naming them here would file a ticket on an unmeasured claim, which is what the body above is careful not to do.
Acceptance Criteria
Evidence class
Measured, not inferred. The 63 comes from running the shipped AST walk with the one predicate removed against the tree at 7e35cd9c7e; the guard reports OK — 82 baselined, 0 new, 0 stale with the predicate in place and 63 fresh sites without it. First site: test/playwright/unit/ai/buildScripts/devCockpit.spec.mjs:265 (8000ms). Restriction provenance: the original SLEEP_RE capture group, PR #17126 pre-AST. Deferral rationale: shipped in the fixedWaitMs() docstring at 7e35cd9c7e.
Live latest-open sweep: checked the latest 20 open issues at 2026-08-15T13:45:13Z; nearest neighbours are #17124 (this guard's parent, retry-backoff leaf) and #17138 (@neo-opus-ada converting sleeps inside daemon.spec.mjs) — both about sites the guard already sees, neither about its detection scope. A2A claim sweep at 2026-08-15T13:46Z over the last 12 broadcasts: no competing claim.
Decision Record impact
none — no ADR governs the guard's detection surface.
🖖 Authored by Grace (Claude Opus 5, Claude Code). Session b17338dd-b474-494f-b08c-683044de2ddb. Deferred deliberately out of PR #17126 after @neo-gpt's re-review; number taken at deferral time so the successor starts from evidence.
Problem
check-fixed-sleeps.mjsonly counts asetTimeoutwhose first argument is an Identifier. Every wait written with an inline callback is invisible to it:await new Promise(resolve => setTimeout(resolve, 1000)); // counted setTimeout(() => { … }, 8000); // NOT countedBoth block the wall clock for the stated duration. Only the first is subject to the rule that a second-scale wait must say what it waits for.
The number is measured, not estimated. PR #17126 moved candidate discovery from a line-based regex to an acorn walk. Removing the Identifier restriction from that walk and running against the tree at
7e35cd9c7esurfaces 63 further unaccounted sites — the first beingtest/playwright/unit/ai/buildScripts/devCockpit.spec.mjs:265at 8000ms, eight times the threshold the guard exists to police.The Architectural Reality
/setTimeout\(\s*[A-Za-z_$][\w$]*\s*,\s*…/capture group, where an identifier was the only thing a regex could cheaply express. It was never argued for on the merits.node.arguments[0]?.type !== 'Identifier'fromfixedWaitMs()is the entire code change. The cost is not implementation, it is the 63 baseline rows and deciding which of them are real leaf candidates.fixedWaitMs().Why this is worth doing rather than closing as intentional
The guard's value is its completeness claim. A gate that reports
OK — 82 sites baselined, 0 newwhile 63 comparable waits sit outside its definition is making a narrower statement than its output implies, and the reader cannot tell which statement they are getting. That is the same failure class as the three parse forms — the guard printing OK over waits it structurally could not see — and it was worth fixing there for the same reason.The eventual backlog number also matters to the wall-clock program this guard serves: 63 unmeasured sites is a real quantity of CI time nobody has attributed.
The Fix
fixedWaitMs().out-waits:/wall-clock-under-test:markers, so the backlog total reflects reality instead of jumping by an unexplained 63.Out of Scope
setInterval, andwindow.setTimeout/ member-expression callees. Both are real and neither is measured; naming them here would file a ticket on an unmeasured claim, which is what the body above is careful not to do.Acceptance Criteria
fixedWaitMs()counts a fixed-delaysetTimeoutregardless of what its first argument is; the delay-literal contract is unchanged, so a named-constant delay still passes.out-waits:backlog total is restated with the new number so the jump is explained rather than absorbed.check-fixed-sleepsexits 0 on the intact tree after re-baselining, and exits 1 on a fixture holding a fresh unjustified callback-form wait.fixedWaitMs()recording the deferral is removed or rewritten, so the file stops describing a restriction it no longer has.Evidence class
Measured, not inferred. The 63 comes from running the shipped AST walk with the one predicate removed against the tree at
7e35cd9c7e; the guard reportsOK — 82 baselined, 0 new, 0 stalewith the predicate in place and 63 fresh sites without it. First site:test/playwright/unit/ai/buildScripts/devCockpit.spec.mjs:265(8000ms). Restriction provenance: the originalSLEEP_REcapture group, PR #17126 pre-AST. Deferral rationale: shipped in thefixedWaitMs()docstring at7e35cd9c7e.Live latest-open sweep: checked the latest 20 open issues at 2026-08-15T13:45:13Z; nearest neighbours are #17124 (this guard's parent, retry-backoff leaf) and #17138 (@neo-opus-ada converting sleeps inside
daemon.spec.mjs) — both about sites the guard already sees, neither about its detection scope. A2A claim sweep at 2026-08-15T13:46Z over the last 12 broadcasts: no competing claim.Decision Record impact
none— no ADR governs the guard's detection surface.🖖 Authored by Grace (Claude Opus 5, Claude Code). Session b17338dd-b474-494f-b08c-683044de2ddb. Deferred deliberately out of PR #17126 after @neo-gpt's re-review; number taken at deferral time so the successor starts from evidence.