@chrono-meta/fh-gate 2.0.1 → 2.2.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.
Files changed (37) hide show
  1. package/.claude/rules/fh_4axis_gate.md +77 -1
  2. package/.claude-plugin/marketplace.json +3 -3
  3. package/AGENTS.md +26 -3
  4. package/CLAUDE.md +240 -4
  5. package/knowledge/shared/harness-core/capability_composition_contract.md +68 -0
  6. package/knowledge/shared/harness-core/fh_three_layer_canon.md +38 -12
  7. package/knowledge/shared/harness-core/field_verdict_crossfamily_gate.md +115 -3
  8. package/knowledge/shared/harness-core/harness_incubator_doctrine.md +82 -0
  9. package/knowledge/shared/harness-core/harness_terminal_correlation_and_recommendations.md +85 -10
  10. package/knowledge/shared/harness-core/ship_readiness_gate.md +241 -1
  11. package/knowledge/shared/learnings/subagent_invocations_log.yaml +102 -0
  12. package/knowledge/shared/rules/sister_asset_protocol.md +12 -0
  13. package/package.json +7 -2
  14. package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
  15. package/plugins/fh-commons/agents/quench-challenger.md +6 -1
  16. package/plugins/fh-commons/skills/ko-tech-writer/fixtures/known_negative.md +19 -0
  17. package/plugins/fh-commons/skills/ko-tech-writer/fixtures/known_positive.md +20 -0
  18. package/plugins/fh-meta/.claude-plugin/plugin.json +1 -1
  19. package/plugins/fh-meta/CHANGELOG.md +88 -0
  20. package/plugins/fh-meta/skills/auto-decorrelation/SKILL.md +14 -2
  21. package/plugins/fh-meta/skills/steel-quench/SKILL_detail.md +13 -0
  22. package/plugins/fh-meta/skills/verify-bidirectional/SKILL.md +34 -1
  23. package/scripts/capability_registry_check.sh +75 -12
  24. package/scripts/chamber_run.sh +39 -5
  25. package/scripts/chamber_witness.sh +25 -0
  26. package/scripts/fh-gate.sh +16 -1
  27. package/scripts/ko_tech_writer_calibrate.py +127 -0
  28. package/scripts/package_coverage_check.sh +10 -0
  29. package/scripts/prepublish_scope_note.sh +139 -0
  30. package/scripts/publish_freshness_check.sh +21 -2
  31. package/scripts/selfcheck.sh +32 -6
  32. package/scripts/test_fh_gate_regressions.sh +21 -9
  33. package/scripts/test_ko_tech_writer_lanes.sh +61 -0
  34. package/scripts/test_marker_axes_run_lanes.sh +87 -3
  35. package/scripts/test_marker_crossfamily_lanes.sh +31 -1
  36. package/templates/.git-hooks/pre-commit +225 -10
  37. package/templates/subagent-tally-hook.json +2 -2
