@junghanacs/entwurf 0.34.2 → 0.35.0

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.
@@ -0,0 +1,64 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "scene": "whole-Herdr Pi to native Claude Code",
4
+ "take": "take-herdr-pi-cc-2026-10-11T11-57-22-297Z",
5
+ "startedAt": "2026-10-11T11:57:22.299Z",
6
+ "stage": "SOURCE checkout, not an npm consumer",
7
+ "gif": {
8
+ "path": "docs/assets/entwurf-demo.gif",
9
+ "sha256": "b686739b5c68fa458dd47c844711aea0f6ab5819eb3eb5d2850449922fa084c0",
10
+ "bytes": 1835518,
11
+ "seconds": 43.5,
12
+ "width": 1126,
13
+ "height": 764,
14
+ "frames": 340,
15
+ "captions": false
16
+ },
17
+ "cast": {
18
+ "sha256": "bbf8a409c58f8b48e88346d232a66ee41ac0c2acf8e605a3f568fd7d40857cf2",
19
+ "public": false
20
+ },
21
+ "runtime": {
22
+ "entwurf": "0.34.2",
23
+ "pi": "1.1.0",
24
+ "herdr": "0.9.3",
25
+ "gitHead": "9f4c9983fed1dd0ba619bc5a50f0f5abccefeef4",
26
+ "gitDirty": true,
27
+ "measuredPiModel": "openai-codex/gpt-6.1-sol",
28
+ "requestedClaudeAlias": "sonnet",
29
+ "measuredClaudeNativeModel": "claude-sonnet-5-5"
30
+ },
31
+ "auditedOriginalSourceSha256": {
32
+ "demo/take-herdr-pi-cc.ts": "f9cd30f1bb5b06236559a9f1283f0e030b300f6a17858d778c40d246327a6349",
33
+ "demo/lib/take-recorder.ts": "d47d824b90a7265c5ab6e7c4bf313b4f81b27cef836b4ddd15f971f3de386256",
34
+ "demo/render-take.ts": "f6e68157d7f8fb1f0e00a359efaa04aa30eeca1112ce2571661f39aea826b55d",
35
+ "demo/lib/herdr-take.ts": "57089d883323b2582aadd7f54f63cfce541f3a2c584bb6940313fbcdc940d83e"
36
+ },
37
+ "render": {
38
+ "aggArgs": [
39
+ "--idle-time-limit",
40
+ "2",
41
+ "--speed",
42
+ "1",
43
+ "--last-frame-duration",
44
+ "8",
45
+ "--font-size",
46
+ "14",
47
+ "--theme",
48
+ "monokai"
49
+ ],
50
+ "trimmedTailEvents": 2,
51
+ "trimmedWhat": "recorder teardown: herdr client exit (alt-screen exit + server shut down line)",
52
+ "renderedAt": "2026-10-11T11:58:32.828Z",
53
+ "aggVersion": null,
54
+ "ffprobeVersion": null,
55
+ "toolVersionEvidence": "not recorded for this original; current versions cannot fill historical fields"
56
+ },
57
+ "limits": [
58
+ "Original cast, manifests and native records remain private; this tree cannot replay that capture.",
59
+ "The tracked command is a new LIVE take recipe, not byte-identical reproduction.",
60
+ "Current maintainer source is amended; original source hashes are historical observations, not a complete import-closure seal.",
61
+ "Operator approved the rough original pixels; this is not privacy or public-readability certification.",
62
+ "No claim of 0.35.0 release, published-package acceptance or future Herdr plugin observation follows from this take."
63
+ ]
64
+ }
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@junghanacs/entwurf",
3
- "version": "0.34.2",
3
+ "version": "0.35.0",
4
4
  "description": "Garden-citizen dispatch substrate and meta-bridge for pi, Claude Code, Copilot CLI, OMP, Antigravity, and Codex CLI; Claude and Snowflake Cortex Code ACP backends.",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -40,6 +40,8 @@
40
40
  "scripts/",
41
41
  "prompts/",
42
42
  "demo/README.md",
43
+ "demo/herdr-pi-cc-20261011.provenance.json",
44
+ "!demo/**/*.ts",
43
45
  "demo/demo.sh",
44
46
  "demo/demo-baseline.sh",
45
47
  "docs/setup-clean-host.md",
@@ -186,7 +188,7 @@
186
188
  "check:core": "pnpm run check:toolchain && pnpm run check:vitest && pnpm run check:contracts",
