@chrono-meta/fh-gate 1.4.97 → 1.4.99

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.
Files changed (44) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/CATALOG.md +19 -0
  3. package/CHEATSHEET.md +9 -1
  4. package/CLAUDE.md +28 -2
  5. package/README.ja.md +229 -42
  6. package/README.ko.md +241 -45
  7. package/README.md +168 -31
  8. package/README.zh.md +219 -40
  9. package/docs/OUTPUT_EVIDENCE.md +21 -12
  10. package/docs/pillars.svg +3 -7
  11. package/knowledge/shared/harness-core/fh_ecosystem_positioning.md +2 -0
  12. package/knowledge/shared/harness-core/fh_global_positioning_and_distribution_roadmap.md +136 -0
  13. package/knowledge/shared/harness-core/fh_three_layer_canon.md +20 -0
  14. package/knowledge/shared/harness-core/field_verdict_crossfamily_gate.md +215 -2
  15. package/knowledge/shared/harness-core/ship_readiness_gate.md +112 -0
  16. package/knowledge/shared/learnings/subagent_invocations_log.yaml +65 -0
  17. package/package.json +5 -1
  18. package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
  19. package/plugins/fh-commons/skills/ko-tech-writer/SKILL.md +63 -12
  20. package/plugins/fh-meta/.claude-plugin/plugin.json +1 -1
  21. package/plugins/fh-meta/CHANGELOG.md +166 -0
  22. package/plugins/fh-meta/skills/auto-decorrelation/SKILL.md +30 -0
  23. package/scripts/consent_registry_check.sh +124 -1
  24. package/scripts/degrade_direction_scan.sh +10 -1
  25. package/scripts/digest_landing_check.sh +20 -4
  26. package/scripts/fh_node_check.sh +128 -1
  27. package/scripts/fh_session_load.sh +22 -2
  28. package/scripts/frontier_digest_autopilot.sh +229 -0
  29. package/scripts/lane_runner_check.sh +294 -26
  30. package/scripts/package_coverage_check.sh +17 -0
  31. package/scripts/postinstall_notice.js +34 -0
  32. package/scripts/selfcheck.sh +183 -5
  33. package/scripts/test_consent_registry.sh +99 -0
  34. package/scripts/test_degrade_scan_shell_probes.sh +75 -0
  35. package/scripts/test_field_canon_lanes.sh +29 -5
  36. package/scripts/test_lane_runner_lanes.sh +295 -0
  37. package/scripts/test_node_check_lanes.sh +217 -0
  38. package/scripts/test_selfcheck_state_lanes.sh +61 -0
  39. package/scripts/test_stale_clone_guard_lanes.sh +21 -7
  40. package/scripts/test_version_lockstep_lanes.sh +62 -0
  41. package/scripts/version_lockstep_check.sh +143 -1
  42. package/templates/.git-hooks/pre-commit +22 -1
  43. package/templates/consent_classes.yaml.example +30 -0
  44. package/templates/degrade_direction_scan.sh +10 -1
