Commit 7ef2965
fix(scripts): classify a missing gate runner by resolving it, not by parsing shell prose (#17731)
Fixes #16717
`check:merge-driver` was permanently red on macOS, and the self-test
failure was the
cheap half of the problem. The expensive half is the field direction:
with both legs of
the runner-missing classifier blind, a genuinely **missing gate runner**
classified as
**`stale`** — the exact false claim the `PREREQUISITE NOT MET` grading
exists to prevent.
It told an operator to regenerate an artifact that nothing had read.
## What was broken, both legs, measured on this host
Darwin 25.5.0, pnpm 10.31.0.
**Leg 1, the shell line.** The matcher required a `\d+:` segment. macOS
`/bin/sh` writes
none — `sh: os-regen-fixture-absent-runner: command not found`. Measured
alongside it,
`zsh` writes the command **last** (`zsh:1: command not found: tsx`), a
fourth spelling the
old pattern could not have matched either.
**Leg 2, the exit code.** `exitCode === 127` never fired because the
immediate child is
`pnpm`, not the shell, and pnpm reports **1** here. Measured directly
with a throwaway
package whose only script is the absent command: `pnpm exit=1`. On Linux
the same path
yields 127, which is why CI stayed green while every macOS checkout was
red.
## What the classifier depends on now
Per the card, I did not simply add the macOS spelling. Prose matching is
not the only
signal available, and it should not be the primary one.
The classification now rests on **resolution** first: the gate's script
is a string in a
manifest this process can read, so the command it starts with is
**looked for on disk**.
That leg reads no prose and no exit code — it is a filesystem fact,
identical on every
platform, every shell and every pnpm. The two older legs remain as
fallbacks for what
resolution declines to judge (a compound command, a shell builtin, a
script the manifest
does not declare), with the shell line widened to the four spellings
actually measured.
**The searched set is deliberately a superset of pnpm's, and that
asymmetry is
load-bearing.** Measured inside a workspace package, pnpm puts two bin
directories on
PATH: the package's own `node_modules/.bin` and the repository root's.
The probe walks
every ancestor's plus PATH itself. A superset can only err toward
"found", which costs one
diagnosis. The opposite mistake would report a gate that really ran, and
really found the
artifact stale, as unmeasured — a refusal that swallows the finding it
was spawned for. A
dangling symlink counts as present for the same reason (`lstatSync` does
not follow).
**What would break it next.** Stated plainly, because this bug was a
silent one:
- The probe goes quiet, by design, when the gate's script is compound
(`a && b`), starts
with a shell builtin, or is not declared in the manifest pnpm was
pointed at. Those fall
through to the prose and exit-code legs — so a fifth shell spelling can
still cost a
diagnosis there, though no longer the whole classification.
- An undeclared script stays `stale` on purpose: measured, pnpm exits
254 and prints
nothing, and that is `gateCwd`'s documented fail-safe. The probe must
not convert it into
a refusal.
- If pnpm ever runs scripts with a PATH the upward walk does not cover,
the probe would
start claiming absence for runners that resolve. That is the dangerous
direction, so the
walk is deliberately wide rather than exact.
## Evidence
Watched failing before, passing after, on the macOS host that fails:
| | before | after (`b82e4ef5f`) |
|:--|:--|:--|
| `node scripts/check-regen-pending.mjs --self-test` | exit 1, the 2
named cases red | exit 0 |
| `pnpm check:merge-driver` | exit 1 | exit 0 |
The self-test's two previously-failing cases already asserted the
classification
(`EXIT_PREREQUISITE_NOT_MET` plus the diagnosis sentence), and they
still do. Added beside
them are classifier-level rows that call the classifier directly, so
every leg is judged on
every host rather than only on whichever (platform, shell, pnpm) the
fixture happens to run
on — which is precisely how this stayed green in CI.
**Each new row is load-bearing, proved by ablation** rather than
asserted. Fix committed
first, then one leg removed at a time; each mutation confirmed on disk
by blob hash, each
restore confirmed by `git diff HEAD` empty and the blob back to
`203605c9`:
| leg removed | rows that went red |
|:--|:--|
| widened shell line, old pattern restored | macOS `/bin/sh`; zsh — and
**only** those two |
| resolution probe | "neither prose nor 127" — and only that |
| `exitCode === 127` | "a bare 127" — and only that |
The third row is the Linux-path guarantee, constructed rather than
reasoned about: the
exit-127 case is carried by the 127 leg alone (its manifest resolves,
its prose matches
nothing), so removing that leg reds it and nothing else.
Controls, all green: a gate that ran and failed is still `stale`; a gate
whose own prose
contains "not found" is not reclassified; a script starting with a shell
builtin is judged
by resolution not at all; and with no gate handed over, the shell-line
leg still answers
alone.
## Gates
Derived from the actual diff with `node scripts/pm/dispatch-gates.mjs`
(35 families,
harvested with `--commands`): **32 green, 3 red — all three proved
pre-existing** by
re-running them with only this file reverted to the base commit, in the
same installed
tree:
- `check:bash32-floor` — red at base too; a macOS bash-3.2 harness fact,
same family as
this card.
- `check:browser-reachable-entries`, `check:generated` — red at base
too; this worktree has
no `packages/spec/dist`, and both gates say so themselves.
`check:docs` was red in the first sweep and green on re-run; the
variable was the
generated `json-schema/` tree, not this file. The three consumers that
import this module
take only `schemaTreeIsStale`, `distIsStale` and `declarationStamp` —
none of which this
change touches. Also green: `check:declaration-mirrors`, and the two
`packages/spec` test
files that import this module (2 files, 25 tests).
1 parent 9bd4344 commit 7ef2965
1 file changed
Lines changed: 208 additions & 15 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
84 | 84 | | |
85 | 85 | | |
86 | 86 | | |
87 | | - | |
| 87 | + | |
88 | 88 | | |
89 | 89 | | |
90 | 90 | | |
| |||
438 | 438 | | |
439 | 439 | | |
440 | 440 | | |
| 441 | + | |
| 442 | + | |
| 443 | + | |
| 444 | + | |
| 445 | + | |
| 446 | + | |
| 447 | + | |
| 448 | + | |
| 449 | + | |
| 450 | + | |
| 451 | + | |
| 452 | + | |
| 453 | + | |
| 454 | + | |
| 455 | + | |
| 456 | + | |
| 457 | + | |
| 458 | + | |
| 459 | + | |
| 460 | + | |
| 461 | + | |
| 462 | + | |
| 463 | + | |
| 464 | + | |
| 465 | + | |
| 466 | + | |
| 467 | + | |
| 468 | + | |
| 469 | + | |
| 470 | + | |
| 471 | + | |
| 472 | + | |
| 473 | + | |
| 474 | + | |
| 475 | + | |
| 476 | + | |
| 477 | + | |
| 478 | + | |
| 479 | + | |
| 480 | + | |
| 481 | + | |
| 482 | + | |
| 483 | + | |
| 484 | + | |
| 485 | + | |
| 486 | + | |
| 487 | + | |
| 488 | + | |
| 489 | + | |
| 490 | + | |
| 491 | + | |
| 492 | + | |
| 493 | + | |
| 494 | + | |
| 495 | + | |
| 496 | + | |
| 497 | + | |
| 498 | + | |
| 499 | + | |
| 500 | + | |
| 501 | + | |
| 502 | + | |
| 503 | + | |
| 504 | + | |
| 505 | + | |
| 506 | + | |
| 507 | + | |
| 508 | + | |
| 509 | + | |
| 510 | + | |
| 511 | + | |
| 512 | + | |
| 513 | + | |
| 514 | + | |
| 515 | + | |
| 516 | + | |
| 517 | + | |
| 518 | + | |
| 519 | + | |
| 520 | + | |
| 521 | + | |
| 522 | + | |
| 523 | + | |
| 524 | + | |
| 525 | + | |
| 526 | + | |
| 527 | + | |
| 528 | + | |
| 529 | + | |
| 530 | + | |
| 531 | + | |
| 532 | + | |
| 533 | + | |
| 534 | + | |
| 535 | + | |
| 536 | + | |
| 537 | + | |
| 538 | + | |
| 539 | + | |
| 540 | + | |
| 541 | + | |
441 | 542 | | |
442 | 543 | | |
443 | 544 | | |
| |||
486 | 587 | | |
487 | 588 | | |
488 | 589 | | |
| 590 | + | |
| 591 | + | |
| 592 | + | |
| 593 | + | |
489 | 594 | | |
490 | 595 | | |
491 | 596 | | |
492 | 597 | | |
493 | | - | |
| 598 | + | |
494 | 599 | | |
495 | 600 | | |
496 | 601 | | |
| |||
515 | 620 | | |
516 | 621 | | |
517 | 622 | | |
518 | | - | |
519 | | - | |
520 | | - | |
521 | | - | |
522 | | - | |
523 | | - | |
524 | | - | |
| 623 | + | |
| 624 | + | |
| 625 | + | |
| 626 | + | |
| 627 | + | |
| 628 | + | |
| 629 | + | |
| 630 | + | |
| 631 | + | |
| 632 | + | |
| 633 | + | |
| 634 | + | |
| 635 | + | |
| 636 | + | |
| 637 | + | |
| 638 | + | |
| 639 | + | |
| 640 | + | |
| 641 | + | |
| 642 | + | |
| 643 | + | |
| 644 | + | |
| 645 | + | |
| 646 | + | |
| 647 | + | |
| 648 | + | |
| 649 | + | |
| 650 | + | |
| 651 | + | |
| 652 | + | |
| 653 | + | |
| 654 | + | |
| 655 | + | |
| 656 | + | |
525 | 657 | | |
526 | 658 | | |
527 | 659 | | |
528 | 660 | | |
529 | 661 | | |
530 | 662 | | |
531 | | - | |
532 | | - | |
533 | | - | |
534 | | - | |
535 | | - | |
| 663 | + | |
| 664 | + | |
| 665 | + | |
| 666 | + | |
| 667 | + | |
| 668 | + | |
| 669 | + | |
| 670 | + | |
| 671 | + | |
| 672 | + | |
| 673 | + | |
| 674 | + | |
536 | 675 | | |
537 | 676 | | |
538 | 677 | | |
| |||
716 | 855 | | |
717 | 856 | | |
718 | 857 | | |
719 | | - | |
| 858 | + | |
720 | 859 | | |
721 | 860 | | |
722 | 861 | | |
| |||
1270 | 1409 | | |
1271 | 1410 | | |
1272 | 1411 | | |
| 1412 | + | |
| 1413 | + | |
| 1414 | + | |
| 1415 | + | |
| 1416 | + | |
| 1417 | + | |
| 1418 | + | |
| 1419 | + | |
| 1420 | + | |
| 1421 | + | |
| 1422 | + | |
| 1423 | + | |
| 1424 | + | |
| 1425 | + | |
| 1426 | + | |
| 1427 | + | |
| 1428 | + | |
| 1429 | + | |
| 1430 | + | |
| 1431 | + | |
| 1432 | + | |
| 1433 | + | |
| 1434 | + | |
| 1435 | + | |
| 1436 | + | |
| 1437 | + | |
| 1438 | + | |
| 1439 | + | |
| 1440 | + | |
| 1441 | + | |
| 1442 | + | |
| 1443 | + | |
| 1444 | + | |
| 1445 | + | |
| 1446 | + | |
| 1447 | + | |
| 1448 | + | |
| 1449 | + | |
| 1450 | + | |
| 1451 | + | |
| 1452 | + | |
| 1453 | + | |
| 1454 | + | |
| 1455 | + | |
| 1456 | + | |
| 1457 | + | |
| 1458 | + | |
| 1459 | + | |
| 1460 | + | |
| 1461 | + | |
| 1462 | + | |
| 1463 | + | |
| 1464 | + | |
| 1465 | + | |
1273 | 1466 | | |
1274 | 1467 | | |
1275 | 1468 | | |
| |||
0 commit comments