187
189
  "check:toolchain": "pnpm lint && pnpm typecheck",
188
190
  "check:vitest": "./run.sh check-mux-fresh-call && ./run.sh check-acp-sdk-surface && ./run.sh check-pi-queue-oracle && ./run.sh check-tests-beside-behavior",
189
- "check:contracts": "./run.sh check-shell-quote && ./run.sh check-entwurf-session-identity && ./run.sh check-meta-session && ./run.sh check-meta-v3-record && ./run.sh check-mailbox-receipt-state && ./run.sh check-entwurf-capabilities && ./run.sh check-harness-admission-parity && ./run.sh check-capability-bundle-reach && ./run.sh check-meta-mailbox-state-write && ./run.sh check-meta-receiver-marker && ./run.sh check-meta-capability-source && ./run.sh check-model-lock && ./run.sh check-dep-versions && ./run.sh check-node-floor-coherence && ./run.sh check-claude-floor-coherence && ./run.sh check-copilot-statusline && ./run.sh check-pi-import-surface && ./run.sh check-env-namespace && ./run.sh check-pi-runtime-version && ./run.sh check-pi-preflight && ./run.sh check-project-trust-handler && ./run.sh check-entwurf-v2-contract && ./run.sh check-entwurf-v2-send && ./run.sh check-entwurf-v2-runner && ./run.sh check-control-send-receipt && ./run.sh check-compaction-send-guard && ./run.sh check-entwurf-resume-args && ./run.sh check-entwurf-v2-native-push && ./run.sh check-entwurf-v2-production && ./run.sh check-entwurf-v2-surface && ./run.sh check-entwurf-bridge-pi-free && ./run.sh check-resume-launch-identity && ./run.sh check-mux-placement && ./run.sh check-mux-launch && ./run.sh check-mux-resume-call && ./run.sh check-mux-parent-artifact && ./run.sh check-mux-launcher-fence && ./run.sh check-entwurf-v2-visible-resume && ./run.sh check-control-socket-path && ./run.sh check-meta-listing && ./run.sh check-entwurf-fact-provider && ./run.sh check-herdr-placement && ./run.sh check-herdr-fresh-call && ./run.sh check-herdr-supply && ./run.sh check-fresh-call-dispatch && ./run.sh check-entwurf-peers-surface && ./run.sh check-entwurf-self-address && ./run.sh check-entwurf-deliverability && ./run.sh check-native-push-adapter && ./run.sh check-native-push-register && ./run.sh check-auth-boundary && ./run.sh check-acp-overlay && ./run.sh check-acp-tool-surface && ./run.sh check-acp-event-mapper && ./run.sh check-acp-prompt-builder && ./run.sh check-acp-config && ./run.sh check-acp-session-store && ./run.sh check-acp-carrier-augment && ./run.sh check-acp-foreground-only && ./run.sh check-acp-vendor-rejection && ./run.sh check-codex-sender-identity && ./run.sh check-codex-native-push && ./run.sh check-pi-durable-beside",
191
+ "check:contracts": "./run.sh check-demo-reproducibility && ./run.sh check-shell-quote && ./run.sh check-entwurf-session-identity && ./run.sh check-meta-session && ./run.sh check-meta-v3-record && ./run.sh check-mailbox-receipt-state && ./run.sh check-entwurf-capabilities && ./run.sh check-harness-admission-parity && ./run.sh check-capability-bundle-reach && ./run.sh check-meta-mailbox-state-write && ./run.sh check-meta-receiver-marker && ./run.sh check-meta-capability-source && ./run.sh check-model-lock && ./run.sh check-dep-versions && ./run.sh check-node-floor-coherence && ./run.sh check-claude-floor-coherence && ./run.sh check-copilot-statusline && ./run.sh check-pi-import-surface && ./run.sh check-env-namespace && ./run.sh check-pi-runtime-version && ./run.sh check-pi-preflight && ./run.sh check-project-trust-handler && ./run.sh check-entwurf-v2-contract && ./run.sh check-entwurf-v2-send && ./run.sh check-entwurf-v2-runner && ./run.sh check-control-send-receipt && ./run.sh check-compaction-send-guard && ./run.sh check-entwurf-resume-args && ./run.sh check-entwurf-v2-native-push && ./run.sh check-entwurf-v2-production && ./run.sh check-entwurf-v2-surface && ./run.sh check-entwurf-bridge-pi-free && ./run.sh check-resume-launch-identity && ./run.sh check-mux-placement && ./run.sh check-mux-launch && ./run.sh check-mux-resume-call && ./run.sh check-mux-parent-artifact && ./run.sh check-mux-launcher-fence && ./run.sh check-entwurf-v2-visible-resume && ./run.sh check-control-socket-path && ./run.sh check-meta-listing && ./run.sh check-entwurf-fact-provider && ./run.sh check-herdr-placement && ./run.sh check-herdr-fresh-call && ./run.sh check-herdr-supply && ./run.sh check-fresh-call-dispatch && ./run.sh check-entwurf-peers-surface && ./run.sh check-entwurf-self-address && ./run.sh check-entwurf-deliverability && ./run.sh check-native-push-adapter && ./run.sh check-native-push-register && ./run.sh check-auth-boundary && ./run.sh check-acp-overlay && ./run.sh check-acp-tool-surface && ./run.sh check-acp-event-mapper && ./run.sh check-acp-prompt-builder && ./run.sh check-acp-config && ./run.sh check-acp-session-store && ./run.sh check-acp-carrier-augment && ./run.sh check-acp-foreground-only && ./run.sh check-acp-vendor-rejection && ./run.sh check-codex-sender-identity && ./run.sh check-codex-native-push && ./run.sh check-pi-durable-beside",
190
192
  "check:hermetic": "./run.sh check-bridge-delivery && ./run.sh smoke-pi-attach && ./run.sh check-fresh-cut-gate && ./run.sh check-hook-launch-topology && ./run.sh check-meta-hook-session-switch && ./run.sh check-copilot-birth-hook && ./run.sh check-copilot-receive-arm && ./run.sh check-copilot-launch && ./run.sh check-pi-launch && ./run.sh check-codex-app-server-launch && ./run.sh check-meta-identity-consumers && ./run.sh check-socket-probe && ./run.sh smoke-meta-honesty && ./run.sh check-meta-doctor-oracle && ./run.sh check-agy-permission-matrix && ./run.sh smoke-agy-statusline-state && ./run.sh smoke-copilot-statusline-state && ./run.sh smoke-copilot-mcp-state && ./run.sh smoke-omp-bridge-state && ./run.sh smoke-omp-mcp-state && ./run.sh smoke-omp-receive-state && ./run.sh check-omp-birth-hook && ./run.sh check-omp-receive-arm && ./run.sh check-omp-fresh-preflight && ./run.sh smoke-agy-hooks-state && ./run.sh smoke-pi-provider-state && ./run.sh smoke-user-scope-citizen && ./run.sh smoke-meta-prune && ./run.sh smoke-meta-keyset-guard && ./run.sh check-meta-manifest-schema && ./run.sh check-entwurf-control-rpc && ./run.sh check-entwurf-bridge-boot && ./run.sh check-probe-bridge-command && ./run.sh check-meta-facts && ./run.sh check-peer-facts && ./run.sh check-herdr-plugin && ./run.sh check-herdr-plugin-profile && ./run.sh check-herdr-plugin-build && ./run.sh check-herdr-runtime-bootstrap && ./run.sh check-herdr-activation && ./run.sh check-agy-sender-identity && ./run.sh check-acp-provider-surface && ./run.sh check-acp-stop-reason && ./run.sh check-acp-prompt-lifecycle && ./run.sh check-acp-usage-accounting && ./run.sh check-acp-launch-namespace && ./run.sh check-acp-stream-hooks && ./run.sh check-acp-backend-preflight && ./run.sh check-acp-session-reuse && ./run.sh check-typing-call-fence && ./run.sh check-gate-manifests && ./run.sh check-release-gate-outcomes && ./run.sh check-probe-ordering && ./run.sh check-probe-cli-shim && ./run.sh check-acp-cortex && ./run.sh check-codex-bridge-identity && ./run.sh check-pi-bridge-sender && ./run.sh check-pi-mcp-register && ./run.sh check-pi-mcp-bridge && ./run.sh check-codex-birth-hook && ./run.sh smoke-codex-config-state && ./run.sh smoke-codex-birth && ./run.sh check-herdr-sandbox",
