@chrono-meta/fh-gate 1.4.89 → 1.4.91

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 (47) hide show
  1. package/.claude/judgment_circuits.txt +14 -0
  2. package/.claude/rules/fh_4axis_gate.md +7 -0
  3. package/.claude-plugin/marketplace.json +2 -2
  4. package/AGENTS.md +25 -0
  5. package/CLAUDE.md +202 -12
  6. package/knowledge/shared/harness-core/dispatch_conditional_prohibition.md +105 -0
  7. package/knowledge/shared/harness-core/fh_three_layer_canon.md +307 -0
  8. package/knowledge/shared/harness-core/harness_incubator_doctrine.md +100 -0
  9. package/knowledge/shared/harness-core/onboarding_acceleration_autopilot.md +3 -1
  10. package/knowledge/shared/harness-core/ship_readiness_gate.md +181 -13
  11. package/knowledge/shared/learnings/subagent_invocations_log.yaml +640 -0
  12. package/knowledge/shared/rules/multi_session_close_protocol.md +118 -0
  13. package/package.json +23 -2
  14. package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
  15. package/plugins/fh-meta/.claude-plugin/plugin.json +1 -1
  16. package/plugins/fh-meta/skills/auto-decorrelation/SKILL.md +56 -8
  17. package/plugins/fh-meta/skills/install-wizard/SKILL.md +33 -0
  18. package/plugins/fh-meta/skills/install-wizard/SKILL_detail.md +104 -15
  19. package/scripts/branch_claim.sh +266 -0
  20. package/scripts/chamber_run.sh +64 -2
  21. package/scripts/chamber_witness.sh +439 -0
  22. package/scripts/compaction_probe.sh +456 -0
  23. package/scripts/digest_landing_check.sh +385 -0
  24. package/scripts/directional_diff_gate.sh +459 -0
  25. package/scripts/fh_env_delta_scan.sh +15 -0
  26. package/scripts/fh_session_load.sh +34 -1
  27. package/scripts/field_canon_preload.sh +129 -0
  28. package/scripts/hook_source_lib.sh +41 -0
  29. package/scripts/judgment_circuit_lint.sh +239 -0
  30. package/scripts/novelty_claim_check.sh +193 -0
  31. package/scripts/relay_channel.sh +645 -0
  32. package/scripts/reviewer_capability_corpus.tsv +124 -0
  33. package/scripts/selfcheck.sh +106 -0
  34. package/scripts/session_close_check.sh +134 -1
  35. package/scripts/test_branch_claim_lanes.sh +231 -0
  36. package/scripts/test_dispatch_log_lanes.sh +35 -1
  37. package/scripts/test_field_canon_lanes.sh +142 -0
  38. package/scripts/test_hook_source_gate_lanes.sh +81 -0
  39. package/scripts/test_marker_crossfamily_lanes.sh +132 -0
  40. package/scripts/test_marker_floor_lanes.sh +9 -8
  41. package/scripts/test_relay_channel_lanes.sh +583 -0
  42. package/scripts/test_reviewer_capability_conformance.sh +173 -0
  43. package/scripts/test_wizard_snippet_merge_lanes.sh +104 -11
  44. package/scripts/utterance_landing_check.sh +209 -0
  45. package/templates/.git-hooks/pre-commit +297 -13
  46. package/templates/settings.Compaction.snippet.json +56 -0
  47. package/templates/settings.FieldCanon.snippet.json +51 -0
