@chrono-meta/fh-gate 1.4.98 → 2.0.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.
- package/.claude-plugin/marketplace.json +2 -2
- package/README.ja.md +219 -42
- package/README.ko.md +231 -45
- package/README.md +157 -31
- package/README.zh.md +209 -40
- package/docs/OUTPUT_EVIDENCE.md +21 -12
- package/knowledge/shared/harness-core/field_verdict_crossfamily_gate.md +36 -0
- package/knowledge/shared/learnings/subagent_invocations_log.yaml +21 -0
- package/package.json +4 -2
- package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-meta/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-meta/CHANGELOG.md +129 -0
- package/scripts/capability_effect_probe.sh +381 -0
- package/scripts/capability_registry_check.sh +115 -6
- package/scripts/consent_registry_check.sh +124 -1
- package/scripts/fh_node_check.sh +69 -1
- package/scripts/lane_runner_check.sh +70 -14
- package/scripts/publish_freshness_check.sh +204 -0
- package/scripts/selfcheck.sh +101 -1
- package/scripts/test_capability_entrypoint_shipping.sh +75 -5
- package/scripts/test_consent_registry.sh +99 -0
- package/scripts/test_lane_runner_lanes.sh +74 -0
- package/scripts/test_node_check_lanes.sh +217 -0
- package/scripts/test_selfcheck_state_lanes.sh +61 -0
- package/templates/consent_classes.yaml.example +30 -0
package/scripts/selfcheck.sh
CHANGED
|
@@ -485,7 +485,11 @@ done
|
|
|
485
485
|
# or trim of that one test name would flip a real PASS into "dispatcher missing?" here (cross-family
|
|
486
486
|
# review caught this, 2026-08-15). chamber_witness/digest_landing_check both print a genuine
|
|
487
487
|
# terminal "캘리브레이션 통과/실패" line, so they stay.
|
|
488
|
-
|
|
488
|
+
# publish_freshness_check joined 2026-08-16. It qualifies for the same reason chamber_witness does
|
|
489
|
+
# and capability_registry_check does not: its terminal line is a genuine VERDICT
|
|
490
|
+
# ("publish_freshness 캘리브레이션 통과/실패"), not a Korean test-case title, so a future rename of
|
|
491
|
+
# any single lane cannot flip a real PASS into a false "dispatcher missing?" here.
|
|
492
|
+
for _subj in compaction_probe judgment_circuit_lint novelty_claim_check chamber_witness digest_landing_check publish_freshness_check; do
|
|
489
493
|
if [ ! -f "scripts/$_subj.sh" ]; then
|
|
490
494
|
_absent_subject_verdict "$_subj --self-test" "scripts/$_subj.sh" || fail=1
|
|
491
495
|
else
|
|
@@ -553,6 +557,30 @@ else
|
|
|
553
557
|
fail=1
|
|
554
558
|
fi
|
|
555
559
|
|
|
560
|
+
# capability_effect_probe — wired 2026-08-16. `lane_runner_check.sh` had been flagging it as an
|
|
561
|
+
# embedded --self-test subject with ZERO dispatchers, and that warning was load-bearing: the same
|
|
562
|
+
# day, two SECURITY regression anchors were added to this suite (L8 args injection · L9 the git
|
|
563
|
+
# metadata channel that let a `read-only` declaration plant a persistent `core.pager` hook in the
|
|
564
|
+
# real repo while the probe printed VERIFIED). Unwired, those anchors would never have run anywhere
|
|
565
|
+
# but on the author's machine — the exact "built but not wired" shape this repo keeps reproducing.
|
|
566
|
+
# Not in the `_subj` loop above: its terminal line is `N PASS / M FAIL`, never 캘리브레이션, so that
|
|
567
|
+
# loop's substring gate would go red for a reason unrelated to its lanes (capability_registry_check
|
|
568
|
+
# was added there and reverted for precisely this). Direct dispatch instead.
|
|
569
|
+
# 🟥 Exit code ALONE is not enough — a suite whose lanes were all deleted still exits 0 (measured
|
|
570
|
+
# 2026-08-15 on directional_diff_gate). Require the terminal verdict AS A WHOLE LINE with a
|
|
571
|
+
# non-zero PASS count, so an emptied suite cannot certify itself.
|
|
572
|
+
if [ ! -f scripts/capability_effect_probe.sh ]; then
|
|
573
|
+
echo "FAIL capability_effect_probe.sh: missing — it ships via package.json files[], so absence is deletion, not package mode"
|
|
574
|
+
fail=1
|
|
575
|
+
elif _out=$(bash scripts/capability_effect_probe.sh --self-test < /dev/null 2>&1) \
|
|
576
|
+
&& printf '%s\n' "$_out" | grep -qE '^── capability_effect_probe lanes: [1-9][0-9]* PASS / 0 FAIL ──$'; then
|
|
577
|
+
echo "PASS capability_effect_probe.sh --self-test ($(printf '%s\n' "$_out" | grep -oE '[0-9]+ PASS / [0-9]+ FAIL' | tail -1))"
|
|
578
|
+
else
|
|
579
|
+
echo "FAIL capability_effect_probe.sh: --self-test failed or produced no terminal verdict line"
|
|
580
|
+
_show_failure "$_out"
|
|
581
|
+
fail=1
|
|
582
|
+
fi
|
|
583
|
+
|
|
556
584
|
# relay_channel — the other embedded --self-test subject kept out of the loop above: its
|
|
557
585
|
# `--self-test` just `exec`s the sibling scripts/test_relay_channel_lanes.sh, whose own exit code
|
|
558
586
|
# is already disciplined (`[ "$fail" -eq 0 ] || exit 1`), so gating on exit code — same shape as
|
|
@@ -582,6 +610,78 @@ else
|
|
|
582
610
|
fail=1
|
|
583
611
|
fi
|
|
584
612
|
|
|
613
|
+
# directional_diff_gate — the LAST embedded --self-test subject with no caller, and it stayed
|
|
614
|
+
# invisible for a reason worth keeping written down: lane_runner_check.sh reported it WIRED off the
|
|
615
|
+
# subject's OWN usage comment (`# bash scripts/directional_diff_gate.sh --self-test`), so the
|
|
616
|
+
# report read `self-test: 10/10 wired` while `grep -rn directional_diff_gate` across every runner
|
|
617
|
+
# surface returned zero real callers. The detector was repaired in the same delta as this wiring
|
|
618
|
+
# (has_selftest_runner now skips the subject's own file, selftest_dispatched now drops comment
|
|
619
|
+
# lines — the two guards has_runner/runner_dispatches always had), which is why the two halves ship
|
|
620
|
+
# together: fixing the detector alone would have left a real debt item newly VISIBLE and still
|
|
621
|
+
# unrun, and wiring alone would have left the detector able to certify the next such subject.
|
|
622
|
+
# ⚠️ The cost of shipping both halves at once, named rather than left for the next reader to
|
|
623
|
+
# discover: the report reads `10/10` before the change and `10/10` after it, so the detector repair
|
|
624
|
+
# is NOT observable from this repo's own run — reverting the repair alone leaves the count
|
|
625
|
+
# unchanged, because the wiring that now exists is real. Its only evidence is the fixture lanes
|
|
626
|
+
# L14–L16 in scripts/test_lane_runner_lanes.sh, which is why L16 in particular is load-bearing:
|
|
627
|
+
# delete it and the self-exclusion guard has nothing anywhere that would go red.
|
|
628
|
+
#
|
|
629
|
+
# NOT in the `for _subj in ...` loop above, and that is the same call made for
|
|
630
|
+
# capability_registry_check: that loop gates on the substring 캘리브레이션, and this subject's
|
|
631
|
+
# terminal verdict is English ("✅ calibration passed (N pairs)"). Adding it there would produce
|
|
632
|
+
# "dispatcher missing?" — a confident and wrong cause — for a suite that passes. Gate on exit code
|
|
633
|
+
# instead, same shape as the two blocks above. It carries genuine HARNESS-ERROR paths (`exit 10` on
|
|
634
|
+
# mktemp failure, `return 10` on unreadable input), so a setup failure must not read as a lane
|
|
635
|
+
# failure. Ships via package.json files[] with no ACCEPTED_ABSENT declaration, so absence is
|
|
636
|
+
# deletion, not package mode. `< /dev/null` for the same stdin-wait reason the loop above documents.
|
|
637
|
+
if [ ! -f scripts/directional_diff_gate.sh ]; then
|
|
638
|
+
echo "FAIL directional_diff_gate.sh: missing — it ships via package.json files[], so absence is deletion, not package mode"
|
|
639
|
+
fail=1
|
|
640
|
+
else
|
|
641
|
+
_out=$($_LANE_TO bash scripts/directional_diff_gate.sh --self-test < /dev/null 2>&1); _dd_rc=$?
|
|
642
|
+
case "$_dd_rc" in
|
|
643
|
+
10|2|124|126|127)
|
|
644
|
+
# 124 is `timeout`'s own kill code, so it belongs in this arm rather than the generic FAIL
|
|
645
|
+
# one below: a suite that was killed did not measure, and calling that "the lane failed"
|
|
646
|
+
# sends the reader to the wrong file. Reachable only because this dispatch is wrapped in
|
|
647
|
+
# `$_LANE_TO` — which is REUSED from the pair-suite loop above, not installed here, so do
|
|
648
|
+
# not delete it from this line believing the timeout is local to this block.
|
|
649
|
+
echo "HARNESS ERROR directional_diff_gate.sh --self-test: exited $_dd_rc — it could not measure,"
|
|
650
|
+
echo " so its verdicts prove nothing. Not a lane failure, and not a pass either."
|
|
651
|
+
fail=1 ;;
|
|
652
|
+
0)
|
|
653
|
+
# rc=0 is NOT sufficient, and this is not a hypothetical: the `for _subj in ...` loop above
|
|
654
|
+
# records the measured case where a subject fell into an unrecognized mode, printed its usage
|
|
655
|
+
# banner and exited 0, and the checker read that as a pass while 25 lanes had vanished. This
|
|
656
|
+
# subject happens to exit 10 from that path TODAY — a revert probe (2026-08-15: dispatcher
|
|
657
|
+
# line deleted) confirmed the HARNESS ERROR arm fires — but that is incidental to how its
|
|
658
|
+
# usage function is written, not a property this block should depend on. Require the
|
|
659
|
+
# terminal verdict itself, so a future subject that exits 0 without running anything goes red
|
|
660
|
+
# here rather than certifying an empty run.
|
|
661
|
+
# Whole-LINE match, not a substring, and the pair count must be non-zero. Both halves were
|
|
662
|
+
# named by cross-family review (2026-08-15) and both are reachable in the real subject, not
|
|
663
|
+
# hypothetical: directional_diff_gate.sh's verdict line is
|
|
664
|
+
# `[ "$f" -eq 0 ] && echo "✅ calibration passed ($n pairs)"` — delete every `t` lane and it
|
|
665
|
+
# prints `calibration passed (0 pairs)` and exits 0, i.e. a suite with nothing left in it
|
|
666
|
+
# certifies itself. That is the quietest face of [[feedback_not_found_is_not_zero_family]]
|
|
667
|
+
# (deletion read as a pass), and a substring match would also let a usage banner or a prose
|
|
668
|
+
# line carrying the same words satisfy the gate.
|
|
669
|
+
_dd_verdict=$(printf '%s\n' "$_out" | grep -oE '^✅ calibration passed \([0-9]+ pairs\)$' | tail -1)
|
|
670
|
+
_dd_pairs=$(printf '%s' "$_dd_verdict" | grep -oE '[0-9]+' | tail -1)
|
|
671
|
+
if [ -n "$_dd_verdict" ] && [ "${_dd_pairs:-0}" -gt 0 ]; then
|
|
672
|
+
echo "PASS directional_diff_gate.sh --self-test ($_dd_verdict)"
|
|
673
|
+
else
|
|
674
|
+
echo "FAIL directional_diff_gate.sh: --self-test exited 0 without a non-zero calibration verdict (dispatcher missing, or every lane deleted?)"
|
|
675
|
+
_show_failure "$_out"
|
|
676
|
+
fail=1
|
|
677
|
+
fi ;;
|
|
678
|
+
*)
|
|
679
|
+
echo "FAIL directional_diff_gate.sh: --self-test failed (exit $_dd_rc)"
|
|
680
|
+
_show_failure "$_out"
|
|
681
|
+
fail=1 ;;
|
|
682
|
+
esac
|
|
683
|
+
fi
|
|
684
|
+
|
|
585
685
|
# memory-link-check — the memory store is a GRAPH (memory_intent_recall.md: nodes=files,
|
|
586
686
|
# edges=[[links]], recall walks one hop). Measured 2026-07-28: 50 of 872 edges pointed at a note
|
|
587
687
|
# that existed under a different separator and 22 at nothing — a dead edge returns nothing and is
|
|
@@ -53,6 +53,16 @@ fi
|
|
|
53
53
|
FILES_JSON=$(node -e 'process.stdout.write(JSON.stringify(require("./package.json").files||[]))' 2>/dev/null) || {
|
|
54
54
|
echo " INSTRUMENT ERROR: could not read package.json files[]"; exit 1; }
|
|
55
55
|
|
|
56
|
+
# ★`mktemp` — 초판은 `${TMPDIR:-/tmp}/cap_entry_$$.txt` 였다. TMPDIR 없는 환경(리눅스 CI·
|
|
57
|
+
# 컨테이너가 흔하다)에서 공용 `/tmp` + PID 기반 **예측 가능한 이름**이라, 미리 심어둔 심링크를
|
|
58
|
+
# `>` 리다이렉트가 따라가 대상 파일을 덮어쓴다(CWE-377, 보안 패스 [B]). 이 파일은 selfcheck 에
|
|
59
|
+
# 배선돼 **소비자 머신에서 `npm test` 로 돈다**.
|
|
60
|
+
# ⚠️ `mktemp -t <prefix>` 는 **BSD 전용**이다 — GNU 는 템플릿에 X 를 요구하고
|
|
61
|
+
# `mktemp: too few X's in template` 로 죽는다(2026-08-16 CI 실측: 로컬 macOS 초록 · ubuntu 적색).
|
|
62
|
+
# 이 레포가 이미 기록한 `stat -f` BSD-first 와 같은 얼굴이다. 양쪽에서 도는 형태는
|
|
63
|
+
# **명시 템플릿 + X 다수**뿐이다. (계기가 fail-closed 로 막아 준 덕에 조용히 안 지나갔다.)
|
|
64
|
+
_CAP_TMP=$(mktemp "${TMPDIR:-/tmp}/cap_entry.XXXXXXXX") || { echo " INSTRUMENT ERROR: mktemp 실패"; exit 1; }
|
|
65
|
+
trap 'rm -f "$_CAP_TMP"' EXIT INT TERM
|
|
56
66
|
printf '%s\n' "$ENTRIES" | while IFS= read -r e; do
|
|
57
67
|
[ -n "$e" ] || continue
|
|
58
68
|
if node -e 'const f=JSON.parse(process.argv[1]);process.exit(f.includes(process.argv[2])?0:1)' "$FILES_JSON" "$e"; then
|
|
@@ -60,14 +70,14 @@ printf '%s\n' "$ENTRIES" | while IFS= read -r e; do
|
|
|
60
70
|
else
|
|
61
71
|
echo " MISSING $e"
|
|
62
72
|
fi
|
|
63
|
-
done > "$
|
|
73
|
+
done > "$_CAP_TMP"
|
|
64
74
|
|
|
65
|
-
missing=$(grep -c '^ MISSING ' "$
|
|
75
|
+
missing=$(grep -c '^ MISSING ' "$_CAP_TMP" || true)
|
|
66
76
|
missing=$(( ${missing:-0} + 0 ))
|
|
67
|
-
shipped=$(grep -c '^ ok ' "$
|
|
77
|
+
shipped=$(grep -c '^ ok ' "$_CAP_TMP" || true)
|
|
68
78
|
shipped=$(( ${shipped:-0} + 0 ))
|
|
69
|
-
cat "$
|
|
70
|
-
rm -f "$
|
|
79
|
+
cat "$_CAP_TMP"
|
|
80
|
+
rm -f "$_CAP_TMP"
|
|
71
81
|
|
|
72
82
|
if [ "$missing" -eq 0 ]; then
|
|
73
83
|
ok "all $shipped capability entry point(s) are in package.json files[]"
|
|
@@ -118,6 +128,66 @@ else
|
|
|
118
128
|
bad "$dangling declared capability entry point(s) missing from disk — npm would ship a broken files[] and the disk-side check alone stays green"
|
|
119
129
|
fi
|
|
120
130
|
|
|
131
|
+
# SIBLING DEPENDENCIES (added 2026-08-16 — occurrence #4 of the class this file's header names).
|
|
132
|
+
# The two directions above both key on the ENTRY-POINT convention (`*_capability.sh`). That is
|
|
133
|
+
# structurally blind to the failure that actually happened this time: a shipped script called a
|
|
134
|
+
# NON-entry-point sibling that did not ship. `capability_registry_check.sh` shipped, its new M6 axis
|
|
135
|
+
# ran `${0%/*}/capability_effect_probe.sh`, and that probe was absent from files[] — so on a consumer
|
|
136
|
+
# install M6 would hit its fail-closed branch and reject every registration, blaming a "missing probe"
|
|
137
|
+
# that exists in the repo.
|
|
138
|
+
#
|
|
139
|
+
# WHY `package_coverage_check.sh` IS BLIND TO IT — and why the fix belongs here, not there: that
|
|
140
|
+
# checker's extraction regex requires a reference to START with a shipped top-level directory
|
|
141
|
+
# (`scripts|templates|bin|docs|knowledge|plugins|.claude`). A script naming its own sibling writes
|
|
142
|
+
# `${0%/*}/name.sh` or `$(dirname "$0")/name.sh` — the path is BUILT AT RUNTIME and no literal
|
|
143
|
+
# `scripts/` appears. The reference-follower cannot see a path that does not textually exist.
|
|
144
|
+
#
|
|
145
|
+
# Scope kept narrow on purpose: only SELF-DIRECTORY forms. `$REPO_ROOT/scripts/x.sh` already contains
|
|
146
|
+
# the literal `scripts/x.sh` and is caught upstream; widening past that would duplicate that checker.
|
|
147
|
+
SIB_PAT='(\$\{0%/\*\}|\$\(dirname [^)]*\)|\$\{BASH_SOURCE[^}]*%/\*\})/[A-Za-z0-9_.-]+\.(sh|py|js)'
|
|
148
|
+
SHIPPED_SH=$(node -e '
|
|
149
|
+
const f=JSON.parse(process.argv[1]);
|
|
150
|
+
process.stdout.write(f.filter(p=>/^scripts\/.*\.sh$/.test(p)).join("\n"));
|
|
151
|
+
' "$FILES_JSON")
|
|
152
|
+
|
|
153
|
+
sib_refs=0; sib_missing=0
|
|
154
|
+
if [ -n "$SHIPPED_SH" ]; then
|
|
155
|
+
while IFS= read -r s; do
|
|
156
|
+
[ -n "$s" ] && [ -f "$s" ] || continue
|
|
157
|
+
# `grep -o` yields the whole matched expression; the sibling NAME is its last path segment.
|
|
158
|
+
# COMMENT LINES ARE STRIPPED FIRST, and that is not cosmetic: the finding's text asserts the
|
|
159
|
+
# callee is "called at runtime". A file whose PROSE names a sibling (this file's own header does)
|
|
160
|
+
# would be reported under a claim that is false about it — the right defect attributed to the
|
|
161
|
+
# wrong caller. Caught during this lane's own known-pair calibration, where the positive arm
|
|
162
|
+
# named two callers and only one of them actually calls anything.
|
|
163
|
+
for ref in $(sed 's/^[[:space:]]*#.*//' "$s" | grep -oE "$SIB_PAT" 2>/dev/null | sed 's|.*/||' | sort -u); do
|
|
164
|
+
sib_refs=$((sib_refs+1))
|
|
165
|
+
# Only assert on siblings that EXIST in the repo. A reference to a name that is absent from
|
|
166
|
+
# disk too is a different defect (a dead call), and it belongs to the dangling check above —
|
|
167
|
+
# claiming it here would report one defect as two.
|
|
168
|
+
[ -f "scripts/$ref" ] || continue
|
|
169
|
+
if node -e 'const f=JSON.parse(process.argv[1]);process.exit(f.includes(process.argv[2])?0:1)' \
|
|
170
|
+
"$FILES_JSON" "scripts/$ref"; then :; else
|
|
171
|
+
echo " SIBLING-MISSING scripts/$ref (called by $s at runtime, absent from files[])"
|
|
172
|
+
sib_missing=$((sib_missing+1))
|
|
173
|
+
fi
|
|
174
|
+
done
|
|
175
|
+
done <<EOF
|
|
176
|
+
$SHIPPED_SH
|
|
177
|
+
EOF
|
|
178
|
+
fi
|
|
179
|
+
|
|
180
|
+
# Discovery-zero is an instrument failure here for the same reason as above: this repo is KNOWN to
|
|
181
|
+
# contain at least one self-directory sibling call (that is why this block exists). Zero means the
|
|
182
|
+
# pattern stopped matching — a silent scanner, not a clean repo.
|
|
183
|
+
if [ "$sib_refs" -eq 0 ]; then
|
|
184
|
+
bad "INSTRUMENT DEAD — zero self-directory sibling references found across $(printf '%s\n' "$SHIPPED_SH" | grep -c .) shipped scripts; the pattern is no longer matching anything"
|
|
185
|
+
elif [ "$sib_missing" -eq 0 ]; then
|
|
186
|
+
ok "all $sib_refs runtime sibling dependenc(ies) of shipped scripts are themselves in files[]"
|
|
187
|
+
else
|
|
188
|
+
bad "$sib_missing runtime sibling dependenc(ies) absent from files[] — the consumer gets a caller whose callee is missing"
|
|
189
|
+
fi
|
|
190
|
+
|
|
121
191
|
# CONTROL — the check must be able to say NO. A checker that only ever prints ok is indistinguishable
|
|
122
192
|
# from a checker that is not looking; assert the negative arm on a name that cannot be in files[].
|
|
123
193
|
if node -e 'const f=JSON.parse(process.argv[1]);process.exit(f.includes(process.argv[2])?0:1)' \
|
|
@@ -782,6 +782,105 @@ else
|
|
|
782
782
|
echo " ✅ PROV-2 line tracks the resolution when it changes (shadowed → different value)"; pass=$((pass+1))
|
|
783
783
|
fi
|
|
784
784
|
|
|
785
|
+
# ---- RC: --require-class — narrowing the verdict from the FILE to ONE CLASS ---------
|
|
786
|
+
# Added 2026-08-16 after a pre-publish security pass found the caller side broken: fh_node_check.sh
|
|
787
|
+
# gated a consumer-machine `git merge --ff-only` on this script's FILE-WIDE 0 plus a raw grep for
|
|
788
|
+
# the class name anywhere in the UAP. One unrelated validly-granted class, plus the target class
|
|
789
|
+
# appearing only in a prose line saying it was REVOKED, satisfied both — the merge ran and the
|
|
790
|
+
# banner reported a standing consent that did not exist.
|
|
791
|
+
# Every lane below is a REFUSAL that leaves the file-wide verdict at 0. That property is asserted,
|
|
792
|
+
# not assumed: without it these lanes would pass for the same reason the existing "nothing granted"
|
|
793
|
+
# lanes do, and the class-join axis would go unexercised a second time.
|
|
794
|
+
lane_args() { # desc want_rc [args...] — same as lane() but the flag order is the point
|
|
795
|
+
local desc="$1" want_rc="$2"; shift 2
|
|
796
|
+
bash "$CHK" "$@" >"$TD/o" 2>&1
|
|
797
|
+
local rc=$?
|
|
798
|
+
if [ "$rc" -ne "$want_rc" ]; then
|
|
799
|
+
bad "$desc — exit $rc, expected $want_rc"; sed 's/^/ /' "$TD/o"; return
|
|
800
|
+
fi
|
|
801
|
+
ok "$desc"
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
TWOCLASS="$OKCLASS
|
|
805
|
+
- {name: other, owner: o2, mode: m2, target: t2, capabilities: [read], sinks: [], feeds: [], promotion_eligible: true}"
|
|
806
|
+
|
|
807
|
+
# a) the class IS granted → 0
|
|
808
|
+
mkreg "$TWOCLASS"
|
|
809
|
+
mkuap 'standing_consent:
|
|
810
|
+
ok: {owner: o, mode: m, sinks: [], granted: 2026-07-29, expires: 2026-12-31, effects: [read], target: t}'
|
|
811
|
+
lane_args "RC-a --require-class on a class that IS granted → 0" 0 --require-class ok "$TD/r.yaml" "$TD/u.md"
|
|
812
|
+
|
|
813
|
+
# b) a DIFFERENT class is granted, the target appears only as prose → 3, file-wide still 0
|
|
814
|
+
mkreg "$TWOCLASS"
|
|
815
|
+
printf -- '---\nstanding_consent:\n other: {owner: o2, mode: m2, sinks: [], granted: 2026-07-29, expires: 2026-12-31, effects: [read], target: t2}\n---\n\n# UAP (fixture)\n\n ok: 안 쓰기로 했다 (revoked)\n' > "$TD/u.md"
|
|
816
|
+
bash "$CHK" "$TD/r.yaml" "$TD/u.md" >/dev/null 2>&1; _fw=$?
|
|
817
|
+
lane_args "RC-b another class granted, target only in prose → 3" 3 --require-class ok "$TD/r.yaml" "$TD/u.md"
|
|
818
|
+
[ "$_fw" -eq 0 ] \
|
|
819
|
+
&& ok "RC-b DISCRIMINATION: the file-wide verdict is still 0, so RC-b refused on the class join" \
|
|
820
|
+
|| bad "RC-b VACUOUS: file-wide verdict was $_fw — this lane re-tests an existing axis, not the class join"
|
|
821
|
+
|
|
822
|
+
# c) the ON switch must not be silently OFF. A whitespace-only name passed a `-z` test in the first
|
|
823
|
+
# draft and then strip()ed to empty, disabling the narrowing and returning the file-wide 0.
|
|
824
|
+
lane_args "RC-c whitespace-only class name → 1 (fail-closed, never a silent fall-back)" 1 --require-class " " "$TD/r.yaml" "$TD/u.md"
|
|
825
|
+
lane_args "RC-d --require-class= empty form → 1" 1 --require-class= "$TD/r.yaml" "$TD/u.md"
|
|
826
|
+
|
|
827
|
+
# e) flag position. The first draft read argv[1] only, so a caller appending the flag got the old
|
|
828
|
+
# file-wide 0 with no warning — the failure mode is silence, which is why this is a lane.
|
|
829
|
+
lane_args "RC-e flag AFTER the paths is still honoured → 3" 3 "$TD/r.yaml" "$TD/u.md" --require-class ok
|
|
830
|
+
lane_args "RC-f --require-class=NAME form is honoured → 3" 3 --require-class=ok "$TD/r.yaml" "$TD/u.md"
|
|
831
|
+
|
|
832
|
+
# g) an unknown option must not be swallowed as a path
|
|
833
|
+
lane_args "RC-g unknown option → 1, not consumed as a path" 1 --bogus "$TD/r.yaml" "$TD/u.md"
|
|
834
|
+
|
|
835
|
+
# h) the human summary must agree with the typed exit — this file's own rule, applied to the new path
|
|
836
|
+
bash "$CHK" --require-class ok "$TD/r.yaml" "$TD/u.md" >"$TD/o" 2>&1
|
|
837
|
+
if grep -qE '^consent-registry: UNMEASURED for class `ok`' "$TD/o" && ! grep -qE '^consent-registry: PASS$' "$TD/o"; then
|
|
838
|
+
ok "RC-h summary agrees with the typed exit (no bare PASS above an exit-3 verdict)"
|
|
839
|
+
else
|
|
840
|
+
bad "RC-h summary contradicts the verdict"; sed 's/^/ /' "$TD/o"
|
|
841
|
+
fi
|
|
842
|
+
|
|
843
|
+
# i) no flag → behaviour unchanged. Backward compatibility is a claim, so it gets a lane.
|
|
844
|
+
lane_args "RC-i no flag → file-wide verdict unchanged (0)" 0 "$TD/r.yaml" "$TD/u.md"
|
|
845
|
+
|
|
846
|
+
# j) IDENTITY IS NOT NORMALISED FOR THE CALLER. A third adversarial round measured the first repair
|
|
847
|
+
# squeezing ALL whitespace out of the name, so `--require-class 'o k'` was answered with the
|
|
848
|
+
# verdict for class `ok` — a different question, silently answered. Asking about a name that is
|
|
849
|
+
# not a name is refused, never repaired.
|
|
850
|
+
mkreg "$TWOCLASS"
|
|
851
|
+
mkuap 'standing_consent:
|
|
852
|
+
ok: {owner: o, mode: m, sinks: [], granted: 2026-07-29, expires: 2026-12-31, effects: [read], target: t}'
|
|
853
|
+
lane_args "RC-j interior whitespace in the class name → 1, never folded onto a real class" 1 --require-class "o k" "$TD/r.yaml" "$TD/u.md"
|
|
854
|
+
lane_args "RC-k surrounding whitespace IS trimmed → 0 (trim, not squeeze)" 0 --require-class " ok " "$TD/r.yaml" "$TD/u.md"
|
|
855
|
+
|
|
856
|
+
# l) argv preservation. The first repair rebuilt "$@" through a newline-delimited string, which split
|
|
857
|
+
# a path containing a newline and DROPPED an empty positional — the drop promoted the UAP path
|
|
858
|
+
# into the registry slot, i.e. the tool silently measured a different pair of files than it was
|
|
859
|
+
# handed. Both are exotic inputs; both are silent, which is why they are lanes.
|
|
860
|
+
_nlreg="$TD/reg
|
|
861
|
+
two.yaml"
|
|
862
|
+
cp "$TD/r.yaml" "$_nlreg" 2>/dev/null && lane_args "RC-l a registry path containing a newline is one argument" 0 --require-class ok "$_nlreg" "$TD/u.md" \
|
|
863
|
+
|| ok "RC-l SKIPPED — this filesystem rejected a newline in a filename (not a defect)"
|
|
864
|
+
# RC-m asserts on WHICH registry path the tool reports, not on a success marker. The first version
|
|
865
|
+
# grepped for `R1/R2`, which only appears when the DEFAULT registry parses — and that default is
|
|
866
|
+
# `tracks/_meta/consent_classes.yaml`, which is **gitignored**, so it exists on the author's machine
|
|
867
|
+
# and not in CI. It passed locally and went red on the first CI run: a lane that measured the
|
|
868
|
+
# author's filesystem rather than the behaviour. This form works in both.
|
|
869
|
+
# Calibrated: the control proves the reported path tracks argv[1] at all, so the assertion below is
|
|
870
|
+
# a discrimination result and not a grep that could never match.
|
|
871
|
+
bash "$CHK" "/nonexistent-registry-probe.yaml" "$TD/u.md" >"$TD/oc" 2>&1
|
|
872
|
+
if grep -q "nonexistent-registry-probe.yaml" "$TD/oc"; then
|
|
873
|
+
ok "RC-m CONTROL: the reported registry path tracks argv[1]"
|
|
874
|
+
else
|
|
875
|
+
bad "RC-m CONTROL dead: the tool did not name the registry it was given — the assertion below proves nothing"; sed 's/^/ /' "$TD/oc"
|
|
876
|
+
fi
|
|
877
|
+
bash "$CHK" "" "$TD/u.md" >"$TD/o" 2>&1
|
|
878
|
+
if grep -qE "registry (at|unparseable).*$(basename "$TD")/u\.md" "$TD/o"; then
|
|
879
|
+
bad "RC-m empty positional shifted the argument list — the UAP was read as the registry"; sed 's/^/ /' "$TD/o"
|
|
880
|
+
else
|
|
881
|
+
ok "RC-m an empty first positional does not promote the UAP into the registry slot"
|
|
882
|
+
fi
|
|
883
|
+
|
|
785
884
|
echo "----"
|
|
786
885
|
echo "consent-registry anchor: $pass passed, $fail failed"
|
|
787
886
|
[ "$fail" -eq 0 ] || exit 1
|
|
@@ -216,6 +216,80 @@ else
|
|
|
216
216
|
printf '%s\n' "$out" | sed 's/^/ │ /'
|
|
217
217
|
fi
|
|
218
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
|
+
|
|
219
293
|
echo "----"
|
|
220
294
|
echo "lane-runner lanes: $pass passed, $fail failed"
|
|
221
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
|