@@ -0,0 +1,295 @@
1
+ #!/usr/bin/env bash
2
+ # test_lane_runner_lanes.sh — behavioural lanes for scripts/lane_runner_check.sh
3
+ #
4
+ # Why this file exists at all: until 2026-08-13 the checker that detects unrun lane suites had
5
+ # **no lane suite of its own**. It guarded its own WIRING (it fails if selfcheck.sh stops calling
6
+ # it) but nothing measured its BEHAVIOUR. That is the same shape it exists to catch, one level up —
7
+ # a check whose verdicts nobody exercises is prose with an exit code.
8
+ #
9
+ # The lanes below drive it against SYNTHETIC fixture trees, not against this repo. Driving it
10
+ # against the live tree would make every lane depend on today's suite list, so a lane would go red
11
+ # for reasons that have nothing to do with the predicate under test.
12
+ #
13
+ # Script-relative, NOT `git rev-parse --show-toplevel`: in a vendored tree rev-parse returns the
14
+ # OUTER repo and this suite would then test someone else's checkout.
15
+ set -uo pipefail
16
+ REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
17
+ CHECK="$REPO_ROOT/scripts/lane_runner_check.sh"
18
+ T=$(mktemp -d); trap 'rm -rf "$T"' EXIT
19
+ pass=0; fail=0
20
+ ok() { printf ' ✅ %s\n' "$1"; pass=$((pass+1)); }
21
+ bad() { printf ' ❌ %s\n' "$1"; fail=$((fail+1)); }
22
+
23
+ # ── fixture: a tree with ONE unwired suite and a selfcheck.sh that calls the checker ──────────
24
+ # The `bash scripts/lane_runner_check.sh` line is load-bearing: the checker refuses to run in a
25
+ # tree whose selfcheck.sh does not invoke it (its self-reference guard).
26
+ mkfixture() { # $1 = dir
27
+ local d="$1"
28
+ mkdir -p "$d/scripts"
29
+ cp "$CHECK" "$d/scripts/lane_runner_check.sh"
30
+ printf '#!/usr/bin/env bash\nbash scripts/lane_runner_check.sh\n' > "$d/scripts/selfcheck.sh"
31
+ printf '#!/usr/bin/env bash\n: orphan lane suite\n' > "$d/scripts/test_orphan_lanes.sh"
32
+ }
33
+ run() { ( cd "$1" && bash scripts/lane_runner_check.sh 2>&1 ); }
34
+ rcof() { ( cd "$1" && bash scripts/lane_runner_check.sh >/dev/null 2>&1; echo $? ); }
35
+
36
+ # ── L1 known-POSITIVE: an undeclared orphan suite must FAIL ───────────────────────────────────
37
+ # Without this arm every lane below proves nothing: a checker that passed unconditionally would
38
+ # satisfy all the "declaration suppresses it" lanes.
39
+ D="$T/l1"; mkfixture "$D"
40
+ out=$(run "$D"); rc=$(rcof "$D")
41
+ if [ "$rc" = "1" ] && printf '%s' "$out" | grep -q 'test_orphan_lanes.sh'; then
42
+ ok "L1 known-positive: undeclared orphan suite → rc=1 and it is named"
43
+ else
44
+ bad "L1 known-positive did not fire (rc=$rc) — every lane below is meaningless without it"
45
+ fi
46
+
47
+ # ── L2 NO-OP: no declaration file → behaviour is exactly the L1 behaviour ─────────────────────
48
+ # This is the arm that protects THIS repo, which ships no company/ directory.
49
+ D="$T/l2"; mkfixture "$D"
50
+ out=$(run "$D")
51
+ if ! printf '%s' "$out" | grep -q 'lane_declarations'; then
52
+ ok "L2 no-op: absent declaration file is silent (no mention in output)"
53
+ else
54
+ bad "L2 no-op: output mentions the declaration file when none exists"
55
+ fi
56
+
57
+ # ── L3 org EXEMPT suppresses the orphan ───────────────────────────────────────────────────────
58
+ D="$T/l3"; mkfixture "$D"; mkdir -p "$D/company"
59
+ printf 'exempt:\n - test_orphan_lanes.sh # org-owned, runs in the org CI only\n' > "$D/company/lane_declarations.yaml"
60
+ out=$(run "$D"); rc=$(rcof "$D")
61
+ if [ "$rc" = "0" ] && printf '%s' "$out" | grep -q '1 exempt'; then
62
+ ok "L3 org exempt: declared suite no longer fails the check (rc=0, counted exempt)"
63
+ else
64
+ bad "L3 org exempt did not suppress (rc=$rc) — out: $(printf '%s' "$out" | tail -1)"
65
+ fi
66
+
67
+ # ── L4 org DEBT suppresses AND is counted as debt ─────────────────────────────────────────────
68
+ D="$T/l4"; mkfixture "$D"; mkdir -p "$D/company"
69
+ printf 'debt:\n - test_orphan_lanes.sh\n' > "$D/company/lane_declarations.yaml"
70
+ out=$(run "$D"); rc=$(rcof "$D")
71
+ if [ "$rc" = "0" ] && printf '%s' "$out" | grep -q 'declared debt'; then
72
+ ok "L4 org debt: declared suite suppressed and reported as debt"
73
+ else
74
+ bad "L4 org debt did not land (rc=$rc)"
75
+ fi
76
+
77
+ # ── L5 VISIBILITY: an org-declared list must never look like this repo's own zero ─────────────
78
+ D="$T/l5"; mkfixture "$D"; mkdir -p "$D/company"
79
+ printf 'debt:\n - test_orphan_lanes.sh\n' > "$D/company/lane_declarations.yaml"
80
+ out=$(run "$D")
81
+ if printf '%s' "$out" | grep -q 'org-declared'; then
82
+ ok "L5 visibility: output names the org file and its counts"
83
+ else
84
+ bad "L5 visibility: org declarations are invisible in the output"
85
+ fi
86
+
87
+ # ── L6 FAIL-CLOSED on an unparseable file ─────────────────────────────────────────────────────
88
+ # The load-bearing distinction. A malformed file must NOT read as "nothing declared" — that would
89
+ # render UNMEASURED as ZERO, and it would do so on the arm where a fork is relying on the file.
90
+ D="$T/l6"; mkfixture "$D"; mkdir -p "$D/company"
91
+ printf 'exempt:\n this line is not an entry\n' > "$D/company/lane_declarations.yaml"
92
+ out=$(run "$D"); rc=$(rcof "$D")
93
+ if [ "$rc" = "2" ] && printf '%s' "$out" | grep -q 'UNMEASURED'; then
94
+ ok "L6 fail-closed: unparseable declaration file → rc=2 and says UNMEASURED"
95
+ else
96
+ bad "L6 fail-closed FAILED (rc=$rc) — a malformed file was read as 'no declarations'"
97
+ fi
98
+
99
+ # ── L6b control for L6: the SAME orphan tree with a WELL-FORMED file must not exit 2 ──────────
100
+ # Without this, L6 would also pass on a checker that exits 2 for any declaration file at all.
101
+ D="$T/l6b"; mkfixture "$D"; mkdir -p "$D/company"
102
+ printf 'exempt:\n - test_orphan_lanes.sh\n' > "$D/company/lane_declarations.yaml"
103
+ if [ "$(rcof "$D")" != "2" ]; then
104
+ ok "L6b control: a well-formed file does NOT trip the fail-closed arm"
105
+ else
106
+ bad "L6b control: every declaration file exits 2 — L6 proved nothing"
107
+ fi
108
+
109
+ # ── L7 unknown section is a parse failure, not a silent skip ──────────────────────────────────
110
+ D="$T/l7"; mkfixture "$D"; mkdir -p "$D/company"
111
+ printf 'allowed:\n - test_orphan_lanes.sh\n' > "$D/company/lane_declarations.yaml"
112
+ out=$(run "$D")
113
+ if [ "$(rcof "$D")" = "2" ] && printf '%s' "$out" | grep -q "unknown section"; then
114
+ ok "L7 unknown section → rc=2 and names the section"
115
+ else
116
+ bad "L7 unknown section was tolerated — a typo'd header would silently declare nothing"
117
+ fi
118
+
119
+ # ── L8 the same name in both sections is ambiguous, not last-wins ─────────────────────────────
120
+ D="$T/l8"; mkfixture "$D"; mkdir -p "$D/company"
121
+ printf 'exempt:\n - test_orphan_lanes.sh\ndebt:\n - test_orphan_lanes.sh\n' > "$D/company/lane_declarations.yaml"
122
+ out=$(run "$D")
123
+ if [ "$(rcof "$D")" = "2" ] && printf '%s' "$out" | grep -q "both exempt and debt"; then
124
+ ok "L8 same suite in both sections → rc=2 (ambiguous, not silently resolved)"
125
+ else
126
+ bad "L8 double declaration was silently resolved"
127
+ fi
128
+
129
+ # ── L9 a stale entry WARNS but does not block (same voice as upstream DEBT hygiene) ───────────
130
+ # Deliberately advisory: making the downstream mirror stricter than the upstream rule it mirrors
131
+ # would train forks to delete the file, which costs more than a stale line.
132
+ D="$T/l9"; mkfixture "$D"; mkdir -p "$D/company"
133
+ printf 'exempt:\n - test_orphan_lanes.sh\n - test_deleted_lanes.sh\n' > "$D/company/lane_declarations.yaml"
134
+ out=$(run "$D"); rc=$(rcof "$D")
135
+ if [ "$rc" = "0" ] && printf '%s' "$out" | grep -q 'test_deleted_lanes.sh'; then
136
+ ok "L9 stale entry: warns and names it, does not block (rc=0)"
137
+ else
138
+ bad "L9 stale entry handling wrong (rc=$rc)"
139
+ fi
140
+
141
+ # ── L10 comments and blank lines are not entries ──────────────────────────────────────────────
142
+ D="$T/l10"; mkfixture "$D"; mkdir -p "$D/company"
143
+ printf '# org lane declarations\n\nexempt:\n\n - test_orphan_lanes.sh # reason lives here\n' > "$D/company/lane_declarations.yaml"
144
+ if [ "$(rcof "$D")" = "0" ]; then
145
+ ok "L10 comments/blank lines tolerated (a file people can annotate)"
146
+ else
147
+ bad "L10 a commented, spaced file failed to parse"
148
+ fi
149
+
150
+ # ── L11/L12 embedded --self-test subjects (found→extend, 2026-08-14) ──────────────────────────
151
+ # A lane suite living INSIDE its subject as a `--self-test` flag has no `test_*.sh`/`*_lanes.sh`
152
+ # filename, so the suites glob above cannot see it at all — measured on this repo's own
153
+ # lane_runner_check.sh header before this feature existed: 4 such subjects had zero callers and
154
+ # nothing here said so. mkfixture without the orphan file, so these lanes isolate the self-test
155
+ # behaviour from L1's own blocking orphan.
156
+ mkfixture_clean() { # $1 = dir
157
+ local d="$1"
158
+ mkdir -p "$d/scripts"
159
+ cp "$CHECK" "$d/scripts/lane_runner_check.sh"
160
+ # A wired ordinary suite so the `suites` set is non-empty — an empty tree fails EXTRACTOR_BROKE
161
+ # before the self-test report section runs at all, which would make L11/L12 pass vacuously.
162
+ printf '#!/usr/bin/env bash\n: wired lane suite\n' > "$d/scripts/test_ok_lanes.sh"
163
+ printf '#!/usr/bin/env bash\nbash scripts/lane_runner_check.sh\nbash scripts/test_ok_lanes.sh\n' > "$d/scripts/selfcheck.sh"
164
+ }
165
+
166
+ # L11 known-POSITIVE: an unwired --self-test subject is reported, advisory (rc stays 0).
167
+ D="$T/l11"; mkfixture_clean "$D"
168
+ printf '#!/usr/bin/env bash\n[ "${1:-}" = "--self-test" ] && { echo ok; exit 0; }\n' > "$D/scripts/orphan_probe.sh"
169
+ out=$(run "$D"); rc=$(rcof "$D")
170
+ if [ "$rc" = "0" ] && printf '%s' "$out" | grep -q 'orphan_probe'; then
171
+ ok "L11 known-positive: unwired --self-test subject named, advisory (rc=0)"
172
+ else
173
+ bad "L11 known-positive did not fire (rc=$rc) — self-test subject not reported"
174
+ printf '%s\n' "$out" | sed 's/^/ │ /'
175
+ fi
176
+
177
+ # L12 known-NEGATIVE: the SAME subject, wired through selfcheck.sh's _subj for-loop idiom, is
178
+ # NOT reported — the exact shape scripts/selfcheck.sh:478 actually uses.
179
+ D="$T/l12"; mkfixture_clean "$D"
180
+ printf '#!/usr/bin/env bash\n[ "${1:-}" = "--self-test" ] && { echo ok; exit 0; }\n' > "$D/scripts/orphan_probe.sh"
181
+ cat > "$D/scripts/selfcheck.sh" <<'SC'
182
+ #!/usr/bin/env bash
183
+ bash scripts/lane_runner_check.sh
184
+ bash scripts/test_ok_lanes.sh
185
+ for _subj in orphan_probe; do
186
+ bash "scripts/$_subj.sh" --self-test
187
+ done
188
+ SC
189
+ out=$(run "$D")
190
+ if ! printf '%s' "$out" | grep -q 'orphan_probe'; then
191
+ ok "L12 known-negative: --self-test subject wired via _subj for-loop is not reported"
192
+ else
193
+ bad "L12 known-negative: wired subject still reported as unwired"
194
+ printf '%s\n' "$out" | sed 's/^/ │ /'
195
+ fi
196
+
197
+ # L13 known-NEGATIVE: the SAME subject, wired through a DIRECT dispatch line — the shape
198
+ # scripts/selfcheck.sh:898/933 actually use (`bash scripts/<name>.sh ... --self-test`), no
199
+ # for-loop at all. Cross-family review 2026-08-14 caught the first draft shipping only the
200
+ # for-loop branch, which meant probe_scope_check.sh and utterance_landing_check.sh — both wired
201
+ # directly, both real — were reported UNDECLARED. This lane pins that class so it cannot silently
202
+ # come back: without it, L11/L12 alone only ever exercise the for-loop shape.
203
+ D="$T/l13"; mkfixture_clean "$D"
204
+ printf '#!/usr/bin/env bash\n[ "${1:-}" = "--self-test" ] && { echo ok; exit 0; }\n' > "$D/scripts/orphan_probe.sh"
205
+ cat > "$D/scripts/selfcheck.sh" <<'SC'
206
+ #!/usr/bin/env bash
207
+ bash scripts/lane_runner_check.sh
208
+ bash scripts/test_ok_lanes.sh
209
+ bash scripts/orphan_probe.sh --self-test >/dev/null 2>&1
210
+ SC
211
+ out=$(run "$D")
212
+ if ! printf '%s' "$out" | grep -q 'orphan_probe'; then
213
+ ok "L13 known-negative: --self-test subject wired via direct dispatch is not reported"
214
+ else
215
+ bad "L13 known-negative: directly-wired subject still reported as unwired"
216
+ printf '%s\n' "$out" | sed 's/^/ │ /'
217
+ fi
218
+
219
+ # ── L14/L15 the SUBJECT'S OWN documentation is not a caller (2026-08-15) ──────────────────────
220
+ # Measured on this repo: `self-test: 10/10 wired` while directional_diff_gate.sh had ZERO callers
221
+ # (`grep -rn directional_diff_gate scripts/*.sh templates/.git-hooks/* .github/workflows/*.yml`,
222
+ # excluding the checker and the subject itself → nothing). The subject's own usage comment
223
+ # (`# bash scripts/directional_diff_gate.sh --self-test`) matched the direct-dispatch regex, so
224
+ # the checker read the file's documentation of itself as evidence that something ran it.
225
+ #
226
+ # This is the SAME class the checker already closes one level up and, until now, only there:
227
+ # `runners` drops lane_runner_check.sh so the tool cannot certify its own DEBT list as done, and
228
+ # has_runner() skips a suite's own file, and runner_dispatches() skips comment lines. None of the
229
+ # three guards reached the --self-test branch — has_selftest_runner() scanned every runner
230
+ # including the subject, over raw text including comments. A guard that exists in one predicate
231
+ # and not in its sibling is not a guard, it is a coincidence.
232
+ #
233
+ # Two lanes because the two halves fail independently: self-exclusion alone still lets a COMMENT
234
+ # in a third file certify a subject, and comment-skipping alone still lets a subject's own
235
+ # non-comment self-reference do it.
236
+
237
+ # L14 known-POSITIVE: the subject's OWN usage comment must not count as a caller.
238
+ D="$T/l14"; mkfixture_clean "$D"
239
+ cat > "$D/scripts/orphan_probe.sh" <<'SUBJ'
240
+ #!/usr/bin/env bash
241
+ # Usage:
242
+ # bash scripts/orphan_probe.sh --self-test # known-pair calibration
243
+ [ "${1:-}" = "--self-test" ] && { echo ok; exit 0; }
244
+ SUBJ
245
+ out=$(run "$D"); rc=$(rcof "$D")
246
+ if [ "$rc" = "0" ] && printf '%s' "$out" | grep -q 'orphan_probe'; then
247
+ ok "L14 known-positive: subject's own usage comment is not a caller — still reported unwired"
248
+ else
249
+ bad "L14 known-positive did not fire (rc=$rc) — self-referential comment read as a dispatcher"
250
+ printf '%s\n' "$out" | sed 's/^/ │ /'
251
+ fi
252
+
253
+ # L15 known-POSITIVE: a comment in a DIFFERENT file must not count either. Same discipline
254
+ # runner_dispatches() already applies to ordinary suites ("a mention is not an invocation").
255
+ D="$T/l15"; mkfixture_clean "$D"
256
+ printf '#!/usr/bin/env bash\n[ "${1:-}" = "--self-test" ] && { echo ok; exit 0; }\n' > "$D/scripts/orphan_probe.sh"
257
+ cat > "$D/scripts/selfcheck.sh" <<'SC'
258
+ #!/usr/bin/env bash
259
+ bash scripts/lane_runner_check.sh
260
+ bash scripts/test_ok_lanes.sh
261
+ # historical note: we used to run `bash scripts/orphan_probe.sh --self-test` here, removed 2026-01-01
262
+ SC
263
+ out=$(run "$D"); rc=$(rcof "$D")
264
+ if [ "$rc" = "0" ] && printf '%s' "$out" | grep -q 'orphan_probe'; then
265
+ ok "L15 known-positive: a commented-out dispatch in another file is not a caller"
266
+ else
267
+ bad "L15 known-positive did not fire (rc=$rc) — a comment read as a live dispatcher"
268
+ printf '%s\n' "$out" | sed 's/^/ │ /'
269
+ fi
270
+
271
+ # L16 known-POSITIVE: a subject that names its own dispatch on a NON-comment line. This is the only
272
+ # lane that actually pins the self-exclusion guard, and it exists because a cross-family round
273
+ # (2026-08-15) showed L14/L15 do not: both of their fixtures put the self-reference in a `#` line,
274
+ # so comment-stripping alone already satisfies them and the guard could be deleted with all lanes
275
+ # still green — a repair with no control, which is the exact failure this file was written to name.
276
+ # The shape is a usage helper, because that is how a real subject names its own flag in live code
277
+ # rather than in a header comment.
278
+ D="$T/l16"; mkfixture_clean "$D"
279
+ cat > "$D/scripts/orphan_probe.sh" <<'SUBJ'
280
+ #!/usr/bin/env bash
281
+ usage() { echo "run: bash scripts/orphan_probe.sh --self-test"; }
282
+ [ "${1:-}" = "--self-test" ] && { echo ok; exit 0; }
283
+ usage
284
+ SUBJ
285
+ out=$(run "$D"); rc=$(rcof "$D")
286
+ if [ "$rc" = "0" ] && printf '%s' "$out" | grep -q 'orphan_probe'; then
287
+ ok "L16 known-positive: a subject's own NON-comment self-reference is not a caller"
288
+ else
289
+ bad "L16 known-positive did not fire (rc=$rc) — self-exclusion guard is unanchored"
290
+ printf '%s\n' "$out" | sed 's/^/ │ /'
291
+ fi
292
+
293
+ echo "----"
294
+ echo "lane-runner lanes: $pass passed, $fail failed"
295
+ [ "$fail" -eq 0 ] || exit 1
@@ -174,6 +174,223 @@ for h in "$FH_REPO"/templates/.git-hooks/pre-commit "$FH_REPO"/templates/.git-ho
174
174
  else bad "lane9 sentinel does NOT match shipped $(basename "$h") — every FH machine would report 'not FH gate'" "$(head -3 "$h")"; fi
