@junghanacs/entwurf 0.18.0 → 0.18.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,18 @@
1
1
  /**
2
2
  * check-gate-qualification — kill-proof qualification of the shipped gates.
3
3
  *
4
+ * TWO ENTRYPOINTS, ONE FILE. `--manifests-only` (shipped as `run.sh
5
+ * check-gate-manifests`) stops after the HEAD — the runner self-test plus the real
6
+ * manifests' validation and lane inventory — having executed ZERO mutants from
7
+ * `scripts/mutants/` and having made NO snapshot of this repo. The default
8
+ * entrypoint is unchanged: it runs the same head first and then the body. The head
9
+ * is separable because of what it has actually caught: across 549 CI runs the
10
+ * qualification step went red five times, and three of those died in the head in
11
+ * 4-5 seconds, before a single mutant gate was invoked (#99 B-3). The head costs
12
+ * ~8s, so it belongs in the deterministic floor every push already pays for; the
13
+ * body's contract — which mutants run, how they are classified, what it prints —
14
+ * is untouched by the split.
15
+ *
4
16
  * Two phases, in a fixed order:
5
17
  *
6
18
  * 1. RUNNER SELF-TEST. The runner is itself a SUT: a qualification harness that
@@ -58,6 +70,10 @@ import {
58
70
  const REPO_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
59
71
  const MUTANTS_DIR = path.join(REPO_DIR, "scripts", "mutants");
60
72
 
73
+ /** Head-only mode: validate, count, and stop — no mutant is executed, no repo snapshot is made. */
74
+ const MANIFESTS_ONLY = process.argv.includes("--manifests-only");
75
+ const SURFACE = MANIFESTS_ONLY ? "check-gate-manifests" : "gate-qualification";
76
+
61
77
  let passed = 0;
62
78
  function ok(label: string, cond: boolean): void {
63
79
  assert.ok(cond, label);
@@ -775,10 +791,16 @@ const quiet = (): void => {};
775
791
  }
776
792
  }
777
793
 
778
- console.log(`\n[gate-qualification] self-test: ${passed} checks passed`);
794
+ console.log(`\n[${SURFACE}] self-test: ${passed} checks passed`);
779
795
 
780
- // ═══ Phase 2 — the real manifests against a snapshot of THIS repo ═══════════
796
+ // ═══ Phase 1d — the real manifests, VALIDATED but not yet run ═══════════════
797
+ //
798
+ // The HEAD ends here. Everything above and in this block is pure reading and
799
+ // checking: no mutant from `scripts/mutants/` is executed and no snapshot of THIS
800
+ // repo exists yet. `--manifests-only` returns from exactly this point.
781
801
 
802
+ let selected: MutantSpec[];
803
+ let manifestCount: number;
782
804
  {
783
805
  const manifests: MutantManifest[] = fs
784
806
  .readdirSync(MUTANTS_DIR)
@@ -813,6 +835,7 @@ console.log(`\n[gate-qualification] self-test: ${passed} checks passed`);
813
835
  "copilot-launch": 14,
814
836
  "copilot-receive": 18,
815
837
  "fresh-cut": 1,
838
+ "gate-qualification": 2,
816
839
  "meta-facts": 4,
817
840
  "meta-hook-session-switch": 17,
818
841
  "meta-identity": 4,
@@ -821,14 +844,14 @@ console.log(`\n[gate-qualification] self-test: ${passed} checks passed`);
821
844
  "mux-fresh-call": 37,
822
845
  "mux-launcher-fence": 7,
823
846
  "mux-parent-artifact": 3,
824
- "pack-install": 1,
847
+ "pack-install": 2,
825
848
  "pi-package-ownership": 6,
826
849
  "mux-resume-call": 12,
827
850
  "omp-birth": 11,
828
851
  "omp-fresh": 24,
829
852
  "omp-receive": 11,
830
853
  "probe-ordering": 1,
831
- "release-gate": 12,
854
+ "release-gate": 18,
832
855
  "resume-args": 6,
833
856
  "resume-launch-identity": 6,
834
857
  "self-address": 5,
@@ -839,17 +862,44 @@ console.log(`\n[gate-qualification] self-test: ${passed} checks passed`);
839
862
  };
840
863
  const laneTally: Record<string, number> = {};
841
864
  for (const man of manifests) laneTally[man.lane] = (laneTally[man.lane] ?? 0) + man.mutants.length;
