@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.
- package/.claude-plugin/marketplace.json +2 -2
- package/CATALOG.md +19 -0
- package/CHEATSHEET.md +9 -1
- package/CLAUDE.md +28 -2
- package/README.ja.md +229 -42
- package/README.ko.md +241 -45
- package/README.md +168 -31
- package/README.zh.md +219 -40
- package/docs/OUTPUT_EVIDENCE.md +21 -12
- package/docs/pillars.svg +3 -7
- package/knowledge/shared/harness-core/fh_ecosystem_positioning.md +2 -0
- package/knowledge/shared/harness-core/fh_global_positioning_and_distribution_roadmap.md +136 -0
- package/knowledge/shared/harness-core/fh_three_layer_canon.md +20 -0
- package/knowledge/shared/harness-core/field_verdict_crossfamily_gate.md +215 -2
- package/knowledge/shared/harness-core/ship_readiness_gate.md +112 -0
- package/knowledge/shared/learnings/subagent_invocations_log.yaml +65 -0
- package/package.json +5 -1
- package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-commons/skills/ko-tech-writer/SKILL.md +63 -12
- package/plugins/fh-meta/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-meta/CHANGELOG.md +166 -0
- package/plugins/fh-meta/skills/auto-decorrelation/SKILL.md +30 -0
- package/scripts/consent_registry_check.sh +124 -1
- package/scripts/degrade_direction_scan.sh +10 -1
- package/scripts/digest_landing_check.sh +20 -4
- package/scripts/fh_node_check.sh +128 -1
- package/scripts/fh_session_load.sh +22 -2
- package/scripts/frontier_digest_autopilot.sh +229 -0
- package/scripts/lane_runner_check.sh +294 -26
- package/scripts/package_coverage_check.sh +17 -0
- package/scripts/postinstall_notice.js +34 -0
- package/scripts/selfcheck.sh +183 -5
- package/scripts/test_consent_registry.sh +99 -0
- package/scripts/test_degrade_scan_shell_probes.sh +75 -0
- package/scripts/test_field_canon_lanes.sh +29 -5
- package/scripts/test_lane_runner_lanes.sh +295 -0
- package/scripts/test_node_check_lanes.sh +217 -0
- package/scripts/test_selfcheck_state_lanes.sh +61 -0
- package/scripts/test_stale_clone_guard_lanes.sh +21 -7
- package/scripts/test_version_lockstep_lanes.sh +62 -0
- package/scripts/version_lockstep_check.sh +143 -1
- package/templates/.git-hooks/pre-commit +22 -1
- package/templates/consent_classes.yaml.example +30 -0
- package/templates/degrade_direction_scan.sh +10 -1
package/scripts/selfcheck.sh
CHANGED
|
@@ -413,7 +413,9 @@ for _pair in \
|
|
|
413
413
|
"templates/.git-hooks/pre-commit|scripts/test_marker_crossfamily_lanes.sh" \
|
|
414
414
|
"templates/.git-hooks/pre-commit|scripts/test_marker_floor_lanes.sh" \
|
|
415
415
|
"scripts/residency_closure_scan.py|scripts/test_residency_closure_lanes.sh" \
|
|
416
|
-
"scripts/reviewer_capability_corpus.tsv|scripts/test_reviewer_capability_conformance.sh"
|
|
416
|
+
"scripts/reviewer_capability_corpus.tsv|scripts/test_reviewer_capability_conformance.sh" \
|
|
417
|
+
"scripts/field_canon_preload.sh|scripts/test_field_canon_lanes.sh" \
|
|
418
|
+
"scripts/stale_clone_guard.sh|scripts/test_stale_clone_guard_lanes.sh"
|
|
417
419
|
do
|
|
418
420
|
_subj="${_pair%%|*}"; _anc="${_pair##*|}"; _lbl="${_anc##*/}"
|
|
419
421
|
if [ ! -f "$_subj" ]; then
|
|
@@ -466,14 +468,24 @@ do
|
|
|
466
468
|
fi
|
|
467
469
|
done
|
|
468
470
|
|
|
469
|
-
# embedded --self-test suites (compaction_probe · judgment_circuit_lint · novelty_claim_check
|
|
470
|
-
#
|
|
471
|
-
#
|
|
471
|
+
# embedded --self-test suites (compaction_probe · judgment_circuit_lint · novelty_claim_check ·
|
|
472
|
+
# chamber_witness · digest_landing_check).
|
|
473
|
+
# These carry their lanes INSIDE the script (`--self-test`) rather than in a sibling
|
|
474
|
+
# test_*_lanes.sh, so the name-list wiring above skipped them silently: lanes existed and ran
|
|
472
475
|
# only when a human typed the command. That is built-but-not-wired applied to the anchors themselves
|
|
473
476
|
# — a later edit that breaks a lane stays green everywhere the project actually checks
|
|
474
477
|
# (high re-review 2026-08-08). Same shape as the block above: subject absent → SKIP, subject present
|
|
475
478
|
# but self-test missing → FAIL, never a silent pass.
|
|
476
|
-
|
|
479
|
+
# chamber_witness/digest_landing_check joined 2026-08-15 (DEBT closed — lane_runner_check.sh
|
|
480
|
+
# flagged 4 undeclared self-test subjects; capability_registry_check and relay_channel are the
|
|
481
|
+
# other two and do NOT join this loop, each for its own reason, see their own blocks below).
|
|
482
|
+
# capability_registry_check was tried here first and reverted: its passing-run output never
|
|
483
|
+
# contains 캘리브레이션 as a VERDICT — the only occurrence is inside a Korean *test-case title*
|
|
484
|
+
# ("M4: 캘리브레이션 쌍 미선언", capability_registry_check.sh's own self-test), so a future rename
|
|
485
|
+
# or trim of that one test name would flip a real PASS into "dispatcher missing?" here (cross-family
|
|
486
|
+
# review caught this, 2026-08-15). chamber_witness/digest_landing_check both print a genuine
|
|
487
|
+
# terminal "캘리브레이션 통과/실패" line, so they stay.
|
|
488
|
+
for _subj in compaction_probe judgment_circuit_lint novelty_claim_check chamber_witness digest_landing_check; do
|
|
477
489
|
if [ ! -f "scripts/$_subj.sh" ]; then
|
|
478
490
|
_absent_subject_verdict "$_subj --self-test" "scripts/$_subj.sh" || fail=1
|
|
479
491
|
else
|
|
@@ -500,6 +512,18 @@ for _subj in compaction_probe judgment_circuit_lint novelty_claim_check; do
|
|
|
500
512
|
# nothing reads the announcement. Fixed by deleting one word.
|
|
501
513
|
_to=""; command -v timeout >/dev/null 2>&1 && _to="timeout 120"
|
|
502
514
|
_st_out="$($_to bash "scripts/$_subj.sh" --self-test < /dev/null 2>&1)"; _st_rc=$?
|
|
515
|
+
# rc=10 (or the sibling harness-error codes) means the subject could not even set up its own
|
|
516
|
+
# known-pair fixture (e.g. mktemp failed) — it never got to run a lane, so it never printed
|
|
517
|
+
# 캘리브레이션 either way. Falling through to the *) arm below would report the confident but
|
|
518
|
+
# WRONG cause "dispatcher missing?" for an instrument that was never reached. chamber_witness.sh
|
|
519
|
+
# and digest_landing_check.sh both have real mktemp-failure → return 10 paths (cross-family
|
|
520
|
+
# review 2026-08-15); the two original loop members never did, so this arm is a no-op for them.
|
|
521
|
+
case "$_st_rc" in
|
|
522
|
+
10|2|126|127)
|
|
523
|
+
echo "HARNESS ERROR $_subj --self-test: the suite exited $_st_rc — it could not measure,"
|
|
524
|
+
echo " so its verdicts prove nothing about $_subj. Not a lane failure, not a pass either."
|
|
525
|
+
fail=1; continue ;;
|
|
526
|
+
esac
|
|
503
527
|
case "$_st_out" in
|
|
504
528
|
*캘리브레이션*) : ;;
|
|
505
529
|
*) echo "FAIL $_subj: --self-test produced no calibration verdict (dispatcher missing?)"
|
|
@@ -514,6 +538,122 @@ for _subj in compaction_probe judgment_circuit_lint novelty_claim_check; do
|
|
|
514
538
|
fi
|
|
515
539
|
done
|
|
516
540
|
|
|
541
|
+
# capability_registry_check — one of the 4 embedded --self-test subjects lane_runner_check.sh
|
|
542
|
+
# flagged (2026-08-15), kept out of the loop above (see that loop's comment) because its own
|
|
543
|
+
# terminal line is `통과 N · 실패 N`, never 캘리브레이션 — gate on exit code instead, same shape
|
|
544
|
+
# as utterance_landing_check.sh below. Ships via package.json files[], so absence is FAIL, not SKIP.
|
|
545
|
+
if [ ! -f scripts/capability_registry_check.sh ]; then
|
|
546
|
+
echo "FAIL capability_registry_check.sh: missing — it ships via package.json files[], so absence is deletion, not package mode"
|
|
547
|
+
fail=1
|
|
548
|
+
elif _out=$(bash scripts/capability_registry_check.sh --self-test < /dev/null 2>&1); then
|
|
549
|
+
echo "PASS capability_registry_check.sh --self-test ($(printf '%s\n' "$_out" | grep -oE '통과 [0-9]+ · 실패 [0-9]+' | tail -1))"
|
|
550
|
+
else
|
|
551
|
+
echo "FAIL capability_registry_check.sh: --self-test failed"
|
|
552
|
+
_show_failure "$_out"
|
|
553
|
+
fail=1
|
|
554
|
+
fi
|
|
555
|
+
|
|
556
|
+
# relay_channel — the other embedded --self-test subject kept out of the loop above: its
|
|
557
|
+
# `--self-test` just `exec`s the sibling scripts/test_relay_channel_lanes.sh, whose own exit code
|
|
558
|
+
# is already disciplined (`[ "$fail" -eq 0 ] || exit 1`), so gating on exit code — same shape as
|
|
559
|
+
# utterance_landing_check.sh below — is both sufficient and correct here; a 캘리브레이션 substring
|
|
560
|
+
# check would be the wrong instrument for this subject's actual completion marker
|
|
561
|
+
# ("N PASS / M FAIL"). Ships via package.json files[] (no ACCEPTED_ABSENT declaration), so absence
|
|
562
|
+
# is FAIL, not SKIP — same reasoning as utterance_landing_check.sh's absence branch below.
|
|
563
|
+
# Existence check covers BOTH files: relay_channel.sh --self-test just execs the sibling lanes
|
|
564
|
+
# file, so a missing sibling alone produces an unrelated-looking `exec: No such file` (rc=127) that
|
|
565
|
+
# the generic FAIL branch would misreport as "the lane broke" rather than "the anchor is gone"
|
|
566
|
+
# (cross-family review 2026-08-15).
|
|
567
|
+
# `_LANE_TO` (defined above, the pair-suite loop's own guard) is reused rather than a fresh
|
|
568
|
+
# timeout var — this is, by lane count, the largest suite dispatched anywhere in this file
|
|
569
|
+
# (test_relay_channel_lanes.sh runs 45+ lanes, each spawning relay_channel.sh, each spawning its
|
|
570
|
+
# own node subprocess), and it was the one dispatch here with NO time bound at all; `< /dev/null`
|
|
571
|
+
# only stops a stdin-wait hang, not a runaway child (cross-family review 2026-08-15 — this file's
|
|
572
|
+
# own comment two loops up already names exactly this failure shape: an unguarded call does not go
|
|
573
|
+
# red, it hangs, and CI dies on a job timeout with the cause unattributable).
|
|
574
|
+
if [ ! -f scripts/relay_channel.sh ] || [ ! -f scripts/test_relay_channel_lanes.sh ]; then
|
|
575
|
+
echo "FAIL relay_channel.sh: missing (relay_channel.sh or its sibling test_relay_channel_lanes.sh) — ships via package.json files[], so absence is deletion, not package mode"
|
|
576
|
+
fail=1
|
|
577
|
+
elif _out=$($_LANE_TO bash scripts/relay_channel.sh --self-test < /dev/null 2>&1); then
|
|
578
|
+
echo "PASS relay_channel.sh --self-test ($(printf '%s\n' "$_out" | grep -oE 'relay_channel lanes: [0-9]+ PASS / [0-9]+ FAIL' | tail -1))"
|
|
579
|
+
else
|
|
580
|
+
echo "FAIL relay_channel.sh: --self-test failed"
|
|
581
|
+
_show_failure "$_out"
|
|
582
|
+
fail=1
|
|
583
|
+
fi
|
|
584
|
+
|
|
585
|
+
# directional_diff_gate — the LAST embedded --self-test subject with no caller, and it stayed
|
|
586
|
+
# invisible for a reason worth keeping written down: lane_runner_check.sh reported it WIRED off the
|
|
587
|
+
# subject's OWN usage comment (`# bash scripts/directional_diff_gate.sh --self-test`), so the
|
|
588
|
+
# report read `self-test: 10/10 wired` while `grep -rn directional_diff_gate` across every runner
|
|
589
|
+
# surface returned zero real callers. The detector was repaired in the same delta as this wiring
|
|
590
|
+
# (has_selftest_runner now skips the subject's own file, selftest_dispatched now drops comment
|
|
591
|
+
# lines — the two guards has_runner/runner_dispatches always had), which is why the two halves ship
|
|
592
|
+
# together: fixing the detector alone would have left a real debt item newly VISIBLE and still
|
|
593
|
+
# unrun, and wiring alone would have left the detector able to certify the next such subject.
|
|
594
|
+
# ⚠️ The cost of shipping both halves at once, named rather than left for the next reader to
|
|
595
|
+
# discover: the report reads `10/10` before the change and `10/10` after it, so the detector repair
|
|
596
|
+
# is NOT observable from this repo's own run — reverting the repair alone leaves the count
|
|
597
|
+
# unchanged, because the wiring that now exists is real. Its only evidence is the fixture lanes
|
|
598
|
+
# L14–L16 in scripts/test_lane_runner_lanes.sh, which is why L16 in particular is load-bearing:
|
|
599
|
+
# delete it and the self-exclusion guard has nothing anywhere that would go red.
|
|
600
|
+
#
|
|
601
|
+
# NOT in the `for _subj in ...` loop above, and that is the same call made for
|
|
602
|
+
# capability_registry_check: that loop gates on the substring 캘리브레이션, and this subject's
|
|
603
|
+
# terminal verdict is English ("✅ calibration passed (N pairs)"). Adding it there would produce
|
|
604
|
+
# "dispatcher missing?" — a confident and wrong cause — for a suite that passes. Gate on exit code
|
|
605
|
+
# instead, same shape as the two blocks above. It carries genuine HARNESS-ERROR paths (`exit 10` on
|
|
606
|
+
# mktemp failure, `return 10` on unreadable input), so a setup failure must not read as a lane
|
|
607
|
+
# failure. Ships via package.json files[] with no ACCEPTED_ABSENT declaration, so absence is
|
|
608
|
+
# deletion, not package mode. `< /dev/null` for the same stdin-wait reason the loop above documents.
|
|
609
|
+
if [ ! -f scripts/directional_diff_gate.sh ]; then
|
|
610
|
+
echo "FAIL directional_diff_gate.sh: missing — it ships via package.json files[], so absence is deletion, not package mode"
|
|
611
|
+
fail=1
|
|
612
|
+
else
|
|
613
|
+
_out=$($_LANE_TO bash scripts/directional_diff_gate.sh --self-test < /dev/null 2>&1); _dd_rc=$?
|
|
614
|
+
case "$_dd_rc" in
|
|
615
|
+
10|2|124|126|127)
|
|
616
|
+
# 124 is `timeout`'s own kill code, so it belongs in this arm rather than the generic FAIL
|
|
617
|
+
# one below: a suite that was killed did not measure, and calling that "the lane failed"
|
|
618
|
+
# sends the reader to the wrong file. Reachable only because this dispatch is wrapped in
|
|
619
|
+
# `$_LANE_TO` — which is REUSED from the pair-suite loop above, not installed here, so do
|
|
620
|
+
# not delete it from this line believing the timeout is local to this block.
|
|
621
|
+
echo "HARNESS ERROR directional_diff_gate.sh --self-test: exited $_dd_rc — it could not measure,"
|
|
622
|
+
echo " so its verdicts prove nothing. Not a lane failure, and not a pass either."
|
|
623
|
+
fail=1 ;;
|
|
624
|
+
0)
|
|
625
|
+
# rc=0 is NOT sufficient, and this is not a hypothetical: the `for _subj in ...` loop above
|
|
626
|
+
# records the measured case where a subject fell into an unrecognized mode, printed its usage
|
|
627
|
+
# banner and exited 0, and the checker read that as a pass while 25 lanes had vanished. This
|
|
628
|
+
# subject happens to exit 10 from that path TODAY — a revert probe (2026-08-15: dispatcher
|
|
629
|
+
# line deleted) confirmed the HARNESS ERROR arm fires — but that is incidental to how its
|
|
630
|
+
# usage function is written, not a property this block should depend on. Require the
|
|
631
|
+
# terminal verdict itself, so a future subject that exits 0 without running anything goes red
|
|
632
|
+
# here rather than certifying an empty run.
|
|
633
|
+
# Whole-LINE match, not a substring, and the pair count must be non-zero. Both halves were
|
|
634
|
+
# named by cross-family review (2026-08-15) and both are reachable in the real subject, not
|
|
635
|
+
# hypothetical: directional_diff_gate.sh's verdict line is
|
|
636
|
+
# `[ "$f" -eq 0 ] && echo "✅ calibration passed ($n pairs)"` — delete every `t` lane and it
|
|
637
|
+
# prints `calibration passed (0 pairs)` and exits 0, i.e. a suite with nothing left in it
|
|
638
|
+
# certifies itself. That is the quietest face of [[feedback_not_found_is_not_zero_family]]
|
|
639
|
+
# (deletion read as a pass), and a substring match would also let a usage banner or a prose
|
|
640
|
+
# line carrying the same words satisfy the gate.
|
|
641
|
+
_dd_verdict=$(printf '%s\n' "$_out" | grep -oE '^✅ calibration passed \([0-9]+ pairs\)$' | tail -1)
|
|
642
|
+
_dd_pairs=$(printf '%s' "$_dd_verdict" | grep -oE '[0-9]+' | tail -1)
|
|
643
|
+
if [ -n "$_dd_verdict" ] && [ "${_dd_pairs:-0}" -gt 0 ]; then
|
|
644
|
+
echo "PASS directional_diff_gate.sh --self-test ($_dd_verdict)"
|
|
645
|
+
else
|
|
646
|
+
echo "FAIL directional_diff_gate.sh: --self-test exited 0 without a non-zero calibration verdict (dispatcher missing, or every lane deleted?)"
|
|
647
|
+
_show_failure "$_out"
|
|
648
|
+
fail=1
|
|
649
|
+
fi ;;
|
|
650
|
+
*)
|
|
651
|
+
echo "FAIL directional_diff_gate.sh: --self-test failed (exit $_dd_rc)"
|
|
652
|
+
_show_failure "$_out"
|
|
653
|
+
fail=1 ;;
|
|
654
|
+
esac
|
|
655
|
+
fi
|
|
656
|
+
|
|
517
657
|
# memory-link-check — the memory store is a GRAPH (memory_intent_recall.md: nodes=files,
|
|
518
658
|
# edges=[[links]], recall walks one hop). Measured 2026-07-28: 50 of 872 edges pointed at a note
|
|
519
659
|
# that existed under a different separator and 22 at nothing — a dead edge returns nothing and is
|
|
@@ -1001,6 +1141,23 @@ if [ -f scripts/test_selfcheck_state_lanes.sh ]; then
|
|
|
1001
1141
|
fi
|
|
1002
1142
|
fi
|
|
1003
1143
|
|
|
1144
|
+
# test_lane_runner_lanes.sh — the checker that finds unrun suites had no suite of its own until
|
|
1145
|
+
# 2026-08-13. It guarded its own WIRING (it fails when selfcheck stops calling it) and nothing
|
|
1146
|
+
# measured its BEHAVIOUR — the shape it exists to catch, one level up. Wired in the same change
|
|
1147
|
+
# that ships it, for the reason the block above states: a suite landed unwired is the defect.
|
|
1148
|
+
if [ -f scripts/test_lane_runner_lanes.sh ]; then
|
|
1149
|
+
if _out=$(bash scripts/test_lane_runner_lanes.sh 2>&1); then
|
|
1150
|
+
echo "PASS test_lane_runner_lanes.sh (org seam: no-op · suppression · fail-closed · controls)"
|
|
1151
|
+
else
|
|
1152
|
+
echo "FAIL test_lane_runner_lanes.sh: the unrun-suite detector's own verdicts have drifted"
|
|
1153
|
+
_show_failure "$_out"
|
|
1154
|
+
fail=1
|
|
1155
|
+
fi
|
|
1156
|
+
elif _ships_per_files "scripts/test_lane_runner_lanes.sh"; then
|
|
1157
|
+
echo "FAIL test_lane_runner_lanes.sh is DECLARED SHIPPED but absent — deletion or broken install"
|
|
1158
|
+
fail=1
|
|
1159
|
+
fi
|
|
1160
|
+
|
|
1004
1161
|
# sync_from_be_lanes.sh — the RETURN path's anchor. Wired in the same change that ships it: the
|
|
1005
1162
|
# script had an operator-side caller (a SessionStart hook outside this repo) while its 70 lanes had
|
|
1006
1163
|
# NO caller anywhere, which is the shape this repo keeps re-finding — a transport that writes into
|
|
@@ -1044,6 +1201,27 @@ else
|
|
|
1044
1201
|
fail=1
|
|
1045
1202
|
fi
|
|
1046
1203
|
|
|
1204
|
+
# sync_to_be_lanes.sh — the FORWARD path's anchor (added 2026-08-14, pmh-dev#69). Same shape as the
|
|
1205
|
+
# return-path block above and wired in the same change that ships it, for the same reason: a
|
|
1206
|
+
# transport with real reported defects (a real add/add conflict, a real hard-abort risk) had no
|
|
1207
|
+
# suite exercising it at all before this. Same subject-presence idiom, same run-once-capture
|
|
1208
|
+
# discipline (a lane suite discarded to /dev/null on a non-deterministic run already burned two days
|
|
1209
|
+
# on this exact file's sibling).
|
|
1210
|
+
if [ ! -f scripts/sync-to-be.sh ]; then
|
|
1211
|
+
echo "SKIP sync_to_be_lanes.sh (subject scripts/sync-to-be.sh absent)"
|
|
1212
|
+
elif [ -f scripts/sync_to_be_lanes.sh ]; then
|
|
1213
|
+
if _out=$(bash scripts/sync_to_be_lanes.sh 2>&1); then
|
|
1214
|
+
echo "PASS sync_to_be_lanes.sh (forward-path lanes)"
|
|
1215
|
+
else
|
|
1216
|
+
echo "FAIL sync_to_be_lanes.sh: forward-path lanes failed"
|
|
1217
|
+
_show_failure "$_out"
|
|
1218
|
+
fail=1
|
|
1219
|
+
fi
|
|
1220
|
+
else
|
|
1221
|
+
echo "FAIL sync_to_be_lanes.sh: sync-to-be.sh present but its anchor is missing"
|
|
1222
|
+
fail=1
|
|
1223
|
+
fi
|
|
1224
|
+
|
|
1047
1225
|
# Referenced-path existence is a source-tree check. The npm package intentionally
|
|
1048
1226
|
# ships a narrower runtime surface, so package-mode selfcheck skips this section.
|
|
1049
1227
|
# ⚠️ The skip predicate used to be `[ -d ".claude/rules" ]`. That broke the moment the tarball
|
|
@@ -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
|
|
@@ -630,6 +630,81 @@ else
|
|
|
630
630
|
bad "PY3 mixed-language markdown lost one language — shadows collide by name or the loop stops at the first hit"
|
|
631
631
|
fi
|
|
632
632
|
|
|
633
|
+
# ── SCOPE: the scanner must HONOR its arguments ────────────────────────────────────────────────
|
|
634
|
+
# This is the contract templates/.git-hooks/pre-commit depends on since 2026-08-13. That hook used
|
|
635
|
+
# to invoke this scanner with NO ARGUMENTS — walking the whole repository — and then discard almost
|
|
636
|
+
# all of it through a `grep -Ff` over the staged load-bearing files it already had in hand.
|
|
637
|
+
# Measured: whole-repo 42s / 260 lines vs one staged file 0s / 2 lines; a sibling harness measured
|
|
638
|
+
# the same call at 2m07s inside a 7m54s–9m40s commit, one of which could not complete at all.
|
|
639
|
+
# The lane exists because the FIX IS SILENTLY REVERSIBLE: if this scanner ever stops honoring its
|
|
640
|
+
# arguments, the hook keeps working and simply becomes slow again — no verdict changes, nothing
|
|
641
|
+
# goes red, and the only symptom is a commit people start bypassing with --no-verify. The same hook
|
|
642
|
+
# carries the Destructive-Op gate, so a trained bypass disarms an irreversible-surface gate too.
|
|
643
|
+
# BEHAVIOURAL, not shape: it asserts the OUTPUT is confined to the named file, which is the property
|
|
644
|
+
# the hook relies on. A grep for "does the hook pass an argument" would pass on a scanner that
|
|
645
|
+
# accepts arguments and ignores them.
|
|
646
|
+
_scope_a="$TMP/scope_a.sh"; _scope_b="$TMP/scope_b.sh"
|
|
647
|
+
printf '#!/usr/bin/env bash\ncheck() { probe || return 0; }\n' > "$_scope_a"
|
|
648
|
+
printf '#!/usr/bin/env bash\nverify() { thing || exit 0; }\n' > "$_scope_b"
|
|
649
|
+
out=$(bash "$SCAN" "$_scope_a" 2>&1)
|
|
650
|
+
if printf '%s' "$out" | grep -q 'scope_a' && ! printf '%s' "$out" | grep -q 'scope_b'; then
|
|
651
|
+
ok "SCOPE1 a named target is scanned and a sibling file is NOT (the scanner honors argv)"
|
|
652
|
+
else
|
|
653
|
+
bad "SCOPE1 scanner did not confine itself to the named target — the pre-commit scoping fix is void"
|
|
654
|
+
fi
|
|
655
|
+
# The negative arm: with no argument at all it must NOT confine itself to nothing. Without this,
|
|
656
|
+
# a scanner that returned empty for every input would satisfy SCOPE1 and the lane would be theatre.
|
|
657
|
+
out=$(bash "$SCAN" "$TMP" 2>&1)
|
|
658
|
+
if printf '%s' "$out" | grep -q 'scope_a' && printf '%s' "$out" | grep -q 'scope_b'; then
|
|
659
|
+
ok "SCOPE2 control: given the containing directory it finds BOTH (it is not simply blind)"
|
|
660
|
+
else
|
|
661
|
+
bad "SCOPE2 control failed — the scanner found neither file, so SCOPE1 proved nothing"
|
|
662
|
+
fi
|
|
663
|
+
|
|
664
|
+
# ── C2: bare X-in-Y substring boolean, with the ALL-CAPS exclusion (2026-08-14) ────────────────
|
|
665
|
+
# Cross-family review found this exclusion shipped with zero fixtures anywhere in this repo —
|
|
666
|
+
# deleting the exclusion left every existing lane green, which is exactly the "advisory noise
|
|
667
|
+
# reduction with no anchor" class this repo's own 4-axis doctrine requires a mechanical test for.
|
|
668
|
+
# Two arms: the probe's own headline example (paid⊂prepaid style substring check) must still
|
|
669
|
+
# fire, and an ALL-CAPS collection-membership check — the shape the exclusion targets — must not.
|
|
670
|
+
_c2_pos="$TMP/c2_pos.sh"; _c2_neg="$TMP/c2_neg.sh"
|
|
671
|
+
printf 'if tok in text:\n pass\n' > "$_c2_pos"
|
|
672
|
+
printf 'if name in PLACEHOLDERS:\n pass\n' > "$_c2_neg"
|
|
673
|
+
out=$(bash "$SCAN" "$_c2_pos" 2>&1)
|
|
674
|
+
if printf '%s' "$out" | grep -q 'C2:substring-boolean'; then
|
|
675
|
+
ok "C2-KP substring-boolean on a bare string var still fires"
|
|
676
|
+
else
|
|
677
|
+
bad "C2-KP the probe's own headline shape (tok in text) no longer fires"
|
|
678
|
+
fi
|
|
679
|
+
out=$(bash "$SCAN" "$_c2_neg" 2>&1)
|
|
680
|
+
if ! printf '%s' "$out" | grep -q 'C2:substring-boolean'; then
|
|
681
|
+
ok "C2-KN ALL-CAPS collection membership (name in PLACEHOLDERS) stays silent"
|
|
682
|
+
else
|
|
683
|
+
bad "C2-KN the ALL-CAPS exclusion is not wired — a legitimate collection check still fires"
|
|
684
|
+
fi
|
|
685
|
+
# Revert probe, run inline rather than trusted by inspection: replace the exclusion clause's line
|
|
686
|
+
# in a COPY of the scanner with a bare closing paren (deleting the line outright would strand the
|
|
687
|
+
# preceding line's trailing backslash continuation and break the pipe chain) and confirm C2-KN
|
|
688
|
+
# flips to a hit — proves the exclusion is load-bearing for this specific fixture, not just
|
|
689
|
+
# present somewhere in the file.
|
|
690
|
+
_scan_reverted="$TMP/scan_reverted.sh"
|
|
691
|
+
_excl_line=$(grep -n "grep -vE 'in \[A-Z_\]" "$SCAN" | head -1 | cut -d: -f1)
|
|
692
|
+
if [ -z "$_excl_line" ]; then
|
|
693
|
+
bad "C2-REVERT could not locate the exclusion line by its known text — fixture cannot run"
|
|
694
|
+
else
|
|
695
|
+
sed "${_excl_line}s/.*/ )/" "$SCAN" > "$_scan_reverted"
|
|
696
|
+
chmod +x "$_scan_reverted"
|
|
697
|
+
# Captured, not piped directly — under `set -o pipefail` (this file's own top line) the scanner's
|
|
698
|
+
# own advisory exit code (non-zero on any finding) would override grep's match result in a direct
|
|
699
|
+
# pipe, reporting FAIL even when the fixture worked exactly as intended.
|
|
700
|
+
_revert_out=$(bash "$_scan_reverted" "$_c2_neg" 2>&1)
|
|
701
|
+
if printf '%s' "$_revert_out" | grep -q 'C2:substring-boolean'; then
|
|
702
|
+
ok "C2-REVERT removing the exclusion line makes C2-KN fire again (the exclusion is load-bearing)"
|
|
703
|
+
else
|
|
704
|
+
bad "C2-REVERT removing the exclusion line did not flip C2-KN — the fixture proves nothing"
|
|
705
|
+
fi
|
|
706
|
+
fi
|
|
707
|
+
|
|
633
708
|
echo "----"
|
|
634
709
|
echo "degrade-scan shell probes: $pass passed, $fail failed"
|
|
635
710
|
[ "$fail" -eq 0 ] || exit 1
|
|
@@ -12,10 +12,32 @@ trap 'rm -rf "$TMP"' EXIT
|
|
|
12
12
|
PASS=0; FAIL=0
|
|
13
13
|
ok(){ PASS=$((PASS+1)); printf ' ✅ %s\n' "$1"; }
|
|
14
14
|
no(){ FAIL=$((FAIL+1)); printf ' ❌ %s — %s\n' "$1" "$2"; }
|
|
15
|
+
|
|
16
|
+
# ── DEBT fix (2026-08-14) — several lanes below depended on the OPERATOR'S REAL
|
|
17
|
+
# `${HOME}/projects/qasp-dev` mapping to resolve a repo and emit anything at all. On a
|
|
18
|
+
# machine/CI runner without that exact local layout, `repo` resolution in field_canon_preload.sh
|
|
19
|
+
# silently fails (`continue`), `emitted` stays 0, and EVERY assertion that checks "is output
|
|
20
|
+
# non-empty" trivially reads as the WRONG kind of empty — reproduced locally: PASS 12 / FAIL 7 with
|
|
21
|
+
# HOME pointed at an empty dir, exactly the CI split. The fix reuses the fixture shape lane ⑭
|
|
22
|
+
# already demonstrates below (a throwaway hub + a throwaway project root with a real `.git` and
|
|
23
|
+
# README.md) instead of depending on the operator's real filesystem — `found→extend`, not a new
|
|
24
|
+
# mechanism. This also turns ⑧'s prior "PASS" honest: before this fix it passed because repo
|
|
25
|
+
# resolution ALWAYS failed (output always empty regardless of dedup), not because dedup worked
|
|
26
|
+
# ([[feedback_anchor_can_be_decorative]]) — with a real resolvable repo, ⑧ now exercises the actual
|
|
27
|
+
# same-session dedup path.
|
|
28
|
+
FHUB="$TMP/fhub"; mkdir -p "$FHUB/tracks/qasp"
|
|
29
|
+
FPROJ="$TMP/fproj"; mkdir -p "$FPROJ/qasp-dev/.git" "$FPROJ/qasp-dev/docs/governance"
|
|
30
|
+
printf '# qasp fixture\n' > "$FPROJ/qasp-dev/README.md"
|
|
31
|
+
printf 'x\n' > "$FPROJ/qasp-dev/docs/governance/g1.md"
|
|
32
|
+
|
|
15
33
|
# ★`${3:+VAR=v}` 는 파라미터 확장이라 bash 가 **환경 할당으로 안 읽는다** — env 로 넘긴다.
|
|
16
34
|
# (이 파일 초판이 그렇게 써서 「레포 부재」 레인이 거짓 적색을 냈다. 계기 결함이었다.)
|
|
17
|
-
|
|
18
|
-
|
|
35
|
+
# $3, when given, overrides FIELD_CANON_PROJECT_ROOT (the "레포 부재" control at line ~35 uses this
|
|
36
|
+
# to deliberately point at /nonexistent — CLAUDE_PROJECT_DIR still resolves via the fixture so that
|
|
37
|
+
# control lane is testing "repo absent", not accidentally "mapped name absent" too).
|
|
38
|
+
run(){ local root="$FPROJ"; [ -n "${3:-}" ] && root="$3"
|
|
39
|
+
printf '%s' "$1" | env CLAUDE_PROJECT_DIR="$FHUB" FIELD_CANON_PROJECT_ROOT="$root" \
|
|
40
|
+
FIELD_CANON_SENTINEL_DIR="$2" bash "$H" 2>&1; }
|
|
19
41
|
|
|
20
42
|
echo "== field-canon preload 레인 =="
|
|
21
43
|
|
|
@@ -47,12 +69,14 @@ printf '' | FIELD_CANON_SENTINEL_DIR="$TMP/6" bash "$H" >/dev/null 2>&1
|
|
|
47
69
|
# (`[[feedback_anchor_can_be_decorative]]` — 레인이 결함 지점을 비켜 가면 초록은 정보가 아니다.)
|
|
48
70
|
# ⚠️ macOS 는 bash **3.2** 다. `set -u` 아래서 빈 배열 `"${a[@]}"` 는 unbound variable 로 죽는다
|
|
49
71
|
# — 초판 레인이 정확히 그렇게 죽어 ⑦⑨⑩ 이 **거짓 적색**을 냈다(대상이 아니라 계기의 사망).
|
|
50
|
-
# 배열 없이 분기한다.
|
|
72
|
+
# 배열 없이 분기한다. (FHUB/FPROJ fixture — see the DEBT-fix comment near the top of this file.)
|
|
51
73
|
runD(){ # $1=payload $2=TMPDIR [$3=CLAUDE_SESSION_ID 를 환경에 심을지]
|
|
52
74
|
if [ -n "${3:-}" ]; then
|
|
53
|
-
printf '%s' "$1" | env TMPDIR="$2"
|
|
75
|
+
printf '%s' "$1" | env TMPDIR="$2" CLAUDE_PROJECT_DIR="$FHUB" FIELD_CANON_PROJECT_ROOT="$FPROJ" \
|
|
76
|
+
CLAUDE_SESSION_ID="$3" bash "$H" 2>&1
|
|
54
77
|
else
|
|
55
|
-
printf '%s' "$1" | env TMPDIR="$2"
|
|
78
|
+
printf '%s' "$1" | env TMPDIR="$2" CLAUDE_PROJECT_DIR="$FHUB" FIELD_CANON_PROJECT_ROOT="$FPROJ" \
|
|
79
|
+
bash "$H" 2>&1
|
|
56
80
|
fi; }
|
|
57
81
|
T7="$TMP/def"; mkdir -p "$T7"
|
|
58
82
|
|