Skip to content

fix(scripts): measure the entry-guard probe's noise floor so both derived gates share one NODE_OPTIONS - #17764

Merged
claude[bot] merged 1 commit into
mainfrom
claude/issue-15234-gate-family-node-options
Sep 12, 2026
Merged

claude[bot] merged 1 commit into
mainfrom
claude/issue-15234-gate-family-node-options

Conversation

@claude

@claude claude Bot commented Sep 12, 2026 •

Copy link
Copy Markdown
Contributor

Closes #15234

Clause-②: no

Two gates derived for the same change surface disagreed about what NODE_OPTIONS may be, and following the correct advice of one turned the other red.

What was wrong

check-required-contexts.mjs --verify-required-set exits 2 = NOT VERIFIED without --use-env-proxy and prints that remedy itself, citing #9642 for why the inference it prevents matters. dispatch-gates derives both gates for the same change surface, so a seat runs them in one batch under one NODE_OPTIONS. Under that flag node opens every child process with two stderr lines, before any user code runs:

(node:NNN) [UNDICI-EHPA] Warning: EnvHttpProxyAgent is experimental, expect them to change at any time.
(Use `node --trace-warnings ...` to show where the warning was created)

The entry-guard case in check-cross-package-test-inputs.mjs --self-test asserted a literally empty child stderr. So obeying gate A turned gate B red on a tree the seat had just changed, and the natural reading is "my diff broke the entry guard". The other direction is worse because it is silent: a seat that reads gate A's exit 2 as noise never runs the required-set sweep at all, which is the entire reason #9642 exists.

The fences, held

Neither assertion was the defect, and neither is weakened. The entry-guard property is still pinned; gate A's proxy advice is still printed, and check-required-contexts.mjs is not touched by this PR.

The direction chosen, and the evidence that decided it

Triage listed three candidates and deliberately chose none. Two were rejected on measurement, not taste:

Rejected — filter node's warning shape. Triage described this as ignoring lines matching (node:NNN) [...] Warning:. Measured against the real output, that shape matches only the first of the two lines:

MATCHED   | (node:7905) [UNDICI-EHPA] Warning: EnvHttpProxyAgent is experimental, ...
UNMATCHED | (Use `node --trace-warnings ...` to show where the warning was created)

A filter written to that description leaves the hint line behind and the case stays red. Worse, any such filter would also swallow a line the module wrote wearing the same shape.

Rejected — --no-warnings, or stripping the flag for that spawn. Both mask real runtime warnings, and both are a per-flag allowlist: the next NODE_OPTIONS value that makes node talk re-breaks the case.

Chosen — measure the noise floor instead of describing it. An identical child that imports nothing is spawned first, with the same argv shape and the same inherited env. Whatever it prints is what this runtime prints unprompted; anything the real probe prints beyond that came from the import. No pattern describes the noise, so this cannot rot when node changes its warning text. The single normalisation is node's pid, which differs between the two children by construction; nothing else about the text is touched.

Four-way control (acceptance item 2)

Same tree, same commit, flag as the only variable:

gate no flag --use-env-proxy
A check-required-contexts.mjs --verify-required-set 2 (NOT VERIFIED, unchanged) 0
B check-cross-package-test-inputs.mjs --self-test — before 0 1 (the spurious red)
B — after this PR 0 (156 cases) 0 (156 cases)

Both gates now pass under one NODE_OPTIONS=--use-env-proxy --max-old-space-size=4096. Gate A's exit 2 without the flag is unchanged and is not a failure of the tree: it classifies the environment.

Positive controls (acceptance item 3)

The case must not be green because it looks at nothing. Four controls are added beside the two original cases and all run in both environments:

  • NEGATIVE CONTROL — importing a module that writes nothing is clean under this runtime.
  • POSITIVE CONTROL — one line the module writes to stderr still reds.
  • POSITIVE CONTROL — a module line disguised as a node warning still reds. This is the case that separates a measured baseline from a shape filter: a shape filter swallows it, subtracting a measured baseline cannot, because the baseline child never wrote it.
  • POSITIVE CONTROL — a module that writes to stdout still reds; that half of the assertion is an exact match and is untouched.

The battery floor for the entry guard, driven for real rises 2 -> 6 so the controls cannot be dropped back out quietly.

Ablation — the real case, not only the controls

A console.error was inserted at module scope, outside the entry guard, so it runs on import. On-disk landing was proven by marker count (0 before, 1 after) and by blob hash before and after; restore was proven by hash equality with the HEAD blob and an empty git diff HEAD, from a trap.