842
- assert.deepEqual(
843
- laneTally,
844
- EXPECTED_LANE_MUTANTS,
845
- "mutant lane inventory drifted from the declared contract — extend EXPECTED_LANE_MUTANTS and the manifests together, never silently",
846
- );
847
- const allMutants = validateManifestSet(manifests, makeOriginChecks(REPO_DIR));
848
- const selected = allMutants;
865
+ try {
866
+ assert.deepEqual(laneTally, EXPECTED_LANE_MUTANTS);
867
+ } catch {
868
+ assert.fail(
869
+ "[QK:LANE-INVENTORY-DECLARED] the mutant lane inventory drifted from the declared contract — extend " +
870
+ "EXPECTED_LANE_MUTANTS and the manifests together, never silently. A lane that lost its manifest in a " +
871
+ "merge, or gained mutants nobody declared, would otherwise pass as `n/n killed` over whatever set " +
872
+ `happened to be on disk. declared=${JSON.stringify(EXPECTED_LANE_MUTANTS)} onDisk=${JSON.stringify(laneTally)}`,
873
+ );
874
+ }
875
+ try {
876
+ selected = validateManifestSet(manifests, makeOriginChecks(REPO_DIR));
877
+ } catch (err) {
878
+ assert.fail(
879
+ "[QK:MANIFEST-SET-INTEGRITY-REFUSED] the committed manifest set broke its cross-manifest contract (global " +
880
+ "claim uniqueness, subject tracked + lstat-regular in the ORIGIN index, signatureSource on the origin work " +
881
+ "surface, claim token present exactly once in its gate source). Every one of those is what makes a KILLED " +
882
+ "verdict mean something, so the set is refused rather than run. Refusal: " +
883
+ (err instanceof Error ? err.message : String(err)),
884
+ );
885
+ }
886
+ manifestCount = manifests.length;
887
+ console.log(`[${SURFACE}] ${selected.length} mutants across ${manifestCount} lanes (no tiers — full set every run)`);
888
+ }
889
+
890
+ if (MANIFESTS_ONLY) {
849
891
  console.log(
850
- `[gate-qualification] ${selected.length} mutants across ${manifests.length} lanes (no tiers full set every run)`,
892
+ `[check-gate-manifests] ok runner self-test green, ${selected.length} committed mutants across ` +
893
+ `${manifestCount} lanes validated against the origin index, and the lane inventory matches its declared ` +
894
+ "contract. ZERO mutants were executed and this repo was never snapshotted: the body " +
895
+ "(check-gate-qualification) owns that, unchanged.",
851
896
  );
897
+ process.exit(0);
898
+ }
899
+
900
+ // ═══ Phase 2 — the real manifests against a snapshot of THIS repo ═══════════
852
901
 
