@chrono-meta/fh-gate 3.0.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/regression/probes_live.yaml +137 -0
- package/.claude/rules/.residency-patterns.defaults +7 -0
- package/.claude/rules/fh_4axis_gate.md +50 -1
- package/.claude-plugin/marketplace.json +8 -2
- package/AGENTS.md +27 -0
- package/CATALOG.md +17 -0
- package/CLAUDE.md +12 -2
- package/README.ja.md +51 -7
- package/README.ko.md +48 -7
- package/README.md +37 -5
- package/README.zh.md +45 -8
- package/docs/STANDARDS_ALIGNMENT.md +61 -0
- package/docs/USER_GUIDE.md +3 -0
- package/docs/USE_CASES.md +50 -0
- package/docs/model_tier_expectations.md +60 -0
- package/knowledge/shared/harness-core/fh_three_layer_canon.md +30 -10
- package/knowledge/shared/harness-core/field_verdict_crossfamily_gate.md +8 -0
- package/knowledge/shared/harness-core/harness_incubator_doctrine.md +9 -0
- package/knowledge/shared/harness-core/iso_ai_standards_crosswalk.md +139 -0
- package/knowledge/shared/harness-core/measurement-integrity-checklist.md +20 -0
- package/knowledge/shared/learnings/subagent_invocations_log.yaml +441 -4
- package/package.json +40 -2
- package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-commons/skills/preprep/README.md +4 -1
- package/plugins/fh-commons/skills/preprep/SKILL.md +99 -3
- package/plugins/fh-commons/skills/preprep/diagram_from_json.py +154 -0
- package/plugins/fh-commons/skills/preprep/fixtures/fixture_R3_negative.pptx +0 -0
- package/plugins/fh-commons/skills/preprep/fixtures/fixture_R3_positive.pptx +0 -0
- package/plugins/fh-commons/skills/preprep/fixtures/mk_slide_fixtures.py +179 -0
- package/plugins/fh-commons/skills/preprep/interslide_deps.py +98 -9
- package/plugins/fh-commons/skills/preprep/lane_adjacent_dup.py +4 -1
- package/plugins/fh-commons/skills/preprep/lane_diagram.py +95 -0
- package/plugins/fh-commons/skills/preprep/lane_geometry.py +181 -0
- package/plugins/fh-commons/skills/preprep/lane_promise.py +4 -1
- package/plugins/fh-commons/skills/preprep/lane_slide_refs.py +134 -0
- package/plugins/fh-commons/skills/preprep/lane_slide_relations.py +300 -0
- package/plugins/fh-commons/skills/preprep/preprep.py +129 -8
- package/plugins/fh-commons/skills/preprep/preprep_wire.py +210 -0
- package/plugins/fh-commons/skills/preprep/presentation_checklist.md +4 -0
- package/plugins/fh-commons/skills/preprep/surfaces.example.yaml +18 -0
- package/plugins/fh-commons/skills/preprep/test_preprep_lanes_rp.py +184 -0
- package/plugins/fh-meta/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-meta/CHANGELOG.md +43 -1
- package/plugins/fh-meta/skills/auto-decorrelation/SKILL.md +87 -2
- package/plugins/fh-meta/skills/frontier-digest/SKILL.md +2 -2
- package/plugins/fh-meta/skills/frontier-digest/SKILL_detail.md +25 -5
- package/plugins/fh-meta/skills/hub-cc-pr-reviewer/SKILL.md +11 -0
- package/plugins/fh-meta/skills/hub-cc-pr-reviewer/SKILL_detail.md +3 -0
- package/plugins/fh-qp/.claude-plugin/plugin.json +22 -0
- package/plugins/fh-qp/README.md +71 -0
- package/plugins/fh-qp/fixtures/evidence_known_clean.txt +3 -0
- package/plugins/fh-qp/fixtures/evidence_known_dirty.txt +6 -0
- package/plugins/fh-qp/fixtures/reach_known_partial.tsv +4 -0
- package/plugins/fh-qp/fixtures/reach_known_reached.tsv +4 -0
- package/plugins/fh-qp/fixtures/reach_known_wall.tsv +3 -0
- package/plugins/fh-qp/fixtures/verdicts_known_bad_branch.tsv +2 -0
- package/plugins/fh-qp/fixtures/verdicts_known_bad_vacuous_machine.tsv +2 -0
- package/plugins/fh-qp/fixtures/verdicts_known_good.tsv +4 -0
- package/plugins/fh-qp/fixtures/verdicts_verify_only.tsv +3 -0
- package/plugins/fh-qp/qp_profile.example.yaml +29 -0
- package/plugins/fh-qp/scripts/qp_tools.sh +216 -0
- package/plugins/fh-qp/skills/qp/SKILL.md +65 -0
- package/plugins/fh-qp/skills/qp-plan/SKILL.md +42 -0
- package/plugins/fh-qp/skills/qp-regress/SKILL.md +45 -0
- package/plugins/fh-qp/skills/qp-run/SKILL.md +49 -0
- package/scripts/chamber_run.sh +14 -5
- package/scripts/com.forge-harness.live-eval.plist +84 -0
- package/scripts/compaction_probe.sh +9 -35
- package/scripts/directional_diff_gate.sh +14 -2
- package/scripts/frontier_digest_autopilot.sh +4 -1
- package/scripts/map_postprocess.py +90 -0
- package/scripts/outbound_query_guard.sh +131 -0
- package/scripts/outbound_query_hook.sh +373 -0
- package/scripts/package_coverage_check.sh +55 -16
- package/scripts/pipe_verdict_guard.sh +41 -1
- package/scripts/probe_live_eval.sh +240 -0
- package/scripts/probe_live_eval_lib.py +579 -0
- package/scripts/proposal_hook.sh +120 -17
- package/scripts/push_zone_check.sh +78 -0
- package/scripts/residency_closure_scan.py +252 -0
- package/scripts/selfcheck.sh +41 -1
- package/scripts/session_close_check.sh +100 -0
- package/scripts/sim_isolated_run.sh +98 -2
- package/scripts/test_action_yml_lanes.sh +97 -0
- package/scripts/test_fh_qp_lanes.sh +105 -0
- package/scripts/test_gate_two_verdicts_lanes.sh +139 -0
- package/scripts/test_map_postprocess_lanes.sh +143 -0
- package/scripts/test_marker_affected_lanes.sh +93 -0
- package/scripts/test_marker_crossfamily_lanes.sh +90 -6
- package/scripts/test_marker_oracle_lanes.sh +136 -0
- package/scripts/test_outbound_query_hook_lanes.sh +433 -0
- package/scripts/test_outbound_query_lanes.sh +87 -0
- package/scripts/test_pipe_verdict_guard_lanes.sh +26 -0
- package/scripts/test_preprep_diagram_lanes.sh +87 -0
- package/scripts/test_preprep_drift_anchor.sh +3 -3
- package/scripts/test_preprep_slide_refs_lanes.sh +169 -0
- package/scripts/test_probe_live_eval_lanes.sh +437 -0
- package/scripts/test_proposal_hook_lanes.sh +22 -1
- package/scripts/test_push_zone_lanes.sh +304 -0
- package/scripts/test_residency_closure_lanes.sh +70 -0
- package/scripts/test_sim_isolated_run_lanes.sh +119 -0
- package/scripts/test_utterance_intake_lanes.sh +414 -0
- package/scripts/test_worktree_reclaim_lanes.sh +70 -0
- package/scripts/transcript_utterances.py +222 -0
- package/scripts/utterance_intake.sh +424 -0
- package/scripts/validate_yaml.sh +27 -0
- package/scripts/worktree_reclaim.sh +95 -0
- package/templates/.git-hooks/pre-commit +353 -1
- package/templates/.git-hooks/pre-push +91 -0
- package/templates/RED_TEAM_REPORT.md +49 -0
- package/templates/settings.PreToolUse.snippet.json +65 -1
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""transcript_utterances.py — Claude Code 전사본(jsonl) → 운영자 발화.
|
|
3
|
+
|
|
4
|
+
WHY THIS FILE EXISTS. 이 추출 로직은 `compaction_probe.sh` 안에 heredoc 으로 인라인돼 있었고,
|
|
5
|
+
「받는 절반」(발화 → 기록 착지) 을 배선하려면 두 번째 소비처가 필요했다. 사본을 두 벌 두는 것은
|
|
6
|
+
`[[feedback_divergent_leniency_duplicate_normalizers]]` — 전처리가 갈리면 **한쪽만 통과하는
|
|
7
|
+
입력이 다른 쪽에서 무음 드롭된다**. 그래서 파일 하나로 꺼낸다.
|
|
8
|
+
|
|
9
|
+
🟥 SEAL 출력은 바이트 동일해야 한다. `--format seal` 은 인라인 판본을 **그대로** 재현한다
|
|
10
|
+
(줄 형식 · 합계/제외 문구 · 카운터 이름까지). `scripts/test_utterance_intake_lanes.sh` L10 이
|
|
11
|
+
골든으로 고정하고, L12/L12b 가 «이 파일이 정말 seal 경로에 있는가» 를 되돌림으로 잰다.
|
|
12
|
+
|
|
13
|
+
── 무엇을 발화로 세는가 (그리고 왜) ──────────────────────────────────────────
|
|
14
|
+
`type=user` 는 **사람 발화가 아니다** — 툴 결과 · peer 메시지 · task 알림이 같은 타입으로 온다
|
|
15
|
+
(`[[feedback_type_user_is_not_human_utterance.md]]`: 39 vs 실제 30). 실측(2026-09-05, 이 레포의
|
|
16
|
+
전사본 전수): `<task-notification` 3791 · `<cross-session-message` 1467 · `<local-command-*` 232 —
|
|
17
|
+
전부 `<` 로 시작하므로 봉투 접두 규칙이 잡는다.
|
|
18
|
+
· content 가 str → 발화 후보
|
|
19
|
+
· content 가 list → tool_result 블록이 하나라도 있으면 **툴 결과**(제외).
|
|
20
|
+
아니면 text 블록만 이어붙인다. 🟥 이미지 첨부 실발화가 list 다 —
|
|
21
|
+
「list = 툴결과」로 가르면 그런 발화가 구조적으로 안 보인다.
|
|
22
|
+
· text 블록이 없음 → 이미지 전용 등. **세되 싣지 않는다**(지우지 않는다).
|
|
23
|
+
· `<` 또는 `/` 로 시작 → 메타 봉투 · 슬래시 커맨드(제외)
|
|
24
|
+
· `--min-chars N` 미만 → 짧은 응답(«ㄱㄱ» «응» «승인»). **별도 카운터**로 센다.
|
|
25
|
+
· `--strip-system-markers` → 아래 §시스템 마커. **기본 OFF** — 켜면 seal 출력이 바뀐다.
|
|
26
|
+
|
|
27
|
+
🟥 제외분을 반드시 인쇄한다. 합계만 찍으면 그 원장이 완전한 것처럼 읽힌다 —
|
|
28
|
+
`[[feedback_not_found_is_not_zero_family]]`.
|
|
29
|
+
|
|
30
|
+
── 사용 ──────────────────────────────────────────────────────────────────────
|
|
31
|
+
python3 scripts/transcript_utterances.py <transcript.jsonl> [--min-chars N]
|
|
32
|
+
[--format tsv|seal] [--strip-system-markers]
|
|
33
|
+
tsv (기본) : `n<TAB>ts<TAB>text` 한 줄 한 발화. ts 가 없으면 `-`.
|
|
34
|
+
seal : compaction_probe seal 블록 (바이트 동일)
|
|
35
|
+
종료코드: 0 정상 · 2 인자/파일 문제(호출부의 «파싱 실패» 폴백이 여기서 뜬다)
|
|
36
|
+
|
|
37
|
+
이식성: 표준 라이브러리만. python3.6+ (f-string).
|
|
38
|
+
"""
|
|
39
|
+
import json
|
|
40
|
+
import re
|
|
41
|
+
import sys
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# ── 시스템 마커 (실측 2026-09-05, 이 레포 전사본 전수 — 대괄호 접두 171 건) ──────────────
|
|
45
|
+
# `type=user` 로 오지만 **운영자가 친 것이 아닌** 것들. 개수를 세서 골랐다:
|
|
46
|
+
# [Cross-session delivery notice] 66 · [Request interrupted by user] 45 ·
|
|
47
|
+
# [automated-run: launchd] 31 · [Image #N] 17 · [Cross-session idle notice] 4
|
|
48
|
+
# 정책 두 갈래 — ⓐ **통째로 버릴 것**(발신자가 사람이 아니다) ⓑ **마커만 벗길 것**(뒤에 실발화가 온다).
|
|
49
|
+
# 🟥 `[자율 루프 …]` 8 건은 **일부러 안 넣었다** — 운영자가 쓴 무인 실행 프롬프트라 과포함(안전)
|
|
50
|
+
# 쪽에 둔다. 과소포함은 무음 손실이라 방향이 다르다.
|
|
51
|
+
# 🟥 기본 OFF. 켜면 seal 출력이 바뀌므로 `--strip-system-markers` 를 준 소비처(intake)만 받는다.
|
|
52
|
+
# 🟥 압축 이어붙임 프리앰블도 `type=user` 로 온다 — 실측 21 건, 그리고 **첫 실사용에서
|
|
53
|
+
# 미착지 목록에 그대로 떴다**(2026-09-05). 런타임이 주입한 요약이지 운영자 발화가 아니다.
|
|
54
|
+
# 괄호로 시작하지 않아 앞의 대괄호 규칙이 구조적으로 못 잡는다 — 별도 접두로 센다.
|
|
55
|
+
_DROP_PREFIXES = ('[Cross-session delivery notice]', '[Cross-session idle notice]',
|
|
56
|
+
'[automated-run:',
|
|
57
|
+
'This session is being continued from a previous conversation')
|
|
58
|
+
_STRIP_MARKER = re.compile(r'^\[(?:Image|Request interrupted by user)[^\]]*\]\s*')
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _is_slash_command(t):
|
|
62
|
+
"""슬래시 커맨드인가, 절대경로로 시작하는 실발화인가.
|
|
63
|
+
|
|
64
|
+
🟥 실측 2026-09-05, 이 레포 전사본 전수: `<` 봉투가 아닌 `/` 접두 발화 **28 건 중 13 건**
|
|
65
|
+
(46%)이 **절대경로로 시작하는 진짜 발화**였다 — «/Users/…/OT.pptx.pdf 이건 발표자 ot자료인…»
|
|
66
|
+
처럼 지시가 통째로 딸려 있다. 옛 규칙(`startswith('/')` → 커맨드)은 그 13 건을 무음 삭제했다.
|
|
67
|
+
판별자: **첫 낱말의 `/` 개수**. `/compact` `/install-wizard --dry-run` = 1 개(커맨드) ·
|
|
68
|
+
`/Users/<name>/…` = 2 개 이상(경로). 이 코퍼스에서 15/13 으로 깨끗이 갈린다.
|
|
69
|
+
|
|
70
|
+
🟥 이 규칙은 `--strip-system-markers` 를 준 소비처에만 적용된다. seal 은 안 준다 —
|
|
71
|
+
바꾸면 기존 원장 출력이 달라지고, 그건 리팩터가 아니라 **행동 변경**이라 운영자 판단이다.
|
|
72
|
+
즉 seal 쪽에는 이 결함이 **아직 남아 있다**(보고서에 이름으로 남긴다).
|
|
73
|
+
"""
|
|
74
|
+
return t.split(' ', 1)[0].count('/') == 1
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _apply_system_markers(t):
|
|
78
|
+
"""→ (text, dropped). dropped=True 면 이 레코드는 발화가 아니다."""
|
|
79
|
+
for pref in _DROP_PREFIXES:
|
|
80
|
+
if t.startswith(pref):
|
|
81
|
+
return '', True
|
|
82
|
+
prev = None
|
|
83
|
+
while prev != t:
|
|
84
|
+
prev = t
|
|
85
|
+
t = _STRIP_MARKER.sub('', t)
|
|
86
|
+
if not t.strip():
|
|
87
|
+
return '', True
|
|
88
|
+
return t, False
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _iter_records(path):
|
|
92
|
+
with open(path, errors='replace') as fh:
|
|
93
|
+
for line in fh:
|
|
94
|
+
try:
|
|
95
|
+
yield json.loads(line)
|
|
96
|
+
except Exception:
|
|
97
|
+
continue
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def extract(path, min_chars=0, strip_system=False):
|
|
101
|
+
"""→ (utterances, counters). utterances = [(n, ts, text), ...] (1-based n)."""
|
|
102
|
+
out = []
|
|
103
|
+
n = 0
|
|
104
|
+
c = {'tool': 0, 'meta': 0, 'other': 0, 'short': 0, 'sys': 0}
|
|
105
|
+
for d in _iter_records(path):
|
|
106
|
+
if d.get('type') != 'user':
|
|
107
|
+
continue
|
|
108
|
+
# Runtime-flagged provenance: isMeta (harness-injected — command expansions, caveats,
|
|
109
|
+
# image placeholders) and isSidechain (a sub-agent's conversation) are not operator
|
|
110
|
+
# utterances. A channel property of the record itself, not a content judgment. Measured on
|
|
111
|
+
# a live transcript 2026-09-05: 1 of 379 user records is isMeta with plain text (the image
|
|
112
|
+
# placeholder), 0 isSidechain — so the seal ledger loses exactly that line. (codex finding 4)
|
|
113
|
+
if d.get('isMeta') or d.get('isSidechain'):
|
|
114
|
+
c['meta'] += 1
|
|
115
|
+
continue
|
|
116
|
+
m = d.get('message') or {}
|
|
117
|
+
content = m.get('content')
|
|
118
|
+
if isinstance(content, str):
|
|
119
|
+
t = content
|
|
120
|
+
elif isinstance(content, list):
|
|
121
|
+
if any(isinstance(b, dict) and b.get('type') == 'tool_result' for b in content):
|
|
122
|
+
c['tool'] += 1
|
|
123
|
+
continue
|
|
124
|
+
parts = [b.get('text', '') for b in content
|
|
125
|
+
if isinstance(b, dict) and b.get('type') == 'text']
|
|
126
|
+
if not parts:
|
|
127
|
+
c['other'] += 1 # 이미지 전용 등 — 셈에서 지우지 않고 센다
|
|
128
|
+
continue
|
|
129
|
+
t = ' '.join(parts)
|
|
130
|
+
else:
|
|
131
|
+
c['other'] += 1
|
|
132
|
+
continue
|
|
133
|
+
t = ' '.join(t.split())
|
|
134
|
+
if not t:
|
|
135
|
+
c['other'] += 1
|
|
136
|
+
continue
|
|
137
|
+
if t.startswith('<'): # 메타 봉투
|
|
138
|
+
c['meta'] += 1
|
|
139
|
+
continue
|
|
140
|
+
if t.startswith('/'):
|
|
141
|
+
# strip_system 이 꺼져 있으면 옛 규칙 그대로 — seal 출력 바이트 동일의 근거다.
|
|
142
|
+
if not (strip_system and not _is_slash_command(t)):
|
|
143
|
+
c['meta'] += 1
|
|
144
|
+
continue
|
|
145
|
+
if strip_system:
|
|
146
|
+
t, dropped = _apply_system_markers(t)
|
|
147
|
+
if dropped:
|
|
148
|
+
c['sys'] += 1
|
|
149
|
+
continue
|
|
150
|
+
if min_chars and len(t) < min_chars:
|
|
151
|
+
c['short'] += 1
|
|
152
|
+
continue
|
|
153
|
+
n += 1
|
|
154
|
+
out.append((n, d.get('timestamp') or '-', t))
|
|
155
|
+
return out, c
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _seal(utterances, c):
|
|
159
|
+
# 🟥 인라인 판본과 **바이트 동일**. 문구·구두점·카운터 이름을 바꾸지 마라 (L10 골든).
|
|
160
|
+
for n, _ts, t in utterances:
|
|
161
|
+
print(f"{n}. {t[:200]}")
|
|
162
|
+
total = len(utterances)
|
|
163
|
+
print(f"\n합계: {total}건" if total else "- (발화 0건)")
|
|
164
|
+
print(f"제외: tool_result {c['tool']} · 메타/커맨드 {c['meta']} · 텍스트없음 {c['other']}")
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _tsv(utterances, c):
|
|
168
|
+
for n, ts, t in utterances:
|
|
169
|
+
# 탭·개행은 위에서 이미 접혔다. 그래도 방어적으로 한 번 더 — 한 줄 = 한 발화가 계약이다.
|
|
170
|
+
print(f"{n}\t{ts}\t" + t.replace('\t', ' '))
|
|
171
|
+
# 제외분은 stderr 로. stdout 은 «한 줄 한 발화» 계약이라 소비처가 `wc -l` 로 세도 안전해야 한다.
|
|
172
|
+
sys.stderr.write(
|
|
173
|
+
f"제외: tool_result {c['tool']} · 메타/커맨드 {c['meta']} · "
|
|
174
|
+
f"텍스트없음 {c['other']} · 짧음 {c['short']} · 시스템마커 {c['sys']}\n")
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def main(argv):
|
|
178
|
+
if len(argv) < 2:
|
|
179
|
+
sys.stderr.write("usage: transcript_utterances.py <transcript.jsonl> "
|
|
180
|
+
"[--min-chars N] [--format tsv|seal]\n")
|
|
181
|
+
return 2
|
|
182
|
+
path = argv[1]
|
|
183
|
+
min_chars = 0
|
|
184
|
+
fmt = 'tsv'
|
|
185
|
+
strip_system = False
|
|
186
|
+
i = 2
|
|
187
|
+
while i < len(argv):
|
|
188
|
+
a = argv[i]
|
|
189
|
+
if a == '--min-chars' and i + 1 < len(argv):
|
|
190
|
+
try:
|
|
191
|
+
min_chars = int(argv[i + 1])
|
|
192
|
+
except ValueError:
|
|
193
|
+
sys.stderr.write(f"transcript_utterances: --min-chars 값이 정수가 아니다: {argv[i+1]}\n")
|
|
194
|
+
return 2
|
|
195
|
+
i += 2
|
|
196
|
+
elif a == '--format' and i + 1 < len(argv):
|
|
197
|
+
fmt = argv[i + 1]
|
|
198
|
+
if fmt not in ('tsv', 'seal'):
|
|
199
|
+
sys.stderr.write(f"transcript_utterances: --format 은 tsv|seal: {fmt}\n")
|
|
200
|
+
return 2
|
|
201
|
+
i += 2
|
|
202
|
+
elif a == '--strip-system-markers':
|
|
203
|
+
strip_system = True
|
|
204
|
+
i += 1
|
|
205
|
+
else:
|
|
206
|
+
sys.stderr.write(f"transcript_utterances: 알 수 없는 인자: {a}\n")
|
|
207
|
+
return 2
|
|
208
|
+
try:
|
|
209
|
+
utterances, counters = extract(path, min_chars, strip_system)
|
|
210
|
+
except OSError as e:
|
|
211
|
+
# 호출부(compaction_probe seal)의 «파싱 실패» 폴백이 이 비영 종료로 뜬다.
|
|
212
|
+
sys.stderr.write(f"transcript_utterances: 전사본을 열 수 없다: {e}\n")
|
|
213
|
+
return 2
|
|
214
|
+
if fmt == 'seal':
|
|
215
|
+
_seal(utterances, counters)
|
|
216
|
+
else:
|
|
217
|
+
_tsv(utterances, counters)
|
|
218
|
+
return 0
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
if __name__ == '__main__':
|
|
222
|
+
sys.exit(main(sys.argv))
|
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# utterance_intake.sh — 전사본의 운영자 발화를 **자동으로** 프로브로 만들어 기록 착지를 검증한다.
|
|
3
|
+
#
|
|
4
|
+
# ── 왜 (진단, 2026-09-05) ────────────────────────────────────────────────────
|
|
5
|
+
# FH 의 «질문하기» 엔진은 external-grounding 엔진인데 **수용 절반이 산문이다**
|
|
6
|
+
# (`fh_three_layer_canon.md §2`: *"묻는 경로는 있고 들어온 통찰을 자산화하는 경로가 약하다"*).
|
|
7
|
+
# 계기는 이미 둘 다 있었다:
|
|
8
|
+
# compaction_probe.sh seal 전사본 → 운영자 발화 축자 원장 (돈다)
|
|
9
|
+
# utterance_landing_check.sh probes.tsv → 기록 파일 grep (돈다, 호출부 0)
|
|
10
|
+
# 그런데 **probes.tsv 를 만드는 기계가 없었다.** 손으로 짜야 하니 아무도 안 짰고,
|
|
11
|
+
# `session_close_check.sh` 어디에서도 두 번째가 호출되지 않았다(grep 0). 두 계기 사이의
|
|
12
|
+
# 한 칸이 비어 있어서 채널이 아니라 산문으로 남아 있던 것 — 이 스크립트가 그 칸이다.
|
|
13
|
+
#
|
|
14
|
+
# 실측 근거(같은 날): 운영자가 세션 중 던진 교리·설계급 발화 6 건이 착지한 경로는 전부
|
|
15
|
+
# **거버너의 손**이었다(signal 파일 2 · UAP 블록 2 · fh_completed). 기계는 한 건도 안 잡았다.
|
|
16
|
+
#
|
|
17
|
+
# ── 이 스크립트가 하는 것 / 안 하는 것 ──────────────────────────────────────
|
|
18
|
+
# 한다 : 발화 추출 → 키 낱말 프로브 생성 → 컨트롤 2 종 삽입 → 착지 검증 → 미착지 목록
|
|
19
|
+
# 안 한다: 기록을 **쓰지 않는다.** 무엇을 어디에 적을지는 판단이고, 판단을 코드로 굳히면
|
|
20
|
+
# 그게 천장이 된다(`CLAUDE.md §Mechanization Boundary`). 이건 «채널이 값을
|
|
21
|
+
# 나르는가» 만 본다 — «그 값이 옳은가» 는 사람 몫이다.
|
|
22
|
+
#
|
|
23
|
+
# ── 🟥 명명된 잔여 셋. 인용하기 전에 읽어라 ─────────────────────────────────
|
|
24
|
+
# ⓐ **이름표는 측정이 아니다** — 프로브는 «키 낱말» 이라서, 기록이 같은 뜻을 **다른 낱말로**
|
|
25
|
+
# 적었으면 착지했는데 미착지로 뜬다. 상속: `utterance_landing_check.sh` 헤더의
|
|
26
|
+
# *"rc=1 은 «기록하라»가 아니라 «확인하라»로 읽어야 한다"*. 이 방향은 **과차단이라 안전**하다.
|
|
27
|
+
# ⓑ **토큰마다 한 행, 발화는 과반으로 판정한다.** 초판은 한 행 `(낱말1|낱말2|낱말3)` 이라
|
|
28
|
+
# **하나만 겹쳐도** 착지였다 — 같은 주제어 하나(«쿠버네티스»)가 든 기록이 정책 발화를 «착지»
|
|
29
|
+
# 로 렌더했다(cross-family codex, 2026-09-05). 이제 발화의 최장 토큰(≤3)을 **각각 한 행**으로
|
|
30
|
+
# 내고, 발화는 착지 행이 ⌈n/2⌉ 이상일 때만 착지다(3 키 → 2 · 2 키 → 1 · 1 키 → 1).
|
|
31
|
+
# 🟥 남은 fail-open: 키가 2 개뿐인 발화는 여전히 하나만 겹쳐도 착지다(OR). rc=0 은 여전히
|
|
32
|
+
# «제대로 적혔다» 의 증명이 아니다. 발화 단위 계상은 map 의 n_keys 로 이 스크립트가 집계한다.
|
|
33
|
+
# ⓒ **advisory 다** — `session_close_check.sh ①-f` 는 이걸 차단으로 쓰지 않는다. 노출 사다리
|
|
34
|
+
# (`fh_three_layer_canon §1-a-2 ⓔ` shadow) 로 몇 세션 관찰한 뒤 승격 여부는 운영자 결정.
|
|
35
|
+
#
|
|
36
|
+
# ── 컨트롤 두 종 (한 종이 아니다) ───────────────────────────────────────────
|
|
37
|
+
# `utterance_landing_check.sh` 의 CONTROL 은 **양성만** 있다 — 「계기가 죽었나」는 재고
|
|
38
|
+
# 「계기가 아무거나 다 잡나」는 못 잰다(`[[feedback_control_presence_is_not_discrimination]]`).
|
|
39
|
+
# POSITIVE **기록 내용에서 뽑은 최장 토큰.** 거기 실제로 있으니 정의상 잡혀야 하고,
|
|
40
|
+
# 안 잡히면 파이프라인이 죽은 것이다(word-split · 경로 · 인코딩) → rc=10.
|
|
41
|
+
# 🟥 초판은 «오늘 날짜» 였는데 **날짜는 파일명에 있지 내용에 있을 의무가 없다** —
|
|
42
|
+
# 그러면 정상 마감마다 거짓 경보가 뜬다. 실측 근거는 아래 `_control_rows` 주석.
|
|
43
|
+
# 날짜는 «있을 때만» 보조 컨트롤로 더한다.
|
|
44
|
+
# NEGATIVE 무작위 토큰 — **없어야 마땅하다.** 잡히면 계기가 아무거나 잡는 것이므로 역시 rc=10
|
|
45
|
+
# NEGATIVE 는 이 래퍼가 직접 검사한다(하위 도구가 모르는 종류라 그 행은 무시하고 지나간다).
|
|
46
|
+
#
|
|
47
|
+
# ── 사용 ────────────────────────────────────────────────────────────────────
|
|
48
|
+
# bash scripts/utterance_intake.sh <transcript.jsonl> <record-file> [<record-file>...]
|
|
49
|
+
# env:
|
|
50
|
+
# FH_INTAKE_MIN_CHARS 기본 20 — 아래 §문턱 참조
|
|
51
|
+
# FH_INTAKE_NEG_TOKEN 음성 컨트롤 토큰 고정(레인용). 미설정이면 매 실행 무작위
|
|
52
|
+
# FH_INTAKE_DUMP_PROBES 생성된 probes.tsv 를 이 경로로도 남긴다(레인/디버그용)
|
|
53
|
+
#
|
|
54
|
+
# ── §문턱: 왜 40 이 아니라 20 인가 (실측 2026-09-05) ────────────────────────
|
|
55
|
+
# 이 레포 전사본 전수 3191 발화(봉투·툴결과 제외)의 길이 분포:
|
|
56
|
+
# 00-09 524 · 10-19 329 · 20-29 284 · 30-39 187 · 40+ 1867
|
|
57
|
+
# 30–39 구간 표본에 하중 지는 발화가 산다 — «그리고 입장리뷰는 정적 동적 모두 수행해야해» ·
|
|
58
|
+
# «4는 초록으로 올리도록 하자». 한글은 자당 정보량이 커서 **40 자 바닥은 교리 발화를 조용히
|
|
59
|
+
# 버린다**(위험한 방향). 20 = 853 제외 · 2338 보존. 과포함은 소음(안전), 과소포함은 무음 손실.
|
|
60
|
+
#
|
|
61
|
+
# ── 종료코드 ────────────────────────────────────────────────────────────────
|
|
62
|
+
# 0 전건 착지 1 미착지 있음 (목록 인쇄)
|
|
63
|
+
# 10 HARNESS-ERROR / UNMEASURED — **PASS 도 FAIL 도 아니다**
|
|
64
|
+
# (전사본 부재 · 추출 발화 0 · 기록 파일 0 · 컨트롤 사망 · 프로브 오류 · rc↔목록 불일치)
|
|
65
|
+
#
|
|
66
|
+
# 이식성: bash 3.2 · 백틱 없음 · heredoc 은 함수 안 + 인용 구분자. python3 표준 라이브러리만.
|
|
67
|
+
|
|
68
|
+
set -uo pipefail
|
|
69
|
+
|
|
70
|
+
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
71
|
+
EXTRACTOR="$REPO_ROOT/scripts/transcript_utterances.py"
|
|
72
|
+
LANDING="$REPO_ROOT/scripts/utterance_landing_check.sh"
|
|
73
|
+
MIN_CHARS="${FH_INTAKE_MIN_CHARS:-20}"
|
|
74
|
+
|
|
75
|
+
_die10() { echo "$1"; exit 10; }
|
|
76
|
+
|
|
77
|
+
# 🟥 `grep -c x f || echo 0` 은 무매치일 때 «0» 을 **두 번** 낸다 — grep 이 0 을 찍고 rc=1 을
|
|
78
|
+
# 내므로 폴백까지 붙는다. `[ "0\n0" -eq 0 ]` 은 «integer expression expected» 로 죽고,
|
|
79
|
+
# 그 죽음이 계기 사망으로 렌더된다(레인이 L1/L4b/L8b 에서 잡았다).
|
|
80
|
+
# `[[feedback_pipefail_fallback_disarms_guard]]` 의 사촌 — 폴백이 값을 오염시키는 형태.
|
|
81
|
+
_num() { case "${1:-}" in ''|*[!0-9]*) echo 0 ;; *) printf '%s' "$1" ;; esac; }
|
|
82
|
+
|
|
83
|
+
# ── 양성 컨트롤 ───────────────────────────────────────────
|
|
84
|
+
# 🟥 초판은 «오늘 날짜» 를 양성 컨트롤로 썼다 — «오늘자 기록 파일에는 오늘 날짜가
|
|
85
|
+
# 반드시 있다» 는 가정이었고, **거짓이다.** 날짜는 파일명에 있지 내용에 있을 의무가 없다.
|
|
86
|
+
# 실측(2026-09-05 sim): 플로어 티어 세션이 만든 `fh_completed_2026-09-05.md` 내용은
|
|
87
|
+
# «- ✅ 계기 확인 — 클론 안 쓰기 가능» 한 줄뿐이다. 그러면 컨트롤이 죽어 rc=10 이 나고,
|
|
88
|
+
# 정상 마감에 거짓 경보가 뜬다 — 그것이 바로 스키밍을 훈련시키는 소음이다.
|
|
89
|
+
# ⇒ 컨트롤을 **기록 내용에서 뽑는다**: 거기 실제로 있는 긴 토큰은 정의상 잡혀야 하므로,
|
|
90
|
+
# 안 잡히면 **파이프라인이 죽은 것**(word-split · 경로 · 인코딩)이다 — 원래 재려던 바로 그 실패다.
|
|
91
|
+
# 날짜는 «있을 때만» 두 번째 컨트롤로 더한다(있으면 더 강하고, 없어도 치명적이지 않다).
|
|
92
|
+
_control_rows() { # $1=오늘날짜 $2=스크래치 $3...=기록 파일들
|
|
93
|
+
local _today="$1" _scratch="$2"; shift 2
|
|
94
|
+
local _py="$_scratch/mkcontrol.py"
|
|
95
|
+
cat > "$_py" <<'CTLPY'
|
|
96
|
+
import re, sys
|
|
97
|
+
|
|
98
|
+
today = sys.argv[1]
|
|
99
|
+
best = ''
|
|
100
|
+
seen_today = False
|
|
101
|
+
for path in sys.argv[2:]:
|
|
102
|
+
try:
|
|
103
|
+
txt = open(path, errors='replace').read()
|
|
104
|
+
except OSError:
|
|
105
|
+
continue
|
|
106
|
+
# degrade-scan C2(substring-boolean) 대응: 이 포함검사는 **가산적**이다 — 맞으면 보조
|
|
107
|
+
# 컨트롤이 하나 늘고, 틀리면 안 늘 뿐이다. 하중은 아래 `best`(파생 컨트롤)가 진다.
|
|
108
|
+
# 어느 방향으로 틀려도 PASS 로 기울지 않으므로 정확 일치로 좁힐 이유가 없다.
|
|
109
|
+
if today in txt: # noqa: degrade — additive only: ADDS a secondary control row, never grounds a verdict
|
|
110
|
+
seen_today = True
|
|
111
|
+
for tok in re.findall(r'[\uac00-\ud7a3]{4,}|[A-Za-z0-9_]{5,}', txt):
|
|
112
|
+
if len(tok) > len(best):
|
|
113
|
+
best = tok
|
|
114
|
+
if best:
|
|
115
|
+
print("CONTROL\t%s\t기록파생(양성컨트롤)" % best)
|
|
116
|
+
if seen_today:
|
|
117
|
+
print("CONTROL\t%s\t오늘날짜(보조컨트롤)" % today)
|
|
118
|
+
CTLPY
|
|
119
|
+
python3 "$_py" "$_today" "$@"
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
# ── 키 낱말 프로브 생성 ──────────────────────────────────────────────────────
|
|
123
|
+
# 한글/영숫자 토큰 중 **가장 긴** 것 3 개. 조사를 벗기고, 불용어와 짧은 토큰을 뺀다.
|
|
124
|
+
# 정규식 메타문자가 섞이지 않도록 토큰 문자군을 [\uac00-\ud7a3]·[A-Za-z0-9_] 로 **닫아 둔다** —
|
|
125
|
+
# `.` `-` 를 허용하면 ERE 에서 각각 와일드카드/범위로 새고, 이스케이프를 손으로 하는 순간
|
|
126
|
+
# 「관대함 갈린 중복 정규화」가 시작된다.
|
|
127
|
+
# 🟥 `python3 - <<'PY'` 를 쓰면 **heredoc 이 python 의 stdin 이 되어** 발화 tsv 가 통째로
|
|
128
|
+
# 안 읽힌다(첫 판본이 그랬고, 레인 L4b 가 « TARGET 0 건 » 으로 잡았다). 프로그램은 파일로
|
|
129
|
+
# 떨어뜨리고 stdin 은 호출부의 리다이렉트가 그대로 나르게 둔다.
|
|
130
|
+
_build_probes() { # stdin: n<TAB>ts<TAB>text stdout: probes rows $1=map 경로 $2=스크래치 디렉터리
|
|
131
|
+
local _py="$2/mkprobes.py"
|
|
132
|
+
cat > "$_py" <<'PY'
|
|
133
|
+
import os, re, sys
|
|
134
|
+
|
|
135
|
+
MAP = open(sys.argv[1], 'w')
|
|
136
|
+
|
|
137
|
+
# 🟥 사설 토큰 마스킹 — **토큰화 전에** 한다. 이 출력은 마감 화면에 찍히고 사람이 카드에
|
|
138
|
+
# 붙여넣는다. 첫 실사용(2026-09-05)에서 실제로 `[Image: source: /var/folders/3h/…]` 가
|
|
139
|
+
# 라벨로 나왔다 — 머신 고유 임시경로다. 마스킹을 라벨에만 걸면 키 낱말 쪽으로 새므로
|
|
140
|
+
# (사용자명 같은 것이 그대로 토큰이 된다) 앞단에서 지운다.
|
|
141
|
+
# 🟥 한글 범위를 `[\uac00-\ud7a3]` 로 적는다 — `[가-힣]` 과 **의미는 같지만**
|
|
142
|
+
# (파이썬 `re` 에서 동치, known-pair 로 확인), 이 파일은 `.sh` 라
|
|
143
|
+
# `test_card_drift_probe.sh` 의 locale-range 린트가 스캔한다. 그 린트는 셸의 `grep` 을
|
|
144
|
+
# 겨냥한 것이고(C 로케일에서 비ASCII 범위는 exit 2 → «무매치» 로 읽힌다), heredoc 안의
|
|
145
|
+
# python 인지 구분하지 못한다. 린트를 끄는 대신 **표기를 바꾼다** — 로케일 함정에 대한
|
|
146
|
+
# 그 경계는 살아 있어야 한다.
|
|
147
|
+
# (`transcript_utterances.py` 는 `.sh` 가 아니라 스캔 대상이 아니므로 가독성 표기를 쓴다.)
|
|
148
|
+
_HOME = os.environ.get('HOME', '')
|
|
149
|
+
_TMP_RE = re.compile(r'/(?:var/folders|private/var/folders)/[^\s\]\)]*')
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
_PATH_RE = re.compile(r'(?<![\w])/(?:[\w.@+-]+/)+[\w.@+-]*') # any absolute path, 2+ segments
|
|
153
|
+
_KV_RE = re.compile(r'\b([A-Z][A-Z0-9_]{2,})=[^\s]+') # KEY=value → KEY=<v>
|
|
154
|
+
_HOST_RE = re.compile(r'\b[a-z0-9-]+(?:\.[a-z0-9-]+)*\.(?:internal|local|corp|lan|intra)\b')
|
|
155
|
+
def mask(t):
|
|
156
|
+
# Labels and probe tokens are an OUTPUT surface (close-check stdout, pasted into cards) — so
|
|
157
|
+
# the masking is not "the operator's own paths" but every absolute path, every KEY=value and
|
|
158
|
+
# every host-like name (codex finding 5, 2026-09-05). The suffix list is a small frozen
|
|
159
|
+
# judgment; it fails loud (an unmasked host shows up in a label), not silent.
|
|
160
|
+
t = _TMP_RE.sub('<tmp>', t)
|
|
161
|
+
if _HOME and len(_HOME) > 4:
|
|
162
|
+
t = t.replace(_HOME, '$HOME')
|
|
163
|
+
t = _PATH_RE.sub('<path>', t)
|
|
164
|
+
t = _KV_RE.sub(r'\1=<v>', t)
|
|
165
|
+
t = _HOST_RE.sub('<host>', t)
|
|
166
|
+
return t
|
|
167
|
+
|
|
168
|
+
# 조사/어미 — 토큰 끝에서 벗긴다. 긴 것부터 시도한다(짧은 게 먼저 맞으면 덜 벗겨진다).
|
|
169
|
+
PARTICLES = ['으로는', '에서는', '에게는', '이라고', '으로도', '까지는', '부터는',
|
|
170
|
+
'으로', '에서', '에게', '한테', '처럼', '보다', '라고', '하고', '까지',
|
|
171
|
+
'부터', '이나', '든지', '마다', '조차', '밖에', '이란', '이든',
|
|
172
|
+
'은', '는', '이', '가', '을', '를', '에', '의', '로', '도', '만', '과', '와']
|
|
173
|
+
|
|
174
|
+
# 불용어 — 이 코퍼스에서 흔해서 «우연 일치» 를 만드는 것들. OR 결합이라 여기가 fail-open 의
|
|
175
|
+
# 주된 입구다(§명명된 잔여 ⓑ). 길이 규칙만으로는 안 걸러진다.
|
|
176
|
+
STOP = set('''그리고 그러니까 그래서 그러면 그런데 이렇게 그렇게 어떻게 하지만 그러나
|
|
177
|
+
마찬가지 이야기 생각 경우 부분 내용 확인 진행 작업 파일 세션 지금 다음 이번 정도
|
|
178
|
+
사람 방법 이유 상태 결과 문제 대해 위해 통해 우리 너는 나는 것을 것이 거기 여기 저기
|
|
179
|
+
해줘 하자 하는 되는 있는 없는 같은 같이 좋은 다시 먼저 아직 이제 조금 그냥 바로
|
|
180
|
+
this that with from have been will your about would should could there which their
|
|
181
|
+
path host tmp home HOME
|
|
182
|
+
then than when what where because those these into over under more most only just'''.split())
|
|
183
|
+
|
|
184
|
+
def strip_particle(tok):
|
|
185
|
+
if not re.match(r'^[\uac00-\ud7a3]+$', tok):
|
|
186
|
+
return tok
|
|
187
|
+
for p in PARTICLES:
|
|
188
|
+
if tok.endswith(p) and len(tok) - len(p) >= 2:
|
|
189
|
+
return tok[:-len(p)]
|
|
190
|
+
return tok
|
|
191
|
+
|
|
192
|
+
n_rows = 0
|
|
193
|
+
n_unprobeable = 0
|
|
194
|
+
for line in sys.stdin:
|
|
195
|
+
line = line.rstrip('\n')
|
|
196
|
+
if not line:
|
|
197
|
+
continue
|
|
198
|
+
parts = line.split('\t', 2)
|
|
199
|
+
if len(parts) < 3:
|
|
200
|
+
continue
|
|
201
|
+
idx, _ts, text = parts
|
|
202
|
+
text = mask(text)
|
|
203
|
+
toks = re.findall(r'[\uac00-\ud7a3]+|[A-Za-z0-9_]+', text)
|
|
204
|
+
cand = []
|
|
205
|
+
seen = set()
|
|
206
|
+
for t in toks:
|
|
207
|
+
s = strip_particle(t)
|
|
208
|
+
if s in seen or s in STOP or s.lower() in STOP:
|
|
209
|
+
continue
|
|
210
|
+
if re.match(r'^[\uac00-\ud7a3]+$', s):
|
|
211
|
+
if len(s) < 3:
|
|
212
|
+
continue
|
|
213
|
+
else:
|
|
214
|
+
if len(s) < 4 or s.isdigit():
|
|
215
|
+
continue
|
|
216
|
+
seen.add(s)
|
|
217
|
+
cand.append(s)
|
|
218
|
+
# 가장 긴 것부터 3 개. 동률은 먼저 나온 것(발화 앞쪽이 대개 주제어).
|
|
219
|
+
cand.sort(key=lambda s: -len(s))
|
|
220
|
+
keys = cand[:3]
|
|
221
|
+
if not keys:
|
|
222
|
+
# 프로브를 만들 수 없는 발화 — **버리지 않고 센다**(not found != 0).
|
|
223
|
+
n_unprobeable += 1
|
|
224
|
+
MAP.write("%s\tUNPROBEABLE\t%s\t0\n" % (idx, text[:60]))
|
|
225
|
+
continue
|
|
226
|
+
label = "#%s %s" % (idx, text[:40])
|
|
227
|
+
# One TARGET row per key: `#<idx>.<k>/<n> <text>` — the wrapper re-aggregates per utterance
|
|
228
|
+
# (codex finding 1). Keys are bare tokens ([가-힣]+ / [A-Za-z0-9_]+): no ERE metacharacters,
|
|
229
|
+
# never dash-first, so the landing checker reads them as literals.
|
|
230
|
+
for k_i, key in enumerate(keys, 1):
|
|
231
|
+
print("TARGET\t%s\t#%s.%d/%d %s" % (key, idx, k_i, len(keys), text[:36]))
|
|
232
|
+
MAP.write("%s\t%s\t%s\t%d\n" % (idx, label, text[:60], len(keys)))
|
|
233
|
+
n_rows += 1
|
|
234
|
+
MAP.close()
|
|
235
|
+
sys.stderr.write("probe utterances=%d · 프로브불가=%d\n" % (n_rows, n_unprobeable))
|
|
236
|
+
PY
|
|
237
|
+
python3 "$_py" "$1"
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
# ── 인자 ────────────────────────────────────────────────────────────────────
|
|
241
|
+
if [ "$#" -lt 2 ]; then
|
|
242
|
+
echo "usage: $0 <transcript.jsonl> <record-file> [<record-file>...]" >&2
|
|
243
|
+
exit 10
|
|
244
|
+
fi
|
|
245
|
+
TRANSCRIPT="$1"; shift
|
|
246
|
+
RECORDS=() # 배열 — zsh word-split 에 의존하지 않는다
|
|
247
|
+
MISSING_RECORDS=0
|
|
248
|
+
for f in "$@"; do
|
|
249
|
+
if [ -f "$f" ]; then RECORDS+=("$f"); else MISSING_RECORDS=$((MISSING_RECORDS+1)); fi
|
|
250
|
+
done
|
|
251
|
+
|
|
252
|
+
[ -f "$EXTRACTOR" ] || _die10 "🟥 HARNESS-ERROR: 추출기 없음: $EXTRACTOR"
|
|
253
|
+
[ -f "$LANDING" ] || _die10 "🟥 HARNESS-ERROR: 착지 검사기 없음: $LANDING"
|
|
254
|
+
|
|
255
|
+
if [ ! -f "$TRANSCRIPT" ]; then
|
|
256
|
+
echo "ℹ️ UNMEASURED — 전사본 부재: $TRANSCRIPT"
|
|
257
|
+
# 🟥 성공 판정 리터럴(«전부 착지»)을 **부정문 안에도** 쓰지 마라 — grep 하는 쪽이
|
|
258
|
+
# 성공으로 읽는다(Grep-Collision Treadmill). 레인 L7c 가 실제로 그렇게 오독했다.
|
|
259
|
+
echo " 부재를 0 으로 접지 마라 — 발화가 없었다는 뜻도, 다 적혔다는 뜻도 아니다."
|
|
260
|
+
exit 10
|
|
261
|
+
fi
|
|
262
|
+
if [ "${#RECORDS[@]}" -eq 0 ]; then
|
|
263
|
+
echo "ℹ️ UNMEASURED — 검사할 기록 파일이 하나도 없다 (인자 $# 건 중 $MISSING_RECORDS 건 부재)"
|
|
264
|
+
echo " 기록이 없는 것과 발화가 안 적힌 것은 다른 명제다."
|
|
265
|
+
exit 10
|
|
266
|
+
fi
|
|
267
|
+
|
|
268
|
+
WORK="$(mktemp -d 2>/dev/null)" || _die10 "🟥 HARNESS-ERROR: mktemp -d 실패 (환경 부재이지 계기 결함이 아니다)"
|
|
269
|
+
trap 'rm -rf "$WORK"' EXIT INT TERM
|
|
270
|
+
|
|
271
|
+
# ── 1. 발화 추출 ────────────────────────────────────────────────────────────
|
|
272
|
+
# `--strip-system-markers`: peer 알림 · 무인 실행 · 이미지 마커는 **운영자 발화가 아니다**.
|
|
273
|
+
# seal 은 이 플래그를 안 주므로 기존 원장 출력은 그대로다(레인 L10 골든이 그걸 고정한다).
|
|
274
|
+
python3 "$EXTRACTOR" "$TRANSCRIPT" --min-chars "$MIN_CHARS" --strip-system-markers \
|
|
275
|
+
> "$WORK/utts.tsv" 2> "$WORK/extract.err"
|
|
276
|
+
_erc=$?
|
|
277
|
+
if [ "$_erc" -ne 0 ]; then
|
|
278
|
+
echo "🟥 HARNESS-ERROR: 발화 추출 실패 (rc=$_erc)"; sed 's/^/ /' "$WORK/extract.err"; exit 10
|
|
279
|
+
fi
|
|
280
|
+
N_UTT=$(_num "$(grep -c . "$WORK/utts.tsv" 2>/dev/null)")
|
|
281
|
+
if [ "${N_UTT:-0}" -eq 0 ]; then
|
|
282
|
+
echo "ℹ️ UNMEASURED — 추출된 발화 0 건 (min-chars=$MIN_CHARS)"
|
|
283
|
+
sed 's/^/ /' "$WORK/extract.err"
|
|
284
|
+
echo " 빈 프로브 세트는 합격이 아니다 — «아무것도 안 쟀다» 다."
|
|
285
|
+
exit 10
|
|
286
|
+
fi
|
|
287
|
+
|
|
288
|
+
# ── 2. 프로브 생성 (컨트롤 2 종 먼저) ───────────────────────────────────────
|
|
289
|
+
TODAY="$(date +%Y-%m-%d)"
|
|
290
|
+
NEG_TOKEN="${FH_INTAKE_NEG_TOKEN:-}"
|
|
291
|
+
if [ -z "$NEG_TOKEN" ]; then
|
|
292
|
+
# 무작위. `date +%N` 은 BSD 에 없으므로 쓰지 않는다.
|
|
293
|
+
NEG_TOKEN="FHNEG$(od -An -N6 -tx1 /dev/urandom 2>/dev/null | tr -d ' \n' | tr 'a-f' 'A-F')"
|
|
294
|
+
case "$NEG_TOKEN" in FHNEG) NEG_TOKEN="FHNEG$$X$(date +%s)" ;; esac
|
|
295
|
+
fi
|
|
296
|
+
{
|
|
297
|
+
_control_rows "$TODAY" "$WORK" "${RECORDS[@]}"
|
|
298
|
+
printf 'NEGATIVE\t%s\t무작위토큰(음성컨트롤)\n' "$NEG_TOKEN"
|
|
299
|
+
} > "$WORK/probes.tsv"
|
|
300
|
+
_build_probes "$WORK/probes.map" "$WORK" < "$WORK/utts.tsv" >> "$WORK/probes.tsv" 2> "$WORK/build.err"
|
|
301
|
+
[ -n "${FH_INTAKE_DUMP_PROBES:-}" ] && cp "$WORK/probes.tsv" "$FH_INTAKE_DUMP_PROBES" 2>/dev/null
|
|
302
|
+
N_TARGET=$(_num "$(grep -c "^TARGET$(printf '\t')" "$WORK/probes.tsv" 2>/dev/null)")
|
|
303
|
+
N_PROBED=$(_num "$(LC_ALL=C awk -F'\t' '$2!="UNPROBEABLE"' "$WORK/probes.map" 2>/dev/null | grep -c .)")
|
|
304
|
+
UNPROBE=$(_num "$(LC_ALL=C awk -F'\t' '$2=="UNPROBEABLE"' "$WORK/probes.map" 2>/dev/null | grep -c .)")
|
|
305
|
+
if [ "${N_TARGET:-0}" -eq 0 ]; then
|
|
306
|
+
echo "ℹ️ UNMEASURED — 발화 $N_UTT 건에서 프로브를 하나도 못 만들었다"
|
|
307
|
+
sed 's/^/ /' "$WORK/build.err"
|
|
308
|
+
exit 10
|
|
309
|
+
fi
|
|
310
|
+
# 양성 컨트롤이 하나도 안 나왔으면(기록이 짧아 4자+ 한글 / 5자+ 영숫자 토큰이 없다) 계기를 돌리지
|
|
311
|
+
# 않는다 — 착지 검사기도 CONTROL 부재에 rc=10 을 내지만, 그 메시지는 «계기 사망» 이고 여기 원인은
|
|
312
|
+
# «컨트롤을 만들 재료가 없다» 라 다르다(codex finding 7). 둘 다 UNMEASURED 다.
|
|
313
|
+
N_CTL=$(_num "$(grep -c "^CONTROL$(printf '\t')" "$WORK/probes.tsv" 2>/dev/null)")
|
|
314
|
+
if [ "${N_CTL:-0}" -eq 0 ]; then
|
|
315
|
+
echo "ℹ️ UNMEASURED — 기록에서 양성 컨트롤 토큰을 못 뽑았다(4자+ 한글 · 5자+ 영숫자 토큰 부재, 오늘 날짜도 없음)"
|
|
316
|
+
echo " 컨트롤 없는 착지 판정은 근거가 아니다 — 기록이 너무 짧다."
|
|
317
|
+
exit 10
|
|
318
|
+
fi
|
|
319
|
+
|
|
320
|
+
# ── 3. 음성 컨트롤 — 하위 도구가 모르는 종류라 여기서 검사한다 ─────────────
|
|
321
|
+
# `grep -F` : 토큰을 리터럴로 본다. 정규식으로 해석되면 «없어야 할 것» 의 의미가 바뀐다.
|
|
322
|
+
if grep -qF -- "$NEG_TOKEN" "${RECORDS[@]}" 2>/dev/null; then
|
|
323
|
+
echo "🟥 HARNESS-ERROR: 음성 컨트롤이 기록에서 잡혔다 — [$NEG_TOKEN]"
|
|
324
|
+
echo " 없어야 마땅한 토큰이 잡힌다 = 계기가 아무거나 잡고 있다. 타깃 결과는 인쇄하지 않는다."
|
|
325
|
+
exit 10
|
|
326
|
+
fi
|
|
327
|
+
|
|
328
|
+
# ── 4. 착지 검증 (양성 컨트롤은 하위 도구가 본다) ───────────────────────────
|
|
329
|
+
LAND_OUT="$WORK/land.txt"
|
|
330
|
+
bash "$LANDING" "$WORK/probes.tsv" "${RECORDS[@]}" > "$LAND_OUT" 2>&1
|
|
331
|
+
LRC=$?
|
|
332
|
+
|
|
333
|
+
# 🟥 판정은 **종료코드**로 낸다. 아래 파싱은 «사람이 읽을 목록» 을 만들 뿐이고, 둘이 어긋나면
|
|
334
|
+
# 그 자체가 신호다(`[[feedback_summary_parsing_hides_failure]]` · 두 신호의 어긋남).
|
|
335
|
+
ROW_MISS=$(_num "$(grep -c '미착지$' "$LAND_OUT" 2>/dev/null)")
|
|
336
|
+
# 발화 단위 집계 — 행(토큰)의 착지/미착지를 `#idx.k/n` 라벨로 발화에 되돌리고, ⌈n/2⌉ 이상 착지면
|
|
337
|
+
# 그 발화는 착지다. 출력: LANDED / UNLANDED 개수 + 미착지 발화 idx 목록.
|
|
338
|
+
_AGG="$WORK/agg.py"
|
|
339
|
+
cat > "$_AGG" <<'AGGPY'
|
|
340
|
+
import re, sys
|
|
341
|
+
land, mapf = sys.argv[1], sys.argv[2]
|
|
342
|
+
n_keys = {}
|
|
343
|
+
for line in open(mapf, errors='replace'):
|
|
344
|
+
parts = line.rstrip('\n').split('\t')
|
|
345
|
+
if len(parts) >= 4 and parts[1] != 'UNPROBEABLE':
|
|
346
|
+
n_keys[parts[0]] = int(parts[3])
|
|
347
|
+
hits = {}
|
|
348
|
+
row_re = re.compile(r'^\s*(✅|🟥)\s+#(\d+)\.(\d+)/(\d+)\s')
|
|
349
|
+
for line in open(land, errors='replace'):
|
|
350
|
+
m = row_re.match(line)
|
|
351
|
+
if not m:
|
|
352
|
+
continue
|
|
353
|
+
idx = m.group(2)
|
|
354
|
+
n_keys.setdefault(idx, int(m.group(4)))
|
|
355
|
+
if m.group(1) == '✅':
|
|
356
|
+
hits[idx] = hits.get(idx, 0) + 1
|
|
357
|
+
landed, unlanded = [], []
|
|
358
|
+
for idx in sorted(n_keys, key=int):
|
|
359
|
+
n = n_keys[idx]
|
|
360
|
+
need = (n + 1) // 2
|
|
361
|
+
(landed if hits.get(idx, 0) >= need else unlanded).append(idx)
|
|
362
|
+
print("LANDED=%d" % len(landed))
|
|
363
|
+
print("UNLANDED=%d" % len(unlanded))
|
|
364
|
+
print("UNLANDED_IDX=%s" % " ".join(unlanded))
|
|
365
|
+
AGGPY
|
|
366
|
+
_AGG_OUT="$(python3 "$_AGG" "$LAND_OUT" "$WORK/probes.map" 2>/dev/null)"
|
|
367
|
+
HIT_N=$(_num "$(printf '%s\n' "$_AGG_OUT" | sed -n 's/^LANDED=//p')")
|
|
368
|
+
MISS_N=$(_num "$(printf '%s\n' "$_AGG_OUT" | sed -n 's/^UNLANDED=//p')")
|
|
369
|
+
MISS_IDX="$(printf '%s\n' "$_AGG_OUT" | sed -n 's/^UNLANDED_IDX=//p')"
|
|
370
|
+
|
|
371
|
+
echo "── 발화 착지 검사 ──"
|
|
372
|
+
echo " 전사본 : $TRANSCRIPT"
|
|
373
|
+
echo " 기록 대상: ${#RECORDS[@]} 파일$([ "$MISSING_RECORDS" -gt 0 ] && printf ' (부재 %s 건 제외)' "$MISSING_RECORDS")"
|
|
374
|
+
echo " 추출 : 발화 $N_UTT 건 (min-chars=$MIN_CHARS) → 프로브 발화 $N_PROBED 건 (토큰 행 $N_TARGET) · 프로브불가 $UNPROBE 건"
|
|
375
|
+
sed 's/^/ /' "$WORK/build.err" 2>/dev/null
|
|
376
|
+
sed 's/^/ /' "$WORK/extract.err" 2>/dev/null
|
|
377
|
+
|
|
378
|
+
case "$LRC" in
|
|
379
|
+
10)
|
|
380
|
+
echo
|
|
381
|
+
sed 's/^/ /' "$LAND_OUT"
|
|
382
|
+
echo " 🟥 계기 사망 — 착지 판정을 내지 않는다(죽은 계기의 출력은 데이터가 아니다)."
|
|
383
|
+
exit 10 ;;
|
|
384
|
+
esac
|
|
385
|
+
|
|
386
|
+
if [ "$LRC" -eq 0 ] && [ "$ROW_MISS" -ne 0 ]; then
|
|
387
|
+
echo "🟥 HARNESS-ERROR: rc=0 인데 미착지 행이 $ROW_MISS 개 — 두 신호가 어긋난다. 원문:"
|
|
388
|
+
sed 's/^/ /' "$LAND_OUT"; exit 10
|
|
389
|
+
fi
|
|
390
|
+
if [ "$LRC" -eq 1 ] && [ "$ROW_MISS" -eq 0 ]; then
|
|
391
|
+
echo "🟥 HARNESS-ERROR: rc=1 인데 미착지 행이 0 개 — 두 신호가 어긋난다. 원문:"
|
|
392
|
+
sed 's/^/ /' "$LAND_OUT"; exit 10
|
|
393
|
+
fi
|
|
394
|
+
if [ $((HIT_N + MISS_N)) -ne "$N_PROBED" ]; then
|
|
395
|
+
echo "🟥 HARNESS-ERROR: 발화 집계 $HIT_N+$MISS_N ≠ 프로브 발화 $N_PROBED — 집계기가 행을 잃었다. 원문:"
|
|
396
|
+
sed 's/^/ /' "$LAND_OUT"; exit 10
|
|
397
|
+
fi
|
|
398
|
+
|
|
399
|
+
echo " 착지 $HIT_N · 미착지 $MISS_N · 프로브불가 $UNPROBE (행: 착지 $((N_TARGET - ROW_MISS)) · 미착지 $ROW_MISS · 판정 = 발화 토큰 ⌈n/2⌉ 이상)"
|
|
400
|
+
if [ "$MISS_N" -gt 0 ]; then
|
|
401
|
+
echo
|
|
402
|
+
echo " 미착지 목록 (발화 앞 60자):"
|
|
403
|
+
# 라벨의 `#<n>` 으로 map 을 되짚는다. 숫자 비교라 비ASCII 동등성 함정에 안 걸린다
|
|
404
|
+
# (`[[feedback_locale_string_equality_breaks_nonascii]]`).
|
|
405
|
+
for _k in $MISS_IDX; do
|
|
406
|
+
LC_ALL=C awk -F'\t' -v k="$_k" '$1==k{printf " · %s\n", $3}' "$WORK/probes.map"
|
|
407
|
+
done
|
|
408
|
+
fi
|
|
409
|
+
# 프로브불가 발화는 «못 쟀다» 지 «착지» 가 아니다 — 미착지와 별개로 세되 rc 에는 같이 든다
|
|
410
|
+
# (codex finding 2: MISS_N=0 이면 조용히 rc=0 이던 것을 닫음). 사람이 확인할 목록으로 낸다.
|
|
411
|
+
if [ "${UNPROBE:-0}" -gt 0 ]; then
|
|
412
|
+
echo
|
|
413
|
+
echo " 프로브불가 $UNPROBE 건 (키 낱말을 못 뽑은 발화 — 미측정, 착지 아님. 눈으로 확인):"
|
|
414
|
+
LC_ALL=C awk -F'\t' '$2=="UNPROBEABLE"{printf " · %s\n", $3}' "$WORK/probes.map"
|
|
415
|
+
fi
|
|
416
|
+
if [ "$MISS_N" -gt 0 ] || [ "${UNPROBE:-0}" -gt 0 ]; then
|
|
417
|
+
echo
|
|
418
|
+
echo " ⚠️ rc=1 은 «기록하라» 가 아니라 «확인하라» 다 — 프로브 노후가 첫 번째 의심 대상이다."
|
|
419
|
+
echo " 가장 잘 빠지는 것은 잊은 발화가 아니라 **행동으로 대응한 발화**다."
|
|
420
|
+
exit 1
|
|
421
|
+
fi
|
|
422
|
+
echo " ✅ 프로브 발화 $N_PROBED 건 전부 착지 (발화별 최장 토큰 ≤3 중 ⌈n/2⌉ 이상 일치)"
|
|
423
|
+
echo " ⚠️ 키 2 개 이하 발화는 하나만 겹쳐도 착지다 — «제대로 적혔다» 의 증명이 아니다."
|
|
424
|
+
exit 0
|