no flag --use-env-proxy
mutated 1 — FAIL importing this module prints NOTHING OF ITS OWN 1 — same single FAIL

The real case reds in both environments, so the green above was not bought with blindness.

The boundary of this measurement

⚠️ What this probe cannot see, since that is not legible from the green.

The subtraction assumes the two children's runtime noise is identical apart from the pid. That holds for everything driven here, but it is an assumption about node's behaviour, not a property proved of it. If some NODE_OPTIONS value ever makes the runtime write something that varies between two otherwise-identical spawns — a port, a temp path, an elapsed-time figure, any token minted per process — the two stderrs differ, that difference is attributed to the import, and the case goes red with the whole probe stderr quoted at it.

That is the opposite failure from the one fixed here, and it is the safer of the two: it is loud rather than silent, and it cannot hide a real entry-guard regression — it can only manufacture a false one. It is a real boundary all the same, and it is not measured.

⭐ What was actually driven is --use-env-proxy, plus the no-flag control, as the four-way table above records. Nothing else in the NODE_OPTIONS space was exercised, so that flag is the single value this baseline is known to be stable under.

⇒ If it ever bites, the remedy is to normalise the varying token at the line, exactly as the pid is normalised today, with the reason recorded beside it. ⛔ Not by widening the comparison into a general "ignore output" filter — that is precisely the trade this PR exists to refuse.

Reverse-read — what this makes false, zeros included

Two sentences, both re-judged in place and neither deleted:

  1. SELF_TEST_BATTERIES['the entry guard, driven for real'] = 2 — raised to 6, with the reason recorded at the line.
  2. The case label importing this module prints NOTHING — now prints NOTHING OF ITS OWN. Read literally, the old wording was already false under the flag; that was the defect.

The zeros, each one read rather than assumed:

  • 0 sentences in docs/**, content/docs/** or AGENTS.md are falsified; the entry-guard probe's stderr assertion is described nowhere outside the file.
  • 0 changes owed to check-required-contexts.mjs; every --use-env-proxy mention in the tree was read and all 21 remain true.
  • 0 movement in the check-published-list-mirrors row for this module: it governs RECOGNISED_PATH_SPELLINGS against AGENTS.md, which this PR does not touch. Gate re-run green.
  • 0 governed surfaces touched.

Changeset — measured, not assumed

skip-changeset, applied as a label. Root manifest is private: true; of the 70 published packages with a files[], 0 ship anything under repo-root scripts/, and the edited path is outside every package directory. Nothing published moves.

验收备注

Noted, not filed: none beyond the finding below.

Filed as #17765: the same defect exists in scripts/pm/dispatch-gates.mjs, whose own self-test asserts (imported.stderr ?? '').trim() === '' on a spawned consumer and reds identically under the flag (measured directly on that consumer shape). It is not fixed here: the landing point for this card is the two gates it names, and that file's self-test exceeds this container's 10-minute foreground cap (it timed out at BASE too, before this diff), so an edit there could not be verified in this run.


Generated by Claude Code

…asserting empty stderr

`check-required-contexts.mjs --verify-required-set` exits 2 = NOT VERIFIED
without `--use-env-proxy` and prescribes exactly that flag. Both gates are
derived for the same change surface, so they run in one batch under one
NODE_OPTIONS -- and under that flag node opens every child with the
UNDICI-EHPA experimental warning plus its `--trace-warnings` hint. The
entry-guard case asserted a literally empty child stderr, so obeying the
other gate turned it red on a tree the seat had just changed.

Neither assertion was wrong, so neither is weakened. The probe now spawns an
identical child that imports nothing, and compares against what that child
prints: anything beyond the measured floor came from the import. Node's pid
is the only normalisation.

Not a shape filter: node's hint line carries no `[CODE] Warning:`, so a
filter written to that shape leaves it behind -- and would swallow a module
line wearing the same shape. Four controls pin this, including a module line
disguised as a node warning, and the battery floor rises 2 -> 6 to hold them.

Closes #15234

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012GKcPZbMoGq7WPzKLfRBTU
@claude claude Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 12, 2026
@claude
claude Bot marked this pull request as ready for review September 12, 2026 03:33
@claude
claude Bot enabled auto-merge September 12, 2026 03:33
@claude
claude Bot added this pull request to the merge queue Sep 12, 2026
Merged via the queue into main with commit e4e22c7 Sep 12, 2026
54 checks passed
@claude
claude Bot deleted the claude/issue-15234-gate-family-node-options branch September 12, 2026 04:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

1 participant