@@ -0,0 +1,139 @@
1
+ #!/usr/bin/env bash
2
+ # prepublish_scope_note.sh — the publish gate must test the ARTIFACT, not this machine.
3
+ #
4
+ # ── 왜 이 파일이 있나 (package.json 은 주석을 못 단다) ────────────────────────────
5
+ # 2026-08-17 실측: `prepublishOnly` 체인이 `scripts/selfcheck.sh` 를 통과 조건으로 삼고
6
+ # 있었고, 그 안의 **peer 하네스 어댑터 레인**은 «이 머신에 어떤 레포가 깔려 있는가» 에 따라
7
+ # 판정이 갈린다. 결과:
8
+ #
9
+ # CI (peer 레포 0개) selfcheck 초록 — PEER_ABSENT 로 SKIP → 출하 가능
10
+ # 클러스터를 쓰는 노드 selfcheck 적색 — 어댑터 없는 peer 계기 → 출하 차단
11
+ #
12
+ # **peer 를 안 깐 머신은 출하할 수 있고, 실제로 클러스터를 쓰는 머신은 출하할 수 없다.**
13
+ # 방향이 거꾸로다. 그리고 이 클래스는 같은 날 반대 방향으로도 밟혔다(픽스처가 gitignored
14
+ # 파일을 인용 → 로컬 초록·CI 적색). 뿌리는 하나다:
15
+ #
16
+ # > 검정이 «저장소에 무엇이 있는가» 가 아니라 «이 머신에 무엇이 있는가» 를 재고 있었다.
17
+ #
18
+ # ── 처방 (`fh_signal_2026-08-17_publish-depends-on-local-env.md` ⓑ) ─────────────
19
+ # `prepublishOnly` 는 **출하물의 성질만** 본다:
20
+ # publish_freshness_check · version_lockstep_check · package_coverage --vs-tarball
21
+ # · public_surface_scan_files
22
+ # peer 클러스터 상태는 출하물의 성질이 아니다. 그래서 selfcheck 는 이 체인에서 빠졌다.
23
+ #
24
+ # 🟥 **빠진 것이 «커버리지 상실» 이 되지 않게 하는 것이 이 스크립트의 실제 일이다.**
25
+ # selfcheck 는 여전히 `npm test` 이고, **CI `validate` 잡이 매 푸시마다 돌린다** — 그리고
26
+ # `validate` 는 main 의 **required check** 다(2026-08-12 실측: legacy
27
+ # `required_status_checks.contexts = ["validate"]`). 즉 출하되는 커밋은 이미 selfcheck 를
28
+ # 통과한 커밋이다. 그 전제가 참인지를 **여기서 기계로 확인한다** — 워크플로에서
29
+ # selfcheck 호출이 사라지면 이 스크립트가 출하를 막는다. 산문 약속이 아니라 채널 검사다.
30
+ #
31
+ # ⚠️ **명시 잔여 — 축소해서 읽지 마라. 아래 넷은 cross-family 실측으로 확정된 구멍이다.**
32
+ # ⓐ 이 검사는 «워크플로가 selfcheck 를 호출한다» 는 **존재**를 볼 뿐, 그 잡이 이 커밋에서
33
+ # 실제로 **초록이었는지**는 안 본다. 그건 ⓒ(CI 태그 출하 배선)의 일이고 미구축이다.
34
+ # ⓐ-2 🟥 **`if: false` 로 꺼진 잡은 못 잡는다**(codex 지적 #2, S). 호출 문자열은 살아 있고
35
+ # 잡은 절대 안 돈다 — 이 게이트는 «커버리지 있음» 을 낸다. YAML 파서 없이는 조건식을
36
+ # 평가할 수 없고, 파서를 여기 넣는 것은 이 스크립트의 일이 아니다. **미봉이라 적는다.**
37
+ # ⓐ-3 🟥 **어느 잡인지 안 본다**(codex 지적 #4, A). 위 주석은 `validate` 잡을 말하는데
38
+ # 술어는 **파일 안 아무 잡이나** 받는다. required check 는 `validate` 하나뿐이므로
39
+ # 다른 잡에 옮겨진 selfcheck 는 필수가 아니게 되지만 이 검사는 통과시킨다.
40
+ # ⓐ-4 🟥 **트리거를 안 본다**(codex 지적 #3, A). `on: pull_request` 만 있어도 통과한다 —
41
+ # 그러면 출하되는 main 커밋 자체는 selfcheck 를 한 번도 안 탄다.
42
+ # ⇒ ⓐ-2~4 를 닫는 정직한 형태는 문자열 검사가 아니라 **`gh api` 로 실제 required check 와
43
+ # 그 커밋의 conclusion 을 읽는 것**이다. 그건 네트워크 의존이라 출하 경로에 넣을지가
44
+ # 별도 결정이고(오프라인 fail-closed 는 또 다른 과차단), 여기서 정하지 않는다.
45
+ # ⓑ `validate` 의 `strict: false` 는 그대로다 — 초록인 체크가 지금의 main 을 본 적이
46
+ # 없을 수 있다(CLAUDE.md §4축 게이트 기계층에 이미 기록된 잔여).
47
+ # ⓒ 어댑터 레인 자체의 수리(레인 분리, 계기 부재 → SKIP)는 **qasp AX-ready 세션으로
48
+ # 라우팅**됐다(`fh_signal_2026-08-17_qasp-adapter-phantom-air.md`). 여기서 손대지 않는다.
49
+ #
50
+ # 종료코드: 0 통과 · 1 커버리지 전제 파손(출하 차단) · 3 계기가 측정 불가(fail-closed)
51
+
52
+ set -u
53
+
54
+ WF=".github/workflows/validate.yml"
55
+ SUBJ="scripts/selfcheck.sh"
56
+
57
+ _note() {
58
+ echo "── prepublish scope ─────────────────────────────────────────────"
59
+ echo " 이 체인은 **출하물의 성질만** 검사한다 (freshness · lockstep ·"
60
+ echo " tarball coverage · public surface). peer 클러스터 상태는 출하물의"
61
+ echo " 성질이 아니므로 selfcheck 는 여기 없다 — CI 'validate' 가 돌린다."
62
+ }
63
+
64
+ check() {
65
+ if [ ! -f "$WF" ]; then
66
+ echo "❌ INSTRUMENT ERROR $WF 가 없다 — selfcheck 의 CI 커버리지를 확인할 수 없다."
67
+ echo " 측정 불가는 통과가 아니다(미측정 ≠ 0). 출하를 막는다."
68
+ return 3
69
+ fi
70
+ # 🟥 주석을 먼저 벗긴다. cross-family(codex/gpt-5.5) 실측 지적 #1(S): 초판은 `grep -qE
71
+ # "selfcheck\.sh"` 였고 **주석 처리된 호출에도 매칭**됐다 — 즉 `# - run: bash
72
+ # scripts/selfcheck.sh` 로 레인을 꺼놓아도 이 게이트는 «커버리지 있음» 을 냈다.
73
+ # 그리고 **언급이 아니라 호출**이어야 한다 — `echo "… scripts/selfcheck.sh …"` 는 실행이
74
+ # 아니다(실물 워크플로에 그런 줄이 실제로 있다). 그래서 줄의 **첫 토큰이 실행자**인 것만 센다.
75
+ # 🟥 첫 수리는 `run:` 을 같은 줄에서 요구했고 **실물에서 즉시 적색**이었다 — 실물은
76
+ # `run: |` 블록 스칼라 안에서 호출한다(`validate.yml:150`). self-test 6/6 초록인데
77
+ # 실물이 빨간 상태였고, 그게 «픽스처가 실물을 대표하지 못한다» 의 교과서 형태다.
78
+ # 그래서 아래 known-pair 는 **실물 형태(블록 스칼라)를 그대로 뜬다.**
79
+ if ! sed 's/#.*//' "$WF" \
80
+ | grep -qE '^[[:space:]]*(-[[:space:]]+run:[[:space:]]*)?(bash|sh|source|\.)[[:space:]]+[^[:space:]]*selfcheck\.sh'; then
81
+ echo "❌ PUBLISH BLOCKED $WF 가 더 이상 $SUBJ 를 호출하지 않는다."
82
+ echo ""
83
+ echo " prepublishOnly 에서 selfcheck 를 뺀 근거는 «CI 가 대신 돌린다» 였다."
84
+ echo " 그 전제가 깨졌으므로 지금 출하하면 selfcheck 커버리지가 **어디에도 없다**."
85
+ echo ""
86
+ echo " 고르는 길 둘:"
87
+ echo " ① $WF 에 selfcheck 호출을 복구한다 (원래 자리)"
88
+ echo " ② selfcheck 를 prepublishOnly 로 되돌린다 — 단 그러면 2026-08-17 의"
89
+ echo " «출하 가능 여부 = 이 머신의 디렉터리 구성» 결함이 함께 돌아온다"
90
+ return 1
91
+ fi
92
+ _note
93
+ echo " ✅ CI 커버리지 확인: $WF 가 $SUBJ 를 호출한다"
94
+ return 0
95
+ }
96
+
97
+ # ── known-pair self-test — 컨트롤이 살아 있는지 보여주는 실행 출력 ────────────────
98
+ if [ "${1:-}" = "--self-test" ]; then
99
+ t=$(mktemp -d) || exit 3
100
+ trap 'rm -rf "$t"' EXIT
101
+ pass=0; fail=0
102
+ _lane() { # $1 = 이름, $2 = 기대 rc, $3 = 워크플로 내용(빈 문자열이면 파일 없음)
103
+ local name="$1" want="$2" body="$3" got
104
+ ( cd "$t" && rm -rf .github && if [ -n "$body" ]; then
105
+ mkdir -p .github/workflows && printf '%s\n' "$body" > "$WF"
106
+ fi
107
+ WF="$WF" SUBJ="$SUBJ"; check >/dev/null 2>&1 ); got=$?
108
+ if [ "$got" = "$want" ]; then pass=$((pass+1)); echo " PASS $name (rc=$got)"
109
+ else fail=$((fail+1)); echo " FAIL $name — 기대 rc=$want, 실제 rc=$got"; fi
110
+ }
111
+ echo "prepublish_scope_note.sh --self-test"
112
+ _lane "known-positive: 워크플로가 selfcheck 호출" 0 " - run: bash scripts/selfcheck.sh"
113
+ _lane "known-negative: 호출이 사라짐" 1 " - run: echo hi"
114
+ _lane "계기 부재: 워크플로 파일 자체가 없음" 3 ""
115
+ # ★ cross-family 지적 #1(S) 회귀 앵커 — 주석 처리된 호출은 커버리지가 아니다.
116
+ _lane "★주석 처리된 호출은 커버리지 아님" 1 " # - run: bash scripts/selfcheck.sh
117
+ - run: echo no selfcheck"
118
+ # ★ 과차단 컨트롤 — 주석을 벗기는 수리가 **정상 호출까지** 죽이지 않았는지. 이게 없으면
119
+ # 위 레인은 «전부 차단하는 계기» 로도 초록이 된다(컨트롤 있음 ≠ 판별력 있음).
120
+ _lane "★컨트롤: 주석과 실호출이 같이 있으면 통과" 0 " # - run: bash scripts/selfcheck.sh (구버전)
121
+ - run: bash scripts/selfcheck.sh"
122
+ # ★ 실물 형태 — `run: |` 블록 스칼라 안에서의 호출. 이게 실제 validate.yml 의 모양이고,
123
+ # 초판 정규식은 여기서 죽었다(self-test 는 초록인데 실물이 적색이었다).
124
+ _lane "★실물 형태: run: | 블록 안 호출" 0 " - name: Selfcheck
125
+ run: |
126
+ echo \"=== scripts/selfcheck.sh ===\"
127
+ bash scripts/selfcheck.sh"
128
+ # ★ 언급 ≠ 실행. 같은 블록 안에 echo 로만 이름이 있으면 커버리지가 아니다.
129
+ _lane "★echo 언급만 있으면 커버리지 아님" 1 " - name: Selfcheck
130
+ run: |
131
+ echo \"=== scripts/selfcheck.sh 는 로컬에서 돌린다 ===\"
132
+ echo done"
133
+ echo " ── $pass pass / $fail fail"
134
+ [ "$fail" -eq 0 ] || exit 1
135
+ exit 0
136
+ fi
137
+
138
+ check
139
+ exit $?
@@ -46,8 +46,27 @@ run_checks() {
46
46
  # ── ① 워킹트리가 깨끗한가 ────────────────────────────────────────────────
47
47
  # `--porcelain` 은 추적 파일의 수정·스테이징·미추적을 전부 낸다. 미추적까지 세는 것은
48
48
  # 의도적이다 — 2026-08-13 사고는 **남의 미커밋 파일**이 팩된 것이었다.
49
- local dirty
50
- dirty=$(git status --porcelain 2>/dev/null | grep -vE '^\?\? tracks/' || true)
49
+ # 🟥 cross-family(codex/gpt-5.5, 2026-08-17) 가 이 한 줄에서 S급 3건을 냈고 S1 은 재현됐다.
50
+ # S1 초판 `git status --porcelain 2>/dev/null` stderr 버리고 rc 안 봐서,
51
+ # **git 이 실패하면 빈 문자열이 「깨끗함」으로 읽혔다.** 비가역 게이트의 fail-open.
52
+ # 재현: printf x >/tmp/badindex; GIT_INDEX_FILE=/tmp/badindex bash <이 파일>
53
+ # S3 `status.showUntrackedFiles=no` 로 untracked 이 은폐된다 → -c 로 덮고 -uall 강제
54
+ # A4 `assume-unchanged`/`skip-worktree` 비트로 tracked 변경이 은폐된다 → ls-files -v 로 검사
55
+ local dirty status_out status_rc
56
+ status_out=$(git -c status.showUntrackedFiles=normal status --porcelain --untracked-files=all 2>&1)
57
+ status_rc=$?
58
+ if [ "$status_rc" -ne 0 ]; then
59
+ bad "git status 가 rc=$status_rc 로 실패했다 — **미측정은 통과가 아니다**:"
60
+ printf '%s\n' "$status_out" | head -5 | sed 's/^/ /'
61
+ return 1
62
+ fi
63
+ local hidden
64
+ hidden=$(git ls-files -v 2>/dev/null | grep -E '^[hS]' || true)
65
+ if [ -n "$hidden" ]; then
66
+ bad "assume-unchanged / skip-worktree 비트가 걸린 tracked 파일 — status 엔 안 보이지만 npm 은 팩한다:"
67
+ printf '%s\n' "$hidden" | head -5 | sed 's/^/ /'
68
+ fi
69
+ dirty=$(printf '%s\n' "$status_out" | grep -vE '^\?\? tracks/' || true)
51
70
  if [ -n "$dirty" ]; then
52
71
  bad "워킹트리가 깨끗하지 않다 — publish 는 커밋이 아니라 이 트리를 팩한다:"
53
72
  printf '%s\n' "$dirty" | head -10 | sed 's/^/ /'
@@ -308,7 +308,11 @@ fi
308
308
  # a consumer who simply has no hooks. That is the "unmeasured → clean" move this file legislates
309
309
  # against — and worse, the --self-test loop 200 lines below already routes rc=10 to `fail=1` saying
310
310
  # "it could not measure, so its verdicts prove nothing". Same file, same code, opposite verdict.
311
- # (Adversarial review 2026-08-16 found this; the contradiction was reproduced by reading :524.)
311
+ # (Adversarial review 2026-08-16 found this. ⚠️ An earlier draft of this comment cited `:524` for
312
+ # it; the standpoint review measured the real sites — the loop dispatches at :528 and routes
313
+ # rc=10 to fail=1 at :562, and the quoted sentence lives at :429/:475. `:524` is none of them, so
314
+ # a reviewer following it lands on prose. A stale line number in the comment that argues against
315
+ # stale instruments is this commit's own defect class, recorded rather than quietly corrected.)
312
316
  #
313
317
  # The discriminator is mechanical, not a guess: the three harness-defect paths use plain `echo
314
318
  # "❌ harness-error: …"`, while the NOT-INSTALLED path goes through `say()` which `--quiet` suppresses.
@@ -415,11 +419,30 @@ fi
415
419
  if [ ! -f scripts/package_coverage_check.sh ]; then
416
420
  _absent_subject_verdict "test_package_coverage_lanes.sh" "scripts/package_coverage_check.sh" || fail=1
417
421
  elif [ -f scripts/test_package_coverage_lanes.sh ]; then
418
- if ! bash scripts/test_package_coverage_lanes.sh; then
422
+ if ! bash scripts/test_package_coverage_lanes.sh; then
419
423
  fail=1
420
424
  fi
421
- if ! bash scripts/package_coverage_check.sh; then
422
- fail=1
425
+ # The CHECKER's semantics are deliberately fail-closed and must not be softened: given a git
426
+ # checkout with no package.json it reports UNMEASURED, never "clean", and its own lane L4 pins
427
+ # exactly that. Applicability is the CALLER's judgment, and it is made mechanically here.
428
+ #
429
+ # A repo that ships no npm surface at all is not "unmeasured npm coverage" — it has no npm
430
+ # coverage to measure. Measured 2026-08-16 in a sibling harness that carries this suite verbatim
431
+ # and is not an npm package: this was a PERMANENT red, on every run forever. A gate that can
432
+ # never go green is not strict, it is a gate people learn to ignore — and this suite is the
433
+ # mandatory-pass surface, so that habit is expensive.
434
+ #
435
+ # Note the asymmetry, because it is the whole point: this repo HAS an npm surface, so if its
436
+ # package.json ever went missing the checker would fire and SHOULD — that is a real defect here.
437
+ # The guard below distinguishes "the surface is absent by nature" from "the surface is missing",
438
+ # which is the distinction `not found ≠ 0` exists to protect.
439
+ if [ -f package.json ] || [ -d bin ]; then
440
+ if ! bash scripts/package_coverage_check.sh; then
441
+ fail=1
442
+ fi
443
+ else
444
+ echo "SKIP package-coverage — NOT APPLICABLE: this repo ships no npm surface"
445
+ echo " (checked mechanically: package.json absent, bin/ absent — not a claim, a test)"
423
446
  fi
424
447
  else
425
448
  echo "FAIL test_package_coverage_lanes.sh: package_coverage_check.sh present but its anchor is missing"
@@ -507,7 +530,8 @@ for _pair in \
507
530
  "scripts/residency_closure_scan.py|scripts/test_residency_closure_lanes.sh" \
508
531
  "scripts/reviewer_capability_corpus.tsv|scripts/test_reviewer_capability_conformance.sh" \
509
532
  "scripts/field_canon_preload.sh|scripts/test_field_canon_lanes.sh" \
510
- "scripts/stale_clone_guard.sh|scripts/test_stale_clone_guard_lanes.sh"
533
+ "scripts/stale_clone_guard.sh|scripts/test_stale_clone_guard_lanes.sh" \
534
+ "plugins/fh-commons/skills/ko-tech-writer/SKILL.md|scripts/test_ko_tech_writer_lanes.sh"
511
535
  do
512
536
  _subj="${_pair%%|*}"; _anc="${_pair##*|}"; _lbl="${_anc##*/}"
513
537
  if [ ! -f "$_subj" ]; then
@@ -889,7 +913,9 @@ else
889
913
  fail=1
890
914
  fi
891
915
 
892
- # marker axes-run lanes — 훅의 4축 자기대조 형식 검사(§CLAUDE.md 3층 자기 대조)의 앵커.
916
+ # marker axes-run lanes — 훅의 축 자기대조 형식 검사(§CLAUDE.md 3층 자기 대조)의 앵커.
917
+ # ⚠️ 「4축」이라고 적혀 있었는데 2026-08-17 부로 **6축 분기가 생겼다**(마커 날짜 >= 그 날이면
918
+ # 기호 키 ⓐ~ⓕ 요구, 미만이면 옛 ASCII 넷). 산문 층이라 레인은 안 깨지고 조용히 stale 이었다.
893
919
  # subject 부재를 FAIL 로 두는 이유는 branch-claim 블록과 같다: 이 subject 는 pre-commit 훅
894
920
  # 자체이고 files[] 에 있으므로 «정당한 부재» 가 없다. 부재 = 삭제다.
895
921
  if [ ! -f templates/.git-hooks/pre-commit ]; then
@@ -21,6 +21,18 @@ pass=0; fail=0
21
21
  TMPROOT=$(mktemp -d "${TMPDIR:-/tmp}/fh_gate_test_XXXXXX")
22
22
  trap 'rm -rf "$TMPROOT"' EXIT
23
23
 
24
+ # Review SUBJECT — a fixture this suite creates, NOT a file it hopes the repo has.
25
+ # Every gate invocation below used the literal `package.json` as its subject. That is an npm-package
26
+ # assumption smuggled into a test: this suite passed here only because this repo happens to ship one.
27
+ # MEASURED 2026-08-16 in a sibling harness that carries this file verbatim and ships no package.json:
28
+ # every lane that actually invokes the gate on a file returned 10 ("0 of N targets resolved →
29
+ # harness error") — 12 failures from this one hardcoded fixture, on top of 18 more from the same
30
+ # missing file killing the gate's version read outright. The suite was measuring the fixture's
31
+ # existence, not the gate's behaviour. A test that only runs in the repo it was written in is not a
32
+ # regression anchor; it is a local habit.
33
+ SUBJECT="$TMPROOT/subject_under_review.txt"
34
+ printf 'fixture file for gate regression lanes — content is irrelevant, existence is not\n' > "$SUBJECT"
35
+
24
36
  # --- fake codex backend: writes $FAKE_PAYLOAD to the -o path, exits 0 ---
25
37
  # Doubles as the live demonstration of the PATH-trusting residual documented in fh-gate.sh.
26
38
  FAKEBIN="$TMPROOT/bin"; mkdir -p "$FAKEBIN"
@@ -68,16 +80,16 @@ check() {
68
80
  fi
69
81
  }
70
82
 
71
- run_gate() { env "$@" bash "$GATE" "package.json" quick test; }
83
+ run_gate() { env "$@" bash "$GATE" "$SUBJECT" quick test; }
72
84
  run_fake() {
73
85
  local payload="$1"; shift
74
86
  env PATH="$FAKEBIN:$PATH" FH_BACKEND=codex FH_MODEL=fake FAKE_PAYLOAD="$payload" \
75
- bash "$GATE" "package.json" quick test
87
+ bash "$GATE" "$SUBJECT" quick test
76
88
  }
77
89
  run_fake_claude() {
78
90
  local payload="$1"; shift
79
91
  env PATH="$FAKEBIN:$PATH" FH_BACKEND=claude FH_MODEL=fake FAKE_PAYLOAD="$payload" \
80
- bash "$GATE" "package.json" quick test
92
+ bash "$GATE" "$SUBJECT" quick test
81
93
  }
82
94
 
83
95
  echo "── argument / env validation ──"
@@ -89,7 +101,7 @@ check "FH_TIMEOUT non-integer rejected" 11 run_gate FH_TIMEOUT="abc" FH_DRY_RUN=
89
101
  check "FH_TIMEOUT integer accepted (no regression)" 12 run_gate FH_TIMEOUT=120 FH_DRY_RUN=1
90
102
  # Newline in FH_CALLER forges an extra column-0 FH_GATE_VERDICT line in the legacy contract.
91
103
  check "FH_CALLER newline injection rejected" 11 \
92
- env FH_CALLER=$'ci\nFH_GATE_VERDICT: PASS' FH_DRY_RUN=1 bash "$GATE" "package.json" quick
104
+ env FH_CALLER=$'ci\nFH_GATE_VERDICT: PASS' FH_DRY_RUN=1 bash "$GATE" "$SUBJECT" quick
93
105
 
94
106
  echo
95
107
  echo "── dry-run must not be readable as PASS ──"
@@ -129,7 +141,7 @@ check "claude path: clean PASS → exit 0" 0 run_fake_claude \
129
141
  check "claude path: is_error envelope → fails closed" 10 \
130
142
  env PATH="$FAKEBIN:$PATH" FH_BACKEND=claude FH_MODEL=fake \
131
143
  FAKE_ENVELOPE='{"is_error":true,"subtype":"error","structured_output":{"status":"SUCCESS","verdict":"PASS","findings_count":0,"findings_a":0,"findings_b":0,"findings":[]}}' \
132
- bash "$GATE" "package.json" quick test
144
+ bash "$GATE" "$SUBJECT" quick test
133
145
 
134
146
  echo
135
147
  echo "── legitimate verdicts still work (no over-blocking regression) ──"
@@ -199,7 +211,7 @@ NOENT="$TMPROOT/noentropy"; mkdir -p "$NOENT"
199
211
  printf '#!/usr/bin/env bash\nexit 1\n' > "$NOENT/openssl"; chmod +x "$NOENT/openssl"
200
212
  printf '#!/usr/bin/env bash\nexit 1\n' > "$NOENT/od"; chmod +x "$NOENT/od"
201
213
  check "no CSPRNG (openssl+od stubbed to fail) → fails closed" 10 \
202
- env PATH="$NOENT:$PATH" FH_DRY_RUN=1 bash "$GATE" "package.json" quick test
214
+ env PATH="$NOENT:$PATH" FH_DRY_RUN=1 bash "$GATE" "$SUBJECT" quick test
203
215
 
204
216
  echo
205
217
  echo "── FH_BACKEND=cross (decorrelated review) ──"
@@ -225,7 +237,7 @@ check_out() { # <name> <expected-exit> <grep-ere that MUST appear> -- <cmd...>
225
237
  run_cross() { # <claude-payload> <codex-payload>
226
238
  env PATH="$FAKEBIN:$PATH" FH_BACKEND=cross FH_MODEL=fake \
227
239
  FAKE_PAYLOAD_CLAUDE="$1" FAKE_PAYLOAD_CODEX="$2" FAKE_PAYLOAD="$1" \
228
- bash "$GATE" "package.json" quick test
240
+ bash "$GATE" "$SUBJECT" quick test
229
241
  }
230
242
 
231
243
  check_out "cross: both PASS → PASS, decorrelated" 0 'FH_GATE_DECORRELATED: yes' \
@@ -241,9 +253,9 @@ ONELEG="$TMPROOT/oneleg"; mkdir -p "$ONELEG"; cp "$FAKEBIN/claude" "$ONELEG/clau
241
253
  check_out "cross: codex absent → single leg, DECORRELATED: no" 0 'FH_GATE_DECORRELATED: no' \
242
254
  env PATH="$ONELEG:/usr/bin:/bin" FH_BACKEND=cross FH_MODEL=fake \
243
255
  FAKE_PAYLOAD_CLAUDE="$CROSS_PASS" FAKE_PAYLOAD="$CROSS_PASS" \
244
- bash "$GATE" "package.json" quick test
256
+ bash "$GATE" "$SUBJECT" quick test
245
257
  check "cross: no family available → fails closed" 10 \
246
- env PATH="/usr/bin:/bin" FH_BACKEND=cross bash "$GATE" "package.json" quick test
258
+ env PATH="/usr/bin:/bin" FH_BACKEND=cross bash "$GATE" "$SUBJECT" quick test
247
259
 
248
260
  echo
249
261
  echo "────────────────────────────────────────────────────────────────────"
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env bash
2
+ # test_ko_tech_writer_lanes.sh — `ko-tech-writer` Step 2 / Step 4-b 판별력 known-pair (얇은 래퍼).
3
+ #
4
+ # ─────────────────────────────────────────────────────────────────────────────
5
+ # 왜 필요한가 — 이 레포가 자기 스캔을 UNCALIBRATED 로 강등해 놨다
6
+ # ─────────────────────────────────────────────────────────────────────────────
7
+ # `knowledge/shared/harness-core/harness_terminal_correlation_and_recommendations.md`:
8
+ # Step 2 "잔여 0건(양성 컨트롤 동반)" 주장 -> UNCALIBRATED: 컨트롤·재현커맨드·출력이
9
+ # 하나도 없다. 재현 불가한 0은 0의 증거가 아니다
10
+ # Step 4 "전칭 단정 잔여 0건" 주장 -> UNCALIBRATED, 자기반증(그 시점에 3건 생존)
11
+ #
12
+ # 챔버 런 #12(2026-08-17, prosody-lens KILL)가 이 부채를 배출 판정의 결정적 근거로
13
+ # 인용했다 — "같은 계열 계기가 미보정인데 하나 더 짓는 것은 미보정 계기의 증식".
14
+ # 운영자 결정: 새 계기보다 이 부채가 먼저. 이 파일이 그 부채를 갚는다.
15
+ #
16
+ # ─────────────────────────────────────────────────────────────────────────────
17
+ # 왜 래퍼인가 — 패턴이 셸 밖에 있어야 하는 실측 이유가 둘 있다
18
+ # ─────────────────────────────────────────────────────────────────────────────
19
+ # 초판은 패턴을 이 파일 안에 두고 엔진을 ripgrep 으로 고정했다. CI 가 둘 다 반증했다
20
+ # (2026-08-17, ubuntu-latest):
21
+ # 1) ripgrep 부재 -> 스위트가 rc=10 으로 죽었다(HARNESS ERROR). 설계는 옳게 작동했다 —
22
+ # SKIP 이 아니라 시끄럽게 죽었다 — 그러나 CI 에서 못 도는 앵커는 앵커가 아니다.
23
+ # 2) 이식성 린트 적중 — 셸 파일 안의 한글 문자 범위는 GNU grep C 로케일에서 exit 2 로
24
+ # 죽고 그 결과가 "무매치"로 읽힌다. 즉 fail-open.
25
+ # 두 결함이 같은 처방으로 닫힌다: **패턴을 Python 으로 옮긴다.** SKILL.md 가 허용 엔진으로
26
+ # Python `re` 를 이미 명시했으므로 격하가 아니라 등가 엔진으로의 이동이고, python3 는
27
+ # 세 허용 엔진 중 어디에나 있는 유일한 것이다.
28
+ #
29
+ # ─────────────────────────────────────────────────────────────────────────────
30
+ # 무엇이 재어지는가 — Step 2 다섯 클래스 + Step 4-b 두 패턴
31
+ # ─────────────────────────────────────────────────────────────────────────────
32
+ # Step 2 C1 줄표 이어붙임 · C2 용어-머리 · C3 콜론 나열투 · C4 조각문 · C5 소유 직역
33
+ # Step 4-b C6 전칭 어휘 · C6b 부정형 전칭
34
+ # 각 레인의 PASS 조건은 **양성 >= 1 그리고 음성 == 0** 이다. 존재 확인이 아니라 판별
35
+ # 확인이라는 점이 핵심 — known-negative 가 없으면 "오탐이 있나" 조차 못 재고, 그러면
36
+ # PASS 는 "늘 잡는 계기" 와 구분되지 않는다.
37
+ #
38
+ # 🟥 첫 캘리브레이션이 정본의 과장을 드러냈다: SKILL.md 는 Step 2 의 "앞 다섯 줄은 기계
39
+ # 검출 가능" 이라 적었는데, 실제로 grep 을 싣고 있는 것은 C1·C5 둘뿐이고 C2·C3·C4 의
40
+ # 검출 힌트 칸은 산문 서술이다. 특히 C4 는 패턴이 없어서, 내가 임의로 지은 패턴이
41
+ # 양성을 0건으로 놓쳤다 — 계기를 세우자마자 대상의 결함이 나온 형태다.
42
+ #
43
+ # 🟥 이 스위트가 PASS 를 내도 주장하면 안 되는 것: "Step 2 잔여 0건" · "Step 4 잔여 0건".
44
+ # 판별력과 잔여는 다른 명제이고, 후자는 문서마다 따로 잰다. 그 혼동이 애초에 두 행을
45
+ # UNCALIBRATED 로 강등시킨 사유다.
46
+ #
47
+ # 사용: bash scripts/test_ko_tech_writer_lanes.sh
48
+ # exit: 0 판별 확인(전 레인 PASS) · 1 판별 실패 · 10 harness error(python3/픽스처 부재)
49
+
50
+ set -uo pipefail
51
+
52
+ SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
53
+ PY="$SRC/ko_tech_writer_calibrate.py"
54
+
55
+ command -v python3 >/dev/null 2>&1 || {
56
+ echo "harness error: python3 absent -- cannot measure. Not a pass." >&2; exit 10; }
57
+ [ -f "$PY" ] || { echo "harness error: missing $PY" >&2; exit 10; }
58
+
59
+ echo "-- ko-tech-writer Step2/4-b discrimination known-pair --"
60
+ python3 "$PY"
61
+ exit $?
@@ -2,9 +2,17 @@
2
2
  # test_marker_axes_run_lanes.sh — regression fixtures for pre-commit validate_marker_axes_run.
3
3
  #
4
4
  # 대상: CLAUDE.md §3층 자기 대조가 요구하는 마커 3줄 중 **기계로 볼 수 있는 두 줄** —
5
- # axes-run: 축(ⓐ다른계열 ⓑ첫실사용 ⓒ기록그라운딩 ⓓ되돌림) 각각의 실행 여부
5
+ # axes-run: 축의 실행 여부. 마커 날짜에 따라 **배열이 둘**이다:
6
+ # >= 2026-08-17 ⓐ계열 · ⓑ입장 · ⓒ격리그라운딩 · ⓓ3자대면 · ⓔ첫실사용 · ⓕ되돌림
7
+ # < 2026-08-17 a=계열 · b=첫실사용 · c=기록그라운딩 · d=되돌림 (옛 ASCII 4축)
6
8
  # controls: 각 축 컨트롤의 **생사**
7
9
  #
10
+ # 🟥 이 헤더 자신이 2026-08-17 까지 그 충돌을 저지르고 있었다 — «네 축(ⓐ다른계열 ⓑ첫실사용
11
+ # ⓒ기록그라운딩 ⓓ되돌림)» 이라고, **기호 표기로 옛 4축 의미**를 가르쳤다. 훅 본문이
12
+ # 「옛 b=첫실사용은 지금 ⓔ」 라고 경고하는 파일의 짝인데 정작 짝이 반대를 가르쳤고,
13
+ # 같은 세션이 이 파일을 11→25레인으로 늘리면서 **헤더는 안 봤다**. 잡은 것은 pmh-dev
14
+ # 입장리뷰(tier2)이고, 이 파일은 전파 자산이라 그 오류가 하류로 배송되던 중이었다.
15
+ #
8
16
  # 🟥 이 스위트가 증명하지 않는 것 (계약을 픽스처로 못박는다):
9
17
  # 「a=codex」 라고 적혀 있을 때 codex 가 **실제로 돌았는지**는 검사하지 않는다.
10
18
  # 마커의 계약은 form + non-vacuity + auditability 이지 provenance 가 아니다 —
@@ -25,12 +33,19 @@ T=$(mktemp -d); trap 'rm -rf "$T"' EXIT
25
33
  # 훅에서 함수 + 유예 상수를 그대로 뽑아 쓴다. 복제하면 드리프트하므로 **소스가 하나**다.
26
34
  {
27
35
  grep -E '^AXES_RUN_GRACE_DATE=' "$HOOK"
36
+ grep -E '^SIX_AXES_GRACE_DATE=' "$HOOK"
28
37
  sed -n '/^validate_marker_axes_run()/,/^}/p' "$HOOK"
29
38
  } > "$T/fn.sh"
30
39
 
31
40
  # 계기 자체가 살아있는지 먼저 본다 — 추출이 실패하면 모든 레인이 «통과»로 초록이 된다.
32
- if ! grep -q 'validate_marker_axes_run()' "$T/fn.sh" || ! grep -q '^AXES_RUN_GRACE_DATE=' "$T/fn.sh"; then
33
- echo "❌ 계기 사망: 훅에서 함수/상수 추출 실패 레인 결과를 신뢰하지 마라"
41
+ # 🟥 상수를 **각각** 확인한다. 하나로 뭉뚱그리면 상수가 추출에서 빠져도 초록이고,
42
+ # 그 경우 함수는 unbound 죽으며 모든 레인이 BLOCK 으로 몰려 «6축이 잘 막는다» 로
43
+ # 오독된다 — 계기 고장이 판정으로 렌더되는 형태다.
44
+ for _c in AXES_RUN_GRACE_DATE SIX_AXES_GRACE_DATE; do
45
+ grep -q "^${_c}=" "$T/fn.sh" || { echo "❌ 계기 사망: 훅에서 ${_c} 추출 실패 — 레인 결과를 신뢰하지 마라"; exit 1; }
46
+ done
47
+ if ! grep -q 'validate_marker_axes_run()' "$T/fn.sh"; then
48
+ echo "❌ 계기 사망: 훅에서 함수 추출 실패 — 레인 결과를 신뢰하지 마라"
34
49
  exit 1
35
50
  fi
36
51
 
@@ -90,6 +105,75 @@ check "$G2" PASS "파일명에 날짜가 없으면 판정 근거가 없다 →
90
105
  B6=$(mk 2026-08-10 'axis2-evidence: PASS')
91
106
  check "$B6" BLOCK "★유예일 **당일**은 요구한다 (경계가 < 이지 <= 가 아님을 고정)"
92
107
 
108
+ # ── 6축 확장 (2026-08-17) ───────────────────────────────────────────────────
109
+ # 표기법이 배열을 선언한다: ASCII(a=…d=)=옛 4축 · 기호(ⓐ=…ⓕ=)=현 6축.
110
+ # 그래서 여기서 고정할 것이 셋이다 — ⓐ 새 날짜는 기호 여섯을 요구하나 ⓑ 옛 날짜는
111
+ # 건드리지 않나(소급 차단 금지) ⓒ **혼용이 막히나**(혼용을 허용하면 판별 가능성이
112
+ # 통째로 사라지고, 옛 b=/d= 는 새 배열에서 다른 축이라 무음 오독이 된다).
113
+ D_SIX=2026-08-18 # 6축 유예일 이후
114
+ SIX_OK='axes-run: ⓐ=codex(4건) ⓑ=→standpoint ⓒ=none ⓓ=none ⓔ=CI배선 ⓕ=되돌림(국소성)
115
+ controls: alive — known-positive 3히트 · known-negative 0
116
+ standpoint: not-applicable'
117
+
118
+ S1=$(mk "$D_SIX" "$SIX_OK")
119
+ check "$S1" PASS "6축: 기호 여섯 + 컨트롤 + standpoint → 통과"
120
+
121
+ S2=$(mk "$D_SIX" 'axes-run: ⓐ=codex ⓑ=→standpoint ⓒ=none ⓓ=none ⓕ=되돌림
122
+ controls: alive — ok
123
+ standpoint: tier1')
124
+ check "$S2" BLOCK "6축: ⓔ 가 빠짐 → 차단"
125
+
126
+ S3=$(mk "$D_SIX" 'axes-run: ⓐ=codex b=CI배선 ⓒ=none ⓓ=none ⓔ=실사용 ⓕ=되돌림
127
+ controls: alive — ok')
128
+ check "$S3" BLOCK "★6축: ASCII 키가 기호 키를 대신함 → 차단 (ⓑ 가 빠졌다는 뜻이므로 missing-key 가 잡는다)"
129
+
130
+ # ★ 오탐 컨트롤 — 전용 혼용 가드를 지었다가 **실측 오탐 때문에 지웠다**. 값이 자유 산문이라
131
+ # `f=0.9` 나 `a=제어 b=처리` 가 값 안에 정당하게 들어온다. 과차단은 `--no-verify` 를 훈련시키고
132
+ # 그건 같은 훅의 Destructive-Op 게이트까지 무장해제한다. 이 두 레인이 그 삭제를 고정한다 —
133
+ # 누가 «안전하게» 혼용 가드를 되살리면 여기서 빨개진다.
134
+ S3b=$(mk "$D_SIX" 'axes-run: ⓐ=codex ⓑ=→standpoint ⓒ=none ⓓ=none ⓔ=실사용 ⓕ=되돌림 f=0.9 상승
135
+ controls: alive — ok
136
+ standpoint: tier1')
137
+ check "$S3b" PASS "★오탐 컨트롤: 값 안의 f=0.9 는 혼용이 아니다 → 통과"
138
+
139
+ S3c=$(mk "$D_SIX" 'axes-run: ⓐ=codex ⓑ=→standpoint ⓒ=none ⓓ=none ⓔ=arm a=제어 b=처리 비교 ⓕ=되돌림
140
+ controls: alive — ok
141
+ standpoint: tier1')
142
+ check "$S3c" PASS "★오탐 컨트롤: 값 안의 a=제어 b=처리 는 혼용이 아니다 → 통과"
143
+
144
+ S4=$(mk "$D_SIX" 'axes-run: a=codex b=CI배선 c=none d=되돌림
145
+ controls: alive — ok')
146
+ check "$S4" BLOCK "★6축 유예 후 옛 ASCII 넷만 → 차단 (같은 글자가 다른 축을 가리키므로 그대로 두면 무음 재해석)"
147
+
148
+ S5=$(mk "$D_NEW" 'axes-run: a=codex b=CI배선 c=none d=되돌림
149
+ controls: alive — ok')
150
+ check "$S5" PASS "★컨트롤: 6축 유예 **이전** 마커는 옛 ASCII 넷으로 그대로 통과 (소급 차단 없음)"
151
+
152
+ S6=$(mk 2026-08-17 'axes-run: a=codex b=CI c=none d=되돌림
153
+ controls: alive — ok')
154
+ check "$S6" BLOCK "★6축 유예일 **당일**은 요구한다 (경계가 < 임을 고정 — 상수를 바꾸면 빨개진다)"
155
+
156
+ S7=$(mk "$D_SIX" 'axes-run: ⓐ=none ⓑ=→standpoint ⓒ=none ⓓ=none ⓔ=none ⓕ=none
157
+ controls: n/a — 이번 델타에 측정이 없다')
158
+ check "$S7" BLOCK "★ⓑ=→standpoint 인데 standpoint: 줄이 없음 → 차단 (죽은 포인터)"
159
+
160
+ # ★ fail-open 컨트롤 — 초판은 화살표(→)를 필수로 봤고, 자기 프로브가 `ⓑ=standpoint`(화살표 없음)이
161
+ # 검사를 통째로 비껴가는 것을 실측했다. 죽은 포인터가 표기 하나로 통과하면 그건 fail-open 이다.
162
+ S7b=$(mk "$D_SIX" 'axes-run: ⓐ=none ⓑ=standpoint ⓒ=none ⓓ=none ⓔ=none ⓕ=none
163
+ controls: n/a — 측정 없음')
164
+ check "$S7b" BLOCK "★화살표 없는 ⓑ=standpoint 도 차단 (표기 하나로 검사를 비껴가면 fail-open)"
165
+
166
+ S7c=$(mk "$D_SIX" 'axes-run: ⓐ=none ⓑ=→standpoint(qasp) ⓒ=none ⓓ=none ⓔ=none ⓕ=none
167
+ controls: n/a — 측정 없음
168
+ standpoint: tier1b(qasp)')
169
+ check "$S7c" PASS "★컨트롤: 포인터에 대상이 붙어도 standpoint: 줄이 있으면 통과 (과차단 방지)"
170
+
171
+ S8=$(mk "$D_SIX" 'axes-run: ⓐ=codex ⓑ=→standpoint ⓒ=none ⓓ=none ⓔ=실사용 ⓕ=되돌림
172
+ axes-run: ⓐ=거짓말 ⓑ=x ⓒ=x ⓓ=x ⓔ=x ⓕ=x
173
+ controls: alive — ok
174
+ standpoint: tier1')
175
+ check "$S8" BLOCK "★axes-run 줄이 둘 → 차단 (첫 줄만 읽히므로 둘째 줄은 «없다»가 아니라 «안 보인다»)"
176
+
93
177
  # ── ★ 배선 판별자 — 위 레인들은 **함수만** 본다 ──────────────────────────────
94
178
  # 실측 2026-08-09: 훅의 호출부에서 `&& validate_marker_axes_run "$MARKER"` 를 떼고
95
179
  # 위 11 레인을 돌렸더니 **전부 초록**이었다. 함수는 멀쩡하고 아무도 안 부르는 상태를
@@ -55,8 +55,38 @@ mk 'crossfamily: panel(codex,gemini) — R1..R3, 9 findings, 8 fixed 1 refuted,
55
55
  check k1 PASS "panel() with family list + verdict"
56
56
  mk 'crossfamily: panel(codex, gemini) — spaced list, the S-grade over-block\n' k1b
57
57
  check k1b PASS "panel(codex, gemini) — SPACE after comma (was truncated to 'panel(codex,')"
58
+ # ⚠️ CONTRACT CHANGE 2026-08-16 — `declined` now requires grounds. It used to pass BARE, which is
59
+ # what the old k3 asserted. That was the enum's soft spot: `declined` means the OPERATOR declined
60
+ # sidecars (UAP), a chosen floor — a factual, attributable claim — and it was the ONLY value with no
61
+ # grounds requirement, so an author who had merely judged decorrelation unnecessary reached for the
62
+ # nearest permissive token and passed clean. Measured in this repo the same day, by a session that
63
+ # had corrected a different marker's crossfamily value hours earlier and had the correct semantics
64
+ # printed to it in the hook's own error text. A closed enum stops prose from collapsing distinct
65
+ # states; it does not stop a WRONG token from being a VALID one — that is what these three lanes are.
66
+ # The old bare-declined expectation is not deleted silently; it is inverted here with its reason.
58
67
  mk 'crossfamily: declined\n' k3
59
- check k3 PASS "declined (operator chose this floor not a degrade)"
68
+ check k3 BLOCK "declined BARE — no grounds (was PASS until 2026-08-16; an operator decision has a record)"
69
+ mk 'crossfamily: declined — operator declined sidecars, chosen floor, per knowledge/shared/rules/operational_adaptation.md\n' k3b
70
+ check k3b PASS "declined + a record path that RESOLVES (the documented form)"
71
+ # The real defect, verbatim-shaped: a genuine authorial judgment, plausibly worded, wrong token.
72
+ # This is the known-POSITIVE that is not synthetic — it is the sentence that actually shipped.
73
+ mk 'crossfamily: declined — this is a small, mechanically-verifiable hook-wiring fix, not a verdict-enum change; the standpoint axis is the decorrelation applied here\n' k3c
74
+ check k3c BLOCK "declined + AUTHOR-judgment grounds — a choice with a panel reachable is DEGRADED_PANEL_UNUSED"
75
+ # ── the three a cross-family review (codex/gpt-5.6-terra) found against the FIRST version of this
76
+ # lane, which grepped for vocabulary (operator|uap|consent|…). All three reproduced on the spot,
77
+ # which is why the check is now a RESOLVABLE-RECORD test rather than a word list.
78
+ mk 'crossfamily: declined — declined because I judged it unnecessary myself\n' k3d
79
+ check k3d BLOCK "SELF-VALIDATING: echoing the value satisfied the old word list ('declin')"
80
+ mk 'crossfamily: declined — operator documentation says authors may choose freely\n' k3e
81
+ check k3e BLOCK "VACUOUS: carries 'operator' but describes no declination at all"
82
+ mk 'crossfamily: declined — operator declined sidecars, see tracks/_meta/nonexistent_record.md\n' k3f
83
+ check k3f BLOCK "FABRICATED PATH: cites a record that does not exist on disk"
84
+ # ⚠️ INSTRUMENT NOTE, recorded because it nearly produced a false verdict on this very lane:
85
+ # the first attempt to known-pair these ran the extracted function under **zsh** (this operator's
86
+ # interactive shell) while the hook runs under **bash**. zsh does not word-split an unquoted
87
+ # parameter expansion, so the `for _tok in $reason` scan saw ONE token and the legitimate
88
+ # path-citing case scored a false BLOCK — the code was correct and the harness was wrong.
89
+ # Re-run under bash: 7/7. If you hand-test this function, invoke it with `bash`, not the login shell.
60
90
  mk 'crossfamily: DEGRADED_SINGLE_FAMILY — probed codex/agy/4090, 0 capable reachable\n' k4
61
91
  check k4 PASS "DEGRADED_SINGLE_FAMILY + substantive reason"
62
92
  mk 'crossfamily: UNKNOWN — consent unset, panel not probed this round\n' k5