175
175
  done
176
176
 
177
+ # ── lane10 — consent-gated auto-pull: the APPLY arm and every REFUSE arm ─────────────────────
178
+ # Added 2026-08-15 with the feature. These lanes exist because the feature's whole safety claim is
179
+ # about what it does NOT do, and "does not" is exactly what a green run cannot demonstrate by
180
+ # itself — every refuse arm below is paired against an apply arm in the same fixture, so a lane
181
+ # that goes silently inert fails the CONTROL rather than passing everything.
182
+ # 🟥 The load-bearing one is lane10-b: in a shared checkout a branch switch yanks the ground out
183
+ # from under a peer session (measured 2026-08-09, two sessions/one worktree). If this feature ever
184
+ # switches branches, that lane is what says so.
185
+ _ap_root="$(mktemp -d)"
186
+ (
187
+ cd "$_ap_root" || exit 1
188
+ # Pin the branch name: `git init` uses init.defaultBranch, which is main on this
189
+ # operator's machine and master on a stock Ubuntu (CI). Hardcoding "main" in the
190
+ # assertions below made lane10-b pass VACUOUSLY there (`rev-parse "$_DEF"` returned
191
+ # empty, and empty==empty compared true) while CONTROL's reset silently no-op'd.
192
+ git -c init.defaultBranch=main init -q up && cd up
193
+ git config user.email t@t; git config user.name t; git config commit.gpgsign false
194
+ mkdir -p scripts tracks/_meta
195
+ cp "$FH_REPO/scripts/fh_node_check.sh" "$FH_REPO/scripts/consent_registry_check.sh" scripts/
196
+ # Build the consent fixture from the SHIPPED TEMPLATE, never from the operator's local
197
+ # tracks/_meta/ — those are gitignored, so on CI (and on any fresh clone) they do not exist and
198
+ # the lanes would report a red FAIL for a file whose absence is correct by design. Measured on
199
+ # CI 2026-08-15: exactly that, "registry/UAP absent — lanes not run". Sourcing the template
200
+ # instead is strictly better than degrading to SKIP: the lanes then actually run everywhere, and
201
+ # they validate the very block a consumer is told to copy.
202
+ cp "$FH_REPO/templates/consent_classes.yaml.example" tracks/_meta/consent_classes.yaml
203
+ # The grant side has no shipped template (a UAP is per-operator by nature), so synthesize the
204
+ # minimum the registry check requires: the full R6 fingerprint, far-future expiry so the fixture
205
+ # never rots into a false refusal, joined to the template's own class name.
206
+ # Dates are computed at fixture-build time, not hardcoded, for two reasons that pull opposite
207
+ # ways: a fixed past date rots into a false REFUSE, and a far-future one is refused outright —
208
+ # R5 caps a lease at 365 days ("an unbounded expiry is a transfer, not a lease"), and the first
209
+ # version of this fixture used 2099-01-01 and was correctly rejected by the very floor the
210
+ # feature depends on. GNU-first with validation, same discipline as every _mtime helper here:
211
+ # `date -d` is GNU, `date -v` is BSD, and neither failing silently is acceptable.
212
+ _g="$(date +%Y-%m-%d)"
213
+ _e="$(date -d '+300 days' +%Y-%m-%d 2>/dev/null || date -v+300d +%Y-%m-%d 2>/dev/null || echo '')"
214
+ case "$_e" in
215
+ [0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]) : ;;
216
+ *) bad "lane10 fixture: could not compute a lease expiry (neither GNU nor BSD date worked)" "$_e"; _e="$_g" ;;
217
+ esac
218
+ cat > tracks/_meta/user_adaptation_profile.md <<UAPEOF
219
+ ---
220
+ standing_consent:
221
+ repo-freshness-autopull:
222
+ granted: $_g
223
+ expires: $_e
224
+ owner: scripts/fh_node_check.sh
225
+ mode: ff-only-merge-on-default-branch
226
+ target: this repo's own default branch, in the local clone, when it is already checked out
227
+ effects: [network, repo-mutation]
228
+ sinks: []
229
+ ---
230
+ lane fixture — not a real profile
231
+ UAPEOF
232
+ echo base > f.txt; git add -A >/dev/null; git commit -qm base
233
+ cd ..; git clone -q up dn; cd up; echo newer > f.txt; git commit -qam ahead
234
+ ) >/dev/null 2>&1
235
+ _dn="$_ap_root/dn"
236
+ if [ ! -d "$_dn/.git" ] || [ ! -f "$_dn/tracks/_meta/consent_classes.yaml" ]; then
237
+ bad "lane10 fixture: could not build the consent fixture (registry/UAP absent — lanes not run)" "$_ap_root"
238
+ else
239
+ git -C "$_dn" config user.email t@t >/dev/null 2>&1; git -C "$_dn" config user.name t >/dev/null 2>&1
240
+ # Derive rather than assume — belt-and-braces over the pin above, so a future git
241
+ # whose init behaviour changes again cannot make these lanes vacuous a second time.
242
+ _DEF="$(git -C "$_dn" symbolic-ref --short refs/remotes/origin/HEAD 2>/dev/null | sed 's#^origin/##')"
243
+ [ -n "$_DEF" ] || _DEF=main
244
+ if ! git -C "$_dn" show-ref --verify --quiet "refs/heads/$_DEF"; then
245
+ bad "lane10 fixture: default branch '$_DEF' has no local ref — assertions below would be vacuous" "$(git -C "$_dn" branch -a)"
246
+ fi
247
+ git -C "$_dn" fetch -q origin >/dev/null 2>&1
248
+ _h0="$(git -C "$_dn" rev-parse HEAD)"
249
+ run "$_dn" "$_ap_root/s_a" >/dev/null 2>&1
250
+ if [ "$(git -C "$_dn" rev-parse HEAD)" != "$_h0" ]; then
251
+ ok "lane10-a APPLY: consented + on default branch → fast-forwarded"
252
+ else
253
+ bad "lane10-a APPLY: consented + on default branch did NOT fast-forward (feature inert — every refuse lane below is then vacuous)" "$_h0"
254
+ fi
255
+
256
+ # b — the envelope. Never applies off the default branch, and NEVER switches branches.
257
+ (cd "$_ap_root/up" && echo newer2 > f.txt && git commit -qam ahead2) >/dev/null 2>&1
258
+ git -C "$_dn" fetch -q origin >/dev/null 2>&1
259
+ git -C "$_dn" switch -c feat-x -q >/dev/null 2>&1
260
+ _m0="$(git -C "$_dn" rev-parse "$_DEF")"
261
+ # ⚠️ CHECKING main AND THE BRANCH NAME IS NOT ENOUGH — measured by a revert probe 2026-08-15.
262
+ # With the envelope deliberately broken, `merge --ff-only origin/main` fast-forwards THE BRANCH
263
+ # YOU ARE ON, so feat-x moved while `main` sat still and the branch name never changed. The first
264
+ # version of this lane asserted only those two and stayed green against a defeated envelope —
265
+ # decorative. The tip that must not move is the CURRENT branch's own.
266
+ _f0="$(git -C "$_dn" rev-parse feat-x)"
267
+ run "$_dn" "$_ap_root/s_b" >/dev/null 2>&1
268
+ if [ "$(git -C "$_dn" rev-parse "$_DEF")" = "$_m0" ] \
269
+ && [ "$(git -C "$_dn" rev-parse feat-x)" = "$_f0" ] \
270
+ && [ "$(git -C "$_dn" branch --show-current)" = "feat-x" ]; then
271
+ ok "lane10-b ENVELOPE: off default branch → no apply on EITHER branch, no switch"
272
+ else
273
+ bad "lane10-b ENVELOPE: a branch tip moved off the default branch — the shared-checkout hazard" "main=$_m0->$(git -C "$_dn" rev-parse "$_DEF") feat-x=$_f0->$(git -C "$_dn" rev-parse feat-x) branch=$(git -C "$_dn" branch --show-current)"
274
+ fi
275
+ git -C "$_dn" switch -q "$_DEF" >/dev/null 2>&1
276
+
277
+ # c — no grant. absent ≠ granted.
278
+ cp "$_dn/tracks/_meta/user_adaptation_profile.md" "$_ap_root/uap.bak"
279
+ python3 - "$_dn/tracks/_meta/user_adaptation_profile.md" <<'PYX'
280
+ import re,sys
281
+ p=sys.argv[1]; s=open(p,encoding='utf-8').read()
282
+ open(p,'w',encoding='utf-8').write(re.sub(r'\nstanding_consent:\n(?: .*\n| .*\n)*', '\n', s, count=1))
283
+ PYX
284
+ _h1="$(git -C "$_dn" rev-parse HEAD)"
285
+ run "$_dn" "$_ap_root/s_c" >/dev/null 2>&1
286
+ [ "$(git -C "$_dn" rev-parse HEAD)" = "$_h1" ] \
287
+ && ok "lane10-c NO-GRANT: absent standing_consent → no apply" \
288
+ || bad "lane10-c NO-GRANT: applied without a grant" "$_h1"
289
+
290
+ # d — expired lease. A lease nobody enforces is not a lease.
291
+ cp "$_ap_root/uap.bak" "$_dn/tracks/_meta/user_adaptation_profile.md"
292
+ sed -i.bak2 's/expires: [0-9-]*/expires: 2020-01-01/' "$_dn/tracks/_meta/user_adaptation_profile.md"
293
+ _h2="$(git -C "$_dn" rev-parse HEAD)"
294
+ run "$_dn" "$_ap_root/s_d" >/dev/null 2>&1
295
+ [ "$(git -C "$_dn" rev-parse HEAD)" = "$_h2" ] \
296
+ && ok "lane10-d EXPIRED: past-dated lease → no apply" \
297
+ || bad "lane10-d EXPIRED: applied under an expired lease" "$_h2"
298
+ cp "$_ap_root/uap.bak" "$_dn/tracks/_meta/user_adaptation_profile.md"
299
+
300
+ # e — divergence. --ff-only must refuse rather than absorb, and local work must survive.
301
+ (cd "$_dn" && echo local-only > mine.txt && git add -A && git commit -qm diverge) >/dev/null 2>&1
302
+ _h3="$(git -C "$_dn" rev-parse HEAD)"
303
+ run "$_dn" "$_ap_root/s_e" >/dev/null 2>&1
304
+ if [ "$(git -C "$_dn" rev-parse HEAD)" = "$_h3" ] && [ -f "$_dn/mine.txt" ]; then
305
+ ok "lane10-e DIVERGED: ff refused, local commit survives"
306
+ else
307
+ bad "lane10-e DIVERGED: local history was moved or lost" "$_h3 -> $(git -C "$_dn" rev-parse HEAD)"
308
+ fi
309
+
310
+ # f — WRONG CLASS. The refusal that the other three arms structurally cannot produce.
311
+ # Arms c/d/e all refuse by breaking the FILE-WIDE verdict (grant block deleted → exit 3; lease
312
+ # past-dated → exit 1; divergence → git itself refuses). None of them touches the question "is
313
+ # THIS class granted", so a caller keyed on the file-wide 0 passed them all — measured 2026-08-15
314
+ # by a pre-publish security pass, with a live control: one unrelated class validly granted plus
315
+ # the target class named only in prose, and the merge ran while the banner claimed a standing
316
+ # consent that never existed. The revoke path was the broken one.
317
+ # The second assertion below is the load-bearing half: this arm must refuse WHILE the bare
318
+ # file-wide check still returns 0. Without it the lane would pass for the same reason lane10-c
319
+ # does, and the axis would go unexercised again.
320
+ # ORDER MATTERS, and getting it wrong is silent: the UAP is a TRACKED file in this fixture, so
321
+ # `git reset --hard` reverts any edit made to it. The first draft transformed the UAP and then
322
+ # reset — restoring the original grant, so the apply fired legitimately and the lane failed for a
323
+ # reason that looked like the defect. Reset and advance origin FIRST, edit the UAP AFTER.
324
+ git -C "$_dn" reset -q --hard "origin/$_DEF" >/dev/null 2>&1 # noqa: destructive-op (throwaway fixture)
325
+ (cd "$_ap_root/up" && echo newer-f > f.txt && git commit -qam ahead-f) >/dev/null 2>&1
326
+ git -C "$_dn" fetch -q origin >/dev/null 2>&1
327
+ cp "$_ap_root/uap.bak" "$_dn/tracks/_meta/user_adaptation_profile.md"
328
+ python3 - "$_dn/tracks/_meta/user_adaptation_profile.md" <<'PYX'
329
+ import sys, re as _re
330
+ p = sys.argv[1]; s = open(p, encoding='utf-8').read()
331
+ old = " repo-freshness-autopull:"
332
+ assert old in s, "lane10-f fixture: grant key not found"
333
+ # Reuse the lease dates the fixture already computed, so this arm cannot fail for a date reason and
334
+ # be mistaken for a class-join refusal. (First draft omitted them: R5 fired, the file-wide verdict
335
+ # went to 1, and the arm's own vacuity guard caught it — which is what that guard is for.)
336
+ _g = _re.search(r"granted: (\S+)", s).group(1)
337
+ _e = _re.search(r"expires: (\S+)", s).group(1)
338
+ # A VALID grant for a different registered, promotion_eligible class, scope-matched to its
339
+ # registration so R6/R7 pass — the point is a file that is entirely well-formed.
340
+ other = (" dispatch-readonly-sim-local-artifact:\n"
341
+ f" granted: {_g}\n"
342
+ f" expires: {_e}\n"
343
+ " owner: fh-meta:sim-conductor\n"
344
+ " mode: blind-target-tier-sim\n"
345
+ " target: a single local file under the current repo\n"
346
+ " effects: [read, dispatch]\n"
347
+ " sinks: []\n")
348
+ s = s.replace(old, " __TARGET_GRANT__:", 1)
349
+ head, sep, tail = s.partition("standing_consent:\n")
350
+ s = head + sep + other + tail
351
+ # the target class survives ONLY as prose — the shape the old raw grep could not tell from a grant
352
+ s = s.replace("lane fixture — not a real profile",
353
+ "lane fixture — not a real profile\n\n repo-freshness-autopull: 철회함 (revoked)")
354
+ import re
355
+ s = re.sub(r" __TARGET_GRANT__:\n(?: .*\n)*", "", s, count=1)
356
+ open(p, 'w', encoding='utf-8').write(s)
357
+ PYX
358
+ # PRECONDITION, asserted rather than assumed. lane10-e leaves the clone DIVERGED, and a diverged
359
+ # clone refuses the fast-forward for a reason that has nothing to do with consent — so without the
360
+ # reset above the arm passes whether the class join works or not. Measured: a revert probe against
361
+ # the repaired fh_node_check.sh left this lane green, i.e. it was decorative. Same shape as the
362
+ # self-exclusion lane earlier the same day; the probe is what separates the two cases.
363
+ _h5="$(git -C "$_dn" rev-parse HEAD)"
364
+ if [ "$_h5" = "$(git -C "$_dn" rev-parse "origin/$_DEF")" ]; then
365
+ bad "lane10-f VACUOUS: clone is not behind origin, so 'no apply' proves nothing" "$_h5"
366
+ fi
367
+ run "$_dn" "$_ap_root/s_f" >/dev/null 2>&1
368
+ _filewide=0
369
+ bash "$FH_REPO/scripts/consent_registry_check.sh" \
370
+ "$_dn/tracks/_meta/consent_classes.yaml" \
371
+ "$_dn/tracks/_meta/user_adaptation_profile.md" >/dev/null 2>&1 || _filewide=$?
372
+ if [ "$(git -C "$_dn" rev-parse HEAD)" = "$_h5" ] && [ "$_filewide" = "0" ]; then
373
+ ok "lane10-f WRONG-CLASS: another class granted, this one only in prose → no apply (file-wide check still 0)"
374
+ elif [ "$_filewide" != "0" ]; then
375
+ bad "lane10-f VACUOUS: the fixture broke the file-wide verdict (rc=$_filewide), so it re-tests lane10-c's axis, not the class join" "rebuild the fixture so the bare check returns 0"
376
+ else
377
+ bad "lane10-f WRONG-CLASS: applied on a class that was never granted" "$_h5 -> $(git -C "$_dn" rev-parse HEAD)"
378
+ fi
379
+ cp "$_ap_root/uap.bak" "$_dn/tracks/_meta/user_adaptation_profile.md"
380
+
381
+ # CONTROL — after all the refuse arms, prove the apply arm still fires. Without this, a lane
382
+ # suite where the feature has gone inert reports 4 clean refusals and looks perfect.
383
+ git -C "$_dn" reset -q --hard "origin/$_DEF" >/dev/null 2>&1 # noqa: destructive-op (throwaway fixture)
384
+ (cd "$_ap_root/up" && echo newer3 > f.txt && git commit -qam ahead3) >/dev/null 2>&1
385
+ git -C "$_dn" fetch -q origin >/dev/null 2>&1
386
+ _h4="$(git -C "$_dn" rev-parse HEAD)"
387
+ run "$_dn" "$_ap_root/s_f" >/dev/null 2>&1
388
+ [ "$(git -C "$_dn" rev-parse HEAD)" != "$_h4" ] \
389
+ && ok "lane10-CONTROL: apply arm still fires after the refuse arms (instrument alive)" \
390
+ || bad "lane10-CONTROL: apply arm went inert — the refuse lanes above prove nothing" "$_h4"
391
+ fi
392
+ rm -rf "$_ap_root"
393
+
177
394
  printf '\nnode-check lanes: %d passed, %d failed\n' "$PASS" "$FAIL"
