@junghanacs/entwurf 0.37.0 → 0.37.1

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.
@@ -26,8 +26,11 @@
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
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;
29
+ // 8. the moved check-gate-qualification stays reachable as the schedule/dispatch
30
+ // drift diagnostic it is since #144 (never a push, never release_gate), that
31
+ // diagnostic runs over the FULL floor, and the exact-SHA release oracle takes
32
+ // only a push run as its witness, refuses every required job that did not
33
+ // conclude success, and never reports the qualification body;
31
34
  // 9. every gate a committed mutant NAMES also runs inside `check:full` (or says
32
35
  // in its own prose why it deliberately does not) — a gate reachable only
33
36
  // through qualification's control-pre puts a whole defect class behind the
@@ -44,7 +47,17 @@
44
47
 
45
48
  import { strict as assert } from "node:assert";
46
49
  import { execFileSync, spawnSync } from "node:child_process";
47
- import { copyFileSync, globSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
50
+ import {
51
+ existsSync,
52
+ globSync,
53
+ mkdirSync,
54
+ mkdtempSync,
55
+ readFileSync,
56
+ realpathSync,
57
+ rmSync,
58
+ symlinkSync,
59
+ writeFileSync,
60
+ } from "node:fs";
48
61
  import { tmpdir } from "node:os";
49
62
  import { basename, join } from "node:path";
50
63
  import { fileURLToPath } from "node:url";
@@ -356,24 +369,19 @@ function runSubcommand(sub: string, env: Record<string, string | undefined>): {
356
369
  // ===========================================================================
357
370
  // 8. qualification scheduling topology — the subtraction has its own oracle.
358
371
  // check-gate-qualification left the default check chains (operator
359
- // inner-loop cost, 2026-08 subtraction). That move is a gate/release
360
- // contract: the step must stay REACHABLE on the axes that now own it — the
361
- // CI check job (on the pushes #103's filter still sends it) and, for a cut,
362
- // the exact-SHA release oracle that requires that CI step at the prepared SHA
363
- // (8c) — and must not silently return to the default chain or to release_gate.
364
- // #132/#133 moved the cut's one BODY off release_gate: a local BODY there re-ran
365
- // the same proof on a SHA nobody ships, so release_gate now invokes it ZERO
366
- // times. Without this cell, deleting the CI line leaves every focused gate green
367
- // while a cut quietly loses its discriminating-power step, and re-adding the
368
- // release_gate block silently doubles every cut again.
372
+ // inner-loop cost, 2026-08 subtraction), and #132/#133 moved it off
373
+ // release_gate. #144 took it off every push and out of release acceptance: the
374
+ // whole body is a drift diagnostic that runs only on the weekly schedule or a
375
+ // `qualify=true` dispatch, and the release oracle reads only the push run's four
376
+ // required jobs. That is a gate/release contract: the step must stay REACHABLE on
377
+ // exactly those triggers, and must not silently return to the default chain, to
378
+ // release_gate, to a push, or to the release skill as an automatic dispatch.
369
379
  //
370
- // THREE claims, because each is an independent contract. 8a is REACHABILITY
371
- // (absent from the default chain and from release_gate; present exactly once in
372
- // CI; named in VERIFY). 8b is what the CI step
373
- // qualifies (the FULL floor, before the qualification run), added by #70. 8c is
374
- // what the RELEASE path ACCEPTS as evidence for one SHA — that the body step
375
- // actually ran there (#103). Each carries its own replant — one mutant must
376
- // never stand in for another.
380
+ // TWO claims, because each is an independent contract. 8a is REACHABILITY
381
+ // (absent from the default chain, from release_gate, from push and from the
382
+ // release skill; present exactly once in CI under its two triggers). 8b is what
383
+ // that diagnostic runs over (the FULL floor, before it), added by #70. Each
384
+ // carries its own replant — one mutant must never stand in for another.
377
385
  // ===========================================================================
378
386
  {
379
387
  // The default chain is tiered (#70): `check` (core) and `check:full` compose the
@@ -391,7 +399,9 @@ function runSubcommand(sub: string, env: Record<string, string | undefined>): {
391
399
  const runShQ = readFileSync(join(REPO_DIR, "run.sh"), "utf8");
392
400
  const qualGateBody = runShQ.slice(runShQ.indexOf("release_gate() {"), runShQ.indexOf("# 5. Summary"));
393
401
  const invocations = qualGateBody.split('bash "$self" check-gate-qualification').length - 1;
394
- const verifyDoc = readFileSync(join(REPO_DIR, "VERIFY.md"), "utf8");
402
+ const qualStepAt = ciYml.indexOf("- run: ./run.sh check-gate-qualification");
403
+ const qualIf = qualStepAt === -1 ? "" : (ciYml.slice(qualStepAt).split("\n")[1] ?? "").trim();
404
+ const releaseSkill = readFileSync(join(REPO_DIR, ".claude/skills/entwurf-release/SKILL.md"), "utf8");
395
405
 
396
406
  // One claim token, one assert (the qualification runner requires the exact-once
397
407
  // signature); each broken axis names itself in the joined message.
@@ -403,32 +413,32 @@ function runSubcommand(sub: string, env: Record<string, string | undefined>): {
403
413
  if (ciHits !== 1) holes.push(`the CI check job runs check-gate-qualification ${ciHits}x (need exactly once)`);
404
414
  if (invocations !== 0)
405
415
  holes.push(
406
- `release_gate invokes check-gate-qualification ${invocations}x (need none: the cut's one BODY is the prepared SHA's CI step)`,
416
+ `release_gate invokes check-gate-qualification ${invocations}x (need none: the body is a schedule/dispatch diagnostic, #144)`,
407
417
  );
408
418
  if (
409
419
  qualGateBody.includes("PASS check-gate-qualification") ||
410
420
  qualGateBody.includes("FAIL check-gate-qualification")
411
421
  )
412
422
  holes.push("release_gate still counts a check-gate-qualification MUST row");
413
- if (
414
- !verifyDoc.includes(
415
- "in the CI `check` job on a branch push that touched the qualification surface, and — for a cut — at the prepared SHA, where the exact-SHA release oracle requires that CI step",
416
- )
417
- )
418
- holes.push("VERIFY.md no longer names the owners of the moved qualification step");
423
+ if (qualIf !== "if: github.event_name == 'schedule' || inputs.qualify == true")
424
+ holes.push(
425
+ `the CI qualification step's trigger is \`${qualIf}\` (need the schedule or a qualify=true dispatch only)`,
426
+ );
427
+ if (ciYml.includes("qualify-decide")) holes.push("ci.yml still carries the retired push qualification filter step");
428
+ if (releaseSkill.includes("-f qualify=true"))
429
+ holes.push("the release skill still dispatches the qualification body (it is not release evidence, #144)");
419
430
  assert.ok(
420
431
  holes.length === 0,
421
432
  "[QK:QUALIFICATION-SCHEDULING-REACHABLE] check-gate-qualification left the default check chains " +
422
- "deliberately, so it must stay REACHABLE on the axes that own it now — absent from the default chain " +
423
- "and from release_gate (the cut's one BODY is the prepared SHA's CI step), exactly once in the CI check job, " +
424
- `and named in VERIFY. Broken: ${holes.join("; ")}`,
433
+ "deliberately, so it must stay REACHABLE on the triggers that own it now — exactly once in the CI check " +
434
+ "job, on the weekly schedule or a qualify=true dispatch only — and absent from the default chain, from " +
435
+ `release_gate, from every push and from the release skill. Broken: ${holes.join("; ")}`,
425
436
  );
426
437
 
427
- // 8b. WHAT the CI qualification step qualifies — its own claim, not a branch
428
- // folded into the one above. #70 split the deterministic floor into tiers,
429
- // so "CI runs qualification once" stopped being sufficient evidence on its
430
- // own: qualification on top of the ≤60s core would certify kill-power over
431
- // a floor no candidate ships on. This is ONE contract with one condition —
438
+ // 8b. WHAT the CI qualification step runs over — its own claim, not a branch
439
+ // folded into the one above. #70 split the deterministic floor into tiers:
440
+ // the diagnostic body on top of the ≤60s core would re-plant mutants over a
441
+ // floor no candidate ships on. This is ONE contract with one condition —
432
442
  // the full floor is present in the CI check job AND qualification follows
433
443
  // it — because a floor that is absent and a floor that runs afterwards
434
444
  // break the same promise identically. The committed replant qualifies the
@@ -440,372 +450,175 @@ function runSubcommand(sub: string, env: Record<string, string | undefined>): {
440
450
  ciFloorAt !== -1 && ciQualAt > ciFloorAt,
441
451
  "[QK:CI-FULL-FLOOR-QUALIFIED] the CI check job owns the FULL deterministic floor (#70): it must run " +
442
452
  "`pnpm run check:full` — not the ≤60s core — and check-gate-qualification must come AFTER it, so the " +
443
- "kill-power proof covers the floor a candidate actually ships on. The committed replant qualifies the " +
453
+ "diagnostic body (schedule or dispatch) re-plants over the floor a candidate actually ships on. The committed replant qualifies the " +
444
454
  "downgrade/omission axis; ordering is directly asserted by this same oracle. " +
445
455
  `Broken: check:full at index ${ciFloorAt}, qualification at index ${ciQualAt}.`,
446
456
  );
447
-
448
- // 8c. The RELEASE oracle requires that body to have actually run at the release
449
- // SHA -- its own contract, not a branch of 8a. 8a is REACHABILITY (the step
450
- // exists in the CI job and in release_gate); this is about what the release
451
- // path ACCEPTS as evidence for one SHA. `verify-exact-ci.sh` read only three
452
- // job names, and a job conclusion says nothing about whether the step inside
453
- // it ran: the moment ci.yml can skip that step (#103 piece 2), a green oracle
454
- // would certify a SHA whose qualification body never executed. That is the
455
- // "green with no evidence" class this repo fails closed on, which is why the
456
- // oracle gets the fourth axis BEFORE the skip exists.
457
- //
458
- // This is a TEXT oracle, the same grade as check-install-surface S7g. A
459
- // behavioural oracle would need either a second file (S7a/S7g bind the
460
- // release surface to one script read from the index) or an injectable
461
- // RUN_JSON seam -- and that seam would be a way to launder release evidence
462
- // past gh. Both cost more than the axis is worth here.
463
- const ciOracle = readFileSync(join(REPO_DIR, ".claude/skills/entwurf-release/scripts/verify-exact-ci.sh"), "utf8");
464
- const axis: string[] = [];
465
- if (!ciOracle.includes('QUAL_JOB = "check"') || !ciOracle.includes('"Run ./run.sh check-gate-qualification"'))
466
- axis.push("the oracle does not name the check job's check-gate-qualification step");
467
- if (!ciOracle.includes('qual != "success"'))
468
- axis.push("the oracle does not guard that step's conclusion against 'success'");
469
- if (!ciOracle.includes('qual == "skipped"'))
470
- axis.push("the oracle does not classify a SKIPPED body as a failure of its own");
471
- assert.ok(
472
- axis.length === 0,
473
- "[QK:RELEASE-SHA-QUALIFIED-IN-CI] the exact-SHA CI oracle must require the qualification BODY to have run " +
474
- "at the release SHA -- the `check` job's `Run ./run.sh check-gate-qualification` step concluding 'success', " +
475
- "with skipped named as its own failure. Three green job names do not prove the step inside one of them " +
476
- `executed. Broken: ${axis.join("; ")}`,
477
- );
478
457
  }
479
458
 
480
459
  // ===========================================================================
481
- // 8d. The qualification filter COVERS every mutant subject (#103 piece 2)
482
- //
483
- // The body no longer runs on every branch push; a decision script reads the
484
- // push range and answers. Its whole safety argument is that the path set is
485
- // DERIVED from the committed manifests rather than copied into a list, so a
486
- // new mutant subject cannot land outside the filter and quietly stop being
487
- // re-proven in CI. This asserts that property behaviourally: every subject
488
- // and signatureSource in scripts/mutants/*.json, fed to the script as a
489
- // one-file change, must decide `run_body=true`. The oracle is the manifests
490
- // themselves, read here independently of the script.
460
+ // 8c'. The release oracle, driven, not read (#144). The shipped verify-exact-ci.sh
461
+ // runs unmodified in verify mode with a PATH holding only bash, python3 and a
462
+ // fake `gh`, so the mocked boundary is the external CLI and the script gains
463
+ // no input of its own (a RUN_JSON seam in the shipped script would be a way to
464
+ // launder release evidence past gh). The fake logs every call first, admits
465
+ // only the four argv shapes verify mode sends, and refuses anything else with
466
+ // its own code and marker. THREE claims, asserted in this order so each
467
+ // mutant dies on its own:
468
+ // - the witness is a PUSH run: the one `run list` call carries
469
+ // `--event push` and a jq keeping only push runs of this SHA. That is the
470
+ // argv the script SENDS; the fake does not evaluate the jq, and GitHub's
471
+ // own filtering is not observed here;
472
+ // - every required job that did not conclude success is refused by name
473
+ // (absent, failure, skipped, cancelled, null);
474
+ // - the qualification body is not a release axis: the oracle passes whether
475
+ // the check job's body step is skipped, absent or green, and never prints
476
+ // it.
477
+ // `wait` mode and newest-run choice are outside this cell. The four job names
478
+ // are this gate's own literals, never read from the script. Each negative
479
+ // cell changes one fact and nothing else.
491
480
  // ===========================================================================
492
481
  {
493
- const decider = join(REPO_DIR, "scripts/ci-qualify-decide.sh");
494
- const paths = new Set<string>();
495
- for (const file of globSync("scripts/mutants/*.json", { cwd: REPO_DIR })) {
496
- const manifest = JSON.parse(readFileSync(join(REPO_DIR, file), "utf8")) as {
497
- mutants?: { subject?: string; signatureSource?: string }[];
498
- };
499
- for (const mutant of manifest.mutants ?? []) {
500
- if (mutant.subject) paths.add(mutant.subject);
501
- if (mutant.signatureSource) paths.add(mutant.signatureSource);
502
- }
503
- }
504
- assert.ok(paths.size > 50, `read only ${paths.size} mutant paths from the manifests`);
505
- const uncovered = [...paths].filter((path) => {
506
- const out = execFileSync("bash", [decider, "--files-from", "-"], {
507
- cwd: REPO_DIR,
508
- encoding: "utf8",
509
- input: `${path}\n`,
510
- stdio: ["pipe", "pipe", "ignore"],
511
- });
512
- return out.trim() !== "run_body=true";
513
- });
514
- assert.ok(
515
- uncovered.length === 0,
516
- "[QK:QUALIFY-FILTER-COVERS-SUBJECTS] the CI qualification filter must run the body for a change to ANY " +
517
- "committed mutant subject or signature source — it derives that set from scripts/mutants/*.json for " +
518
- "exactly this reason, so a path it cannot see is a claim that silently stops being re-proven in CI. " +
519
- `Uncovered (${uncovered.length} of ${paths.size}): ${uncovered.slice(0, 8).join(", ")}`,
520
- );
521
- }
522
-
523
- // ===========================================================================
524
- // 8e. The filter still runs the body for every RED the body has ever produced
525
- //
526
- // Five reds in 549 runs (#99 stage-2). The first reading of them used the
527
- // tip COMMIT and concluded the filter would have missed four; replaying the
528
- // real two-dot push range — what GitHub actually compares — showed all five
529
- // hit. That reversal is the whole evidentiary basis for this filter.
530
- //
531
- // It is asserted from RECORDED file lists, not from live history, and that
532
- // is a constraint rather than a convenience: check-gate-qualification runs
533
- // every gate inside a snapshot with its own fresh git baseline, where these
534
- // 2026-07/08 commits do not exist. A cell that read history there would be
535
- // CONTROL-RED for the whole lane — measured, not predicted (this cell did
536
- // exactly that on 2026-09-06 and cost the release-gate lane its 17 kills).
537
- // The fixture carries what history said, measured once, in a repo where the
538
- // objects are present.
539
- //
540
- // It has its own replant, and the reason is the SHAPE of the input: 8d feeds
541
- // ONE path per call, this feeds a whole push at once. A matcher that only
542
- // looked at the first entry would leave 8d green — every single-file call is
543
- // its own first entry — while every one of these five pushes opens with a
544
- // non-surface path (DELIVERY.md, AGENTS.md, NEXT.md, AGENTS.md,
545
- // demo/demo-baseline.sh). Invisible to 8d, fatal here.
546
- //
547
- // The two-dot READING itself is proven separately, below.
548
- // ===========================================================================
549
- {
550
- const decider = join(REPO_DIR, "scripts/ci-qualify-decide.sh");
551
- const fixture = JSON.parse(readFileSync(join(REPO_DIR, "scripts/fixtures/qualify-replay.json"), "utf8")) as {
552
- runs: { runId: string; before: string; head: string; files: string[]; tipOnlyFiles: string[] }[];
553
- };
554
- assert.equal(fixture.runs.length, 5, "the replay fixture holds all five historical qualification reds");
482
+ const oracle = join(REPO_DIR, ".claude/skills/entwurf-release/scripts/verify-exact-ci.sh");
483
+ const required = ["check", "install-surface", "artifact-consumer", "macos-install-surface"];
484
+ const qualStep = "Run ./run.sh check-gate-qualification";
485
+ const sha = "0123456789abcdef0123456789abcdef01234567";
486
+ const unexpected = "UNEXPECTED-GH-ARGV";
487
+ const tmp = mkdtempSync(join(tmpdir(), "entwurf-exact-ci-"));
488
+ const bin = join(tmp, "bin");
489
+ const resolve = (tool: string) =>
490
+ realpathSync(execFileSync("bash", ["-c", `command -v ${tool}`], { encoding: "utf8" }).trim());
555
491
  const wrong: string[] = [];
556
- for (const run of fixture.runs) {
557
- assert.ok(run.files.length > 0, `run ${run.runId} has no recorded push range`);
558
- const out = execFileSync("bash", [decider, "--files-from", "-"], {
559
- cwd: REPO_DIR,
560
- encoding: "utf8",
561
- input: `${run.files.join("\n")}\n`,
562
- stdio: ["pipe", "pipe", "ignore"],
563
- }).trim();
564
- if (out !== "run_body=true") wrong.push(`run ${run.runId} (${run.files.length} files) decided ${out}`);
565
- }
566
- assert.ok(
567
- wrong.length === 0,
568
- "[QK:QUALIFY-FILTER-REPLAYS-PAST-CATCHES] every qualification RED in this repo's CI history must still " +
569
- "run the body under the filter, over the two-dot push range GitHub compares — the measurement that " +
570
- "overturned the tip-commit reading and justified filtering at all. This reads a whole push at once, " +
571
- "which 8d cannot: each of these five opens with a non-surface path, so a matcher that stopped after " +
572
- "the first entry would pass every single-file check 8d makes and fail here. " +
573
- `Broken: ${wrong.join("; ")}`,
574
- );
575
- }
576
-
577
- // ===========================================================================
578
- // 8f. The filter reads the two-dot PUSH RANGE, not the tip commit
579
- //
580
- // The fixture above proves the matcher's verdict on recorded file lists; it
581
- // cannot prove which git range produced them, because it calls no git. This
582
- // does, hermetically: a throwaway repo shaped like the real defect — a push
583
- // of two commits whose TIP is docs-only while the commit under it touched a
584
- // mutant subject. The two-dot range sees the code; the tip alone does not.
585
- // That is the exact misreading #99 corrected, and it is what four of the
586
- // five recorded pushes look like when read the wrong way.
587
- //
588
- // Hermetic on purpose: no repo history, so it runs identically inside the
589
- // qualification snapshot.
590
- // ===========================================================================
591
- {
592
- const decider = join(REPO_DIR, "scripts/ci-qualify-decide.sh");
593
- const tmp = mkdtempSync(join(tmpdir(), "entwurf-qualify-range-"));
594
- // core.hooksPath is set globally on this operator's machine; a fixture repo
595
- // must not run their hooks.
596
- const git = (...args: string[]) =>
597
- execFileSync("git", ["-c", "core.hooksPath=/dev/null", "-c", "user.email=g@e", "-c", "user.name=g", ...args], {
598
- cwd: tmp,
599
- stdio: "ignore",
600
- });
492
+ const wrongWitness: string[] = [];
493
+ const wrongNoBody: string[] = [];
601
494
  try {
602
- git("init", "-q", "-b", "main");
603
- // The decider reads the repo it SITS IN, so the fixture gets a real copy
604
- // of it and its own one-entry manifest: no seam, no env override, and the
605
- // manifest-reading path is exercised too.
606
- mkdirSync(join(tmp, "scripts/mutants"), { recursive: true });
607
- mkdirSync(join(tmp, "pi-extensions/lib"), { recursive: true });
608
- copyFileSync(decider, join(tmp, "scripts/ci-qualify-decide.sh"));
495
+ mkdirSync(bin);
496
+ mkdirSync(join(tmp, "home"));
497
+ symlinkSync(resolve("bash"), join(bin, "bash"));
498
+ symlinkSync(resolve("python3"), join(bin, "python3"));
499
+ const runList = `run list --repo fixture/entwurf --workflow ci.yml --commit ${sha} --event push --limit 10 --json databaseId,headSha,createdAt,event --jq`;
500
+ const pushOnlyJq = `map(select(.headSha == "${sha}" and .event == "push")) | sort_by(.createdAt) | reverse | .[0].databaseId // empty`;
609
501
  writeFileSync(
610
- join(tmp, "scripts/mutants/fixture.json"),
611
- `${JSON.stringify(
612
- {
613
- schemaVersion: 1,
614
- lane: "fixture",
615
- mutants: [{ claim: "FIXTURE", subject: "pi-extensions/lib/fixture-subject.ts" }],
616
- },
617
- null,
618
- "\t",
619
- )}\n`,
620
- );
621
- writeFileSync(join(tmp, "seed.txt"), "seed\n");
622
- git("add", "-A");
623
- git("commit", "-qm", "seed");
624
- const base = execFileSync("git", ["rev-parse", "HEAD"], { cwd: tmp, encoding: "utf8" }).trim();
625
- writeFileSync(join(tmp, "pi-extensions/lib/fixture-subject.ts"), "// a mutant subject\n");
626
- git("add", "-A");
627
- git("commit", "-qm", "code commit (mid-push)");
628
- writeFileSync(join(tmp, "README.md"), "docs only\n");
629
- git("add", "-A");
630
- git("commit", "-qm", "docs commit (tip)");
631
- const head = execFileSync("git", ["rev-parse", "HEAD"], { cwd: tmp, encoding: "utf8" }).trim();
632
- const decide = (from: string) =>
633
- execFileSync("bash", [join(tmp, "scripts/ci-qualify-decide.sh"), from, head], {
634
- cwd: tmp,
635
- encoding: "utf8",
636
- env: { ...process.env, CI_EVENT_NAME: "push", CI_FORCED: "false" },
637
- stdio: ["ignore", "pipe", "ignore"],
638
- }).trim();
639
- const overRange = decide(base);
640
- const overTip = decide(`${head}~1`);
641
- assert.ok(
642
- overRange === "run_body=true" && overTip === "run_body=false",
643
- "[QK:QUALIFY-FILTER-READS-PUSH-RANGE] the filter must diff the whole two-dot push range, not the tip " +
644
- "commit: a push whose tip is docs-only can still carry a mutant subject underneath it, which is how " +
645
- "four of the five historical reds look like docs pushes when read tip-first. " +
646
- `Broken: range=${overRange}, tip-only=${overTip} (tip-only must be false, or this fixture proves nothing).`,
502
+ join(bin, "gh"),
503
+ [
504
+ `#!${join(bin, "bash")}`,
505
+ 'd="${0%/*}/.."',
506
+ `printf '%s\\n' "$*" >> "$d/gh.log"`,
507
+ 'case "$*" in',
508
+ ' "auth status") exit 0 ;;',
509
+ " \"repo view --json nameWithOwner --jq .nameWithOwner\") printf 'fixture/entwurf\\n'; exit 0 ;;",
510
+ ' "run view 4242 --repo fixture/entwurf --json databaseId,headSha,status,conclusion,url,event,jobs")',
511
+ ` printf '%s\\n' "$(<"$d/run.json")"; exit 0 ;;`,
512
+ "esac",
513
+ `if [ "$#" -eq 16 ] && [ "\${*:1:15}" = "${runList}" ]; then printf '4242\\n'; exit 0; fi`,
514
+ `printf '${unexpected}: %s\\n' "$*" >&2`,
515
+ "exit 86",
516
+ "",
517
+ ].join("\n"),
518
+ { mode: 0o755 },
647
519
  );
648
- } finally {
649
- rmSync(tmp, { recursive: true, force: true });
650
- }
651
- }
652
-
653
- // ===========================================================================
654
- // 8g. #137: TOML data-subject hunks, not path identity; Markdown/JSON data and
655
- // code/signatures/machinery remain file-level. Hermetic like 8f: real history is NOT
656
- // in a qualification snapshot.
657
- // ==========================================================================
658
- {
659
- const tmp = mkdtempSync(join(tmpdir(), "entwurf-qualify-hunks-"));
660
- const git = (...args: string[]) =>
661
- execFileSync("git", ["-c", "core.hooksPath=/dev/null", "-c", "user.email=g@e", "-c", "user.name=g", ...args], {
662
- cwd: tmp,
663
- stdio: "ignore",
664
- });
665
- const head = () => execFileSync("git", ["rev-parse", "HEAD"], { cwd: tmp, encoding: "utf8" }).trim();
666
- const data = 'version = "1"\n[[panes]]\ncommand = ["node", "build.mjs"]\n# outside\n';
667
- const wrong: string[] = [];
668
- try {
669
- git("init", "-q", "-b", "main");
670
- mkdirSync(join(tmp, "scripts/mutants"), { recursive: true });
671
- mkdirSync(join(tmp, "fixture"));
672
- mkdirSync(join(tmp, "scripts/fixtures"));
673
- copyFileSync(join(REPO_DIR, "scripts/ci-qualify-decide.sh"), join(tmp, "scripts/ci-qualify-decide.sh"));
674
- const manifest = JSON.stringify({
675
- mutants: [
676
- { subject: "fixture/data.toml", find: ["[[panes]]"] },
677
- { subject: "fixture/data.toml", find: ['command = ["node", "build.mjs"]'] },
678
- { subject: "fixture/data.toml", find: ["[[panes]]", 'command = ["node", "build.mjs"]'] },
679
- { subject: "fixture/code.ts", find: ["return 1"], signatureSource: "fixture/signature.md" },
680
- { subject: "fixture/signature.md", find: ["signature"] },
681
- { subject: "fixture/joined.toml", find: ["alpha = 1", "beta = 2"] },
682
- { subject: "fixture/doc.md", find: ["Legacy rows are documented: kept for history."] },
683
- { subject: "fixture/reg.json", find: ['"mode": "self-fetch",'] },
684
- { subject: "scripts/fixtures/pin.json", find: ['"pinned": 1'] },
685
- { subject: "scripts/ci-qualify-decide.sh", find: ["set -euo pipefail"] },
686
- ],
520
+ // The check job carries the body step the way a push run renders it since #144:
521
+ // present and skipped. The no-body cells below vary only that step.
522
+ const job = (name: string, body: string | null = "skipped") => ({
523
+ name,
524
+ status: "completed",
525
+ conclusion: "success" as string | null,
526
+ steps: name === "check" && body !== null ? [{ name: qualStep, status: "completed", conclusion: body }] : [],
687
527
  });
688
- writeFileSync(join(tmp, "scripts/mutants/fixture.json"), manifest);
689
- writeFileSync(join(tmp, "fixture/data.toml"), data);
690
- writeFileSync(join(tmp, "fixture/code.ts"), "// outside\nfunction f() { return 1; }\n");
691
- writeFileSync(join(tmp, "fixture/signature.md"), "signature\n# outside\n");
692
- writeFileSync(join(tmp, "fixture/joined.toml"), "version = 1\nalpha = 1\nbeta = 2\n");
693
- const doc = "# Doc\nLegacy rows are documented: kept for history.\n# outside\n";
694
- const reg = '{\n"copilot": {\n"mode": "self-fetch",\n"level": 6\n}\n}\n';
695
- writeFileSync(join(tmp, "fixture/doc.md"), doc);
696
- writeFileSync(join(tmp, "fixture/reg.json"), reg);
697
- writeFileSync(join(tmp, "scripts/fixtures/pin.json"), '{\n"version": 1,\n"pinned": 1\n}\n');
698
- git("add", "-A");
699
- git("commit", "-qm", "seed");
700
- const base = head();
701
- const nonToml: string[] = [];
702
- const check = (name: string, expected: boolean, reason?: string, before = base, into = wrong) => {
703
- const r = spawnSync("bash", [join(tmp, "scripts/ci-qualify-decide.sh"), before, head()], {
704
- cwd: tmp,
528
+ const drive = (jobs: ReturnType<typeof job>[]) => {
529
+ writeFileSync(
530
+ join(tmp, "run.json"),
531
+ JSON.stringify({
532
+ databaseId: 4242,
533
+ headSha: sha,
534
+ status: "completed",
535
+ conclusion: "success",
536
+ url: "https://example.invalid/runs/4242",
537
+ event: "push",
538
+ jobs,
539
+ }),
540
+ );
541
+ rmSync(join(tmp, "gh.log"), { force: true });
542
+ const r = spawnSync(join(bin, "bash"), [oracle, sha, "verify"], {
705
543
  encoding: "utf8",
706
- env: { ...process.env, CI_EVENT_NAME: "push", CI_FORCED: "false" },
544
+ env: { PATH: bin, HOME: join(tmp, "home") },
545
+ timeout: 20_000,
707
546
  });
708
- if (r.status !== 0 || r.stdout.trim() !== `run_body=${expected}` || (reason && !r.stderr.includes(reason)))
709
- into.push(`${name}: status=${r.status}, out=${r.stdout.trim()}, err=${r.stderr.trim()}`);
710
- };
711
- const commit = () => {
712
- git("add", "-A");
713
- git("commit", "-qm", "cell");
547
+ // No log means no call reached the fake; the cell names that through its own fields.
548
+ const log = existsSync(join(tmp, "gh.log")) ? readFileSync(join(tmp, "gh.log"), "utf8") : "";
549
+ return { ...r, log };
714
550
  };
715
- const reset = () => git("reset", "--hard", base); // ONLY this test-owned throwaway repository.
716
- writeFileSync(join(tmp, "fixture/data.toml"), data.replace('version = "1"', 'version = "2"'));
717
- commit();
718
- check("outside", false, "fixture/data.toml: no find range touched");
719
- // A replay must read HEAD's manifest, not an unrelated dirty worktree's find.
720
- writeFileSync(
721
- join(tmp, "scripts/mutants/fixture.json"),
722
- '{"mutants":[{"subject":"fixture/data.toml","find":["version"]}]}',
723
- );
724
- check("HEAD manifest, not dirty worktree", false);
725
- reset();
726
- for (const [name, content] of [
727
- ["panes line", data.replace("[[panes]]", "[[panes]] # changed")],
728
- ["command line", data.replace('command = ["node", "build.mjs"]', 'command = ["node", "build.mjs"] # changed')],
729
- ["moved", `# inserted above\n${data}`],
730
- ["ambiguous", `${data}[[panes]]\n`],
731
- ["missing find", data.replace("[[panes]]", "[[other]]")],
732
- ] as const) {
733
- writeFileSync(join(tmp, "fixture/data.toml"), content);
734
- commit();
735
- check(name, true);
736
- reset();
551
+
552
+ const control = drive(required.map((name) => job(name)));
553
+ const passLines = [
554
+ `exact-ci: PASS sha=${sha} run=4242 event=push`,
555
+ ...required.map((name) => `exact-ci: job ${name}=success`),
556
+ "exact-ci: qualification BODY is not a release axis (#144)",
557
+ ];
558
+ const calls = control.log.trim().split("\n");
559
+ const listCalls = calls.filter((call) => call.startsWith("run list "));
560
+ if (listCalls.length !== 1 || listCalls[0] !== `${runList} ${pushOnlyJq}`)
561
+ wrongWitness.push(`run list calls: ${JSON.stringify(listCalls)}`);
562
+ if (
563
+ control.status !== 0 ||
564
+ passLines.some((line) => !control.stdout.includes(line)) ||
565
+ control.stderr.includes(unexpected) ||
566
+ calls.length !== 4 ||
567
+ calls.some((call) => call.startsWith("run watch"))
568
+ )
569
+ wrong.push(`control: status=${control.status}, out=${control.stdout.trim()}, err=${control.stderr.trim()}`);
570
+
571
+ for (const name of required) {
572
+ for (const state of ["absent", "failure", "skipped", "cancelled", "null"]) {
573
+ const jobs = required.map((n) => job(n)).filter((j) => state !== "absent" || j.name !== name);
574
+ for (const j of jobs) if (j.name === name) j.conclusion = state === "null" ? null : state;
575
+ const shown = state === "absent" || state === "null" ? "None" : `'${state}'`;
576
+ const diagnostic = `job '${name}' conclusion=${shown}, expected 'success'`;
577
+ const r = drive(jobs);
578
+ if (
579
+ r.status === 0 ||
580
+ !r.stderr.includes("ABORT: exact-SHA CI contract failed:") ||
581
+ !r.stderr.includes(diagnostic) ||
582
+ r.stdout.includes("exact-ci: PASS") ||
583
+ r.stderr.includes(unexpected)
584
+ )
585
+ wrong.push(`${name}=${state}: status=${r.status}, want "${diagnostic}", err=${r.stderr.trim()}`);
586
+ }
587
+ }
588
+
589
+ // The body step skipped (the control), absent, or green: the oracle passes on
590
+ // the four jobs alone and its output never names the body.
591
+ const bodyCells: [string, { status: number | null; stdout: string }][] = [["skipped", control]];
592
+ bodyCells.push(["absent", drive(required.map((name) => job(name, null)))]);
593
+ bodyCells.push(["success", drive(required.map((name) => job(name, "success")))]);
594
+ for (const [body, r] of bodyCells) {
595
+ if (r.status !== 0 || !r.stdout.includes("exact-ci: PASS") || r.stdout.includes("check-gate-qualification"))
596
+ wrongNoBody.push(`body step ${body}: status=${r.status}, out=${r.stdout.trim()}`);
737
597
  }
738
- git("rm", "fixture/data.toml");
739
- commit();
740
- check("deleted subject", true);
741
- reset();
742
- git("mv", "fixture/data.toml", "fixture/renamed.toml");
743
- commit();
744
- check("renamed subject", true);
745
- reset();
746
- writeFileSync(join(tmp, "fixture/code.ts"), "// outside changed\nfunction f() { return 1; }\n");
747
- commit();
748
- check("code outside find remains file-level", true);
749
- reset();
750
- writeFileSync(join(tmp, "fixture/signature.md"), "signature\n# outside changed\n");
751
- commit();
752
- check("signature also data subject remains file-level", true);
753
- reset();
754
- writeFileSync(join(tmp, "fixture/joined.toml"), "version = 1\nalpha = 1\nbeta = 2 # changed\n");
755
- commit();
756
- check("only multiline find, second line touched", true);
757
- reset();
758
- writeFileSync(join(tmp, "scripts/fixtures/pin.json"), '{\n"version": 2,\n"pinned": 1\n}\n');
759
- commit();
760
- check("machinery also data subject remains file-level", true);
761
- reset();
762
- writeFileSync(
763
- join(tmp, "scripts/ci-qualify-decide.sh"),
764
- `${readFileSync(join(tmp, "scripts/ci-qualify-decide.sh"), "utf8")}\n# outside find\n`,
765
- );
766
- commit();
767
- check("machinery also subject remains file-level", true);
768
- reset();
769
- // Outside an untouched exact-once span, a shorter presence literal (measured on DELIVERY.md)
770
- // or a later JSON duplicate key (JSON.parse: last wins) can re-satisfy the oracle.
771
- writeFileSync(join(tmp, "fixture/doc.md"), `${doc}Legacy rows are documented\n`);
772
- commit();
773
- check("markdown restatement outside find", true, "conservative:", base, nonToml);
774
- reset();
775
- writeFileSync(
776
- join(tmp, "fixture/reg.json"),
777
- reg.replace("6\n}\n}\n", '6\n},\n"copilot": { "level": 6, "mode": "self-fetch" }\n}\n'),
778
- );
779
- commit();
780
- check("json duplicate key outside find", true, "conservative:", base, nonToml);
781
- reset();
782
- // The first quoted name must still fail open after a >pipe-buffer list:
783
- // printf | grep -q can lose its match to SIGPIPE under pipefail.
784
- writeFileSync(join(tmp, '000"quoted'), "x\n");
785
- for (let i = 0; i < 768; i++) writeFileSync(join(tmp, "fixture", `${i}_${"x".repeat(180)}.txt`), "x\n");
786
- commit();
787
- check("large quoted changed-name list", true, "quoted changed path cannot be read honestly");
788
- reset();
789
- check("unreadable range", true, "fail-open 4", "f".repeat(40));
790
- git("rm", "scripts/mutants/fixture.json");
791
- commit();
792
- check("missing manifests", true);
793
- assert.ok(
794
- wrong.length === 0,
795
- "[QK:QUALIFY-FILTER-SUBJECT-HUNKS] only untouched, unmoved, exact-once TOML find ranges may skip; " +
796
- "unknown ranges run, and code/signature/machinery retain file-level protection. " +
797
- wrong.join("; "),
798
- );
799
- assert.ok(
800
- nonToml.length === 0,
801
- "[QK:QUALIFY-FILTER-TOML-ONLY-REFINEMENT] Markdown and JSON subjects stay file-level: an untouched exact-once " +
802
- "find does not pin kill-power when a shorter presence literal or a later duplicate JSON key outside it can " +
803
- "re-satisfy the oracle. " +
804
- nonToml.join("; "),
805
- );
806
598
  } finally {
807
599
  rmSync(tmp, { recursive: true, force: true });
808
600
  }
601
+ assert.ok(
602
+ wrongWitness.length === 0,
603
+ "[QK:RELEASE-WITNESS-IS-PUSH-RUN] the exact-SHA CI oracle must ask for the PUSH run of this SHA only: one " +
604
+ "`run list` call carrying `--event push` and a jq that keeps only push runs at this headSha. A " +
605
+ "workflow_dispatch run is the optional qualification diagnostic, never release evidence; admitted, a " +
606
+ `running or red dispatch would become the run judged. Broken: ${wrongWitness.join("; ")}`,
607
+ );
608
+ assert.ok(
609
+ wrong.length === 0,
610
+ "[QK:RELEASE-ORACLE-REFUSES-NON-SUCCESS-JOB] the exact-SHA CI oracle must accept a run whose four required " +
611
+ "jobs concluded success and refuse it, naming the job, when any one of them is absent, failed, skipped, " +
612
+ "cancelled or unconcluded -- a release SHA is certified only by success, never by the absence of failure. " +
613
+ `Broken:\n ${wrong.join("\n ")}`,
614
+ );
615
+ assert.ok(
616
+ wrongNoBody.length === 0,
617
+ "[QK:RELEASE-ORACLE-CLAIMS-NO-BODY] the qualification body is not a release axis (#144): the exact-SHA CI " +
618
+ "oracle must pass on the four required jobs whether the check job's body step is skipped, absent or green, " +
619
+ "and must never print it, since a PASS that names the body would read as kill-power evidence the release " +
620
+ `did not measure. Broken:\n ${wrongNoBody.join("\n ")}`,
621
+ );
809
622
  }
810
623
 
811
624
  // ===========================================================================
@@ -1255,11 +1068,11 @@ console.log(
1255
1068
  "observed to propagate the code: a smoke with LIVE unset (through run_ts, with its operator-readable marker) and " +
1256
1069
  "a run.sh wrapper declining its own prerequisite (including the measured LIVE=1 no-cortex-connection cell); and every " +
1257
1070
  "LIVE smoke is either wired into release_gate or excluded by a sentence the docs still carry; and the moved " +
1258
- "check-gate-qualification stays reachable on its owners (absent from the default chain, exactly once in CI, " +
1259
- "absent from release_gate: the one body is the prepared SHA's CI step) and the CI step qualifies the FULL floor, which runs before it, " +
1260
- "while the exact-SHA release oracle requires that BODY step to have concluded success at the release SHA, " +
1261
- "and the CI filter that decides when that body runs covers every mutant subject and still runs it for all " +
1262
- "five historical reds; and " +
1071
+ "check-gate-qualification stays reachable only as the schedule/dispatch drift diagnostic (absent from the " +
1072
+ "default chain, from release_gate, from every push and from the release skill; exactly once in CI) over the " +
1073
+ "FULL floor, which runs before it, while the exact-SHA release oracle, driven through a fake gh, asks only for " +
1074
+ "the push run, refuses every required job that did not conclude success, and passes without ever naming the " +
1075
+ "qualification body; and " +
1263
1076
  "every gate a committed mutant names is itself inside check:full or states its exclusion in prose an operator " +
1264
1077
  "reads; and the CI push trigger is filtered to branch refs, so a release tag creates no duplicate run; and " +
1265
1078
  "Pi -> visible Codex -> visible Pi first-admission callback/addressed/outbound acceptance is a release MUST " +