902
+ {
853
903
  const headBefore = originHead(REPO_DIR);
854
904
  const workSurfaceBefore = originWorkSurfaceSha(REPO_DIR);
855
905
 
@@ -25,7 +25,15 @@
25
25
  // "internal prerequisite" case, Cortex being the one that was measured).
26
26
  // 7. every LIVE smoke is either wired into the aggregate or excluded for a
27
27
  // reason the docs actually state — the protocol cannot vouch for a step the
28
- // gate never lists.
28
+ // gate never lists;
29
+ // 8. the moved check-gate-qualification stays reachable on its owners, and the
30
+ // CI step qualifies the FULL floor;
31
+ // 9. every gate a committed mutant NAMES also runs inside `check:full` (or says
32
+ // in its own prose why it deliberately does not) — a gate reachable only
33
+ // through qualification's control-pre puts a whole defect class behind the
34
+ // 28-minute body;
35
+ // 10. the CI push trigger is filtered to BRANCH refs, so pushing a release tag
36
+ // does not rebuild a SHA its branch run already built.
29
37
  //
30
38
  // Cells 5-6 SPAWN the real subcommands rather than reasoning about them: the
31
39
  // whole defect was an assumption about what a step would do, so an assumption is
@@ -36,7 +44,8 @@
36
44
 
37
45
  import { strict as assert } from "node:assert";
38
46
  import { execFileSync } from "node:child_process";
39
- import { globSync, readFileSync } from "node:fs";
47
+ import { copyFileSync, globSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
48
+ import { tmpdir } from "node:os";
40
49
  import { basename, join } from "node:path";
41
50
  import { fileURLToPath } from "node:url";
42
51
  import { LIVE_SKIP_EXIT, LIVE_SKIP_MARKER } from "./lib/live-skip.ts";
@@ -329,17 +338,20 @@ function runSubcommand(sub: string, env: Record<string, string | undefined>): {
329
338
  // check-gate-qualification left the default check chains (operator
330
339
  // inner-loop cost, 2026-08 subtraction). That move is a gate/release
331
340
  // contract: the step must stay REACHABLE on the axes that now own it — the
332
- // CI check job on every push and release_gate as its own MUST step — and
341
+ // CI check job (on the pushes #103's filter still sends it) and release_gate
342
+ // as its own MUST step — and
333
343
  // must not silently return to the default chain. Without this cell,
334
344
  // deleting the release_gate qualification block or the CI line leaves every
335
345
  // focused gate green while a cut quietly loses its discriminating-power
336
346
  // step.
337
347
  //
338
- // TWO claims, because #70 added a second independent contract here. 8a is
339
- // REACHABILITY (absent from the default chain; present exactly once in CI and
340
- // exactly once as a wired release_gate MUST step; named in VERIFY). 8b is what
341
- // the CI step qualifies (the FULL floor, before the qualification run). Each
342
- // carries its own replant one mutant must never stand in for both.
348
+ // THREE claims, because each is an independent contract. 8a is REACHABILITY
349
+ // (absent from the default chain; present exactly once in CI and exactly once
350
+ // as a wired release_gate MUST step; named in VERIFY). 8b is what the CI step
351
+ // qualifies (the FULL floor, before the qualification run), added by #70. 8c is
352
+ // what the RELEASE path ACCEPTS as evidence for one SHA that the body step
353
+ // actually ran there (#103). Each carries its own replant — one mutant must
354
+ // never stand in for another.
343
355
  // ===========================================================================
344
356
  {
345
357
  // The default chain is tiered (#70): `check` (core) and `check:full` compose the
@@ -374,7 +386,11 @@ function runSubcommand(sub: string, env: Record<string, string | undefined>): {
374
386
  !qualGateBody.includes('results+=("FAIL check-gate-qualification")')
375
387
  )
376
388
  holes.push("the release_gate qualification step does not wire PASS/FAIL into the MUST counters");
377
- if (!verifyDoc.includes("in the CI `check` job on every push, and as a release-gate MUST step"))
389
+ if (
390
+ !verifyDoc.includes(
391
+ "in the CI `check` job on a branch push that touched the qualification surface, and as a release-gate MUST step",
392
+ )
393
+ )
378
394
  holes.push("VERIFY.md no longer names the owners of the moved qualification step");
379
395
  assert.ok(
380
396
  holes.length === 0,
@@ -404,6 +420,357 @@ function runSubcommand(sub: string, env: Record<string, string | undefined>): {
404
420
  "downgrade/omission axis; ordering is directly asserted by this same oracle. " +
405
421
  `Broken: check:full at index ${ciFloorAt}, qualification at index ${ciQualAt}.`,
406
422
  );
423
+
424
+ // 8c. The RELEASE oracle requires that body to have actually run at the release
425
+ // SHA -- its own contract, not a branch of 8a. 8a is REACHABILITY (the step
426
+ // exists in the CI job and in release_gate); this is about what the release
427
+ // path ACCEPTS as evidence for one SHA. `verify-exact-ci.sh` read only three
428
+ // job names, and a job conclusion says nothing about whether the step inside
429
+ // it ran: the moment ci.yml can skip that step (#103 piece 2), a green oracle
430
+ // would certify a SHA whose qualification body never executed. That is the
431
+ // "green with no evidence" class this repo fails closed on, which is why the
432
+ // oracle gets the fourth axis BEFORE the skip exists.
433
+ //
434
+ // This is a TEXT oracle, the same grade as check-install-surface S7g. A
435
+ // behavioural oracle would need either a second file (S7a/S7g bind the
436
+ // release surface to one script read from the index) or an injectable
437
+ // RUN_JSON seam -- and that seam would be a way to launder release evidence
438
+ // past gh. Both cost more than the axis is worth here.
439
+ const ciOracle = readFileSync(join(REPO_DIR, ".claude/skills/entwurf-release/scripts/verify-exact-ci.sh"), "utf8");
440
+ const axis: string[] = [];
441
+ if (!ciOracle.includes('QUAL_JOB = "check"') || !ciOracle.includes('"Run ./run.sh check-gate-qualification"'))
442
+ axis.push("the oracle does not name the check job's check-gate-qualification step");
443
+ if (!ciOracle.includes('qual != "success"'))
444
+ axis.push("the oracle does not guard that step's conclusion against 'success'");
445
+ if (!ciOracle.includes('qual == "skipped"'))
446
+ axis.push("the oracle does not classify a SKIPPED body as a failure of its own");
447
+ assert.ok(
448
+ axis.length === 0,
449
+ "[QK:RELEASE-SHA-QUALIFIED-IN-CI] the exact-SHA CI oracle must require the qualification BODY to have run " +
450
+ "at the release SHA -- the `check` job's `Run ./run.sh check-gate-qualification` step concluding 'success', " +
451
+ "with skipped named as its own failure. Three green job names do not prove the step inside one of them " +
452
+ `executed. Broken: ${axis.join("; ")}`,
453
+ );
454
+ }
455
+
456
+ // ===========================================================================
457
+ // 8d. The qualification filter COVERS every mutant subject (#103 piece 2)
458
+ //
459
+ // The body no longer runs on every branch push; a decision script reads the
460
+ // push range and answers. Its whole safety argument is that the path set is
461
+ // DERIVED from the committed manifests rather than copied into a list, so a
462
+ // new mutant subject cannot land outside the filter and quietly stop being
463
+ // re-proven in CI. This asserts that property behaviourally: every subject
464
+ // and signatureSource in scripts/mutants/*.json, fed to the script as a
465
+ // one-file change, must decide `run_body=true`. The oracle is the manifests
466
+ // themselves, read here independently of the script.
467
+ // ===========================================================================
468
+ {
469
+ const decider = join(REPO_DIR, "scripts/ci-qualify-decide.sh");
470
+ const paths = new Set<string>();
471
+ for (const file of globSync("scripts/mutants/*.json", { cwd: REPO_DIR })) {
472
+ const manifest = JSON.parse(readFileSync(join(REPO_DIR, file), "utf8")) as {
473
+ mutants?: { subject?: string; signatureSource?: string }[];
474
+ };
475
+ for (const mutant of manifest.mutants ?? []) {
476
+ if (mutant.subject) paths.add(mutant.subject);
477
+ if (mutant.signatureSource) paths.add(mutant.signatureSource);
478
+ }
479
+ }
480
+ assert.ok(paths.size > 50, `read only ${paths.size} mutant paths from the manifests`);
481
+ const uncovered = [...paths].filter((path) => {
482
+ const out = execFileSync("bash", [decider, "--files-from", "-"], {
483
+ cwd: REPO_DIR,
484
+ encoding: "utf8",
485
+ input: `${path}\n`,
486
+ stdio: ["pipe", "pipe", "ignore"],
487
+ });
488
+ return out.trim() !== "run_body=true";
489
+ });
490
+ assert.ok(
491
+ uncovered.length === 0,
492
+ "[QK:QUALIFY-FILTER-COVERS-SUBJECTS] the CI qualification filter must run the body for a change to ANY " +
493
+ "committed mutant subject or signature source — it derives that set from scripts/mutants/*.json for " +
494
+ "exactly this reason, so a path it cannot see is a claim that silently stops being re-proven in CI. " +
495
+ `Uncovered (${uncovered.length} of ${paths.size}): ${uncovered.slice(0, 8).join(", ")}`,
496
+ );
497
+ }
498
+
499
+ // ===========================================================================
500
+ // 8e. The filter still runs the body for every RED the body has ever produced
501
+ //
502
+ // Five reds in 549 runs (#99 stage-2). The first reading of them used the
503
+ // tip COMMIT and concluded the filter would have missed four; replaying the
504
+ // real two-dot push range — what GitHub actually compares — showed all five
505
+ // hit. That reversal is the whole evidentiary basis for this filter.
506
+ //
507
+ // It is asserted from RECORDED file lists, not from live history, and that
508
+ // is a constraint rather than a convenience: check-gate-qualification runs
509
+ // every gate inside a snapshot with its own fresh git baseline, where these
510
+ // 2026-07/08 commits do not exist. A cell that read history there would be
511
+ // CONTROL-RED for the whole lane — measured, not predicted (this cell did
512
+ // exactly that on 2026-09-06 and cost the release-gate lane its 17 kills).
513
+ // The fixture carries what history said, measured once, in a repo where the
514
+ // objects are present.
515
+ //
516
+ // It has its own replant, and the reason is the SHAPE of the input: 8d feeds
517
+ // ONE path per call, this feeds a whole push at once. A matcher that only
518
+ // looked at the first entry would leave 8d green — every single-file call is
519
+ // its own first entry — while every one of these five pushes opens with a
520
+ // non-surface path (DELIVERY.md, AGENTS.md, NEXT.md, AGENTS.md,
521
+ // demo/demo-baseline.sh). Invisible to 8d, fatal here.
522
+ //
523
+ // The two-dot READING itself is proven separately, below.
524
+ // ===========================================================================
525
+ {
526
+ const decider = join(REPO_DIR, "scripts/ci-qualify-decide.sh");
527
+ const fixture = JSON.parse(readFileSync(join(REPO_DIR, "scripts/fixtures/qualify-replay.json"), "utf8")) as {
528
+ runs: { runId: string; before: string; head: string; files: string[]; tipOnlyFiles: string[] }[];
529
+ };
530
+ assert.equal(fixture.runs.length, 5, "the replay fixture holds all five historical qualification reds");
531
+ const wrong: string[] = [];
532
+ for (const run of fixture.runs) {
533
+ assert.ok(run.files.length > 0, `run ${run.runId} has no recorded push range`);
534
+ const out = execFileSync("bash", [decider, "--files-from", "-"], {
535
+ cwd: REPO_DIR,
536
+ encoding: "utf8",
537
+ input: `${run.files.join("\n")}\n`,
538
+ stdio: ["pipe", "pipe", "ignore"],
539
+ }).trim();
540
+ if (out !== "run_body=true") wrong.push(`run ${run.runId} (${run.files.length} files) decided ${out}`);
541
+ }
542
+ assert.ok(
543
+ wrong.length === 0,
544
+ "[QK:QUALIFY-FILTER-REPLAYS-PAST-CATCHES] every qualification RED in this repo's CI history must still " +
545
+ "run the body under the filter, over the two-dot push range GitHub compares — the measurement that " +
546
+ "overturned the tip-commit reading and justified filtering at all. This reads a whole push at once, " +
547
+ "which 8d cannot: each of these five opens with a non-surface path, so a matcher that stopped after " +
548
+ "the first entry would pass every single-file check 8d makes and fail here. " +
549
+ `Broken: ${wrong.join("; ")}`,
550
+ );
551
+ }
552
+
553
+ // ===========================================================================
554
+ // 8f. The filter reads the two-dot PUSH RANGE, not the tip commit
555
+ //
556
+ // The fixture above proves the matcher's verdict on recorded file lists; it
557
+ // cannot prove which git range produced them, because it calls no git. This
558
+ // does, hermetically: a throwaway repo shaped like the real defect — a push
559
+ // of two commits whose TIP is docs-only while the commit under it touched a
560
+ // mutant subject. The two-dot range sees the code; the tip alone does not.
561
+ // That is the exact misreading #99 corrected, and it is what four of the
562
+ // five recorded pushes look like when read the wrong way.
563
+ //
564
+ // Hermetic on purpose: no repo history, so it runs identically inside the
565
+ // qualification snapshot.
566
+ // ===========================================================================
567
+ {
568
+ const decider = join(REPO_DIR, "scripts/ci-qualify-decide.sh");
569
+ const tmp = mkdtempSync(join(tmpdir(), "entwurf-qualify-range-"));
570
+ // core.hooksPath is set globally on this operator's machine; a fixture repo
571
+ // must not run their hooks.
572
+ const git = (...args: string[]) =>
573
+ execFileSync("git", ["-c", "core.hooksPath=/dev/null", "-c", "user.email=g@e", "-c", "user.name=g", ...args], {
574
+ cwd: tmp,
575
+ stdio: "ignore",
576
+ });
577
+ try {
578
+ git("init", "-q", "-b", "main");
579
+ // The decider reads the repo it SITS IN, so the fixture gets a real copy
580
+ // of it and its own one-entry manifest: no seam, no env override, and the
581
+ // manifest-reading path is exercised too.
582
+ mkdirSync(join(tmp, "scripts/mutants"), { recursive: true });
583
+ mkdirSync(join(tmp, "pi-extensions/lib"), { recursive: true });
584
+ copyFileSync(decider, join(tmp, "scripts/ci-qualify-decide.sh"));
585
+ writeFileSync(
586
+ join(tmp, "scripts/mutants/fixture.json"),
587
+ `${JSON.stringify(
588
+ {
589
+ schemaVersion: 1,
590
+ lane: "fixture",
591
+ mutants: [{ claim: "FIXTURE", subject: "pi-extensions/lib/fixture-subject.ts" }],
592
+ },
593
+ null,
594
+ "\t",
595
+ )}\n`,
596
+ );
597
+ writeFileSync(join(tmp, "seed.txt"), "seed\n");
598
+ git("add", "-A");
599
+ git("commit", "-qm", "seed");
600
+ const base = execFileSync("git", ["rev-parse", "HEAD"], { cwd: tmp, encoding: "utf8" }).trim();
601
+ writeFileSync(join(tmp, "pi-extensions/lib/fixture-subject.ts"), "// a mutant subject\n");
602
+ git("add", "-A");
603
+ git("commit", "-qm", "code commit (mid-push)");
604
+ writeFileSync(join(tmp, "README.md"), "docs only\n");
605
+ git("add", "-A");
606
+ git("commit", "-qm", "docs commit (tip)");
607
+ const head = execFileSync("git", ["rev-parse", "HEAD"], { cwd: tmp, encoding: "utf8" }).trim();
608
+ const decide = (from: string) =>
609
+ execFileSync("bash", [join(tmp, "scripts/ci-qualify-decide.sh"), from, head], {
610
+ cwd: tmp,
611
+ encoding: "utf8",
612
+ env: { ...process.env, CI_EVENT_NAME: "push", CI_FORCED: "false" },
613
+ stdio: ["ignore", "pipe", "ignore"],
614
+ }).trim();
615
+ const overRange = decide(base);
616
+ const overTip = decide(`${head}~1`);
617
+ assert.ok(
618
+ overRange === "run_body=true" && overTip === "run_body=false",
619
+ "[QK:QUALIFY-FILTER-READS-PUSH-RANGE] the filter must diff the whole two-dot push range, not the tip " +
620
+ "commit: a push whose tip is docs-only can still carry a mutant subject underneath it, which is how " +
621
+ "four of the five historical reds look like docs pushes when read tip-first. " +
622
+ `Broken: range=${overRange}, tip-only=${overTip} (tip-only must be false, or this fixture proves nothing).`,
623
+ );
624
+ } finally {
625
+ rmSync(tmp, { recursive: true, force: true });
626
+ }
627
+ }
628
+
629
+ // ===========================================================================
630
+ // 9. Every gate a mutant manifest NAMES runs inside `pnpm run check:full`
631
+ //
632
+ // The measurement behind this (#99 B-3/B-4): across 549 CI runs the mutant-
633
+ // EXECUTION half of qualification never once produced a SURVIVED / WRONG-REASON /
634
+ // MUTANT-STALE / HANG. What it did catch twice was a gate that was already red on a
635
+ // clean tree — CONTROL-PRE — and that class is caught 5.4 minutes earlier, and for
636
+ // free, by any gate the deterministic floor already runs. So a mutant-named gate
637
+ // that sits OUTSIDE `check:full` is the one place where a baseline break can only
638
+ // be found by paying the 28-minute body. `check-omp-birth-hook` was exactly that
639
+ // gate, reachable in the whole repo only through qualification's control-pre.
640
+ //
641
+ // The exclusion arm is not a loophole: `scripts/check-setup-qualification.sh` is a
642
+ // mutation-attribution oracle the manifests invoke DIRECTLY and its own header says
643
+ // it is deliberately outside every tier. Like cell 7, the excuse is checked against
644
+ // the text an operator can read, never against a list only this gate believes in.
645
+ //
646
+ // ONE PREMISE, stated because it is invisible from here: the script-path join below
647
+ // ignores MODE FLAGS in a gate argv. The `--attribution-self-test` gate therefore
648
+ // counts as covered by the `check-gate-manifests` arm only because
649
+ // `checkVitestAttribution()` runs UNCONDITIONALLY, before that flag is read
650
+ // (`scripts/check-gate-qualification.ts`). Move that call behind a flag and this
651
+ // cell stays green while saying something false.
652
+ // ===========================================================================
653
+ {
654
+ const pkg = JSON.parse(readFileSync(join(REPO_DIR, "package.json"), "utf8")) as { scripts: Record<string, string> };
655
+ // The floor is composed (#70): `check:full` names group scripts, and a group script
656
+ // may name further ones. Expand transitively rather than hard-coding the tier list.
657
+ const expand = (name: string, seen: Set<string>): string => {
658
+ if (seen.has(name)) return "";
659
+ seen.add(name);
660
+ const body = pkg.scripts[name] ?? "";
661
+ let out = body;
662
+ for (const ref of body.matchAll(/pnpm run ([\w:.-]+)/g)) out += ` ${expand(ref[1], seen)}`;
663
+ const elapsed = /check-elapsed\.sh\s+[\w:.-]+\s+(.+)$/.exec(body);
664
+ if (elapsed) for (const g of elapsed[1].trim().split(/\s+/)) out += ` ${expand(g, seen)}`;
665
+ return out;
666
+ };
667
+ const floorText = expand("check:full", new Set());
668
+ const floorSubcommands = new Set([...floorText.matchAll(/\.\/run\.sh ([\w:.-]+)/g)].map((m) => m[1]));
669
+
670
+ // run.sh case arms, so a gate invoked by SCRIPT PATH can be joined to the floor
671
+ // subcommand that runs that same script.
672
+ const arms = new Map<string, string>();
673
+ {
674
+ let names: string[] = [];
675
+ let body: string[] = [];
676
+ for (const line of readFileSync(join(REPO_DIR, "run.sh"), "utf8").split("\n")) {
677
+ const head = /^ {2}([\w|:.-]+)\)$/.exec(line);
678
+ if (head) {
679
+ names = head[1].split("|");
680
+ body = [];
681
+ continue;
682
+ }
683
+ if (line === " ;;") {
684
+ for (const n of names) arms.set(n, body.join("\n"));
685
+ names = [];
686
+ continue;
687
+ }
688
+ if (names.length > 0) body.push(line);
689
+ }
690
+ }
691
+
692
+ const gates = new Map<string, string[]>();
693
+ for (const rel of globSync("scripts/mutants/*.json", { cwd: REPO_DIR }).sort()) {
694
+ const doc = JSON.parse(readFileSync(join(REPO_DIR, rel), "utf8")) as { mutants: Array<{ gate: string[] }> };
695
+ for (const m of doc.mutants) gates.set(m.gate.join(" "), m.gate);
696
+ }
697
+ assert.ok(gates.size >= 40, `expected the committed mutant gate set, found ${gates.size} distinct gate argvs`);
698
+
699
+ // argv → the file that states, in prose an operator reads, why it is outside the floor.
700
+ const DOCUMENTED_OUTSIDE: Record<string, [file: string, sentence: string]> = {
701
+ "bash scripts/check-setup-qualification.sh": [
702
+ "scripts/check-setup-qualification.sh",
703
+ "run.sh subcommand and NOT in any check tier",
704
+ ],
705
+ };
706
+
707
+ const unreached: string[] = [];
708
+ for (const [key, argv] of gates) {
709
+ if (argv[0] === "bash" && argv[1] === "run.sh") {
710
+ if (!floorSubcommands.has(argv[2])) unreached.push(`${key} — run.sh ${argv[2]} is in no check:full group`);
711
+ continue;
712
+ }
713
+ const scriptPath = argv.find((t) => /^scripts\/.+\.(sh|ts|py)$/.test(t));
714
+ if (scriptPath !== undefined && [...floorSubcommands].some((sub) => (arms.get(sub) ?? "").includes(scriptPath))) {
715
+ continue;
716
+ }
717
+ const excused = DOCUMENTED_OUTSIDE[key];
718
+ if (excused === undefined) {
719
+ unreached.push(`${key} — no check:full subcommand invokes ${scriptPath ?? "(no script path in its argv)"}`);
720
+ continue;
721
+ }
722
+ const [file, sentence] = excused;
723
+ if (!readFileSync(join(REPO_DIR, file), "utf8").includes(sentence)) {
724
+ unreached.push(`${key} claims a documented exclusion, but ${file} no longer says "${sentence}"`);
725
+ }
726
+ }
727
+ assert.ok(
728
+ unreached.length === 0,
729
+ "[QK:MUTANT-GATES-INSIDE-FULL-FLOOR] every gate a committed mutant names must also run inside `pnpm run " +
730
+ "check:full`, or state its exclusion where an operator reads it. A gate reachable ONLY through " +
731
+ "qualification's control-pre makes the 28-minute mutant body the only thing that can notice it going red on " +
732
+ "a clean tree — the one CI class that body has actually caught, and the one the 5.4-minute floor catches for " +
733
+ `free everywhere else. Unreached: ${unreached.join("; ")}`,
734
+ );
735
+ }
736
+
737
+ // ===========================================================================
738
+ // 10. A TAG push rebuilds nothing
739
+ //
740
+ // Measured over this repo's whole history (#99 B-1): 66 of 66 semver tag-push runs
741
+ // rebuilt a SHA some other run had already built, and no tag run ever reported a
742
+ // fact its branch run did not — the single non-green one failed at the same step as
743
+ // its main run, two seconds later. That is 28% of a release window's runner minutes
744
+ // buying a badge on a ref. The exact-SHA evidence a release quotes is the BRANCH
745
+ // run, which is also the run `entwurf-release` land mode reports.
746
+ //
747
+ // So `on.push` must carry a REF FILTER that selects branches. `branches:` alone is
748
+ // what makes a tag push create no run at all; a `tags:` key would put them back.
749
+ // ===========================================================================
750
+ {
751
+ const ciYml = readFileSync(join(REPO_DIR, ".github/workflows/ci.yml"), "utf8");
752
+ const onAt = ciYml.indexOf("\non:\n");
753
+ const envAt = ciYml.indexOf("\nenv:\n");
754
+ assert.ok(onAt !== -1 && envAt > onAt, "located the workflow `on:` block");
755
+ const onLines = ciYml.slice(onAt, envAt).split("\n");
756
+ const pushAt = onLines.findIndex((l) => /^ {2}push:\s*$/.test(l));
757
+ const nextKeyAt = onLines.findIndex((l, i) => i > pushAt && /^ {2}\S/.test(l));
758
+ const pushBody = onLines
759
+ .slice(pushAt + 1, nextKeyAt === -1 ? undefined : nextKeyAt)
760
+ .filter((l) => l.trim() !== "" && !/^\s*#/.test(l));
761
+ const gaps: string[] = [];
762
+ if (pushAt === -1) gaps.push("the `on:` block has no `push:` trigger");
763
+ if (!pushBody.some((l) => /^ {4}branches(-ignore)?:/.test(l)))
764
+ gaps.push(`the push trigger carries no branch ref filter (body: ${JSON.stringify(pushBody)})`);
765
+ if (pushBody.some((l) => /^ {4}tags(-ignore)?:/.test(l)))
766
+ gaps.push("the push trigger names tags — a tag push would create a second run for a SHA already built");
767
+ assert.ok(
768
+ gaps.length === 0,
769
+ "[QK:CI-TAG-PUSH-NOT-REBUILT] the CI `push` trigger must be filtered to BRANCH refs, so pushing a release tag " +
770
+ "creates no run: 66/66 semver tag runs in this repo's history rebuilt an already-built SHA and none ever " +
771
+ "reported a fact its branch run had not, while costing 28% of a release window's runner minutes. Dropping " +
772
+ `the filter does not add evidence, it duplicates it. Broken: ${gaps.join("; ")}`,
773
+ );
407
774
  }
408
775
 
409
776
  // ===========================================================================
@@ -500,6 +867,11 @@ console.log(
500
867
  "a run.sh wrapper declining its own prerequisite (including the measured LIVE=1 no-cortex-connection cell); and every " +
501
868
  "LIVE smoke is either wired into release_gate or excluded by a sentence the docs still carry; and the moved " +
502
869
  "check-gate-qualification stays reachable on its owners (absent from the default chain, exactly once in CI, " +
503
- "exactly once as a release-gate MUST step) and the CI step qualifies the FULL floor, which runs before it; and " +
870
+ "exactly once as a release-gate MUST step) and the CI step qualifies the FULL floor, which runs before it, " +
871
+ "while the exact-SHA release oracle requires that BODY step to have concluded success at the release SHA, " +
872
+ "and the CI filter that decides when that body runs covers every mutant subject and still runs it for all " +
873
+ "five historical reds; and " +
874
+ "every gate a committed mutant names is itself inside check:full or states its exclusion in prose an operator " +
875
+ "reads; and the CI push trigger is filtered to branch refs, so a release tag creates no duplicate run; and " +
504
876
  "the operator's CONFIGURED bridge invocation is booted exactly once through run_step, before the ACP LIVE tier",
505
877
  );