178
395
  [ "$FAIL" -eq 0 ] || exit 1
179
396
  exit 0
@@ -357,6 +357,67 @@ CTLPY
357
357
  [ "$_BARE_CTL" = FIRED ]
358
358
  chk $? "CONTROL: that detector fires on a planted declared-shipped subject (got $_BARE_CTL)"
359
359
 
360
+ echo ""
361
+ echo "── directional_diff_gate verdict arm: exit 0 is not a pass ──"
362
+ # WHY THIS LANE EXISTS (2026-08-15): the block wiring directional_diff_gate.sh --self-test gates on
363
+ # the subject's terminal verdict, not on rc=0 alone, and both halves of that were put there by a
364
+ # cross-family round rather than by the author. Its first draft gated on rc=0 plus a SUBSTRING of
365
+ # the verdict, and two reachable inputs satisfied it while nothing ran:
366
+ # · `✅ calibration passed (0 pairs)` — the subject's own verdict line is
367
+ # `[ "$f" -eq 0 ] && echo "✅ calibration passed ($n pairs)"`, so deleting every lane prints a
368
+ # PASS with n=0. Deletion read as a pass is the quietest face of the not-found-is-not-zero class.
369
+ # · `usage: calibration passed (28 pairs)` — a prose/usage line carrying the words.
370
+ # Without this lane both repairs are reversible to green, which is anchor-is-decorative.
371
+ # LIFTED from selfcheck.sh, not re-spelled — a lane that restates the condition stops seeing edits
372
+ # to it, which is the failure the two lanes above were written to avoid.
373
+ DD_ARM=$(sed -n '/^ _dd_verdict=\$(printf/,/^ fi ;;$/p' "$SELFCHECK" | sed 's/ ;;$//')
374
+ if [ -z "$DD_ARM" ]; then
375
+ echo "FAIL the directional_diff_gate verdict arm is no longer where this lane lifts it from."
376
+ echo " If the block was moved or renamed, update the lane WITH the subject."
377
+ exit 1
378
+ fi
379
+ # An empty lift is not the only bad lift. If the END anchor stops matching — re-indent the arm, or
380
+ # move it out of the `case` — the START anchor still matches and the range runs to EOF, which is
381
+ # non-empty and therefore passes the guard above. What saves it today is luck, not design: the
382
+ # `;;`-stripping applies to every line in the range, so downstream case arms lose their terminators
383
+ # and `eval` dies of a syntax error. A downstream span that happened to be self-contained would
384
+ # instead `eval` several hundred lines of selfcheck.sh, once per fixture, at the repo root. Bound
385
+ # the lift by size so the failure is a lane message rather than an accident. Cross-family, 2026-08-15.
386
+ if [ "$(printf '%s\n' "$DD_ARM" | wc -l)" -gt 15 ]; then
387
+ echo "FAIL the lifted verdict arm is $(printf '%s\n' "$DD_ARM" | wc -l) lines — the sed END anchor"
388
+ echo " stopped matching and the range ran past the block. Fix the anchor, do not widen this bound."
389
+ exit 1
390
+ fi
391
+ dd_arm_says() { # $1 = the subject's stdout; echoes PASS or FAIL
392
+ ( _out="$1"; fail=0; _show_failure() { :; }
393
+ eval "$DD_ARM" ) | grep -oE '^(PASS|FAIL)' | head -1
394
+ }
395
+
396
+ [ "$(dd_arm_says '✅ calibration passed (28 pairs)')" = PASS ]
397
+ chk $? "a real verdict with a non-zero pair count passes"
398
+
399
+ [ "$(dd_arm_says '✅ calibration passed (0 pairs)')" = FAIL ]
400
+ chk $? "every lane deleted (0 pairs, exit 0) is caught, not certified"
401
+
402
+ [ "$(dd_arm_says 'usage: calibration passed (28 pairs)')" = FAIL ]
403
+ chk $? "a usage/prose line carrying the words does not satisfy the gate"
404
+
405
+ # CONTROL — the pre-repair form (bare substring, no pair-count floor) must ACCEPT both bad
406
+ # fixtures. Without it these lanes could pass because the fixtures are wrong rather than because
407
+ # the repair works.
408
+ _pre() { printf '%s\n' "$1" | grep -oE 'calibration passed \([0-9]+ pairs\)' | tail -1; }
409
+ [ -n "$(_pre '✅ calibration passed (0 pairs)')" ] && [ -n "$(_pre 'usage: calibration passed (28 pairs)')" ]
410
+ chk $? "CONTROL: the pre-repair substring form accepts both — the fixtures discriminate"
411
+
412
+ # SCOPE of these four lanes, stated because the inherited §SCOPE block below is about a different
413
+ # anchor and does not cover them. The lift is the `0)` arm's BODY only, so these lanes see a change
414
+ # to the verdict predicate and are blind to the routing around it: widening `case ... in 0)` to
415
+ # `0|1)`, dropping `< /dev/null`, dropping the `$_LANE_TO` wrapper, or deleting the rc=124
416
+ # HARNESS-ERROR arm all leave every lane here green. Those are guarded by review, not by this file.
417
+ # The `_pre` control is also a RE-SPELLING of the pre-repair form rather than a lift of it — it has
418
+ # to be, since that form no longer exists in the subject to lift, but it means the control asserts
419
+ # what the old code did from memory. Both limits found by cross-family review, 2026-08-15.
420
+
360
421
  # ── SCOPE OF THIS ANCHOR — stated so it is not over-trusted ───────────────────
361
422
  # These lanes catch REVERSION (the run-twice form coming back, the helper being gutted, the
362
423
  # call-site redirect returning). They do NOT catch deliberate EVASION: a cross-family round