@@ -0,0 +1,129 @@
1
+ #!/usr/bin/env bash
2
+ # field_canon_preload.sh — 매핑된 필드 하네스가 언급되면 **그 하네스의 정본 경로**를 띄운다.
3
+ #
4
+ # WHY (2026-08-09 실측, 하루 4회 정정 · 자력 적발 0):
5
+ # FH 세션 시작 자동 적재는 **FH 자기 정본만** 싣는다(`fh_session_load.sh` = 동반자 저장소).
6
+ # 그런데 한 세션이 종일 qasp 를 파면서 qasp `README.md`(604줄) · `docs/governance/`(32개)를
7
+ # **하나도 안 읽고** 코드에서 역추론했다. 네 번 정정당했고 네 번 다 **정본에 답이 있었다**:
8
+ # · MTM 을 "조건부 화이트박스 모드" 로 요약 → 정본은 블박+화박 **동시**(이중시야)
9
+ # · "매트릭스 = 축이 여럿" → 영화 매트릭스에서 **네오가 보는 시야**
10
+ # · 3막 본체가 act2 에 있는 걸 배치 결함 판정 → README 가 **의도**라고 명시(L1 공유·중복 0)
11
+ # · 전수조사 모수를 src/act2 로 한정 → b레인·mate 는 모수 밖
12
+ # README 는 그중 하나를 *"이 축이 안 보이면 3막을 mate 판정 전용으로 오해한다"* 로
13
+ # **경고까지 하고 있었다.** 산문 규율로는 안 읽힌다 — `fh_session_load.sh` 헤더가 이미 같은
14
+ # 결론을 적었다("prose is salience-dependent"). 같은 처방을 필드 하네스에 적용한다.
15
+ #
16
+ # WHAT: 프롬프트에 매핑된 프로젝트 이름이 나오면, **실재하는 정본 파일 경로만** 골라 한 번 띄운다.
17
+ # ★ 일반 훈계("정본을 읽어라")가 아니라 **이 레포의 이 파일들**이어야 한다 — 오늘의 실패는
18
+ # 규율을 몰라서가 아니라 **그 파일들이 거기 있는 줄 몰라서**였다.
19
+ #
20
+ # 프로젝트당 세션당 1회. 센티넬로 재나그 방지 — 반복 알림은 무시를 학습시킨다.
21
+ #
22
+ # 종료: 항상 0 · 보고는 stdout. (비영 종료는 stdout 이 폐기되고 stderr 도 안 전달된다 —
23
+ # `[[feedback_hook_nonzero_exit_is_silent]]`.)
24
+ set -uo pipefail
25
+
26
+ HUB="${CLAUDE_PROJECT_DIR:-${HOME:-}/projects/forge-harness}"
27
+ PROJ_ROOT="${FIELD_CANON_PROJECT_ROOT:-${HOME:-}/projects}"
28
+ # ★ `${HOME:-}` — `set -u` 아래서 HOME 부재 시 여기가 unbound 로 죽어 rc=1 이 됐다.
29
+ # 비영 종료는 stdout 이 폐기되므로 헤더의 "항상 0" 계약이 깨진다(cross-family 지적, 재현됨).
30
+ [ -n "${HUB:-}" ] && [ "$HUB" != "/projects/forge-harness" ] || exit 0
31
+ # stdin 은 훅 페이로드(JSON). prompt 를 못 뽑아도 **조용히 죽지 않는다** — 원문 전체로 폴백한다.
32
+ # ★ `session_id` 를 **같은 페이로드에서** 뽑는다. 센티넬 키가 여기 달려 있다(바로 아래).
33
+ # 🟥 SID 는 **자르거나 치환하지 않고 해시한다.** 초판 수리가 슬래시 치환 + 64자 절단이었는데
34
+ # cross-family(gpt-5.5)가 재현했다: 'A/B' 와 'A_B' 가 같은 키 · 65자 이상은 접두 충돌 ·
35
+ # session_id 에 개행이 들어오면 아래 「1행=SID · 2행이후=PROMPT」 프로토콜이 깨져
36
+ # **프롬프트까지 오염**된다(무관 프롬프트로 발화 실증). 원래 버그(전 세션이 한 칸 공유)와
37
+ # **같은 일가를 수리가 다시 연** 것이다. 해시가 충돌·개행·길이·경로문자를 한 번에 없앤다.
38
+ # ⚠️ 아래 python 은 **큰따옴표 문자열**이다 — 주석에 백틱을 넣으면 bash 명령치환이 터진다
39
+ # (이 파일에서 실제로 터뜨렸다. 설명은 여기 bash 주석에 둔다).
40
+ RAW=$(cat 2>/dev/null || true)
41
+ FIELDS=$(printf '%s' "$RAW" | python3 -c "
42
+ import json,sys
43
+ try:
44
+ d = json.load(sys.stdin)
45
+ import hashlib
46
+ sid = d.get('session_id') or ''
47
+ print(hashlib.sha1(sid.encode('utf-8','surrogatepass')).hexdigest()[:16] if sid else '')
48
+ print(d.get('prompt') or '')
49
+ except Exception:
50
+ print(''); print('')
51
+ " 2>/dev/null)
52
+ SID=$(printf '%s' "$FIELDS" | sed -n '1p')
53
+ PROMPT=$(printf '%s' "$FIELDS" | sed -n '2,$p')
54
+ [ -n "$PROMPT" ] || PROMPT="$RAW"
55
+ [ -n "$PROMPT" ] || exit 0
56
+
57
+ # ── 센티넬 키 — 초판이 여기서 틀렸다 (2026-08-09 첫 실사용 실측으로 수리) ──
58
+ # 초판: `${TMPDIR}/fh_field_canon_${CLAUDE_SESSION_ID:-shared}`
59
+ # 🟥 **훅 환경에 `CLAUDE_SESSION_ID` 가 없다.** 전 세션이 literal `shared` 한 칸으로 접혔고
60
+ # TMPDIR 는 재부팅까지 산다 → "프로젝트당 **세션당** 1회" 가 실제로는 **"머신당 1회"** 였다.
61
+ # 실측: 21:18 의 세션이 qasp 센티넬을 소비 → 21:40 에 실제로 qasp 를 파기 시작한 세션은
62
+ # 안내를 **못 받았다**. 이 훅이 막으려던 바로 그 상황에서 침묵했다.
63
+ # ★ 같은 얼굴이 같은 날 다른 스크립트에서도 났다(`branch_claim.sh` 의 `CLAUDE_PID` — 저자 셸엔
64
+ # 있고 CI 엔 없어 검사가 무력화). **환경변수 부재가 조용한 폴백으로 접히고 그 폴백이 검사를
65
+ # 무력화한다** — 처방은 같다: **페이로드에서 읽고 환경에서 빌리지 않는다.**
66
+ # (`session_id` 는 훅 페이로드 필드다 — `compaction_probe.sh:415` 가 이미 그렇게 읽는다.)
67
+ #
68
+ # 폴백 방향은 **과다 알림 쪽**이다: 세션을 못 가르면 `$PPID`(CC 프로세스별로 갈린다)로,
69
+ # 그것도 없으면 dedup 을 **포기**한다. 침묵보다 중복이 낫다 — 초판 폴백은 조용한 쪽이었고
70
+ # 그래서 실패가 안 보였다(`[[feedback_not_found_is_not_zero_family]]`).
71
+ # 🟥 **literal 상수로 폴백하지 마라.** 그 순간 전 세션이 한 칸을 공유한다.
72
+ if [ -n "${FIELD_CANON_SENTINEL_DIR:-}" ]; then
73
+ SENT_DIR="$FIELD_CANON_SENTINEL_DIR"
74
+ elif [ -n "$SID" ]; then
75
+ SENT_DIR="${TMPDIR:-/tmp}/fh_field_canon_sid_${SID}"
76
+ elif [ -n "${PPID:-}" ] && [ "${PPID:-0}" != "0" ]; then
77
+ SENT_DIR="${TMPDIR:-/tmp}/fh_field_canon_ppid_${PPID}"
78
+ else
79
+ SENT_DIR="" # 못 가른다 → dedup 포기(매번 알린다). 침묵 금지.
80
+ fi
81
+ [ -z "$SENT_DIR" ] || mkdir -p "$SENT_DIR" 2>/dev/null || true
82
+
83
+ # 매핑된 프로젝트 = tracks/ 하위 디렉토리(언더스코어 접두는 메타라 제외)
84
+ mapped=$(ls -d "$HUB"/tracks/*/ 2>/dev/null | while read -r d; do
85
+ b=$(basename "$d")
86
+ [ "${b#_}" = "$b" ] || continue # 언더스코어 접두 = 메타 디렉토리, 제외
87
+ printf '%s\n' "$b"
88
+ done)
89
+ [ -n "$mapped" ] || exit 0
90
+
91
+ emitted=0
92
+ for name in $mapped; do
93
+ # 프롬프트에 이름이 나오는가 (대소문자 무시). 짧은 이름의 오탐은 감수 — 과소보다 낫다.
94
+ printf '%s' "$PROMPT" | grep -qiF -- "$name" || continue # -F: 이름의 regex 문자 오탐/미탐 방지
95
+ [ -n "$SENT_DIR" ] && [ -e "$SENT_DIR/$name" ] && continue # 이 세션에서 이미 띄웠다
96
+
97
+ # 레포 해석: tracks 이름 그대로 → 없으면 `-dev` 접미 (qasp → qasp-dev 실측 사례)
98
+ repo=""
99
+ for cand in "$PROJ_ROOT/$name" "$PROJ_ROOT/${name}-dev"; do
100
+ [ -d "$cand/.git" ] && { repo="$cand"; break; }
101
+ done
102
+ [ -n "$repo" ] || continue
103
+
104
+ # **실재하는 것만** 싣는다. 없는 파일을 가리키면 다음 사람이 그 지시를 못 믿게 된다.
105
+ lines=""
106
+ [ -f "$repo/README.md" ] && lines="$lines\n README.md ($(wc -l < "$repo/README.md" | tr -d ' ')줄) — §구조·§지도 절 먼저"
107
+ [ -f "$repo/CLAUDE.md" ] && lines="$lines\n CLAUDE.md ($(wc -l < "$repo/CLAUDE.md" | tr -d ' ')줄)"
108
+ gov=$(ls "$repo/docs/governance" 2>/dev/null | wc -l | tr -d ' ')
109
+ [ "${gov:-0}" -gt 0 ] && lines="$lines\n docs/governance/ — 정본 ${gov}개 (용어·모드·계약의 출처)"
110
+ [ -n "$lines" ] || continue
111
+
112
+ if [ "$emitted" -eq 0 ]; then
113
+ echo ""
114
+ echo "📚 [field-canon] 매핑된 필드 하네스가 언급됐다 — **코드 역추론 전에 그쪽 정본을 읽어라.**"
115
+ emitted=1
116
+ fi
117
+ echo " ▸ $name → $repo"
118
+ printf '%b\n' "$lines"
119
+ [ -z "$SENT_DIR" ] || : > "$SENT_DIR/$name" 2>/dev/null || true
120
+ done
121
+
122
+ if [ "$emitted" -eq 1 ]; then
123
+ cat <<'EOF'
124
+ ⚠️ 이 안내는 프로젝트당 세션당 1회다. 2026-08-09 실측: 정본 미독으로 하루 4회 정정,
125
+ 자력 적발 0 — 네 번 다 답이 정본에 있었고 그중 하나는 README 가 그 오해를 경고까지 했다.
126
+ ★ 필드 하네스 용어를 일반 개념으로 정규화하지 마라(「MTM=화이트박스 모드」가 그 실패다).
127
+ EOF
128
+ fi
129
+ exit 0
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env bash
2
+ # hook_source_lib.sh — SessionStart 의 `source` 를 읽는 **단일 소스**.
3
+ #
4
+ # WHY: `/clear` 는 SessionStart 를 재발동시키고 `source: "clear"` 를 준다(startup·resume·compact·
5
+ # fork 와 구분되는 별개 값, 공식 hooks 문서). 그런데 FH 훅들은 stdin 페이로드를 안 읽어서 진짜
6
+ # 시작과 구분하지 못했고, 결과적으로 **캐던스 나그가 /clear 마다 재발화**했다(2026-08-09 운영자 지적).
7
+ #
8
+ # WHY A LIB: 파서를 두 스크립트에 복붙하면 한쪽만 고쳐지는 순간 같은 입력이 서로 다르게 분류된다
9
+ # — 이 레포가 이미 이름 붙인 «관대함 갈린 중복 정규화» 다. 여섯 줄이라도 단일 소스로 둔다.
10
+ #
11
+ # 판별 기준(호출부가 지켜야 하는 것): 「세션이 잃어버린 상태」는 다시 보여주고,
12
+ # 「사람이 이미 보고 결정한 나그」는 안 보여준다. freshness/state = 항상, cadence = startup 계열만.
13
+ #
14
+ # DEGRADE: source 를 못 읽으면 `unknown` 이고, unknown 은 **보여주는 쪽**이다.
15
+ # 중복 알림은 눈에 보이는 소음이지만 놓친 캐던스는 무음 누락이고, 이 레포는 무음 누락을 더 나쁘게
16
+ # 친다. `unknown` 을 조용히 `clear` 로 접는 것이 정확히 그 결함이다(not-found ≠ zero).
17
+ #
18
+ # 이식성: `timeout` 을 쓰지 않는다 — GNU coreutils 이고 stock macOS 에 없다(같은 날 selfcheck.sh
19
+ # 에서 실측으로 물린 defect). `head`/`sed` 만 쓴다.
20
+
21
+ # fh_hook_source — stdout 에 source 를 한 단어로 출력한다.
22
+ fh_hook_source() {
23
+ # TTY = 사람이 손으로 돌린 것. 페이로드가 없으니 읽으려 하면 매달린다.
24
+ if [ -t 0 ]; then printf 'unknown\n'; return 0; fi
25
+ local _p _s
26
+ _p="$(head -c 65536 2>/dev/null || true)"
27
+ _s="$(printf '%s' "$_p" \
28
+ | sed -n 's/.*"source"[[:space:]]*:[[:space:]]*"\([A-Za-z_]*\)".*/\1/p' \
29
+ | head -1)"
30
+ [ -n "$_s" ] || _s="unknown"
31
+ printf '%s\n' "$_s"
32
+ }
33
+
34
+ # fh_cadence_due — 캐던스 나그를 띄워도 되는 source 인가. 0=띄운다 / 1=억제.
35
+ # 억제 대상은 «같은 세션의 연속»인 두 값뿐이다. resume 은 며칠 뒤일 수 있으므로 띄운다.
36
+ fh_cadence_due() {
37
+ case "${1:-unknown}" in
38
+ clear|compact) return 1 ;;
39
+ *) return 0 ;;
40
+ esac
41
+ }
@@ -0,0 +1,239 @@
1
+ #!/usr/bin/env bash
2
+ # judgment_circuit_lint.sh — 정체성 문서가 **판단 회로**인지 **페르소나 선언**인지 기계로 가른다.
3
+ #
4
+ # ── 명제 (105런 실측, 2026-08-07~08) ──
5
+ # *"정체성 선언 ≠ 판단 회로."* 한 파일 안에 방향이 반대인 둘이 섞여 있고, 측정은 이렇게 갈렸다:
6
+ #
7
+ # ❌ 역할 부여 "너는 ~이다" Haiku 순손실(A 2.00 → B0 1.67) · **제거 시 +0.67 회복**(양 채점자 일치)
8
+ # 외부 앵커: arXiv 2311.10054 — 162 페르소나 · 2,410 MMLU · 9모델, 개선 없음
9
+ # ❌ 강도 주문 "꼼꼼히" 지적 2.8배 · **팬텀 3.1배** · 검출 +0.33 (n=3)
10
+ # 기전: 만족 조건이 모델 자기평가에 맡겨지고 degrade 방향이 무제약 → 분량으로 샌다
11
+ # ✅ 분기·봉인·충돌·방향·형태 전부 검증 가능. 이것이 회로다
12
+ #
13
+ # **우변은 전부 검증 가능하고 좌변은 전부 검증 불가능하다.** 그게 새는 것과 조이는 것의 경계다.
14
+ #
15
+ # ── 형제 관계 (셋째 판본을 만들지 마라) ──
16
+ # 강도 주문은 FH 독트린 *"prose grants discretion; degrade direction unconstrained"* 의 **프롬프트-층
17
+ # 판본**이다. 코드-층 판본은 `scripts/degrade_direction_scan.sh` 이고, 그건 `.py`/`.sh` 만 보고 `.md` 를
18
+ # 명시적으로 건너뛴다. 같은 명제 · 다른 코퍼스 · 겹침 0. 합치지도, 옆에 셋째를 만들지도 마라.
19
+ # (`harness-doctor --lint` 은 또 다른 축 — 저자의 **마케팅 voice**(self-marketing·cushion·version)를
20
+ # 본다. 어휘 교집합 0.)
21
+ #
22
+ # ── ⚠️ 이 계기의 가장 큰 오채점: 언급(mention) vs 사용(use) ──
23
+ # 금지 형식을 **설명하는** 문서(이 스크립트의 헤더 · 심지 설계 정본 · 금지어 표)는 그 어휘를 잔뜩
24
+ # 포함한다. 순진하게 grep 하면 **금지형식을 가르치는 문서가 최악 점수**를 받는다 — 계기가 자기를
25
+ # 채점하는 함정이고, 2026-08-08 세션이 실제로 밟았다(코퍼스를 자기 regex 에 맞춰 재단).
26
+ # 완화: 금지 마커(❌·🟥·금지·forbidden·never·안 된다·쓰지 마라)가 같은 줄에 있으면 **언급**으로 본다.
27
+ # ⚠️ **완화지 해결이 아니다** — 마커 없이 금지형식을 서술하는 줄은 여전히 오탐이고, 반대로 마커를
28
+ # 달아 놓고 실제로 쓰는 줄은 놓친다. 그래서 이 스크립트는 **advisory 지 게이트가 아니다.**
29
+ #
30
+ # ── 판정 ──
31
+ # CIRCUIT 금지 0 ∧ 허용형식 ≥3/5 — 판단 회로로 선다
32
+ # PARTIAL 그 사이
33
+ # PERSONA 금지 ≥1 ∧ 허용형식 ≤2/5 — 페르소나 선언이지 회로가 아니다
34
+ #
35
+ # ── 사용 ──
36
+ # bash scripts/judgment_circuit_lint.sh <file.md> [file.md ...]
37
+ # bash scripts/judgment_circuit_lint.sh --self-test
38
+ # ── 종료 코드 ──
39
+ # 0 CIRCUIT 2 PARTIAL/PERSONA (ADVISORY — 단독 차단 금지) 10 입력 불량
40
+ #
41
+ # 히트는 "버그다"가 아니라 **"이게 회로임을 증명하라"** 로 읽어라.
42
+
43
+ set -uo pipefail
44
+
45
+ # 금지 ①: 역할 부여 — "너는 ~이다" / "You are a ..."
46
+ RE_ROLE='(너는[[:space:]].*(이다|다\.|야\.|이야)|당신은[[:space:]].*(입니다|이다)|[Yy]ou are (a|an|the)[[:space:]])'
47
+ # 금지 ②: 강도 주문 — 만족 조건이 모델 자기평가에 맡겨지는 부사/명령
48
+ RE_INTENSITY='(꼼꼼히|철저히|신중하게|충분히|깊이를[[:space:]]*더|판단을[[:space:]]*잘|최대한|가능한[[:space:]]*한|반복해서[[:space:]]*보완|thoroughly|carefully|diligently|as much as possible|do your best|be sure to be)'
49
+ # 언급(mention) 마커 — 같은 줄에 있으면 사용이 아니라 설명으로 본다
50
+ RE_META='(❌|🟥|🚫|금지|forbidden|never|하지[[:space:]]*마라|쓰지[[:space:]]*마라|안[[:space:]]*된다|測|기각|반증|안티패턴|anti-pattern)'
51
+
52
+ # 허용 5형식 — 존재를 센다 (회로라면 이것들이 있어야 한다)
53
+ declare -a ALLOW_NAME=(분기규정 자기봉인 충돌해소 판단방향 형태강제)
54
+ declare -a ALLOW_RE=(
55
+ '(못[[:space:]]*찾으면|없으면|실패하면|아니면).*(하지[[:space:]]*마|중단|멈춰|금지|안[[:space:]]*된다)|if[[:space:]].*(not|missing|absent|unavailable).*(do not|stop|abort|block|never)|fail-closed|unless[[:space:]].*(do not|never)'
56
+ '(대상이[[:space:]]*아니다|범위[[:space:]]*밖|다루지[[:space:]]*않는다|out of scope|not the target of)'
57
+ '(상충하면|충돌하면|부딪히면).*(이긴다|우선|우세|앞선다)|(wins|takes precedence|overrides)'
58
+ '(불확실하면|모르면|판단이[[:space:]]*안[[:space:]]*서면|애매하면).*(기운다|쪽으로|보고|default)|when (uncertain|unclear|unsure).*(default|lean|err)|default(s)? to|degrade(s)? (toward|to)'
59
+ # ⚠️ 🟥 명명된 미커버: ALLOW_RE[3] 은 판단방향 형식의 **존재**를 셀 뿐 **방향을 안 본다.**
60
+ # `when unclear, default to PASS` 도 여기서 크레딧을 받는다 — 즉 이 계기는 degrade-toward-PASS
61
+ # 문서를 "판단 회로 있음"으로 셀 수 있다. cross-family 지적 (b), 2026-08-08. 옳은 지적이다.
62
+ #
63
+ # 그런데도 **DENY 로직을 제거했다.** 한 세션에서 세 판본을 시도했고 매번 오류가 방향만 바꿨다:
64
+ # · 무앵커 부분문자열 → `OK` 가 look·token·broken 에 걸려 정당한 fail-closed 문장의 크레딧을
65
+ # 뺏었다 ("default to blocking and look at the source" 가 4/5→3/5)
66
+ # · 단어경계로 조이자 → 굴절형이 새어 "default to passing the check" 가 2/5→3/5 로 **승격**,
67
+ # degrade-toward-PASS 문서를 회로로 인증했다 (net regression)
68
+ # · 명사형 `default to approval` 은 어느 판본으로도 안 잡혔다
69
+ # 세 번 다 같은 벽이다 — 재려는 성질이 **문장의 방향**인데 계기는 **키워드**다. 키워드는 방향을
70
+ # 판정할 수 없고, 조일수록 반대편 오류가 커진다. 정확도가 안 오르는데 복잡도만 오르는 구간에서는
71
+ # 조이는 게 아니라 줄이는 게 맞다([[feedback_convergence_measured_by_change_not_rounds]]).
72
+ #
73
+ # 그래서 방향 판정은 이 계기의 일이 아니다. 코드 층 판본은 `scripts/degrade_direction_scan.sh`
74
+ # 가 이미 갖고 있고(`.py`/`.sh` 만 본다), **산문 층의 방향은 지금 아무 계기도 커버하지 않는다.**
75
+ # 그게 잔여다 — 셋째 판본을 여기 붙이지 말고, 커버하려면 방향을 볼 수 있는 계기로 따로 지어라.
76
+ # 이 계기가 advisory 라서(게이트 아님) 이 미커버가 fail-open 으로 환전되지는 않는다.
77
+ '```|^\|.*\|.*\||출력[[:space:]]*(형식|칸|형태)|output format'
78
+ )
79
+
80
+ scan_file() {
81
+ local f="$1"
82
+ local role=0 inten=0 mention=0
83
+ local -a HITS=()
84
+
85
+ while IFS= read -r ln; do
86
+ local num="${ln%%:*}" txt="${ln#*:}"
87
+ if printf '%s' "$txt" | grep -qE "$RE_META"; then mention=$((mention+1)); continue; fi
88
+ # 인용부호 안의 금지어는 **지칭**이지 발화가 아니다. 판단 회로의 실제 지시는 문서 자신의
89
+ # 목소리로 인용 없이 나온다. 실측 2026-08-08: 실파일 첫 히트 3/3 이 전부 인용 안이었다
90
+ # (트리거 키워드 표 행 · `Not "..."` 부정문 · "나쁜 예시" 인용). 명명된 잔여: **인용부호로
91
+ # 감싸 쓴 진짜 지시는 놓친다** — 그 대가로 오탐을 막는다. advisory 라 방향이 이쪽이 맞다.
92
+ local bare; bare="$(printf '%s' "$txt" | sed -E 's/"[^"]*"/ /g; s/“[^”]*”/ /g; s/「[^」]*」/ /g; s/`[^`]*`/ /g')"
93
+ if ! printf '%s' "$bare" | grep -qE "$RE_ROLE|$RE_INTENSITY"; then
94
+ mention=$((mention+1)); continue
95
+ fi
96
+ # 분류도 $bare 로 한다. $txt 로 하면 "인용은 지칭"이라고 통과시켜 놓고 그 인용을 히트로
97
+ # 기록한다 — cross-family 지적 (d-2), 2026-08-08. 같은 손이 규칙과 테스트를 써서 못 봤다.
98
+ if printf '%s' "$bare" | grep -qE "$RE_ROLE"; then
99
+ role=$((role+1)); HITS+=(" 🟥 역할부여 $f:$num $(printf '%s' "$txt" | cut -c1-72)")
100
+ elif printf '%s' "$bare" | grep -qE "$RE_INTENSITY"; then
101
+ inten=$((inten+1)); HITS+=(" 🟥 강도주문 $f:$num $(printf '%s' "$txt" | cut -c1-72)")
102
+ fi
103
+ done < <(grep -nE "$RE_ROLE|$RE_INTENSITY" "$f" 2>/dev/null)
104
+
105
+ local cov=0; local -a MISSING=()
106
+ local i
107
+ for i in "${!ALLOW_RE[@]}"; do
108
+ _hit=$(grep -hoE "${ALLOW_RE[$i]}" "$f" 2>/dev/null | head -20)
109
+ if [ -n "$_hit" ]; then cov=$((cov+1))
110
+ else MISSING+=("${ALLOW_NAME[$i]}"); fi
111
+ done
112
+
113
+ local forb=$((role+inten)) verdict
114
+ if [ "$forb" -eq 0 ] && [ "$cov" -ge 3 ]; then verdict=CIRCUIT
115
+ elif [ "$forb" -ge 1 ] && [ "$cov" -le 2 ]; then verdict=PERSONA
116
+ else verdict=PARTIAL; fi
117
+
118
+ echo "── $f"
119
+ printf ' 판정: %-8s 금지 %d건 (역할 %d · 강도 %d) 허용형식 %d/5\n' \
120
+ "$verdict" "$forb" "$role" "$inten" "$cov"
121
+ [ "${#MISSING[@]}" -gt 0 ] && echo " 없는 형식: ${MISSING[*]}"
122
+ [ "$mention" -gt 0 ] && echo " (언급으로 제외한 줄: ${mention}건 — 금지 마커 동반)"
123
+ local h; for h in "${HITS[@]:-}"; do [ -n "$h" ] && echo "$h"; done
124
+ [ "$verdict" = CIRCUIT ] && return 0 || return 2
125
+ }
126
+
127
+ self_test() {
128
+ local T f=0 n=0 rc
129
+ T=$(mktemp -d); trap 'rm -rf "$T"' RETURN
130
+ t() { n=$((n+1)); if [ "$2" = "$3" ]; then echo "✅ $1 → $3"; else echo "❌ $1 → $3 (기대 $2)"; f=1; fi; }
131
+ v() { scan_file "$1" 2>/dev/null | awk '/판정:/{print $2}'; }
132
+
133
+ # known-positive ① 실측으로 기각된 형식만 있는 문서 = PERSONA
134
+ cat > "$T/persona.md" <<'EOF'
135
+ 너는 최고의 QA 엔지니어다.
136
+ 모든 항목을 꼼꼼히 검토하고 깊이를 더해라.
137
+ 충분히 반복해서 보완해라.
138
+ EOF
139
+ t "역할부여+강도주문만 → PERSONA" PERSONA "$(v "$T/persona.md")"
140
+
141
+ # known-negative ① 실측으로 채택된 5형식 = CIRCUIT
142
+ cat > "$T/circuit.md" <<'EOF'
143
+ 근거를 못 찾으면 그 항목을 보고하지 마라.
144
+ 성능 최적화는 이 작업의 대상이 아니다.
145
+ 원문 인용과 비중 추정이 상충하면 원문 인용이 이긴다.
146
+ 불확실하면 보고 쪽으로 기운다.
147
+ 출력 형식:
148
+ ```
149
+ [등급] 근거 | 위치
150
+ ```
151
+ EOF
152
+ t "허용 5형식만 → CIRCUIT" CIRCUIT "$(v "$T/circuit.md")"
153
+
154
+ scan_file "$T/circuit.md" >/dev/null 2>&1; rc=$?
155
+ t "CIRCUIT 는 exit 0" 0 "$rc"
156
+ scan_file "$T/persona.md" >/dev/null 2>&1; rc=$?
157
+ t "PERSONA 는 exit 2 (advisory)" 2 "$rc"
158
+
159
+ # ★ 언급 vs 사용 — 금지형식을 **가르치는** 문서가 최악 점수를 받으면 안 된다
160
+ cat > "$T/teaching.md" <<'EOF'
161
+ ❌ 금지: "너는 최고의 QA 다" 같은 역할 부여는 쓰지 마라.
162
+ ❌ 금지: "꼼꼼히", "깊이를 더해라" 같은 강도 주문은 안 된다.
163
+ 근거를 못 찾으면 보고하지 마라.
164
+ 성능은 이 작업의 대상이 아니다.
165
+ A 와 B 가 상충하면 A 가 이긴다.
166
+ EOF
167
+ t "금지형식을 가르치는 문서 → PERSONA 아님" CIRCUIT "$(v "$T/teaching.md")"
168
+
169
+ # ★ 그 완화가 진짜 히트까지 숨기면 안 된다 (완화의 known-negative)
170
+ cat > "$T/sneaky.md" <<'EOF'
171
+ ❌ 금지: 역할 부여는 쓰지 마라.
172
+ 너는 세계 최고의 보안 전문가다.
173
+ 모든 것을 꼼꼼히 본다.
174
+ EOF
175
+ local sn; sn="$(scan_file "$T/sneaky.md" 2>/dev/null | grep -c '🟥')"
176
+ [ "$sn" -ge 2 ] && rc=YES || rc=NO
177
+ t "마커 있는 줄만 제외 — 실사용 줄은 여전히 잡힌다" YES "$rc"
178
+
179
+ # 빈 파일: 금지 0 이지만 허용형식도 0 → CIRCUIT 이면 안 된다 (공허한 통과 방지)
180
+ : > "$T/empty.md"
181
+ t "빈 파일 → CIRCUIT 아님 (공허한 통과 금지)" PARTIAL "$(v "$T/empty.md")"
182
+
183
+ # ── 회귀 레인: 실파일 첫 실행에서 나온 오탐 3건 (2026-08-08, 전부 손으로 확인) ──
184
+ # 초판은 실파일 히트 3/3 이 오탐이었고 CLAUDE.md 를 PERSONA 로 오판정했다.
185
+ # 셋 다 **인용부호 안**이라는 공통 구조였다. 원문 그대로 픽스처로 박는다.
186
+ cat > "$T/fp_real.md" <<'EOF'
187
+ | "research this deeply", "survey the literature", "comprehensive analysis" | /deep-research |
188
+ **Therefore: do not commit FH assets from a worktree.** Not "carry the evidence in carefully"
189
+ 기계가 혼자 쓰면 "정확하고 신중하게 검증한다" 가 된다 ← 방향이 없다. 검증 불가
190
+ EOF
191
+ local fp; fp="$(scan_file "$T/fp_real.md" 2>/dev/null | grep -c '🟥')"
192
+ t "실파일 오탐 3건 재현 → 금지 0건" 0 "$fp"
193
+
194
+ # (d-2) 인용을 지칭으로 통과시켰으면 분류도 인용 밖에서 해야 한다
195
+ printf 'Not "너는 심사자이다"; 꼼꼼히 검토하라\n' > "$T/mixed.md"
196
+ local mk; mk="$(scan_file "$T/mixed.md" 2>/dev/null | grep -oE '역할부여|강도주문' | head -1)"
197
+ t "인용 안 역할부여는 세지 않고 실사용 강도주문만" "강도주문" "$mk"
198
+
199
+ # ── 🟥 미커버를 **못박는** 레인 (2026-08-08, DENY 제거와 함께 방향이 뒤집혔다) ──
200
+ # 이 계기는 판단방향 형식의 **존재**만 세고 **방향은 안 본다**. 그건 결함이 아니라 결정이다 —
201
+ # 키워드로 방향을 판정하려던 세 판본이 전부 반대편 오류로 갈아탔고(관대문 누락 ↔ 보수문 강등),
202
+ # 정확도가 안 오르는데 복잡도만 오르는 구간이라 조이는 대신 줄였다. 근거는 헤더 ALLOW_RE[3] 주석.
203
+ #
204
+ # ⚠️ 이 레인들을 **삭제하지 마라.** 삭제하면 미커버가 침묵이 되고, 다음 사람이 "방향도 보겠지"
205
+ # 라고 가정하게 된다. 기대값을 뒤집어 남겨 두면 미커버가 **기계로 기록**되고, 누군가 넷째 DENY
206
+ # 판본을 붙이는 순간 여기가 빨개져서 *의도적* 미커버를 건드렸다고 알려준다.
207
+ # ⚠️ **커버리지 숫자에 건다. verdict 에 걸면 못 본다** — 다른 형식이 3개 있으면 판단방향 크레딧
208
+ # 유무와 무관하게 verdict 는 CIRCUIT 로 같다. 지난 라운드 레인이 verdict 에 걸려서 구멍을 놓쳤다.
209
+ _cov() { # $1=판단방향 자리에 넣을 문장 → "n/5"
210
+ local d; d="$(mktemp -d)"
211
+ printf 'When unclear, default to %s.\n근거를 못 찾으면 보고하지 마라.\n성능은 이 작업의 대상이 아니다.\nA 와 B 가 상충하면 A 가 이긴다.\n' "$1" > "$d/x.md"
212
+ scan_file "$d/x.md" 2>/dev/null | awk '/판정:/{print $NF}'
213
+ rm -rf "$d"
214
+ }
215
+ # 관대 4종과 보수 2종이 **같은 4/5** 를 받는다 = 이 계기는 방향을 구분하지 않는다.
216
+ # 그 동일성 자체가 여기서 검증되는 성질이다.
217
+ t "🟥 미커버: 관대 'PASS' 도 크레딧 받음" "4/5" "$(_cov 'PASS')"
218
+ t "🟥 미커버: 관대 'passing' 굴절형도 크레딧" "4/5" "$(_cov 'passing the check')"
219
+ t "🟥 미커버: 관대 'approval' 명사형도 크레딧" "4/5" "$(_cov 'approval')"
220
+ t "🟥 미커버: 관대 'allowing' 도 크레딧" "4/5" "$(_cov 'allowing the operation')"
221
+ t "보수 'blocking' 도 같은 크레딧 (방향 무구분)" "4/5" "$(_cov 'blocking and look at the source')"
222
+ t "보수 'token/broken' 도 같은 크레딧" "4/5" "$(_cov 'blocking on a broken token')"
223
+
224
+ echo
225
+ [ "$f" -eq 0 ] && echo "✅ 캘리브레이션 통과 ($n 쌍)" || echo "❌ 캘리브레이션 실패 ($n 쌍)"
226
+ return "$f"
227
+ }
228
+
229
+ [ "${1:-}" = "--self-test" ] && { self_test; exit $?; }
230
+ [ "$#" -eq 0 ] && { echo "usage: $0 <file.md> [...]"; echo " $0 --self-test"; exit 10; }
231
+
232
+ WORST=0
233
+ for target in "$@"; do
234
+ [ -f "$target" ] || { echo "🟥 파일 없음: $target"; exit 10; }
235
+ scan_file "$target" || WORST=2
236
+ echo
237
+ done
238
+ echo "ⓘ ADVISORY — 단독으로 차단하지 마라. 히트는 '이게 회로임을 증명하라'는 뜻이다."
239
+ exit "$WORST"
@@ -0,0 +1,193 @@
1
+ #!/usr/bin/env bash
2
+ # novelty_claim_check.sh — **신규성·부재 주장**이 외부 앵커 없이 서 있는지 기계로 잡는다.
3
+ #
4
+ # ── 왜 (실측된 결함) ──
5
+ # 2026-08-08 세션: 내부 grep 만 돌리고 `net-new` 를 단정했다가 철회했다. 외부 조회는 운영자가
6
+ # *"지금 물어봐"* 라고 해서 비로소 돌았다. `persona-innovator` Mode F 는 존재하지만
7
+ # **context-entry 제안**이지 짓기 직전 자동 발화가 아니다.
8
+ #
9
+ # "짓기 직전"은 **의도 트리거**라 훅으로 못 잡는다(go-public 표면과 같은 부류). 그래서 이 계기는
10
+ # 발화를 강제하지 않는다 — 대신 **안 물어본 흔적을 커밋 시점에 보이게** 만든다.
11
+ # `[[feedback_shift_left_ledger_not_rule]]`: 일반 규율은 살리언스 의존이라 이미 실패했고,
12
+ # 구체 목록은 놓칠 여지가 없다. 규율을 한 번 더 적는 대신 원장을 앞단에 놓는다.
13
+ #
14
+ # ── 왜 하필 이 주장 클래스인가 (방향 비대칭) ──
15
+ # `[[feedback_source_grounding_direction_asymmetry]]`: 결함 주장(FAIL)에는 소스 확인을 요구하면서
16
+ # 강점·신규성 주장(PASS)은 무검증으로 통과시켜 왔다 = **낙관 방향만 뚫린다.**
17
+ # 그리고 부재 주장은 구조적으로 더 나쁘다 — **인용할 대상이 없어서** 팬텀 검사가 원리적으로 안 걸린다
18
+ # (phantom-quench 는 *인용된 것이 주장을 지지하는가*를 보지, *인용이 아예 없는 부재 주장*은 못 본다).
19
+ # 이 스크립트는 그 빈칸 하나만 본다. 겹침 없음.
20
+ #
21
+ # ── 이 계기가 재는 것과 못 재는 것 ──
22
+ # 잰다: 신규성/부재 주장 옆에 외부 앵커가 있는가 (±N 줄)
23
+ # 못 잰다: 그 앵커가 **그 주장을 실제로 지지하는가** — 그건 phantom-quench 의 일이다.
24
+ # 앵커 존재가 진위를 못 본다(`[[feedback_metric_measures_presence_not_relation]]`).
25
+ # 즉 통과는 "물어는 봤다"까지지 "맞다"가 아니다.
26
+ #
27
+ # ── 사용 ──
28
+ # bash scripts/novelty_claim_check.sh <file.md> [...]
29
+ # bash scripts/novelty_claim_check.sh --self-test
30
+ # ── 종료 코드 ──
31
+ # 0 전건 앵커 있음 2 무앵커 주장 있음 (ADVISORY — 단독 차단 금지) 10 입력 불량
32
+
33
+ set -uo pipefail
34
+
35
+ # ⚠️ 레인에서 `cmd | grep -q` 를 쓰지 마라. `grep -q` 는 매치 즉시 파이프를 닫고, `pipefail` 이
36
+ # 그 SIGPIPE(141)를 파이프라인 실패로 렌더한다 — **출력이 맞는데 레인이 빨개지고, 경합이라 flaky 하다**
37
+ # (2026-08-08 실측: 같은 레인이 실행마다 갈렸다). 출력을 변수로 캡처한 뒤 `case` 로 검사한다.
38
+
39
+ WINDOW=6 # 앵커 탐색 반경(줄). 좁히면 오탐↑, 넓히면 미탐↑ — 6은 FH 문서의 문단 길이 기준
40
+
41
+ # 강한 신규성·부재 주장만. 일반 부정("없다")은 한국어 문서에 편재해서 넣으면 Grep-Collision
42
+ # Treadmill 이 된다(`[[feedback_typed_verdict_channel]]`). 단정 어휘만 좁게 잡는다.
43
+ # ⚠️ `net-new` 는 FH 내부 어휘에서 **두 뜻으로 과부하**돼 있다 (실측 2026-08-08, 손 확인 6/6 오탐):
44
+ # ① 세상에 없다 = 신규성 주장 → 외부 앵커 필요
45
+ # ② 이 레인이 새로 찾은 지적("cross-family 가 net-new finding 3건") → 앵커 불요, 로컬 카운트다
46
+ # 그래서 `net-new` 단독은 주장으로 안 센다 — **세계-스코프 한정어와 같은 줄일 때만** 센다.
47
+ # 나머지 문구(선례 없·상용 0개·no prior art…)는 그 자체로 세계-스코프라 단독으로 센다.
48
+ RE_WORLD='(시장|학계|외부|선례|전례|상용|업계|prior art|state of the art|SOTA|공개된|published)'
49
+ RE_CLAIM='(선례[[:space:]]*(가[[:space:]]*)?없|전례[[:space:]]*(가[[:space:]]*)?없|유일하|전무하|존재하지[[:space:]]*않는다|시장에[[:space:]]*(는[[:space:]]*)?없|학계엔?[[:space:]]*(는[[:space:]]*)?없|상용[[:space:]]*0개|no prior art|first of its kind|unprecedented|nothing comparable|does not exist anywhere)'
50
+
51
+ # 외부 앵커 — "세계에 물어본" 흔적
52
+ RE_ANCHOR='(arXiv|arxiv|DOI|doi:|https?://|WebSearch|WebFetch|서베이|외부[[:space:]]*(조회|앵커|문헌)|T1|T2|Sources?:|출처|원문[[:space:]]*확인)'
53
+
54
+ # 언급(mention) 마커 — 규칙 자체를 서술하는 줄
55
+ RE_META='(❌|🟥|🚫|금지|forbidden|never|안티패턴|anti-pattern|예:|example:|RE_CLAIM|RE_ANCHOR)'
56
+
57
+ scan_file() {
58
+ local f="$1" total=0 bad=0 meta=0
59
+ local -a HITS=()
60
+ # `wc -l` 은 **개행을 센다** — 마지막 줄에 개행이 없으면 한 줄 적게 나오고, 앵커 창 상한이
61
+ # 한 줄 짧아져 **주장 자기 줄을 창 밖으로 밀어낸다** → 거짓 무앵커(high 재리뷰 #10).
62
+ # `git show :file` 이 blob 바이트를 그대로 쓰므로 pre-commit 경로에서 실제로 도달 가능하다.
63
+ local nlines; nlines=$(awk 'END{print NR}' "$f" 2>/dev/null)
64
+ [ -z "$nlines" ] && nlines=0
65
+
66
+ while IFS= read -r ln; do
67
+ local num="${ln%%:*}" txt="${ln#*:}"
68
+ # ⚠️ 초판은 meta 줄을 **아무 데도 안 세고** continue 했다. 그래서 주장 3건이 전부 meta 어휘를
69
+ # 동반하면 출력이 "주장 없음" 이 된다 — 부분 미스캔을 **깨끗한 코퍼스로** 렌더하는 것이고,
70
+ # 이 계기가 막으라고 존재하는 `not found ≠ 0` 그 자체다(high 리뷰 #9).
71
+ if printf '%s' "$txt" | grep -qE "$RE_META"; then meta=$((meta+1)); continue; fi
72
+ # 인용부호 안의 주장은 지칭이지 발화가 아니다 (judgment_circuit_lint 와 동일 규칙)
73
+ local bare; bare="$(printf '%s' "$txt" | sed -E 's/"[^"]*"/ /g; s/“[^”]*”/ /g; s/「[^」]*」/ /g; s/`[^`]*`/ /g')"
74
+ if printf '%s' "$bare" | grep -qE "$RE_CLAIM"; then :
75
+ elif printf '%s' "$bare" | grep -qE 'net-new' && printf '%s' "$bare" | grep -qE "$RE_WORLD"; then :
76
+ else continue; fi
77
+ total=$((total+1))
78
+ local lo=$(( num - WINDOW )); [ "$lo" -lt 1 ] && lo=1
79
+ local hi=$(( num + WINDOW )); [ "$hi" -gt "$nlines" ] && hi="$nlines"
80
+ if sed -n "${lo},${hi}p" "$f" 2>/dev/null | grep -qE "$RE_ANCHOR"; then
81
+ HITS+=(" ✅ 앵커있음 $f:$num $(printf '%s' "$txt" | cut -c1-60)")
82
+ else
83
+ bad=$((bad+1))
84
+ HITS+=(" 🟥 무앵커 $f:$num $(printf '%s' "$txt" | cut -c1-60)")
85
+ fi
86
+ done < <(grep -nE "$RE_CLAIM|net-new" "$f" 2>/dev/null)
87
+
88
+ echo "── $f"
89
+ if [ "$total" -eq 0 ]; then
90
+ if [ "$meta" -gt 0 ]; then
91
+ echo " 신규성·부재 주장 0건 — 단 ${meta}줄을 언급(meta)으로 제외했다. **전수 스캔이 아니다.**"
92
+ echo " 제외된 줄에 실제 주장이 있을 수 있다 — 미스캔이지 깨끗함이 아니다."
93
+ else
94
+ echo " 신규성·부재 주장 없음"
95
+ fi
96
+ return 0
97
+ fi
98
+ printf ' 주장 %d건 · 무앵커 %d건 · 언급제외 %d줄 (앵커 반경 ±%d줄)\n' "$total" "$bad" "$meta" "$WINDOW"
99
+ local h; for h in "${HITS[@]:-}"; do [ -n "$h" ] && echo "$h"; done
100
+ [ "$bad" -eq 0 ] && return 0 || return 2
101
+ }
102
+
103
+ self_test() {
104
+ local T f=0 n=0 rc
105
+ T=$(mktemp -d); trap 'rm -rf "$T"' RETURN
106
+ t() { n=$((n+1)); if [ "$2" = "$3" ]; then echo "✅ $1 → $3"; else echo "❌ $1 → $3 (기대 $2)"; f=1; fi; }
107
+ bad() { scan_file "$1" 2>/dev/null | awk '/무앵커 [0-9]+건/{for(i=1;i<=NF;i++) if($i=="·"){print $(i+2)}}' | tr -d '건'; }
108
+
109
+ # known-positive: 앵커 없는 부재 주장
110
+ printf '이 패턴은 선례가 없다.\n그래서 우리가 처음 만든다.\n' > "$T/ungrounded.md"
111
+ scan_file "$T/ungrounded.md" >/dev/null 2>&1; rc=$?
112
+ t "무앵커 부재 주장 → exit 2" 2 "$rc"
113
+
114
+ # known-negative: 같은 주장 + 외부 앵커
115
+ printf '이 패턴은 선례가 없다 (arXiv 2607.08032 서베이가 그렇게 진술한다).\n' > "$T/grounded.md"
116
+ scan_file "$T/grounded.md" >/dev/null 2>&1; rc=$?
117
+ t "앵커 동반 주장 → exit 0" 0 "$rc"
118
+
119
+ # 앵커가 반경 안/밖일 때 갈리는가 (계기가 반경을 실제로 쓰는지)
120
+ { printf '이 패턴은 선례가 없다.\n'; for i in $(seq 1 12); do echo "채움 $i"; done; echo "출처: https://example.org"; } > "$T/far.md"
121
+ scan_file "$T/far.md" >/dev/null 2>&1; rc=$?
122
+ t "앵커가 반경 밖 → 여전히 무앵커" 2 "$rc"
123
+ { printf '이 패턴은 선례가 없다.\n'; echo "출처: https://example.org"; } > "$T/near.md"
124
+ scan_file "$T/near.md" >/dev/null 2>&1; rc=$?
125
+ t "앵커가 반경 안 → 통과" 0 "$rc"
126
+
127
+ # 주장 자체가 없는 문서는 통과 (과발화 방지)
128
+ printf '오늘은 훅을 배선했다. 캘리브레이션 8/8.\n' > "$T/plain.md"
129
+ scan_file "$T/plain.md" >/dev/null 2>&1; rc=$?
130
+ t "주장 없는 문서 → 무발화 통과" 0 "$rc"
131
+
132
+ # 일반 부정은 잡으면 안 된다 (Grep-Collision 방지)
133
+ printf '이 파일에는 마커가 없다. 훅은 차단하지 않는다.\n' > "$T/plainneg.md"
134
+ _o="$(scan_file "$T/plainneg.md" 2>/dev/null)"; case "$_o" in *"주장 없음"*) rc=YES ;; *) rc=NO ;; esac
135
+ t "일반 부정('마커가 없다')은 주장 아님" YES "$rc"
136
+
137
+ # 인용 안의 주장은 지칭 (judgment_circuit_lint 와 같은 규칙)
138
+ printf '초판은 "이 패턴은 선례가 없다" 고 적었다가 철회했다.\n' > "$T/quoted.md"
139
+ _o="$(scan_file "$T/quoted.md" 2>/dev/null)"; case "$_o" in *"주장 없음"*) rc=YES ;; *) rc=NO ;; esac
140
+ t "인용 안의 주장은 지칭으로 제외" YES "$rc"
141
+
142
+ # ── 회귀 레인: `net-new` 과부하 (2026-08-08 실측, 손 확인 6/6 오탐) ──
143
+ # 원문 그대로. 로컬 발견 라벨은 주장이 아니다.
144
+ cat > "$T/netnew_local.md" <<'EOF'
145
+ The one net-new finding was the most severe one.
146
+ terra 6/6 known-positives + 5 net-new (approval-negation regex, heredoc tail-drop)
147
+ Second consecutive session where the cross-family leg produced net-new findings.
148
+ EOF
149
+ _o="$(scan_file "$T/netnew_local.md" 2>/dev/null)"; case "$_o" in *"주장 없음"*) rc=YES ;; *) rc=NO ;; esac
150
+ t "net-new 로컬 발견 라벨 3건 → 주장 아님" YES "$rc"
151
+
152
+ # 반대편: 세계-스코프 net-new 는 여전히 잡혀야 한다 (완화가 진짜를 숨기면 안 된다)
153
+ printf 'net-new 다 — 시장에 이런 상용 제품이 하나도 없다.\n' > "$T/netnew_world.md"
154
+ scan_file "$T/netnew_world.md" >/dev/null 2>&1; rc=$?
155
+ t "세계-스코프 net-new 는 여전히 주장" 2 "$rc"
156
+
157
+ # 회귀 레인: #9 meta 제외분이 "주장 없음" 으로 렌더되면 안 된다 (high 리뷰 실측)
158
+ cat > "$T/metahidden.md" <<'EOF'
159
+ 이 아키텍처는 선례가 없다 — 그러니 절대 relabel 하지 마라, never.
160
+ 시장에 이런 상용 제품은 존재하지 않는다. 금지 사항은 아래와 같다.
161
+ EOF
162
+ _o="$(scan_file "$T/metahidden.md" 2>/dev/null)"; case "$_o" in *"전수 스캔이 아니다"*) rc=YES ;; *) rc=NO ;; esac
163
+ t "#9 meta 제외분이 있으면 '깨끗함' 으로 렌더 안 한다" YES "$rc"
164
+ case "$_o" in *"신규성·부재 주장 없음"*) rc=YES ;; *) rc=NO ;; esac
165
+ t "#9 '주장 없음' 단독 출력은 안 나온다" NO "$rc"
166
+
167
+ # #10 마지막 줄에 개행이 없어도 앵커 창이 자기 줄을 포함해야 한다 (high 재리뷰)
168
+ printf '더미 줄\n이 패턴은 선례가 없다 (arXiv 2607.08032 서베이).' > "$T/nonl.md"
169
+ scan_file "$T/nonl.md" >/dev/null 2>&1; rc=$?
170
+ t "#10 개행 없는 마지막 줄도 앵커가 잡힌다" 0 "$rc"
171
+ printf '더미 줄\n이 패턴은 선례가 없다 (arXiv 2607.08032 서베이).\n' > "$T/wnl.md"
172
+ scan_file "$T/wnl.md" >/dev/null 2>&1; rc=$?
173
+ t "#10 개행 있는 판본과 동일 판정" 0 "$rc"
174
+
175
+ echo
176
+ [ "$f" -eq 0 ] && echo "✅ 캘리브레이션 통과 ($n 쌍)" || echo "❌ 캘리브레이션 실패 ($n 쌍)"
177
+ return "$f"
178
+ }
179
+
180
+ [ "${1:-}" = "--self-test" ] && { self_test; exit $?; }
181
+ [ "$#" -eq 0 ] && { echo "usage: $0 <file.md> [...]"; echo " $0 --self-test"; exit 10; }
182
+
183
+ WORST=0
184
+ for target in "$@"; do
185
+ [ -f "$target" ] || { echo "🟥 파일 없음: $target"; exit 10; }
186
+ scan_file "$target" || WORST=2
187
+ done
188
+ [ "$WORST" -eq 2 ] && {
189
+ echo
190
+ echo "ⓘ ADVISORY — 차단하지 않는다. 무앵커 주장은 '틀렸다'가 아니라 **'아직 안 물어봤다'** 다."
191
+ echo " 통과해도 앵커가 그 주장을 지지하는지는 별도(phantom-quench) — 존재가 진위를 못 본다."
192
+ }
193
+ exit "$WORST"