@junghanacs/entwurf 0.37.0 → 0.38.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.
@@ -1,256 +0,0 @@
1
- #!/usr/bin/env bash
2
- # Decide whether a push needs the qualification BODY (#103 piece 2).
3
- #
4
- # The body re-proves every committed mutant's kill-power and costs ~28 minutes.
5
- # Across 549 CI runs it produced five reds, and replaying the real push ranges
6
- # shows this filter would have run the body for all five (#99 stage-2 §1). So a
7
- # push that cannot have touched the qualification surface skips it -- and the
8
- # surface is READ FROM THE MANIFESTS, never copied into a list that can drift.
9
- #
10
- # TWO ENTRYPOINTS, ONE MATCHER:
11
- # ci-qualify-decide.sh <before-sha> <head-sha> # CI and replay: a git range
12
- # ci-qualify-decide.sh --files-from <file|-> # a literal changed-file list
13
- # Both print `run_body=true|false` on stdout and their reasoning on stderr.
14
- #
15
- # Pure local git plus python3 for the manifest read: no gh, no network, so the
16
- # replay fixture gate runs offline and exercises this exact code.
17
- set -euo pipefail
18
-
19
- REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
20
-
21
- # Fail-open is numbered so a log line says WHICH one fired. Every one of these
22
- # means "we cannot compute an honest diff", and the honest answer to that is to
23
- # run the body, never to skip it.
24
- fail_open() {
25
- echo "ci-qualify-decide: fail-open $1 ($2) -> run_body=true" >&2
26
- echo "run_body=true"
27
- exit 0
28
- }
29
-
30
- collect_paths_from_manifests() {
31
- python3 - "$REPO_DIR" "${1:-}" <<'PY'
32
- import glob
33
- import json
34
- import os
35
- import sys
36
- import subprocess
37
-
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)
53
- for mutant in manifest.get("mutants", []):
54
- subject = mutant.get("subject")
55
- if subject:
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)
61
- signature_source = mutant.get("signatureSource")
62
- if signature_source:
63
- out.add(signature_source)
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}))
68
- PY
69
- }
70
-
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.
136
- matches_qualification_surface() {
137
- local file="$1"
138
- local subject
139
- while IFS= read -r subject; do
140
- [ "$file" = "$subject" ] && { echo "manifest-signature-source"; return 0; }
141
- done <<< "$SIGNATURE_PATHS"
142
- case "$file" in
143
- scripts/mutants/*) echo "glob:scripts/mutants/**"; return 0 ;;
144
- scripts/check-*) echo "glob:scripts/check-*"; return 0 ;;
145
- scripts/lib/*) echo "glob:scripts/lib/**"; return 0 ;;
146
- # The replay fixture is the independent oracle input cell 8e reads. Once a
147
- # claim rests on it, changing it changes what the body is qualified against.
148
- scripts/fixtures/*) echo "glob:scripts/fixtures/**"; return 0 ;;
149
- .github/workflows/*) echo "glob:.github/workflows/**"; return 0 ;;
150
- run.sh) echo "exact:run.sh"; return 0 ;;
151
- package.json) echo "exact:package.json"; return 0 ;;
152
- # The decider itself: change the filter and the body runs, so a filter
153
- # that stopped covering something cannot hide behind its own change.
154
- scripts/ci-qualify-decide.sh) echo "exact:scripts/ci-qualify-decide.sh"; return 0 ;;
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"
170
- return 1
171
- }
172
-
173
- decide_from_files() {
174
- local files="$1"
175
- local count=0
176
- local hits=0
177
- local file rule
178
- while IFS= read -r file; do
179
- [ -n "$file" ] || continue
180
- count=$((count + 1))
181
- if rule="$(matches_qualification_surface "$file")"; then
182
- hits=$((hits + 1))
183
- [ "$hits" -le 5 ] && echo "ci-qualify-decide: hit $file ($rule)" >&2
184
- fi
185
- done <<< "$files"
186
- if [ "$hits" -gt 0 ]; then
187
- echo "ci-qualify-decide: $hits of $count changed files touch the qualification surface -> run_body=true" >&2
188
- echo "run_body=true"
189
- else
190
- echo "ci-qualify-decide: no qualification-surface path among $count changed files -> run_body=false" >&2
191
- echo "run_body=false"
192
- fi
193
- }
194
-
195
- if [ "${1:-}" = "--files-from" ]; then
196
- MATCH_MODE=files
197
- load_manifest_surface
198
- SRC="${2:-}"
199
- [ -n "$SRC" ] || { echo "ABORT: --files-from needs a path or -" >&2; exit 1; }
200
- if [ "$SRC" = "-" ]; then FILES="$(cat)"; else FILES="$(cat "$SRC")"; fi || fail_open 4 "unreadable files-from input"
201
- decide_from_files "$FILES"
202
- exit 0
203
- fi
204
-
205
- BEFORE="${1:-}"
206
- HEAD_SHA="${2:-}"
207
- EVENT="${CI_EVENT_NAME:-push}"
208
- FORCED="${CI_FORCED:-false}"
209
-
210
- # 3. A human or the schedule asked for this run; there is no push range to read.
211
- # `qualify` is what makes the dispatch an explicit request for the BODY. A
212
- # dispatch WITHOUT it is a floor-only rerun, so it must not silently satisfy
213
- # the release oracle's fourth axis -- the input would be dead configuration if
214
- # every dispatch ran the body regardless of its value.
215
- case "$EVENT" in
216
- workflow_dispatch)
217
- [ "${CI_QUALIFY:-false}" = "true" ] && fail_open 3 "event=workflow_dispatch with qualify=true asked for the body"
218
- echo "ci-qualify-decide: dispatch without qualify=true is a floor-only rerun; the release oracle will still refuse this SHA until the body runs -> run_body=false" >&2
219
- echo "run_body=false"
220
- exit 0
221
- ;;
222
- schedule) fail_open 3 "event=schedule runs the body unconditionally (drift ceiling)" ;;
223
- # 5. A pull_request event carries no `before`, and GitHub compares it
224
- # three-dot. Narrowing PRs would need a second comparison rule for an
225
- # event this repo barely uses, so PRs always run the body.
226
- pull_request) fail_open 5 "event=pull_request has no two-dot push range" ;;
227
- esac
228
-
229
- [ -n "$BEFORE" ] && [ -n "$HEAD_SHA" ] || { echo "ABORT: usage: $0 <before-sha> <head-sha>" >&2; exit 1; }
230
-
231
- # 1. A new branch's first push: GitHub sends all-zeros, and its own documented
232
- # base ("the parent of the ancestor of the deepest commit pushed") is not
233
- # what a two-dot diff from zeros would give.
234
- case "$BEFORE" in
235
- *[!0]*) ;;
236
- *) fail_open 1 "before=$BEFORE is the all-zero SHA (new branch)" ;;
237
- esac
238
-
239
- # 2. Force push. What `before` even means here is UNDOCUMENTED (#99 stage-2,
240
- # "measured not"): if it is the old head, the two-dot diff runs backwards and
241
- # the verdict can invert. Never guess -- run the body.
242
- [ "$FORCED" = "true" ] && fail_open 2 "the push was forced; its two-dot base is undocumented"
243
-
244
- # 4. The range is unreadable on this checkout -- a shallow clone, or an object
245
- # the remote no longer has (which is also how a force push's old base
246
- # disappears, so 2 and 4 can both be true and each still names itself).
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"
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"
249
-
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
255
-
256
- decide_from_files "$CHANGED"
@@ -1,149 +0,0 @@
1
- {
2
- "note": "Every qualification RED this repo's CI has ever produced (#99 stage-2 SS1), with the file list of the two-dot push range GitHub actually compared. `before` is the head of the previous ci.yml run on the same branch. `files` was measured from this clone's history on 2026-09-06 and is committed BECAUSE history is not readable everywhere the gate runs: check-gate-qualification executes gates inside a snapshot with its own fresh git baseline, where these commits do not exist. `tipOnlyFiles` is the same push read the wrong way -- the tip commit alone -- which is how four of these five once looked like docs-only pushes; it is kept as the record of the misreading, not as an input.",
3
- "runs": [
4
- {
5
- "runId": "32421631735",
6
- "branch": "issue-82-copilot-citizen",
7
- "before": "1da40c9994d5",
8
- "head": "dff731d415d5",
9
- "diedAt": "manifest validation (head)",
10
- "files": [
11
- "DELIVERY.md",
12
- "NEXT--issue-82-copilot-citizen.md",
13
- "ROADMAP.md",
14
- "mcp/entwurf-bridge/tsconfig.build.json",
15
- "package.json",
16
- "pi-extensions/lib/meta-session.ts",
17
- "pi-extensions/meta-bridge-hook-copilot.ts",
18
- "pi/entwurf-capabilities.json",
19
- "pi/meta-bridge-copilot/.claude-plugin/marketplace.json",
20
- "pi/meta-bridge-copilot/entwurf-meta-receive-copilot/.claude-plugin/plugin.json",
21
- "pi/meta-bridge-copilot/entwurf-meta-receive-copilot/hooks/hooks.json",
22
- "pi/meta-bridge-copilot/entwurf-meta-receive-copilot/scripts/copilot-hook-launch.sh",
23
- "run.sh",
24
- "scripts/check-copilot-birth-hook.ts",
25
- "scripts/check-entwurf-capabilities.ts",
26
- "scripts/check-gate-qualification.ts",
27
- "scripts/check-meta-doctor-oracle.sh",
28
- "scripts/check-meta-manifest-schema.py",
29
- "scripts/copilot-bridge-doctor.sh",
30
- "scripts/copilot-bridge-install.sh",
31
- "scripts/meta-bridge-hook-log.sh",
32
- "scripts/mutants/copilot-birth.json",
33
- "scripts/tsconfig.json",
34
- "tsconfig.json"
35
- ],
36
- "tipOnlyFiles": ["NEXT--issue-82-copilot-citizen.md"]
37
- },
38
- {
39
- "runId": "32450955048",
40
- "branch": "issue-82-copilot-citizen",
41
- "before": "0aed67df2f22",
42
- "head": "88d0641b001e",
43
- "diedAt": "lane inventory (head)",
44
- "files": [
45
- "AGENTS.md",
46
- "NEXT--issue-82-copilot-citizen.md",
47
- "VERIFY.md",
48
- "docs/adding-a-harness.md",
49
- "docs/external-mcp-host.md",
50
- "package.json",
51
- "pi-extensions/lib/meta-sender-identity.ts",
52
- "pi-extensions/lib/meta-session.ts",
53
- "pi-extensions/meta-bridge-hook-copilot.ts",
54
- "pi/meta-bridge-copilot/entwurf-meta-receive-copilot/.claude-plugin/plugin.json",
55
- "pi/meta-bridge-copilot/entwurf-meta-receive-copilot/scripts/copilot-hook-launch.sh",
56
- "run.sh",
57
- "scripts/check-copilot-birth-hook.ts",
58
- "scripts/check-meta-session.ts",
59
- "scripts/copilot-bridge-doctor.sh",
60
- "scripts/copilot-mcp-bridge.sh",
61
- "scripts/copilot-mcp-config.py",
62
- "scripts/copilot-statusline-bridge.sh",
63
- "scripts/copilot-statusline-config.py",
64
- "scripts/mutants/copilot-birth.json",
65
- "scripts/smoke-copilot-mcp-state.sh",
66
- "scripts/smoke-copilot-statusline-state.sh"
67
- ],
68
- "tipOnlyFiles": [
69
- "AGENTS.md",
70
- "NEXT--issue-82-copilot-citizen.md",
71
- "docs/adding-a-harness.md",
72
- "docs/external-mcp-host.md",
73
- "pi-extensions/lib/meta-sender-identity.ts",
74
- "pi-extensions/meta-bridge-hook-copilot.ts",
75
- "pi/meta-bridge-copilot/entwurf-meta-receive-copilot/.claude-plugin/plugin.json",
76
- "pi/meta-bridge-copilot/entwurf-meta-receive-copilot/scripts/copilot-hook-launch.sh",
77
- "run.sh",
78
- "scripts/check-copilot-birth-hook.ts",
79
- "scripts/copilot-bridge-doctor.sh",
80
- "scripts/mutants/copilot-birth.json"
81
- ]
82
- },
83
- {
84
- "runId": "33154263432",
85
- "branch": "issue-87-omp-vendor-measurement",
86
- "before": "7c414f55bd69",
87
- "head": "9b70a4938fd0",
88
- "diedAt": "lane inventory (head)",
89
- "files": [
90
- "NEXT.md",
91
- "mcp/entwurf-bridge/src/index.ts",
92
- "package.json",
93
- "pi-extensions/lib/entwurf-self-address.ts",
94
- "run.sh",
95
- "scripts/check-entwurf-self-address.ts",
96
- "scripts/mutants/self-address.json",
97
- "scripts/omp-mcp-bridge.sh",
98
- "scripts/omp-tool-surface.py",
99
- "scripts/smoke-omp-mcp-state.sh"
100
- ],
101
- "tipOnlyFiles": ["NEXT.md"]
102
- },
103
- {
104
- "runId": "30991752256",
105
- "branch": "mux-placement",
106
- "before": "e5689808c9ba",
107
- "head": "e05af7294724",
108
- "diedAt": "CONTROL pre-red",
109
- "files": [
110
- "AGENTS.md",
111
- "NEXT--mux-placement.md",
112
- "README.md",
113
- "ROADMAP.md",
114
- "VERIFY.md",
115
- "docs/mux-launch-rail.md",
116
- "mcp/entwurf-bridge/src/index.ts",
117
- "package.json",
118
- "pi-extensions/entwurf-control.ts",
119
- "pi-extensions/lib/entwurf-v2-contract.ts",
120
- "pi-extensions/lib/mux-fresh-call.ts",
121
- "pi-extensions/lib/mux-launch.ts",
122
- "pi-extensions/lib/mux-placement.ts",
123
- "run.sh",
124
- "scripts/check-gate-qualification.ts",
125
- "scripts/check-mux-fresh-call.ts",
126
- "scripts/check-mux-launch.ts",
127
- "scripts/check-release-gate-outcomes.ts",
128
- "scripts/mutants/mux-fresh-call.json",
129
- "scripts/smoke-mux-fresh-call-live.ts",
130
- "tsconfig.json"
131
- ],
132
- "tipOnlyFiles": ["NEXT--mux-placement.md"]
133
- },
134
- {
135
- "runId": "31062287903",
136
- "branch": "mux-placement",
137
- "before": "e05af7294724",
138
- "head": "79edfe7738a4",
139
- "diedAt": "CONTROL pre-red",
140
- "files": [
141
- "demo/demo-baseline.sh",
142
- "demo/demo.sh",
143
- "scripts/lib/mutation-qualify.ts",
144
- "scripts/new-session-id.ts"
145
- ],
146
- "tipOnlyFiles": ["demo/demo-baseline.sh", "demo/demo.sh", "scripts/new-session-id.ts"]
147
- }
148
- ]
149
- }