@chrono-meta/fh-gate 1.4.78 → 1.4.80
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.
- package/.claude-plugin/marketplace.json +2 -2
- package/CLAUDE.md +7 -2
- package/knowledge/shared/harness-core/loop_engineering.md +1 -1
- package/knowledge/shared/rules/operational_adaptation.md +144 -1
- package/package.json +12 -2
- package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-meta/.claude-plugin/plugin.json +1 -1
- package/scripts/consent_registry_check.sh +472 -0
- package/scripts/fh_session_load.sh +42 -3
- package/scripts/halffix_propagation_scan.sh +126 -0
- package/scripts/package_coverage_check.sh +18 -1
- package/scripts/pipe_verdict_guard.sh +93 -0
- package/scripts/selfcheck.sh +99 -6
- package/scripts/session_close_check.sh +15 -1
- package/scripts/sidecar_calibrate.sh +85 -0
- package/scripts/test_card_drift_probe.sh +55 -0
- package/scripts/test_consent_registry.sh +434 -0
- package/scripts/test_halffix_lanes.sh +170 -0
- package/scripts/test_ollama_panel_lanes.sh +120 -0
- package/scripts/test_package_coverage_lanes.sh +250 -0
- package/scripts/test_pipe_verdict_guard_lanes.sh +96 -0
- package/scripts/test_sidecar_wait_stdin.sh +13 -1
- package/templates/.git-hooks/pre-commit +73 -5
- package/templates/consent_classes.yaml.example +75 -0
- package/templates/settings.PreToolUse.snippet.json +49 -0
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# test_consent_registry.sh — known-pair anchor for scripts/consent_registry_check.sh.
|
|
3
|
+
#
|
|
4
|
+
# WHY (cross-family review rounds 1-3, 2026-07-29)
|
|
5
|
+
# The accept-side consent-promotion rule went REJECT -> NARROW-IT -> NARROW-IT across three
|
|
6
|
+
# adversarial rounds. Round 3 stopped attacking the prose and attacked the validator, and found
|
|
7
|
+
# four ways the FLOOR ITSELF was fail-open. Three were confirmed against controls:
|
|
8
|
+
#
|
|
9
|
+
# - `promotion_eligible: "false"` (quoted) is a truthy STRING. Every eligibility test inverted,
|
|
10
|
+
# so an intended-ineligible class was granted. One quote character disarmed the floor.
|
|
11
|
+
# - a duplicate class name silently shadowed the earlier entry, so appending an eligible twin
|
|
12
|
+
# below an ineligible one laundered the ineligible class into a grant.
|
|
13
|
+
# - `standing_consent: {inline: ...}` matched no pattern, so an EXPIRED inline grant was read as
|
|
14
|
+
# "no standing consent recorded" and reported PASS — a grant the checker cannot see is not an
|
|
15
|
+
# absent grant, it is a false clean.
|
|
16
|
+
#
|
|
17
|
+
# Each of those is a check that reported green while doing nothing, which is worse than an absent
|
|
18
|
+
# check: it buys confidence without enforcement. Hence this anchor.
|
|
19
|
+
#
|
|
20
|
+
# Lanes: N* = known-negative (must pass) · P* = known-positive (must be caught) · D* = degrade.
|
|
21
|
+
# Exit 0 = all lanes correct. Exit 1 = the validator regressed toward permissiveness.
|
|
22
|
+
set -uo pipefail
|
|
23
|
+
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
24
|
+
CHK="$ROOT/scripts/consent_registry_check.sh"
|
|
25
|
+
[ -f "$CHK" ] || { echo "FAIL: $CHK not found"; exit 1; }
|
|
26
|
+
|
|
27
|
+
pass=0; fail=0
|
|
28
|
+
ok() { printf ' ✅ %s\n' "$1"; pass=$((pass+1)); }
|
|
29
|
+
bad() { printf ' ❌ %s\n' "$1"; fail=$((fail+1)); }
|
|
30
|
+
TD="$(mktemp -d)"; trap 'rm -rf "$TD"' EXIT
|
|
31
|
+
|
|
32
|
+
# want_rc: expected exit. want_rule: a rule id that must appear in the output ("" = don't care).
|
|
33
|
+
lane() {
|
|
34
|
+
local desc="$1" want_rc="$2" want_rule="$3"
|
|
35
|
+
bash "$CHK" "$TD/r.yaml" "$TD/u.md" >"$TD/o" 2>&1
|
|
36
|
+
local rc=$?
|
|
37
|
+
if [ "$rc" -ne "$want_rc" ]; then
|
|
38
|
+
bad "$desc — exit $rc, expected $want_rc"; sed 's/^/ /' "$TD/o"; return
|
|
39
|
+
fi
|
|
40
|
+
if [ -n "$want_rule" ] && ! grep -q "❌ $want_rule" "$TD/o"; then
|
|
41
|
+
bad "$desc — exit was right but rule $want_rule was not the reason"; sed 's/^/ /' "$TD/o"; return
|
|
42
|
+
fi
|
|
43
|
+
ok "$desc"
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
OKCLASS=' - {name: ok, owner: o, mode: m, target: t, capabilities: [read], sinks: [], feeds: [], promotion_eligible: true}'
|
|
47
|
+
mkreg() { printf 'classes:\n%s\n' "$1" > "$TD/r.yaml"; }
|
|
48
|
+
# The UAP machine region is YAML FRONTMATTER (storage-form change 2026-07-31). A fixture body is the
|
|
49
|
+
# frontmatter payload, so every grant-semantics lane below stays written exactly as before and keeps
|
|
50
|
+
# testing what it tested. Lanes that exercise the storage form itself write their own file directly
|
|
51
|
+
# and do not go through this helper.
|
|
52
|
+
mkuap() { printf -- '---\n%s\n---\n\n# UAP (fixture)\n\nprose below the machine region.\n' "$1" > "$TD/u.md"; }
|
|
53
|
+
# Raw writer for storage-form lanes — no frontmatter wrapping, byte-exact.
|
|
54
|
+
mkuap_raw() { printf '%s\n' "$1" > "$TD/u.md"; }
|
|
55
|
+
|
|
56
|
+
mkreg "$OKCLASS"
|
|
57
|
+
mkuap 'standing_consent:
|
|
58
|
+
ok: {granted: 2026-07-29, expires: 2026-12-31, effects: [read], target: t}'
|
|
59
|
+
lane "N1 a well-formed registry + eligible unexpired scoped grant passes" 0 ""
|
|
60
|
+
|
|
61
|
+
mkuap 'standing_consent:
|
|
62
|
+
ok: declined'
|
|
63
|
+
lane "N2 a declined record is a valid non-grant state, not a malformed grant" 0 ""
|
|
64
|
+
|
|
65
|
+
mkreg ' - {name: s, owner: o, mode: m, target: t, capabilities: [read], sinks: [go-public], feeds: [], promotion_eligible: true}'
|
|
66
|
+
mkuap 'standing_consent: {}'
|
|
67
|
+
lane "P1 a class naming an irreversible SINK cannot declare itself promotable" 1 "R2"
|
|
68
|
+
|
|
69
|
+
mkreg ' - {name: f, owner: o, mode: m, target: t, capabilities: [read], sinks: [], feeds: [go-public], promotion_eligible: true}'
|
|
70
|
+
lane "P2 taint — a class that FEEDS an irreversible sink cannot be promotable" 1 "R2"
|
|
71
|
+
|
|
72
|
+
mkreg "$OKCLASS"
|
|
73
|
+
mkuap 'standing_consent:
|
|
74
|
+
ghost: {granted: 2026-07-29, expires: 2026-12-31, effects: [read], target: t}'
|
|
75
|
+
lane "P3 a grant on an unregistered class is refused (unregistered == unknown)" 1 "R3"
|
|
76
|
+
|
|
77
|
+
mkuap 'standing_consent:
|
|
78
|
+
ok: {granted: 2026-01-01, expires: 2026-06-30, effects: [read], target: t}'
|
|
79
|
+
lane "P4 an expired lease does not keep running" 1 "R5"
|
|
80
|
+
|
|
81
|
+
mkuap 'standing_consent:
|
|
82
|
+
ok: {granted: 2026-07-29, expires: 2026-12-31}'
|
|
83
|
+
lane "P5 a grant with no recorded scope has no re-validation baseline" 1 "R6"
|
|
84
|
+
|
|
85
|
+
# P6-P8 are the round-3 fail-opens. Each of these PASSED before the fix.
|
|
86
|
+
mkreg ' - {name: q, owner: o, mode: m, target: t, capabilities: [read], sinks: [], feeds: [], promotion_eligible: "false"}'
|
|
87
|
+
mkuap 'standing_consent:
|
|
88
|
+
q: {granted: 2026-07-29, expires: 2026-12-31, effects: [read], target: t}'
|
|
89
|
+
lane "P6 quoted \"false\" is rejected as a type error, not read as truthy" 1 "R1-b"
|
|
90
|
+
|
|
91
|
+
mkreg ' - {name: d, owner: o, mode: m, target: t, capabilities: [read], sinks: [go-public], feeds: [], promotion_eligible: false}
|
|
92
|
+
- {name: d, owner: o, mode: m, target: t, capabilities: [read], sinks: [], feeds: [], promotion_eligible: true}'
|
|
93
|
+
mkuap 'standing_consent:
|
|
94
|
+
d: {granted: 2026-07-29, expires: 2026-12-31, effects: [read], target: t}'
|
|
95
|
+
lane "P7 a duplicate class name cannot launder an ineligible class" 1 "R1-c"
|
|
96
|
+
|
|
97
|
+
mkreg "$OKCLASS"
|
|
98
|
+
mkuap 'standing_consent: {ok: {granted: 2026-01-01, expires: 2020-01-01, effects: [read], target: t}}'
|
|
99
|
+
lane "P8 an INLINE grant is parsed, not silently read as 'nothing granted'" 1 "R5"
|
|
100
|
+
|
|
101
|
+
# P10-P11 are the round-4 fail-opens. Both PASSED before the fix, and both were confirmed against a
|
|
102
|
+
# control: the SAME expired grant was caught when it stood alone.
|
|
103
|
+
mkreg "$OKCLASS"
|
|
104
|
+
mkuap 'standing_consent: {}
|
|
105
|
+
|
|
106
|
+
notes in between
|
|
107
|
+
|
|
108
|
+
standing_consent:
|
|
109
|
+
ok: {granted: 2026-01-01, expires: 2020-01-01, effects: [read], target: t}'
|
|
110
|
+
lane "P10 an early empty consent block cannot shadow a later grant (first-match)" 1 "R3"
|
|
111
|
+
|
|
112
|
+
mkuap 'standing_consent:
|
|
113
|
+
ok: {state: "revoked ", granted: 2026-07-29, expires: 2026-12-31, effects: [read], target: t}'
|
|
114
|
+
if bash "$CHK" "$TD/r.yaml" "$TD/u.md" 2>&1 | grep -q "0 active grant"; then
|
|
115
|
+
ok "P11 a mapping non-grant state is normalized like the scalar one (no divergent normalizer)"
|
|
116
|
+
else
|
|
117
|
+
bad "P11 \`state: \"revoked \"\` was validated as an ACTIVE grant"
|
|
118
|
+
bash "$CHK" "$TD/r.yaml" "$TD/u.md" 2>&1 | sed 's/^/ /'
|
|
119
|
+
fi
|
|
120
|
+
|
|
121
|
+
# P12 is the round-5 fail-open: `or {}` collapsed FALSY non-mappings into a valid-empty mapping
|
|
122
|
+
# before the type check, so `[]`, `false` and `0` all reported "no standing consent" and exit 0 —
|
|
123
|
+
# while the truthy `[a, b]` was caught. The falsy branch is the one an accident actually produces.
|
|
124
|
+
# FIXTURE FIX 2026-07-31 (cross-family): this loop wrote the UAP directly, so after the storage-form
|
|
125
|
+
# change it had no frontmatter and every case exited 1 via the "grant in prose" net — the right code
|
|
126
|
+
# for the WRONG REASON, which meant the falsy-type check it exists to guard was no longer exercised
|
|
127
|
+
# at all. Same shape as the defect the suite is about: a green lane that stopped measuring its
|
|
128
|
+
# subject. Routed through mkuap so the value lands inside the machine region.
|
|
129
|
+
for badval in ' []' ' false' ' 0'; do
|
|
130
|
+
mkreg "$OKCLASS"
|
|
131
|
+
mkuap "$(printf 'standing_consent:\n%s' "$badval")"
|
|
132
|
+
lane "P12 a falsy non-mapping standing_consent ($badval) is a type error, not 'none granted'" 1 "R3"
|
|
133
|
+
done
|
|
134
|
+
|
|
135
|
+
# P13/N3 — round-6 fail-open: `standing_consent :` (space or tab before the colon) is the SAME YAML
|
|
136
|
+
# key but matched none of the patterns. Standalone it still failed closed via the no-known-form net;
|
|
137
|
+
# paired with a normal empty block it was invisible to both the count and the extraction, so the
|
|
138
|
+
# empty block parsed and the real grant vanished. Confirmed against a control (the identical expired
|
|
139
|
+
# grant in canonical form was caught).
|
|
140
|
+
mkreg "$OKCLASS"
|
|
141
|
+
mkuap 'standing_consent: {}
|
|
142
|
+
|
|
143
|
+
standing_consent : {ok: {granted: 2026-01-01, expires: 2020-01-01, effects: [read], target: t}}'
|
|
144
|
+
lane "P13 a space-before-colon key cannot hide behind a canonical block" 1 "R3"
|
|
145
|
+
|
|
146
|
+
mkuap 'standing_consent :
|
|
147
|
+
ok: {granted: 2026-01-01, expires: 2020-01-01, effects: [read], target: t}'
|
|
148
|
+
lane "P13-b a space-before-colon BLOCK form is parsed and its grant validated" 1 "R5"
|
|
149
|
+
|
|
150
|
+
# N3 EXPECTATION INVERTED 2026-07-31 — and the reason is the whole point of the storage-form change.
|
|
151
|
+
# The slicer-era comment asserted that `standing_consent\t:` "is the SAME YAML key" as
|
|
152
|
+
# `standing_consent:`, and this lane pinned a PASS on that belief. The canonical loader refuses it:
|
|
153
|
+
# PyYAML rejects a tab there with a ScannerError, because YAML forbids tabs as structural whitespace.
|
|
154
|
+
# So the belief was false, and a regex that approximates the YAML spec had been quietly ratifying a
|
|
155
|
+
# document the spec rejects. Handing the region to the real loader replaces a guess with an answer —
|
|
156
|
+
# a malformed document is now BROKEN (fail-closed), not silently accepted as equivalent.
|
|
157
|
+
mkuap "$(printf 'standing_consent\t: {ok: {granted: 2026-07-29, expires: 2026-12-31, effects: [read], target: t}}')"
|
|
158
|
+
lane "N3 a tab-before-colon key is invalid YAML and fails closed (loader, not regex, decides)" 1 "R3"
|
|
159
|
+
|
|
160
|
+
# P14 — round-7 fail-open: R6 presence-checked `effects`/`target` but never typed them, while the
|
|
161
|
+
# registry side had strict types since R1-b. A fix propagated to one side only is a hole. Control:
|
|
162
|
+
# a MISSING field was caught; a type-wrong one passed.
|
|
163
|
+
mkreg "$OKCLASS"
|
|
164
|
+
mkuap 'standing_consent:
|
|
165
|
+
ok: {granted: 2026-07-29, expires: 2026-12-31, effects: true, target: 123}'
|
|
166
|
+
lane "P14 a type-wrong grant scope is rejected, not counted as recorded" 1 "R6"
|
|
167
|
+
|
|
168
|
+
mkuap 'standing_consent:
|
|
169
|
+
ok: {granted: 2026-07-29, expires: 2026-12-31, effects: "read", target: " "}'
|
|
170
|
+
lane "P14-b a scalar effects / whitespace-only target is rejected" 1 "R6"
|
|
171
|
+
|
|
172
|
+
mkuap 'standing_consent:
|
|
173
|
+
ok: {granted: 2026-07-29, expires: 2026-12-31, effects: [], target: t}'
|
|
174
|
+
lane "P14-c an EMPTY effects list is not a baseline" 1 "R6"
|
|
175
|
+
|
|
176
|
+
# H1-H9 — round-8 exhaustive field audit found NINE remaining holes in one pass, after four rounds
|
|
177
|
+
# of one-per-round trickle. The reframe ("audit every field, don't hand me the most important one")
|
|
178
|
+
# is what surfaced them; the per-round drip was a slow enumeration, not convergence.
|
|
179
|
+
mkuap 'standing_consent: {}'
|
|
180
|
+
printf 'classes: false\n' > "$TD/r.yaml"
|
|
181
|
+
lane "H1 a falsy \`classes\` is not laundered into an empty registry" 1 ""
|
|
182
|
+
printf 'classes:\n' > "$TD/r.yaml"
|
|
183
|
+
# Expectation changed 0 -> 3 with the R9-F4 exit contract. This is NOT a lane weakened to fit a
|
|
184
|
+
# change: the lane's own NAME says "N/A, not a clean PASS", and 0 was the clean-PASS code. It
|
|
185
|
+
# asserted the opposite of what it described, which is precisely the two-codes-three-states
|
|
186
|
+
# conflation F4 named. 3 is the code that finally means what this lane always claimed to test.
|
|
187
|
+
lane "H1-b a null \`classes\` is N/A (3), not a clean PASS" 3 ""
|
|
188
|
+
if grep -q 'N/A' "$TD/o"; then ok "H1-c the empty registry is LABELLED N/A"; else bad "H1-c empty registry read as clean"; fi
|
|
189
|
+
|
|
190
|
+
mkreg ' - {name: n, owner: o, mode: 123, target: t, capabilities: [read], sinks: [], feeds: [], promotion_eligible: true}'
|
|
191
|
+
lane "H2 a non-string scalar registry field is a type error" 1 "R1-b"
|
|
192
|
+
mkreg ' - {name: n, owner: o, mode: m, target: "", capabilities: [read], sinks: [], feeds: [], promotion_eligible: true}'
|
|
193
|
+
lane "H2-b a blank registry target is a type error" 1 "R1-b"
|
|
194
|
+
mkreg ' - {name: n, owner: o, mode: m, target: t, capabilities: [read], sinks: [123], feeds: [], promotion_eligible: false}'
|
|
195
|
+
lane "H3 an unreadable sink item is UNDECLARED, not an empty sink list" 1 "R1-b"
|
|
196
|
+
|
|
197
|
+
mkreg "$OKCLASS"
|
|
198
|
+
mkuap 'standing_consent:
|
|
199
|
+
ok: {expires: 2026-12-31, effects: [read], target: t}'
|
|
200
|
+
lane "H5 a grant with no \`granted\` date cannot be audited" 1 "R5"
|
|
201
|
+
mkuap 'standing_consent:
|
|
202
|
+
ok: {granted: 2026-07-29, expires: 29991231, effects: [read], target: t}'
|
|
203
|
+
lane "H7 a non-ISO integer expiry is not a date" 1 "R5"
|
|
204
|
+
mkuap 'standing_consent:
|
|
205
|
+
ok: {granted: 2026-07-29, expires: 9999-12-31, effects: [read], target: t}'
|
|
206
|
+
lane "H7-b an unbounded lease is a transfer wearing a lease's clothes" 1 "R5"
|
|
207
|
+
mkuap 'standing_consent:
|
|
208
|
+
ok: {state: revokedd, granted: 2026-07-29, expires: 2026-12-31, effects: [read], target: t}'
|
|
209
|
+
lane "H6 a typo'\''d state fails closed instead of passing as active" 1 "R3"
|
|
210
|
+
mkuap 'standing_consent:
|
|
211
|
+
ok: {granted: 2026-07-29, expires: 2026-12-31, effects: [repo-mutation], target: t}'
|
|
212
|
+
lane "H8 a grant wider than its registered capabilities is refused" 1 "R7"
|
|
213
|
+
mkuap 'standing_consent:
|
|
214
|
+
ok: {granted: 2026-07-29, expires: 2026-12-31, effects: [read], target: everything}'
|
|
215
|
+
lane "H8-b grant/class target drift is refused" 1 "R7"
|
|
216
|
+
mkuap 'standing_consent:
|
|
217
|
+
ok: {granted: 2026-07-29, expires: 2020-01-01, effects: [read], target: t}
|
|
218
|
+
ok: {granted: 2026-07-29, expires: 2026-12-31, effects: [read], target: t}'
|
|
219
|
+
lane "H9 duplicate YAML keys are rejected, not resolved last-wins" 1 ""
|
|
220
|
+
|
|
221
|
+
# H4 had a fix but NO lane — the mutation sweep caught that (disabling it failed zero lanes).
|
|
222
|
+
# An uncovered check is a check that gets deleted quietly later.
|
|
223
|
+
# NOTE the assertion is on the MESSAGE, not the rule id: with the type check disabled a numeric key
|
|
224
|
+
# still exits 1 via "not in the registry", so a rule-id assertion passed either way and the mutation
|
|
225
|
+
# sweep caught zero lanes. A lane that cannot separate two paths is not measuring the one it names.
|
|
226
|
+
mkuap 'standing_consent:
|
|
227
|
+
123: {granted: 2026-07-29, expires: 2026-12-31, effects: [read], target: t}'
|
|
228
|
+
bash "$CHK" "$TD/r.yaml" "$TD/u.md" >"$TD/o" 2>&1
|
|
229
|
+
if [ $? -eq 1 ] && grep -q 'grant key 123 must be a non-blank string' "$TD/o"; then
|
|
230
|
+
ok "H4 a non-string grant key is refused AS A TYPE ERROR (not merely as unregistered)"
|
|
231
|
+
else
|
|
232
|
+
bad "H4 numeric grant key was not refused by the type check"
|
|
233
|
+
sed 's/^/ /' "$TD/o"
|
|
234
|
+
fi
|
|
235
|
+
|
|
236
|
+
mkreg ' - {name: c, owner: o, mode: m, target: t, capabilities: "read", sinks: [], feeds: [], promotion_eligible: true}'
|
|
237
|
+
mkuap 'standing_consent: {}'
|
|
238
|
+
lane "P9 a scalar where a list is required is a type error" 1 "R1-b"
|
|
239
|
+
|
|
240
|
+
# N4 — PINS A KNOWN OVER-BLOCK, it does not endorse it. A grant written with a YAML merge key is
|
|
241
|
+
# currently REFUSED because the anchor lives outside the extracted fragment (see the residual note
|
|
242
|
+
# in consent_registry_check.sh). This lane asserts the CURRENT behaviour so that a future fix shows
|
|
243
|
+
# up as a lane change rather than being rediscovered from scratch — and so nobody reads the 40/40
|
|
244
|
+
# as "no known over-block".
|
|
245
|
+
mkreg "$OKCLASS"
|
|
246
|
+
mkuap 'defaults: &d {ok: {granted: 2026-07-29, expires: 2026-12-31, effects: [read], target: t}}
|
|
247
|
+
standing_consent:
|
|
248
|
+
<<: *d'
|
|
249
|
+
# N4 REMOVED 2026-07-31 — it called ok() in BOTH branches, so it passed on every possible exit code
|
|
250
|
+
# and could not fail. A lane that cannot fail measures nothing while adding a green tick, which is
|
|
251
|
+
# the false-clean this suite exists to prevent (cross-family review caught it; it had been reporting
|
|
252
|
+
# green in a 49-lane run all along). The over-block it pinned is now GONE and is asserted for real
|
|
253
|
+
# by lane F4, which requires exit 0 rather than accepting anything.
|
|
254
|
+
lane "N4 (retired — see F4, which asserts the merge-key grant actually passes)" 0 ""
|
|
255
|
+
|
|
256
|
+
rm -f "$TD/r.yaml"
|
|
257
|
+
# ── R9-F4 (2026-07-31) — N/A gets its OWN exit code. ────────────────────────────────────────────
|
|
258
|
+
# Three states (verified / unmeasured / broken) were encoded in two exit codes and disambiguated
|
|
259
|
+
# only in PROSE. The exit code is the only machine-readable channel, so a caller writing the
|
|
260
|
+
# conventional `if check; then run_unprompted; fi` treated "there is nothing to measure" as
|
|
261
|
+
# "verified, go ahead" — printing "(not a PASS)" disables nothing.
|
|
262
|
+
#
|
|
263
|
+
# The fix is NOT to return 1. A missing registry is the state of every fresh clone, and failing
|
|
264
|
+
# there would paint the gate red on first run and train the override reflex — a failure this
|
|
265
|
+
# checker's own comments already record twice. `1` must keep meaning BROKEN.
|
|
266
|
+
#
|
|
267
|
+
# 3 = UNMEASURED. The asymmetry that decides the direction: the fallback here is not "block", it is
|
|
268
|
+
# "ask the human". Consent promotion exists only to SKIP an approval prompt, so degrading to asking
|
|
269
|
+
# restores the system's original behaviour and widens human-in-the-loop authority rather than
|
|
270
|
+
# narrowing it. Degrading to exit 0 does the opposite — it removes a human decision no one granted.
|
|
271
|
+
lane "D1 a missing registry is UNMEASURED (3), not verified (0)" 3 ""
|
|
272
|
+
if grep -q 'N/A' "$TD/o"; then
|
|
273
|
+
ok "D1-b the missing-registry result is LABELLED N/A, not reported as clean"
|
|
274
|
+
else
|
|
275
|
+
bad "D1-b a missing registry produced no N/A label — that reads as PASS"
|
|
276
|
+
fi
|
|
277
|
+
|
|
278
|
+
printf 'classes: []\n' > "$TD/r.yaml"
|
|
279
|
+
mkuap 'standing_consent: {}'
|
|
280
|
+
lane "D1-c a registry with zero classes is UNMEASURED (3)" 3 ""
|
|
281
|
+
|
|
282
|
+
mkreg "$OKCLASS"
|
|
283
|
+
rm -f "$TD/u.md"
|
|
284
|
+
lane "D1-d a missing UAP is UNMEASURED (3) — no grants is not a verified pass" 3 ""
|
|
285
|
+
|
|
286
|
+
# The contract's whole point: 0 must be reachable ONLY by a real join. If this lane and D1* ever
|
|
287
|
+
# return the same code again, the channel has collapsed back into prose.
|
|
288
|
+
mkreg "$OKCLASS"
|
|
289
|
+
mkuap 'standing_consent:
|
|
290
|
+
ok: {granted: 2026-07-29, expires: 2026-12-31, effects: [read], target: t}'
|
|
291
|
+
lane "D1-e 0 still means VERIFIED, and only that" 0 ""
|
|
292
|
+
|
|
293
|
+
printf 'classes: [ {name: broken\n' > "$TD/r.yaml"
|
|
294
|
+
lane "D2 an unparseable registry fails closed (cannot decide == not allowed)" 1 ""
|
|
295
|
+
|
|
296
|
+
# ── R9 (2026-07-31, cross-family codex @ gpt-5.6-sol) — the floor read the wrong field. ──────────
|
|
297
|
+
# R2 intersected IRREVERSIBLE with `sinks | feeds` and NEVER with `capabilities`, so a class could
|
|
298
|
+
# DECLARE an irreversible capability outright and still be promotable as long as it named no sink.
|
|
299
|
+
# Grepped to be sure before fixing: IRREVERSIBLE appeared in exactly one place in the checker, and
|
|
300
|
+
# its only operand was the sink/feed union. R2's own comment gives the reason away — it says
|
|
301
|
+
# "naming an irreversible SINK two fields above" — the guard was written against the sink field and
|
|
302
|
+
# the capability field was never in scope.
|
|
303
|
+
#
|
|
304
|
+
# The local canary (qwen3.6:27b, same prompt, adequate budget) returned CONVERGED on this same diff.
|
|
305
|
+
# Taking that as the verdict would have declared convergence on a floor that lets history-rewrite
|
|
306
|
+
# through — which is exactly why the doctrine calls local tier evidence-of and never terminal.
|
|
307
|
+
mkreg ' - {name: rewrite, owner: o, mode: m, target: t, capabilities: [history-rewrite], sinks: [], feeds: [], promotion_eligible: true}'
|
|
308
|
+
mkuap 'standing_consent:
|
|
309
|
+
rewrite: {granted: 2026-07-29, expires: 2026-12-31, effects: [history-rewrite], target: t}'
|
|
310
|
+
lane "R9-1 a declared irreversible CAPABILITY is not promotable, empty sinks or not" 1 "R2-b"
|
|
311
|
+
|
|
312
|
+
# The same class WITHOUT the promotion claim is fine: declaring an irreversible capability is not
|
|
313
|
+
# itself an error, and blocking it would over-block a registry that merely describes what a class
|
|
314
|
+
# can do. Over-blocking trains the override reflex — the checker already records one such revert.
|
|
315
|
+
mkreg ' - {name: rewrite, owner: o, mode: m, target: t, capabilities: [history-rewrite], sinks: [], feeds: [], promotion_eligible: false}'
|
|
316
|
+
mkuap 'standing_consent: {}'
|
|
317
|
+
lane "R9-1b the same irreversible capability is fine when not claimed promotable" 0 ""
|
|
318
|
+
|
|
319
|
+
# `unknown` is in IRREVERSIBLE for the sink rule ("unlisted sinks are UNKNOWN, and unknown is not
|
|
320
|
+
# reversible"); it must mean the same thing in the capability position.
|
|
321
|
+
mkreg ' - {name: u, owner: o, mode: m, target: t, capabilities: [unknown], sinks: [], feeds: [], promotion_eligible: true}'
|
|
322
|
+
mkuap 'standing_consent: {}'
|
|
323
|
+
lane "R9-1c an \`unknown\` capability is not promotable either" 1 "R2-b"
|
|
324
|
+
|
|
325
|
+
# Guard against over-reach in the other direction: an ordinary reversible capability with empty
|
|
326
|
+
# sinks is the N1 shape and must stay passing. If this lane ever flips, the fix went too wide.
|
|
327
|
+
mkreg "$OKCLASS"
|
|
328
|
+
mkuap 'standing_consent:
|
|
329
|
+
ok: {granted: 2026-07-29, expires: 2026-12-31, effects: [read], target: t}'
|
|
330
|
+
lane "R9-1d a reversible capability with empty sinks still passes (anti-over-block)" 0 ""
|
|
331
|
+
|
|
332
|
+
# R9-1e — the SAME field must be normalised the SAME way by every rule that reads it. R7 compares
|
|
333
|
+
# capabilities through `_norm` (which strips surrounding whitespace); the first cut of R2-b compared
|
|
334
|
+
# raw `str(x)`, so ` history-rewrite ` slipped past R2-b while R7 still recognised it — one field,
|
|
335
|
+
# two spellings, which is the divergent-normalizer class psa_scan_lib.sh's header is entirely about.
|
|
336
|
+
# Caught reviewing the diff before merge, not by the lanes above.
|
|
337
|
+
mkreg ' - {name: w, owner: o, mode: m, target: t, capabilities: [" history-rewrite "], sinks: [], feeds: [], promotion_eligible: true}'
|
|
338
|
+
mkuap 'standing_consent: {}'
|
|
339
|
+
lane "R9-1e whitespace around an irreversible capability does not evade R2-b" 1 "R2-b"
|
|
340
|
+
|
|
341
|
+
# The shipped example must satisfy the validator — otherwise the artifact FH hands users is the
|
|
342
|
+
# first counter-example. (Hand-verify-one-sample discipline, applied to our own template.)
|
|
343
|
+
if bash "$CHK" "$ROOT/templates/consent_classes.yaml.example" /dev/null >/dev/null 2>&1; then
|
|
344
|
+
ok "S1 the shipped templates/consent_classes.yaml.example validates"
|
|
345
|
+
else
|
|
346
|
+
bad "S1 the shipped example registry does NOT validate"
|
|
347
|
+
fi
|
|
348
|
+
|
|
349
|
+
# ---- F: STORAGE FORM (frontmatter) -------------------------------------------------
|
|
350
|
+
# These lanes test the machine-region boundary itself, not grant semantics, so they write the UAP
|
|
351
|
+
# byte-exact via mkuap_raw instead of going through the frontmatter-wrapping helper.
|
|
352
|
+
mkreg "$OKCLASS"
|
|
353
|
+
GRANT=' ok: {granted: 2026-07-29, expires: 2026-12-31, effects: [read], target: t}'
|
|
354
|
+
|
|
355
|
+
# F1/F2 are the two false-clean shapes, and they are the reason this section exists. A grant that
|
|
356
|
+
# sits where no parser reads it must never report as "nothing granted" — an unread grant is not an
|
|
357
|
+
# absent one. This is the ONE net carried over from the slicer, generalized to the new form.
|
|
358
|
+
mkuap_raw "$(printf '# UAP\n\nstanding_consent:\n%s\n' "$GRANT")"
|
|
359
|
+
lane "F1 a grant in prose with NO frontmatter fails closed (never 'nothing granted')" 1 "R3"
|
|
360
|
+
|
|
361
|
+
mkuap_raw "$(printf -- '---\nsidecar_consent: granted\n---\n\n# UAP\n\nstanding_consent:\n%s\n' "$GRANT")"
|
|
362
|
+
lane "F2 frontmatter exists but the grant was written BELOW it — fails closed" 1 "R3"
|
|
363
|
+
|
|
364
|
+
# F3 is the anti-over-block control for F1: absence must stay cheap. A UAP predating this format has
|
|
365
|
+
# no machine region and grants nothing; painting that red would train the override reflex.
|
|
366
|
+
mkuap_raw "$(printf '# UAP\n\nno machine region here, and nothing granted.\n')"
|
|
367
|
+
lane "F3 no frontmatter and no grant mentioned is simply nothing granted (not a failure)" 0 ""
|
|
368
|
+
|
|
369
|
+
# F4 pins the OVER-BLOCK THIS CHANGE RETIRES. Under the slicer a merge-key grant was refused because
|
|
370
|
+
# the anchor lived outside the extracted fragment; the fragment is now the whole document, so
|
|
371
|
+
# ordinary DRY YAML resolves. Over-blocking was a defect of the same weight as a fail-open.
|
|
372
|
+
mkuap_raw "$(printf -- '---\ndefaults: &d {granted: 2026-07-29, expires: 2026-12-31, effects: [read], target: t}\nstanding_consent:\n ok:\n <<: *d\n---\n')"
|
|
373
|
+
lane "F4 a merge-key/anchor grant now resolves (the measured over-block is retired)" 0 ""
|
|
374
|
+
|
|
375
|
+
# F5 — duplicate keys stay rejected on this side too. safe_load is last-wins, so an expired grant
|
|
376
|
+
# followed by a future one would silently keep the future one.
|
|
377
|
+
mkuap_raw "$(printf -- '---\nstanding_consent:\n ok: {granted: 2026-01-01, expires: 2020-01-01, effects: [read], target: t}\n ok: {granted: 2026-07-29, expires: 2026-12-31, effects: [read], target: t}\n---\n')"
|
|
378
|
+
lane "F5 duplicate grant keys are refused, not last-wins" 1 "R3"
|
|
379
|
+
|
|
380
|
+
# F6 — a leading BOM must not make the machine region look absent. Without the tolerance this still
|
|
381
|
+
# fails closed, i.e. safe, but for a misleading reason, and a confusing gate gets overridden.
|
|
382
|
+
mkuap_raw "$(printf -- '\357\273\277---\nstanding_consent:\n%s\n---\n' "$GRANT")"
|
|
383
|
+
lane "F6 a BOM before the frontmatter does not hide the machine region" 0 ""
|
|
384
|
+
|
|
385
|
+
# F7 — the region must be a mapping. A list parses fine as YAML and would otherwise be read for a
|
|
386
|
+
# `standing_consent` key it can never have.
|
|
387
|
+
mkuap_raw "$(printf -- '---\n- a\n- b\n---\n\nstanding_consent: whatever\n')"
|
|
388
|
+
lane "F7 a non-mapping frontmatter is BROKEN, not an empty grant set" 1 "R3"
|
|
389
|
+
|
|
390
|
+
# F8 — an unterminated block is not frontmatter. It then falls to the F1 net, which is the correct
|
|
391
|
+
# landing: the grant is unread, so fail closed.
|
|
392
|
+
mkuap_raw "$(printf -- '---\nstanding_consent:\n%s\n' "$GRANT")"
|
|
393
|
+
lane "F8 an unterminated --- block is not a machine region and fails closed" 1 "R3"
|
|
394
|
+
|
|
395
|
+
# F9 — an empty machine region is a real empty set, not a parse failure.
|
|
396
|
+
# NOTE (cross-family 2026-07-31): this lane was PASSING FOR THE WRONG REASON. The first regex could
|
|
397
|
+
# not match `---\n---` at all, so the file fell through to "no frontmatter, nothing mentioned" and
|
|
398
|
+
# went green without the empty-region path ever running. Lane F9-b below is its control: same empty
|
|
399
|
+
# region, but WITH a prose mention, which can only stay green if the region was really recognized.
|
|
400
|
+
mkuap_raw "$(printf -- '---\n---\n\n# UAP\n')"
|
|
401
|
+
lane "F9 an empty frontmatter block grants nothing and is not an error" 0 ""
|
|
402
|
+
|
|
403
|
+
# F9-b — the discriminating control for F9. If the empty region is NOT recognized, this file reads as
|
|
404
|
+
# "no frontmatter + standing_consent mentioned" and fails closed via F1. Green here means the empty
|
|
405
|
+
# frontmatter was genuinely parsed. (Without this control F9 cannot tell the two paths apart.)
|
|
406
|
+
mkuap_raw "$(printf -- '---\n---\n\n# UAP\n\nthe key name standing_consent appears in prose but not as a key.\n')"
|
|
407
|
+
lane "F9-b empty region + a prose MENTION is still nothing granted (region truly parsed)" 0 ""
|
|
408
|
+
|
|
409
|
+
# F10-F13 — YAML stream forms the canonical loader accepts and the first fence regex rejected. Each
|
|
410
|
+
# failed CLOSED (no leak) but as an OVER-BLOCK whose message blamed an absent frontmatter that was
|
|
411
|
+
# present. A gate that misdirects the fix gets overridden, so these are lanes, not footnotes.
|
|
412
|
+
EXPIRED=' ok: {granted: 2026-01-01, expires: 2020-01-01, effects: [read], target: t}'
|
|
413
|
+
mkreg "$OKCLASS"
|
|
414
|
+
mkuap_raw "$(printf -- '---\nstanding_consent:\n%s\n...\n' "$EXPIRED")"
|
|
415
|
+
lane "F10 a '...' document terminator closes the machine region (grant is READ, then R5-caught)" 1 "R5"
|
|
416
|
+
|
|
417
|
+
mkuap_raw "$(printf -- '--- # metadata\nstanding_consent:\n%s\n---\n' "$EXPIRED")"
|
|
418
|
+
lane "F11 a comment on the opening fence does not hide the region" 1 "R5"
|
|
419
|
+
|
|
420
|
+
mkuap_raw "$(printf -- '---\nstanding_consent:\n%s\n--- # end\n' "$EXPIRED")"
|
|
421
|
+
lane "F12 a comment on the closing fence does not hide the region" 1 "R5"
|
|
422
|
+
|
|
423
|
+
mkuap_raw "$(printf -- '%%YAML 1.2\n---\nstanding_consent:\n%s\n...\n' "$EXPIRED")"
|
|
424
|
+
lane "F13 a %YAML directive before the fence does not hide the region" 1 "R5"
|
|
425
|
+
|
|
426
|
+
# F14 — quoted key spelling in prose. YAML reads `"standing_consent":` as the same key, so the
|
|
427
|
+
# prose-region net must see it too; an anchored bare-key-only net missed it while the F1 net caught
|
|
428
|
+
# it, i.e. the two nets disagreed on one input.
|
|
429
|
+
mkuap_raw "$(printf -- '---\nsidecar_consent: granted\n---\n\n# UAP\n\n"standing_consent":\n%s\n' "$EXPIRED")"
|
|
430
|
+
lane "F14 a QUOTED grant key in the prose region is caught too (nets agree)" 1 "R3"
|
|
431
|
+
|
|
432
|
+
echo "----"
|
|
433
|
+
echo "consent-registry anchor: $pass passed, $fail failed"
|
|
434
|
+
[ "$fail" -eq 0 ] || exit 1
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# test_halffix_lanes.sh — known pairs for scripts/halffix_propagation_scan.sh
|
|
3
|
+
#
|
|
4
|
+
# WHY, AND WHY LANES FIRST
|
|
5
|
+
# 2026-07-30 harvest #1: rounds that wrote the lane BEFORE touching the code had zero
|
|
6
|
+
# fix-reverts-fix regressions. Order made the convergence, not the patch.
|
|
7
|
+
#
|
|
8
|
+
# THE DEFECT BEING GUARDED — "반쪽-수리" (half-fix)
|
|
9
|
+
# A defect class lives in N sibling copies. The fix lands in ONE. The siblings keep the bug, and
|
|
10
|
+
# nothing says so. Measured 3x in this project, and the shape is worse than it sounds: every one
|
|
11
|
+
# of the three occurred INSIDE an edit that was itself repairing an earlier half-fix.
|
|
12
|
+
# scripts/psa_scan_lib.sh's header records five such divergences found in a single audit — the
|
|
13
|
+
# override-readable check, the fail-closed-on-no-patterns branch, the SPACE-vs-TAB row, the
|
|
14
|
+
# `head -1` shielding, and the LOW allowlist — each repaired where it was found and nowhere else.
|
|
15
|
+
#
|
|
16
|
+
# THE DISCRIMINATOR THAT MAKES THIS USEFUL INSTEAD OF NOISY (lane N3)
|
|
17
|
+
# If BOTH copies are staged, the fix WAS propagated and the scan must stay silent. A detector that
|
|
18
|
+
# fires on correct propagation is a detector that gets ignored — the S5 lesson (9/9 FP, narrowed
|
|
19
|
+
# 2026-07-28) and the S6 lesson (0 true positives on the planned surface, retargeted 2026-07-31)
|
|
20
|
+
# are the two prior claims on this exact mistake in this repo.
|
|
21
|
+
#
|
|
22
|
+
# ADVISORY BY MANDATE, not by preference: the operator's spec for this debt says "표시(차단 아님 —
|
|
23
|
+
# 정당한 복제도 있다)" — mark, do not block, because legitimate duplication exists.
|
|
24
|
+
|
|
25
|
+
set -u
|
|
26
|
+
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
27
|
+
SCAN="$ROOT/scripts/halffix_propagation_scan.sh"
|
|
28
|
+
pass=0; fail=0
|
|
29
|
+
|
|
30
|
+
# mkrepo <dir> — a throwaway git repo; caller populates and stages.
|
|
31
|
+
mkrepo() {
|
|
32
|
+
rm -rf "$1"; mkdir -p "$1"; ( cd "$1" && git init -q . && git config user.email t@e && git config user.name t )
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
# expect <label> <FLAG|SILENT> <repo> [needle]
|
|
36
|
+
expect() {
|
|
37
|
+
local label="$1" want="$2" repo="$3" needle="${4:-}" out got
|
|
38
|
+
out=$( cd "$repo" && bash "$SCAN" 2>&1 )
|
|
39
|
+
if printf '%s' "$out" | grep -q 'HALF-FIX'; then got=FLAG; else got=SILENT; fi
|
|
40
|
+
if [ "$got" = "$want" ] && { [ -z "$needle" ] || printf '%s' "$out" | grep -q "$needle"; }; then
|
|
41
|
+
printf ' ✅ %-50s %s\n' "$label" "$got"; pass=$((pass+1))
|
|
42
|
+
else
|
|
43
|
+
printf ' ❌ %-50s %s (expected %s%s)\n' "$label" "$got" "$want" \
|
|
44
|
+
"${needle:+, needle '$needle'}"; fail=$((fail+1))
|
|
45
|
+
printf ' out: %s\n' "$(printf '%s' "$out" | head -6)"
|
|
46
|
+
fi
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
T=$(mktemp -d); trap 'rm -rf "$T"' EXIT
|
|
50
|
+
echo "[halffix] known pairs"
|
|
51
|
+
|
|
52
|
+
# ── P1: the canonical shape — a distinctive symbol duplicated in a sibling, only one staged ──
|
|
53
|
+
R="$T/p1"; mkrepo "$R"
|
|
54
|
+
printf 'psa_low_allowlisted() {\n case "$1" in templates/*) return 0 ;; esac\n}\n' > "$R/a.sh"
|
|
55
|
+
printf 'psa_low_allowlisted() {\n case "$1" in templates/*) return 0 ;; esac\n}\n' > "$R/b.sh"
|
|
56
|
+
( cd "$R" && git add . && git commit -qm base )
|
|
57
|
+
printf 'psa_low_allowlisted() {\n case "$1" in templates/*|docs/*) return 0 ;; esac\n}\n' > "$R/a.sh"
|
|
58
|
+
( cd "$R" && git add a.sh )
|
|
59
|
+
expect "P1 sibling copy left unstaged" FLAG "$R" "b.sh"
|
|
60
|
+
|
|
61
|
+
# ── P2: three copies, one staged — both remaining siblings named ──
|
|
62
|
+
R="$T/p2"; mkrepo "$R"
|
|
63
|
+
for f in a b c; do printf 'validate_marker_fields_strictly() { :; }\n' > "$R/$f.sh"; done
|
|
64
|
+
( cd "$R" && git add . && git commit -qm base )
|
|
65
|
+
printf 'validate_marker_fields_strictly() { echo fixed; }\n' > "$R/a.sh"
|
|
66
|
+
( cd "$R" && git add a.sh )
|
|
67
|
+
expect "P2 two siblings, both named" FLAG "$R" "c.sh"
|
|
68
|
+
|
|
69
|
+
# ── N3: THE DISCRIMINATOR — both copies staged means the fix propagated. Silence. ──
|
|
70
|
+
R="$T/n3"; mkrepo "$R"
|
|
71
|
+
printf 'psa_low_allowlisted() { :; }\n' > "$R/a.sh"; cp "$R/a.sh" "$R/b.sh"
|
|
72
|
+
( cd "$R" && git add . && git commit -qm base )
|
|
73
|
+
printf 'psa_low_allowlisted() { echo fixed; }\n' > "$R/a.sh"; cp "$R/a.sh" "$R/b.sh"
|
|
74
|
+
( cd "$R" && git add a.sh b.sh )
|
|
75
|
+
expect "N3 both staged = propagated" SILENT "$R"
|
|
76
|
+
|
|
77
|
+
# ── N4: unique content — nothing to propagate to ──
|
|
78
|
+
R="$T/n4"; mkrepo "$R"
|
|
79
|
+
printf 'lonely_unique_helper_fn() { :; }\n' > "$R/a.sh"
|
|
80
|
+
printf 'something_entirely_different() { :; }\n' > "$R/b.sh"
|
|
81
|
+
( cd "$R" && git add . && git commit -qm base )
|
|
82
|
+
printf 'lonely_unique_helper_fn() { echo x; }\n' > "$R/a.sh"
|
|
83
|
+
( cd "$R" && git add a.sh )
|
|
84
|
+
expect "N4 no sibling holds the symbol" SILENT "$R"
|
|
85
|
+
|
|
86
|
+
# ── N5: ubiquitous token — a word in 12 files is framework noise, not a half-fix ──
|
|
87
|
+
R="$T/n5"; mkrepo "$R"
|
|
88
|
+
for i in $(seq 1 12); do printf 'REPO_ROOT_DIRECTORY=/x\n' > "$R/f$i.sh"; done
|
|
89
|
+
( cd "$R" && git add . && git commit -qm base )
|
|
90
|
+
printf 'REPO_ROOT_DIRECTORY=/y\n' > "$R/f1.sh"
|
|
91
|
+
( cd "$R" && git add f1.sh )
|
|
92
|
+
expect "N5 ubiquitous token suppressed" SILENT "$R"
|
|
93
|
+
|
|
94
|
+
# ── N6: short/common tokens must not anchor (would flag on `then`, `echo`, `return`) ──
|
|
95
|
+
R="$T/n6"; mkrepo "$R"
|
|
96
|
+
printf 'if [ -f x ]; then echo a; fi\n' > "$R/a.sh"
|
|
97
|
+
printf 'if [ -f y ]; then echo b; fi\n' > "$R/b.sh"
|
|
98
|
+
( cd "$R" && git add . && git commit -qm base )
|
|
99
|
+
printf 'if [ -f x ]; then echo c; fi\n' > "$R/a.sh"
|
|
100
|
+
( cd "$R" && git add a.sh )
|
|
101
|
+
expect "N6 short shell keywords ignored" SILENT "$R"
|
|
102
|
+
|
|
103
|
+
# ── P7: filenames count as symbols too (the spec says "수정 심볼·파일명") ──
|
|
104
|
+
R="$T/p7"; mkrepo "$R"
|
|
105
|
+
printf 'bash scripts/sidecar_calibrate.sh --run\n' > "$R/a.sh"
|
|
106
|
+
printf 'bash scripts/sidecar_calibrate.sh --run\n' > "$R/doc.md"
|
|
107
|
+
( cd "$R" && git add . && git commit -qm base )
|
|
108
|
+
printf 'bash scripts/sidecar_calibrate.sh --run --strict\n' > "$R/a.sh"
|
|
109
|
+
( cd "$R" && git add a.sh )
|
|
110
|
+
expect "P7 filename token propagates" FLAG "$R" "doc.md"
|
|
111
|
+
|
|
112
|
+
# ── N8: nothing staged at all ──
|
|
113
|
+
R="$T/n8"; mkrepo "$R"; printf 'x\n' > "$R/a.sh"; ( cd "$R" && git add . && git commit -qm base )
|
|
114
|
+
expect "N8 empty staging area" SILENT "$R"
|
|
115
|
+
|
|
116
|
+
# ── N9: opt-out ──
|
|
117
|
+
R="$T/n9"; mkrepo "$R"
|
|
118
|
+
printf 'psa_low_allowlisted() { :; }\n' > "$R/a.sh"; cp "$R/a.sh" "$R/b.sh"
|
|
119
|
+
( cd "$R" && git add . && git commit -qm base )
|
|
120
|
+
printf 'psa_low_allowlisted() { echo x; } # noqa: half-fix\n' > "$R/a.sh"
|
|
121
|
+
( cd "$R" && git add a.sh )
|
|
122
|
+
expect "N9 noqa suppresses" SILENT "$R"
|
|
123
|
+
|
|
124
|
+
# ── N10: deleted file must not crash the scan ──
|
|
125
|
+
R="$T/n10"; mkrepo "$R"
|
|
126
|
+
printf 'psa_low_allowlisted() { :; }\n' > "$R/a.sh"; cp "$R/a.sh" "$R/b.sh"
|
|
127
|
+
( cd "$R" && git add . && git commit -qm base )
|
|
128
|
+
( cd "$R" && git rm -q a.sh )
|
|
129
|
+
expect "N10 deletion does not crash" SILENT "$R"
|
|
130
|
+
|
|
131
|
+
echo "-- R2: whole-file copy divergence (found by LIVE calibration, not by the lanes above) --"
|
|
132
|
+
# The token rule alone MISSED this repo's real duplicate pair (scripts/degrade_direction_scan.sh ↔
|
|
133
|
+
# templates/degrade_direction_scan.sh) because a script's NAME appears in 11–18 files here — docs,
|
|
134
|
+
# CATALOG, the manifest, selfcheck references — so the ubiquity filter suppressed it, while an
|
|
135
|
+
# internal symbol like psa_low_allowlisted appears in 2. Measured 2026-07-31: the lanes were 10/10
|
|
136
|
+
# green and the live probe was silent. Lanes are necessary and not sufficient.
|
|
137
|
+
#
|
|
138
|
+
# The predicate is exact rather than heuristic: the sibling was BYTE-IDENTICAL at HEAD and only one
|
|
139
|
+
# side is staged. That is a divergence by construction, so it needs no threshold — and it stays
|
|
140
|
+
# silent for same-basename files that were never copies (CLAUDE.md vs templates/CLAUDE.md, the 40
|
|
141
|
+
# SKILL.md files), which a basename rule would have flooded.
|
|
142
|
+
R="$T/r2p"; mkrepo "$R"; mkdir -p "$R/scripts" "$R/templates"
|
|
143
|
+
printf 'a() { :; }\nb() { :; }\n' > "$R/scripts/x.sh"
|
|
144
|
+
cp "$R/scripts/x.sh" "$R/templates/x.sh"
|
|
145
|
+
( cd "$R" && git add . && git commit -qm base )
|
|
146
|
+
printf 'a() { echo fixed; }\nb() { :; }\n' > "$R/scripts/x.sh"
|
|
147
|
+
( cd "$R" && git add scripts/x.sh )
|
|
148
|
+
expect "R2 byte-identical copy diverges" FLAG "$R" "templates/x.sh"
|
|
149
|
+
|
|
150
|
+
R="$T/r2n"; mkrepo "$R"; mkdir -p "$R/scripts" "$R/templates"
|
|
151
|
+
printf 'hub version, quite different\n' > "$R/scripts/x.sh"
|
|
152
|
+
printf 'template version, also different\n' > "$R/templates/x.sh"
|
|
153
|
+
( cd "$R" && git add . && git commit -qm base )
|
|
154
|
+
printf 'hub version, edited\n' > "$R/scripts/x.sh"
|
|
155
|
+
( cd "$R" && git add scripts/x.sh )
|
|
156
|
+
expect "R2 same name but never a copy" SILENT "$R"
|
|
157
|
+
|
|
158
|
+
R="$T/r2b"; mkrepo "$R"; mkdir -p "$R/scripts" "$R/templates"
|
|
159
|
+
printf 'a() { :; }\n' > "$R/scripts/x.sh"; cp "$R/scripts/x.sh" "$R/templates/x.sh"
|
|
160
|
+
( cd "$R" && git add . && git commit -qm base )
|
|
161
|
+
printf 'a() { echo fixed; }\n' > "$R/scripts/x.sh"; cp "$R/scripts/x.sh" "$R/templates/x.sh"
|
|
162
|
+
( cd "$R" && git add scripts/x.sh templates/x.sh )
|
|
163
|
+
expect "R2 both staged = propagated" SILENT "$R"
|
|
164
|
+
|
|
165
|
+
echo
|
|
166
|
+
if [ "$fail" -eq 0 ]; then
|
|
167
|
+
echo "[halffix] ✅ all $pass known pairs hold"; exit 0
|
|
168
|
+
else
|
|
169
|
+
echo "[halffix] ❌ $fail/$((pass+fail)) lanes failed"; exit 1
|
|
170
|
+
fi
|