191
193
  "check:package": "./run.sh check-install-surface && ./run.sh smoke-meta-install-state && ./run.sh smoke-agy-install-state && ./run.sh smoke-setup-verdict && ./run.sh check-pack-pin-matcher && ./run.sh check-pack-consumer-roots && ./run.sh check-package-source-routing && ./run.sh check-install-preflight && ./run.sh check-pack"
192
194
  },
package/run.sh CHANGED
@@ -134,6 +134,7 @@ Usage:
134
134
  ./run.sh check-omp-birth-hook # #87 gate: drives the real OMP assembler into a temp dir, imports the ASSEMBLED index.ts into a MOCK omp host and fires session_start/session_switch. tui mints one backend:"omp" record + a sender marker keyed to the host's OWN pid (the one-process join) + the garden id on the status line; print/rpc/json mint NOTHING (hasUI is true on the rpc rows, as in the vendor); switch attaches on the same native id and mints the replacement on a new one; the CERTIFIED receiver reader still finds no marker. Also the four-root joint binding: extension and omp-labeled bridge child resolve the same sessions/mailbox/senders/receivers bundle against test-built literals (never the production resolver as its own oracle), under a poisoned PI_CODING_AGENT_DIR and under four distinct ENTWURF_META_* overrides; the override grammar is absolute-or-~ and both halves plus the doctor refuse anything else by name; and a drifted provenance label selects no root policy at all. Hermetic; no omp, no model turn
135
135
  ./run.sh check-pi-durable-contact # #129 L-identity gate, CHECKOUT-ONLY (an installed package refuses it in run_ts). Opens the REAL durable app from THIS checkout's verified carrier (pi/pi-durable/carrier/ under its resolver, against the checkout's pinned pi SDK set — refused by name first) with the pi-durable contact, births the citizen and runs its tools adapter-direct through a privately emitted branch bridge (ENTWURF_PI_DURABLE_BRIDGE_ENTRY; its registry must match this checkout). Matrix roots {default, explicit} x {fresh, --continue}: one record keyed by the durable session dir, sender marker on the spawned host pid, exactly one bridge child whose /proc PPID is that host, poisoned identity carriers kept from it, its four root carriers bound to the roots written, the receiver ARMED (raw marker owned by the host pid + start key as pi-durable-host, joined to the sender marker; removed on close), self = the record and replyable, peers from an outside bridge, same garden id on reopen; the bundle's compiled owner join must admit pi-durable-host. Reaps only the host/bridges it owns on any failure. Harness-bypassed: no model turn, no ToolTask. Missing inputs → SKIP (97), never a pass
136
136
  ./run.sh check-pi-durable-send # #129 L-send FIRST CELL, CHECKOUT-ONLY. Same input as check-pi-durable-contact. One native durable turn: the app's own controller input → GenerationTask → ToolTask → the contact's entwurf_v2 → the private bridge → a seeded self-fetch target (C2 recipe, owned by a separate idle child). S = a 127.0.0.1 scripted OpenAI-compatible endpoint configured through the app's documented models.json (dummy key, PI_OFFLINE); H = the native ToolTask path; V (a vendor turn) is NOT run. Oracles: the wire (tools offered, v2 schema = the bridge's tools/list, exactly two requests, the tool result + receipt in the second), the raw mailbox (.msg 1, signal, sender = the host record), the native view (one call, its settled result, root current). Not exactly-once, not recovery, not a recipient turn. Missing inputs → SKIP (97)
137
+ ./run.sh check-demo-reproducibility # source-checkout-only: discover offline demo Node tests; no LIVE, model, render or publication
137
138
  ./run.sh check-pi-durable-beside # #129 mutant execution coordinate (VERIFY rule 3): run_vitest over the six pi-durable beside tests (adapter, carrier/SDK-set verifier, preflight, owner join, carrier relocation/resolver/emitter/task footer, native-module file and directory ingress) — the pi-durable qualification lane's gate argv. Same files check-tests-beside-behavior already runs.
138
139
  ./run.sh check-pi-durable-receive # #129 L-receive FIRST CELL, CHECKOUT-ONLY. Same input (the bundle must carry the pi-durable-host owner join). The host driver gives the app NO input; a seeded claude-code sender (marker owned by a separate idle child, handed to a gate-owned sending bridge through ENTWURF_META_SENDER_MARKER) enqueues one message, the host's doorbell admits its notice to the ROOT conversation, and the scripted model drains it with entwurf_inbox_read. S withholds response #1 until the ENQUEUE receipt, the .delivered bytes, the signal, the host-owned receiver marker and the notice are captured. Oracles: receiver() admission, native view, raw archive (.delivered.read + lastReadAt), wire (exactly two requests, drain result = view), and after a green reap a forensic read-only SQLite copy (submission row by request id = reported id, root, input, done, placed entry = the notice). V NOT run. Missing inputs → SKIP (97)
139
140
  ./run.sh check-copilot-receive-arm # #82 RAIL 5 gate: the REAL receiver installer + the REAL extension.mjs forked with a stubbed SDK. Arms only after birth, marker owned by the WATCHER pid, self-fetch dispatch answer, doorbell carries the garden id and NOT the body, id-drift/foreign-parent refusals. Hermetic; no Copilot, no model turn
@@ -1029,6 +1030,37 @@ check_capability_bundle_reach() {
1029
1030
  run_ts scripts/check-capability-bundle-reach.ts
1030
1031
  }
1031
1032
 
1033
+ check_demo_reproducibility() {
1034
+ # Source-only Node strip-types/CLI and subprocess fixtures, not adapter Vitest tests.
1035
+ # Discovery lives beside demo behavior; no filename list or npm TS crossing.
1036
+ section "demo reproducibility (offline source fixtures, no model or recording)"
1037
+ case "$REPO_DIR" in */node_modules/*) fail "check-demo-reproducibility is source-checkout-only"; return 1 ;; esac
1038
+ local -a tests=()
1039
+ local f
1040
+ while IFS= read -r f; do [ -n "$f" ] && tests+=("$f"); done < <(cd "$REPO_DIR" && find demo -type f -name '*.test.ts' -print | LC_ALL=C sort)
1041
+ if [ "${#tests[@]}" -eq 0 ]; then
1042
+ fail "[QK:DEMO-TEST-DISCOVERY] demo test discovery is empty — no fixtures is not a passing floor"
1043
+ return 1
1044
+ fi
1045
+ (
1046
+ local sandbox name
1047
+ sandbox=$(mktemp -d /tmp/entwurf-demo-unit.XXXXXX)
1048
+ trap 'rm -rf -- "$sandbox"' EXIT
1049
+ while IFS= read -r name; do
1050
+ case "$name" in ENTWURF_*|HERDR_*|PI_SESSION_ID|PI_AGENT_ID|CLAUDECODE|CLAUDE_CODE_SESSION_ID|TMUX|TMUX_PANE|LIVE) unset "$name" ;; esac
1051
+ done < <(compgen -e)
1052
+ export HOME="$sandbox/home" PI_CODING_AGENT_DIR="$sandbox/pi"
1053
+ export XDG_CONFIG_HOME="$sandbox/config" XDG_DATA_HOME="$sandbox/data" XDG_STATE_HOME="$sandbox/state" XDG_CACHE_HOME="$sandbox/cache" XDG_RUNTIME_DIR="$sandbox/runtime"
1054
+ mkdir -m 700 "$HOME" "$PI_CODING_AGENT_DIR" "$XDG_CONFIG_HOME" "$XDG_DATA_HOME" "$XDG_STATE_HOME" "$XDG_CACHE_HOME" "$XDG_RUNTIME_DIR" "$sandbox/bin"
1055
+ # Take.finish records PATH versions; fixtures must not depend on an installed Pi or run it.
1056
+ printf '#!/bin/sh\n[ "$1" = --version ] && { printf "offline-fixture\\n"; exit 0; }\nexit 81\n' > "$sandbox/bin/pi"
1057
+ chmod +x "$sandbox/bin/pi"
1058
+ export PATH="$sandbox/bin:$PATH"
1059
+ cd "$REPO_DIR"
1060
+ node --test "${tests[@]}"
1061
+ )
1062
+ }
1063
+
1032
1064
  check_bridge_delivery() {
1033
1065
  # demo/demo.sh scene 3, recovered as a deterministic gate. demo drove two real pi
1034
1066
  # panes so it proved DELIVERY through the installed binary (at model cost); what
@@ -3756,6 +3788,8 @@ check_pack() {
3756
3788
  # pi-extensions/ so it also covers any future allowlist entry; `plugins/` needs no
3757
3789
  # companion negation because `^plugins/` above already refuses that whole tree.
3758
3790
  '\.test\.[cm]?[jt]s$'
3791
+ # Maintainer take/renderer/helper TS is source-only, including non-test implementations.
3792
+ '^demo/.*\.ts$'
3759
3793
  )
3760
3794
 
3761
3795
  local pass=1 f pat hit
@@ -8175,6 +8209,9 @@ case "$cmd" in
8175
8209
  check-pi-queue-oracle)
8176
8210
  check_pi_queue_oracle
8177
8211
  ;;
8212
+ check-demo-reproducibility)
8213
+ check_demo_reproducibility
8214
+ ;;
8178
8215
  check-tests-beside-behavior)
8179
8216
  check_tests_beside_behavior
8180
8217
  ;;
@@ -3455,6 +3455,7 @@ let manifestCount: number;
3455
3455
  "codex-native": 82,
3456
3456
  "compaction-send-guard": 7,
3457
3457
  "control-socket-disconnect": 4,
3458
+ "demo-reproducibility": 12,
3458
3459
  "copilot-birth": 19,
3459
3460
  "copilot-launch": 14,
3460
3461
  "pi-launch": 9,
@@ -3493,7 +3494,7 @@ let manifestCount: number;
3493
3494
  "omp-fresh": 24,
3494
3495
  "omp-receive": 11,
3495
3496
  "probe-ordering": 1,
3496
- "release-gate": 22,
3497
+ "release-gate": 24,
3497
3498
  "retired-sent-surface": 2,
3498
3499
  "resume-args": 6,
3499
3500
  "resume-launch-identity": 6,
@@ -43,7 +43,7 @@
43
43
  // Pure + subprocess, no network/model — IN pnpm run check:full.
44
44
 
45
45
  import { strict as assert } from "node:assert";
46
- import { execFileSync } from "node:child_process";
46
+ import { execFileSync, spawnSync } from "node:child_process";
47
47
  import { copyFileSync, globSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
48
48
  import { tmpdir } from "node:os";
49
49
  import { basename, join } from "node:path";
@@ -650,6 +650,164 @@ function runSubcommand(sub: string, env: Record<string, string | undefined>): {
650
650
  }
651
651
  }
652
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
+ ],
687
+ });
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,
705
+ encoding: "utf8",
706
+ env: { ...process.env, CI_EVENT_NAME: "push", CI_FORCED: "false" },
707
+ });
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");
714
+ };
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();
737
+ }
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
+ } finally {
807
+ rmSync(tmp, { recursive: true, force: true });
808
+ }
809
+ }
810
+
653
811
  // ===========================================================================
654
812
  // 9. Every gate a mutant manifest NAMES runs inside `pnpm run check:full`
655
813
  //
@@ -28,37 +28,117 @@ fail_open() {
28
28
  }
29
29
 
30
30
  collect_paths_from_manifests() {
31
- python3 - "$REPO_DIR" <<'PY'
31
+ python3 - "$REPO_DIR" "${1:-}" <<'PY'
32
32
  import glob
33
33
  import json
34
34
  import os
35
35
  import sys
36
+ import subprocess
36
37
 
37
- repo = sys.argv[1]
38
- out = set()
39
- for path in sorted(glob.glob(os.path.join(repo, "scripts/mutants/*.json"))):
40
- with open(path, encoding="utf-8") as handle:
41
- manifest = json.load(handle)
38
+ repo, rev = sys.argv[1:]
39
+ def git(*args):
40
+ return subprocess.check_output(["git", "-C", repo, *args])
41
+ paths = (git("ls-tree", "-r", "--name-only", rev, "scripts/mutants").decode().splitlines()
42
+ if rev else sorted(glob.glob(os.path.join(repo, "scripts/mutants/*.json"))))
43
+ paths = [p for p in paths if p.endswith(".json")]
44
+ if not paths:
45
+ raise ValueError("no mutant manifests")
46
+ out, signatures, subjects = set(), set(), {}
47
+ for path in paths:
48
+ if rev:
49
+ manifest = json.loads(git("show", f"{rev}:{path}"))
50
+ else:
51
+ with open(path, encoding="utf-8") as handle:
52
+ manifest = json.load(handle)
42
53
  for mutant in manifest.get("mutants", []):
43
54
  subject = mutant.get("subject")
44
55
  if subject:
45
56
  out.add(subject)
57
+ find = mutant.get("find")
58
+ # The runner joins a find ARRAY into ONE multiline occurrence.
59
+ needle = "\n".join(find) if isinstance(find, list) and all(isinstance(s, str) for s in find) else None
60
+ subjects.setdefault(subject, []).append(needle)
46
61
  signature_source = mutant.get("signatureSource")
47
62
  if signature_source:
48
63
  out.add(signature_source)
49
- for entry in sorted(out):
50
- print(entry)
64
+ signatures.add(signature_source)
65
+ if not out or any(any(c in p for c in '\n\r\t"\\') for p in out):
66
+ raise ValueError("empty or unrepresentable manifest paths")
67
+ print(json.dumps({"paths": sorted(out), "signatures": sorted(signatures), "subjects": subjects}))
51
68
  PY
52
69
  }
53
70
 
54
- # A changed path matters when it is a mutant's subject or signature source, or
55
- # when it sits in the machinery that decides what those mutants prove.
71
+ load_manifest_surface() {
72
+ MANIFEST_DATA="$(collect_paths_from_manifests "${1:-}")" || fail_open 6 "missing/unreadable mutant manifests"
73
+ MANIFEST_PATHS="$(printf '%s' "$MANIFEST_DATA" | python3 -c 'import json,sys;print("\n".join(json.load(sys.stdin)["paths"]))')" || fail_open 6 "unreadable manifest paths"
74
+ SIGNATURE_PATHS="$(printf '%s' "$MANIFEST_DATA" | python3 -c 'import json,sys;print("\n".join(json.load(sys.stdin)["signatures"]))')" || fail_open 6 "unreadable signature paths"
75
+ }
76
+
77
+ # Return success ONLY when every exact-once TOML find span is demonstrably
78
+ # untouched and unmoved on BOTH blobs. Any uncertainty leaves the body armed.
79
+ subject_find_untouched() {
80
+ python3 - "$REPO_DIR" "$BEFORE" "$HEAD_SHA" "$1" 3<<< "$MANIFEST_DATA" <<'PY'
81
+ import json
82
+ import os
83
+ import re
84
+ import subprocess
85
+ import sys
86
+
87
+ repo, before, head, path = sys.argv[1:]
88
+ def git(*args):
89
+ return subprocess.check_output(["git", "-C", repo, *args], stderr=subprocess.DEVNULL).decode("utf-8")
90
+ def span(source, needle):
91
+ if not needle or source.count(needle) != 1:
92
+ raise ValueError("find absent or ambiguous")
93
+ start = source[:source.index(needle)].count("\n") + 1
94
+ return start, start + needle.count("\n") - int(needle.endswith("\n"))
95
+ try:
96
+ finds = json.load(os.fdopen(3))["subjects"].get(path)
97
+ if not finds or any(not needle for needle in finds):
98
+ raise ValueError("missing find spans")
99
+ old, new = git("show", f"{before}:{path}"), git("show", f"{head}:{path}")
100
+ if "\0" in old or "\0" in new:
101
+ raise ValueError("binary subject")
102
+ diff = git("diff", "--no-renames", "--no-ext-diff", "--no-textconv", "--no-color", "-U0", before, head, "--", path)
103
+ if "Binary files " in diff or "GIT binary patch" in diff:
104
+ raise ValueError("binary diff")
105
+ hunks = []
106
+ for line in diff.splitlines():
107
+ if not line.startswith("@@"):
108
+ continue
109
+ match = re.fullmatch(r"@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(?: .*)?", line)
110
+ if not match:
111
+ raise ValueError("unreadable hunk")
112
+ a, b, c, d = match.groups()
113
+ hunks.append((int(a), int(b or 1), int(c), int(d or 1)))
114
+ if not hunks:
115
+ raise ValueError("no readable text hunks")
116
+ for needle in set(finds):
117
+ os_, oe = span(old, needle)
118
+ ns, ne = span(new, needle)
119
+ if (os_, oe) != (ns, ne):
120
+ raise ValueError("find range moved")
121
+ for a, b, c, d in hunks:
122
+ if (b > 0 and a <= oe and a + b - 1 >= os_) or (d > 0 and c <= ne and c + d - 1 >= ns):
123
+ raise ValueError("find range touched")
124
+ except (ValueError, KeyError, TypeError, OSError, subprocess.SubprocessError) as error:
125
+ print(f"ci-qualify-decide: fail-open 7 {path}: {error} -> run_body=true", file=sys.stderr)
126
+ sys.exit(1)
127
+ print(f"ci-qualify-decide: {path}: no find range touched (all exact-once spans unmoved)", file=sys.stderr)
128
+ PY
129
+ }
130
+
131
+ # Signature sources and machinery MUST precede subjects: many paths are BOTH.
132
+ # Code/unknown subjects retain file-level protection; only TOML data gets hunk
133
+ # refinement (#137 non-goal: no skipping code/gate/mutant/matrix changes). TOML
134
+ # refuses a redefined key, but a shorter Markdown presence literal or a later JSON
135
+ # duplicate key outside an untouched find can still re-satisfy an oracle.
56
136
  matches_qualification_surface() {
57
137
  local file="$1"
58
138
  local subject
59
139
  while IFS= read -r subject; do
60
- [ "$file" = "$subject" ] && { echo "manifest-subject"; return 0; }
61
- done <<< "$MANIFEST_PATHS"
140
+ [ "$file" = "$subject" ] && { echo "manifest-signature-source"; return 0; }
141
+ done <<< "$SIGNATURE_PATHS"
62
142
  case "$file" in
63
143
  scripts/mutants/*) echo "glob:scripts/mutants/**"; return 0 ;;
64
144
  scripts/check-*) echo "glob:scripts/check-*"; return 0 ;;
@@ -73,6 +153,20 @@ matches_qualification_surface() {
73
153
  # that stopped covering something cannot hide behind its own change.
74
154
  scripts/ci-qualify-decide.sh) echo "exact:scripts/ci-qualify-decide.sh"; return 0 ;;
75
155
  esac
156
+ while IFS= read -r subject; do
157
+ if [ "$file" = "$subject" ]; then
158
+ if [ "${MATCH_MODE:-files}" = range ]; then
159
+ case "$file" in
160
+ *.toml) ;;
161
+ *) echo "conservative:non-toml-subject"; return 0 ;;
162
+ esac
163
+ if subject_find_untouched "$file"; then
164
+ return 1
165
+ fi
166
+ fi
167
+ echo "manifest-subject"; return 0
168
+ fi
169
+ done <<< "$MANIFEST_PATHS"
76
170
  return 1
77
171
  }
78
172
 
@@ -98,13 +192,12 @@ decide_from_files() {
98
192
  fi
99
193
  }
100
194
 
101
- MANIFEST_PATHS="$(collect_paths_from_manifests)"
102
- [ -n "$MANIFEST_PATHS" ] || { echo "ABORT: no mutant manifests read from $REPO_DIR/scripts/mutants" >&2; exit 1; }
103
-
104
195
  if [ "${1:-}" = "--files-from" ]; then
196
+ MATCH_MODE=files
197
+ load_manifest_surface
105
198
  SRC="${2:-}"
106
199
  [ -n "$SRC" ] || { echo "ABORT: --files-from needs a path or -" >&2; exit 1; }
107
- if [ "$SRC" = "-" ]; then FILES="$(cat)"; else FILES="$(cat "$SRC")"; fi
200
+ if [ "$SRC" = "-" ]; then FILES="$(cat)"; else FILES="$(cat "$SRC")"; fi || fail_open 4 "unreadable files-from input"
108
201
  decide_from_files "$FILES"
109
202
  exit 0
110
203
  fi
@@ -154,6 +247,10 @@ esac
154
247
  git -C "$REPO_DIR" cat-file -e "$BEFORE^{commit}" 2>/dev/null || fail_open 4 "before=$BEFORE is not a commit object in this checkout"
155
248
  git -C "$REPO_DIR" cat-file -e "$HEAD_SHA^{commit}" 2>/dev/null || fail_open 4 "head=$HEAD_SHA is not a commit object in this checkout"
156
249
 
157
- CHANGED="$(git -C "$REPO_DIR" diff --name-only "$BEFORE".."$HEAD_SHA" 2>/dev/null)" || fail_open 4 "git diff $BEFORE..$HEAD_SHA failed"
250
+ MATCH_MODE=range
251
+ load_manifest_surface "$HEAD_SHA"
252
+ CHANGED="$(git -C "$REPO_DIR" diff --no-renames --no-ext-diff --no-textconv --name-only "$BEFORE".."$HEAD_SHA" 2>/dev/null)" || fail_open 4 "git diff $BEFORE..$HEAD_SHA failed"
253
+ # Git quotes unrepresentable changed names; never interpret those as absent paths.
254
+ if grep -q '^"' <<< "$CHANGED"; then fail_open 4 "quoted changed path cannot be read honestly"; fi
158
255
 
159
256
  decide_from_files "$CHANGED"