@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.
Files changed (111) hide show
  1. package/.claude/regression/probes_live.yaml +137 -0
  2. package/.claude/rules/.residency-patterns.defaults +7 -0
  3. package/.claude/rules/fh_4axis_gate.md +50 -1
  4. package/.claude-plugin/marketplace.json +8 -2
  5. package/AGENTS.md +27 -0
  6. package/CATALOG.md +17 -0
  7. package/CLAUDE.md +12 -2
  8. package/README.ja.md +51 -7
  9. package/README.ko.md +48 -7
  10. package/README.md +37 -5
  11. package/README.zh.md +45 -8
  12. package/docs/STANDARDS_ALIGNMENT.md +61 -0
  13. package/docs/USER_GUIDE.md +3 -0
  14. package/docs/USE_CASES.md +50 -0
  15. package/docs/model_tier_expectations.md +60 -0
  16. package/knowledge/shared/harness-core/fh_three_layer_canon.md +30 -10
  17. package/knowledge/shared/harness-core/field_verdict_crossfamily_gate.md +8 -0
  18. package/knowledge/shared/harness-core/harness_incubator_doctrine.md +9 -0
  19. package/knowledge/shared/harness-core/iso_ai_standards_crosswalk.md +139 -0
  20. package/knowledge/shared/harness-core/measurement-integrity-checklist.md +20 -0
  21. package/knowledge/shared/learnings/subagent_invocations_log.yaml +441 -4
  22. package/package.json +40 -2
  23. package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
  24. package/plugins/fh-commons/skills/preprep/README.md +4 -1
  25. package/plugins/fh-commons/skills/preprep/SKILL.md +99 -3
  26. package/plugins/fh-commons/skills/preprep/diagram_from_json.py +154 -0
  27. package/plugins/fh-commons/skills/preprep/fixtures/fixture_R3_negative.pptx +0 -0
  28. package/plugins/fh-commons/skills/preprep/fixtures/fixture_R3_positive.pptx +0 -0
  29. package/plugins/fh-commons/skills/preprep/fixtures/mk_slide_fixtures.py +179 -0
  30. package/plugins/fh-commons/skills/preprep/interslide_deps.py +98 -9
  31. package/plugins/fh-commons/skills/preprep/lane_adjacent_dup.py +4 -1
  32. package/plugins/fh-commons/skills/preprep/lane_diagram.py +95 -0
  33. package/plugins/fh-commons/skills/preprep/lane_geometry.py +181 -0
  34. package/plugins/fh-commons/skills/preprep/lane_promise.py +4 -1
  35. package/plugins/fh-commons/skills/preprep/lane_slide_refs.py +134 -0
  36. package/plugins/fh-commons/skills/preprep/lane_slide_relations.py +300 -0
  37. package/plugins/fh-commons/skills/preprep/preprep.py +129 -8
  38. package/plugins/fh-commons/skills/preprep/preprep_wire.py +210 -0
  39. package/plugins/fh-commons/skills/preprep/presentation_checklist.md +4 -0
  40. package/plugins/fh-commons/skills/preprep/surfaces.example.yaml +18 -0
  41. package/plugins/fh-commons/skills/preprep/test_preprep_lanes_rp.py +184 -0
  42. package/plugins/fh-meta/.claude-plugin/plugin.json +1 -1
  43. package/plugins/fh-meta/CHANGELOG.md +43 -1
  44. package/plugins/fh-meta/skills/auto-decorrelation/SKILL.md +87 -2
  45. package/plugins/fh-meta/skills/frontier-digest/SKILL.md +2 -2
  46. package/plugins/fh-meta/skills/frontier-digest/SKILL_detail.md +25 -5
  47. package/plugins/fh-meta/skills/hub-cc-pr-reviewer/SKILL.md +11 -0
  48. package/plugins/fh-meta/skills/hub-cc-pr-reviewer/SKILL_detail.md +3 -0
  49. package/plugins/fh-qp/.claude-plugin/plugin.json +22 -0
  50. package/plugins/fh-qp/README.md +71 -0
  51. package/plugins/fh-qp/fixtures/evidence_known_clean.txt +3 -0
  52. package/plugins/fh-qp/fixtures/evidence_known_dirty.txt +6 -0
  53. package/plugins/fh-qp/fixtures/reach_known_partial.tsv +4 -0
  54. package/plugins/fh-qp/fixtures/reach_known_reached.tsv +4 -0
  55. package/plugins/fh-qp/fixtures/reach_known_wall.tsv +3 -0
  56. package/plugins/fh-qp/fixtures/verdicts_known_bad_branch.tsv +2 -0
  57. package/plugins/fh-qp/fixtures/verdicts_known_bad_vacuous_machine.tsv +2 -0
  58. package/plugins/fh-qp/fixtures/verdicts_known_good.tsv +4 -0
  59. package/plugins/fh-qp/fixtures/verdicts_verify_only.tsv +3 -0
  60. package/plugins/fh-qp/qp_profile.example.yaml +29 -0
  61. package/plugins/fh-qp/scripts/qp_tools.sh +216 -0
  62. package/plugins/fh-qp/skills/qp/SKILL.md +65 -0
  63. package/plugins/fh-qp/skills/qp-plan/SKILL.md +42 -0
  64. package/plugins/fh-qp/skills/qp-regress/SKILL.md +45 -0
  65. package/plugins/fh-qp/skills/qp-run/SKILL.md +49 -0
  66. package/scripts/chamber_run.sh +14 -5
  67. package/scripts/com.forge-harness.live-eval.plist +84 -0
  68. package/scripts/compaction_probe.sh +9 -35
  69. package/scripts/directional_diff_gate.sh +14 -2
  70. package/scripts/frontier_digest_autopilot.sh +4 -1
  71. package/scripts/map_postprocess.py +90 -0
  72. package/scripts/outbound_query_guard.sh +131 -0
  73. package/scripts/outbound_query_hook.sh +373 -0
  74. package/scripts/package_coverage_check.sh +55 -16
  75. package/scripts/pipe_verdict_guard.sh +41 -1
  76. package/scripts/probe_live_eval.sh +240 -0
  77. package/scripts/probe_live_eval_lib.py +579 -0
  78. package/scripts/proposal_hook.sh +120 -17
  79. package/scripts/push_zone_check.sh +78 -0
  80. package/scripts/residency_closure_scan.py +252 -0
  81. package/scripts/selfcheck.sh +41 -1
  82. package/scripts/session_close_check.sh +100 -0
  83. package/scripts/sim_isolated_run.sh +98 -2
  84. package/scripts/test_action_yml_lanes.sh +97 -0
  85. package/scripts/test_fh_qp_lanes.sh +105 -0
  86. package/scripts/test_gate_two_verdicts_lanes.sh +139 -0
  87. package/scripts/test_map_postprocess_lanes.sh +143 -0
  88. package/scripts/test_marker_affected_lanes.sh +93 -0
  89. package/scripts/test_marker_crossfamily_lanes.sh +90 -6
  90. package/scripts/test_marker_oracle_lanes.sh +136 -0
  91. package/scripts/test_outbound_query_hook_lanes.sh +433 -0
  92. package/scripts/test_outbound_query_lanes.sh +87 -0
  93. package/scripts/test_pipe_verdict_guard_lanes.sh +26 -0
  94. package/scripts/test_preprep_diagram_lanes.sh +87 -0
  95. package/scripts/test_preprep_drift_anchor.sh +3 -3
  96. package/scripts/test_preprep_slide_refs_lanes.sh +169 -0
  97. package/scripts/test_probe_live_eval_lanes.sh +437 -0
  98. package/scripts/test_proposal_hook_lanes.sh +22 -1
  99. package/scripts/test_push_zone_lanes.sh +304 -0
  100. package/scripts/test_residency_closure_lanes.sh +70 -0
  101. package/scripts/test_sim_isolated_run_lanes.sh +119 -0
  102. package/scripts/test_utterance_intake_lanes.sh +414 -0
  103. package/scripts/test_worktree_reclaim_lanes.sh +70 -0
  104. package/scripts/transcript_utterances.py +222 -0
  105. package/scripts/utterance_intake.sh +424 -0
  106. package/scripts/validate_yaml.sh +27 -0
  107. package/scripts/worktree_reclaim.sh +95 -0
  108. package/templates/.git-hooks/pre-commit +353 -1
  109. package/templates/.git-hooks/pre-push +91 -0
  110. package/templates/RED_TEAM_REPORT.md +49 -0
  111. package/templates/settings.PreToolUse.snippet.json +65 -1
@@ -0,0 +1,210 @@
1
+ #!/usr/bin/env python3
2
+ """pptx-only 자산군 어댑터 — preprep 레인이 읽을 수 있는 형태를 pptx 에서 세운다.
3
+
4
+ preprep 은 「원고 마크다운 + 빌더 스크립트」를 전제한다. **원고·빌더가 없고 pptx 자체가
5
+ 정본**인 자산군엔 그동안 붙일 길이 없었다(신호 §3-4). 이 어댑터가 그 자리를 메운다 — pptx 에서
6
+ 세 가지를 뽑아 세운다:
7
+
8
+ <out>/manuscript.md 장마다 🖥(화면 텍스트) · 🗣(발표자 노트). 둘 다 pptx 에서 뽑는다
9
+ <out>/note_map.py NOTE_MAP — 어느 절이 실제 장으로 배선됐나 (이 어댑터는 절=장 1:1)
10
+ <out>/surfaces.yaml 표면 선언. canon_terms/jargon_terms 는 이 파일의 basename 으로만
11
+ 적는다 — root(=<out>) 기준으로 못 찾으면 이제 스킬 위치로 자동
12
+ 폴백하므로(2026-09-04 수리), 사람이 자기 목록을 만들기 전엔 배포본의
13
+ example 을 그대로 가리켜도 UNMEASURED 로 안 죽는다
14
+
15
+ 🟥 이건 «대본을 다시 만드는 것»이 아니다 — **읽기 전용 파생물**이고, 원본 pptx 는 절대 안
16
+ 건드린다. 손으로 고치지 마라 — 재실행이 덮어쓴다.
17
+ 🟥 NOTE_MAP 이 전 절을 1:1 로 담으므로 L8 의 BROKEN(배선 끊김)은 이 어댑터 산출물에서
18
+ 구조적으로 안 뜬다. 값을 내는 것은 UNRESOLVED — «화면/앞 절을 가리키는데 대상을 못
19
+ 찾겠다» — 뿐이다.
20
+
21
+ 🟥 **함정(신호 §3-4 실측, 재발 방지로 이 주석과 known-pair 를 같이 둔다)**: 초판은 「첫
22
+ 도형은 제목이니 빼고 나머지만 🖥 로 담자」로 짰다가, **제목 도형 하나뿐인 목차형 장의
23
+ 화면이 통째로 비었다.** 첫 도형도 화면이다 — `_shapes()` 는 절대 첫 항목을 자르지 않는다.
24
+ `--self-test` 가 known-positive(제목 하나뿐인 장)로 이 재발을 계속 감시한다.
25
+
26
+ usage:
27
+ preprep_wire.py <deck.pptx> [--out DIR]
28
+ preprep_wire.py --self-test
29
+ """
30
+ import zipfile, re, os, sys
31
+
32
+ # 텍스트가 있어도 화면으로 안 셀 도형의 name(예: 장식용 섹션 안내 도형). 프로젝트마다
33
+ # 다르므로 사람이 편집하는 자리다 — 비워 두면 아무것도 안 거른다.
34
+ SKIP_SHAPES = ()
35
+
36
+
37
+ def _order(z):
38
+ rels = dict(re.findall(r'Id="([^"]+)"[^>]*Target="slides/slide(\d+)\.xml"',
39
+ z.read('ppt/_rels/presentation.xml.rels').decode('utf-8')))
40
+ lst = re.search(r'<p:sldIdLst>.*?</p:sldIdLst>',
41
+ z.read('ppt/presentation.xml').decode('utf-8'), re.S).group(0)
42
+ return [int(rels[r]) for r in re.findall(r'<p:sldId id="\d+" r:id="([^"]+)"/>', lst)]
43
+
44
+
45
+ def _shapes(z, sn):
46
+ """장 sn 의 화면 도형 텍스트 전부, 등장 순서대로.
47
+
48
+ 🟥 **첫 항목을 자르지 않는다.** 목차형 장(제목 도형 하나뿐)은 첫 도형을 빼면 화면이
49
+ 0개가 된다 — 신호 §3-4 실사고. `SKIP_SHAPES` 로 걸러진 것을 뺀 **나머지 전부**가 화면이다."""
50
+ x = z.read('ppt/slides/slide%d.xml' % sn).decode('utf-8')
51
+ out = []
52
+ for m in re.finditer(r'<p:(sp|graphicFrame)>.*?</p:\1>', x, re.S):
53
+ b = m.group(0)
54
+ nm = re.search(r'name="([^"]*)"', b)
55
+ nm = nm.group(1) if nm else '?'
56
+ if nm in SKIP_SHAPES:
57
+ continue
58
+ t = ' '.join(re.findall(r'<a:t>([^<]*)</a:t>', b)).strip()
59
+ if t:
60
+ out.append((nm, re.sub(r'\s+', ' ', t)))
61
+ return out
62
+
63
+
64
+ def _notes(z, sn):
65
+ p = 'ppt/slides/_rels/slide%d.xml.rels' % sn
66
+ if p not in z.namelist():
67
+ return ''
68
+ m = re.search(r'notesSlide(\d+)', z.read(p).decode('utf-8'))
69
+ if not m:
70
+ return ''
71
+ npath = 'ppt/notesSlides/notesSlide%s.xml' % m.group(1)
72
+ if npath not in z.namelist():
73
+ return ''
74
+ t = ' '.join(re.findall(r'<a:t>([^<]*)</a:t>', z.read(npath).decode('utf-8')))
75
+ return re.sub(r'\s+', ' ', t).strip()
76
+
77
+
78
+ def build(deck_path):
79
+ """계산부 — 디스크에 안 쓴다. (manuscript_lines, ids) 를 돌려준다.
80
+ known-pair 셀프테스트가 파일 없이 이 함수를 직접 부른다."""
81
+ z = zipfile.ZipFile(deck_path)
82
+ order = _order(z)
83
+ lines, ids = [], []
84
+ for i, sn in enumerate(order, 1):
85
+ sid = 'S%d' % i
86
+ ids.append(sid)
87
+ sh = _shapes(z, sn)
88
+ head = (sh[0][1] if sh else '(제목 없음)')[:50]
89
+ lines.append('### %s · %s' % (sid, head))
90
+ lines.append('🖥')
91
+ for _nm, t in sh: # 🖥 전체 — 첫 도형 포함, 절대 자르지 않는다
92
+ lines.append(t)
93
+ lines.append('🗣')
94
+ lines.append(_notes(z, sn) or '(발화 없음)')
95
+ lines.append('')
96
+ return lines, ids
97
+
98
+
99
+ def write(deck_path, out_dir):
100
+ os.makedirs(out_dir, exist_ok=True)
101
+ lines, ids = build(deck_path)
102
+ open(os.path.join(out_dir, 'manuscript.md'), 'w', encoding='utf-8').write('\n'.join(lines))
103
+
104
+ with open(os.path.join(out_dir, 'note_map.py'), 'w', encoding='utf-8') as f:
105
+ f.write('# 생성물 — preprep_wire.py 가 만든다. 손으로 고치지 마라(재실행이 덮어쓴다).\n')
106
+ f.write('# 이 어댑터는 절과 장을 1:1 로 배선한다(pptx 자체가 정본인 자산군 전제).\n')
107
+ f.write('NOTE_MAP = {\n')
108
+ for k, sid in enumerate(ids, 1):
109
+ f.write(' %d: (%r, None),\n' % (k, sid))
110
+ f.write('}\n')
111
+
112
+ # 🟥 canon_terms/jargon_terms 는 basename 만 적는다 — 2026-09-04 수리로 root(<out_dir>)
113
+ # 기준을 먼저 보고, 없으면 스킬 위치(HERE)로 자동 폴백하므로 example 을 그대로 가리켜도
114
+ # 죽지 않는다(그 자리 대신 「(skill-dir fallback)」 표기가 뜬다). 실전 목록은 사람이
115
+ # <out_dir>/ 아래에 같은 이름으로 두면 root 쪽이 우선한다.
116
+ surf = '''\
117
+ asset_family: "pptx-only 자산군 — preprep_wire 배선"
118
+ root: "."
119
+ canon_ledger: "CANON_LEDGER.md" # 🟥 사람이 채운다. 없으면 L1-a 는 UNCOVERED 로 뜬다
120
+ canon_terms: "canon_terms.example.yaml" # 🟥 root 기준 — 여기 없으면 스킬 위치로 폴백
121
+ jargon_terms: "jargon_terms.example.yaml"
122
+
123
+ interslide:
124
+ manuscript: script
125
+ builder: "note_map.py"
126
+
127
+ surfaces:
128
+ - id: script
129
+ path: "manuscript.md"
130
+ kind: markdown
131
+ canonical_for: [발화 문자열, 화면 문자열]
132
+ unit_pattern: '^###\\\\s+(S[0-9]+)\\\\s*·'
133
+ speech_marker: "🗣"
134
+ spoken: true
135
+ retire_markers: ["🚫", "제거됨", "폐기"]
136
+
137
+ adjacent_dup:
138
+ max_dup_chars: 40
139
+ '''
140
+ open(os.path.join(out_dir, 'surfaces.yaml'), 'w', encoding='utf-8').write(surf)
141
+ print('세움: %s/manuscript.md (%d절) · note_map.py · surfaces.yaml' % (out_dir, len(ids)))
142
+ print('🟥 canon_terms/jargon_terms 는 example 을 가리킨다 — 실전은 사람이 %s 에 자기'
143
+ ' 코퍼스의 금지어/조어 목록을 채워라(파일명을 같게 두면 root 쪽이 우선한다).'
144
+ % out_dir)
145
+
146
+
147
+ def selftest():
148
+ """known-pair — 신호 §3-4 «첫 도형 유실» 재발 방지. python-pptx 로 최소 덱을 지어 돈다."""
149
+ import tempfile
150
+ try:
151
+ from pptx import Presentation
152
+ from pptx.util import Inches
153
+ except ImportError:
154
+ print('preprep_wire SELFTEST: SKIPPED — python-pptx 없음 (0 아님, UNMEASURED)')
155
+ return 2
156
+
157
+ ok = True
158
+ with tempfile.TemporaryDirectory() as d:
159
+ deck = os.path.join(d, 't.pptx')
160
+ prs = Presentation()
161
+ blank = prs.slide_layouts[6]
162
+
163
+ # known-positive: 제목 도형 «하나뿐»인 목차형 장 — 화면이 비면 재발이다
164
+ s1 = prs.slides.add_slide(blank)
165
+ tb1 = s1.shapes.add_textbox(Inches(1), Inches(1), Inches(4), Inches(1))
166
+ tb1.text_frame.text = '목차'
167
+
168
+ # known-negative: 도형 둘(제목+본문) — 둘 다 화면에 남아야 정상
169
+ s2 = prs.slides.add_slide(blank)
170
+ tb2a = s2.shapes.add_textbox(Inches(1), Inches(1), Inches(4), Inches(1))
171
+ tb2a.text_frame.text = '본문 제목'
172
+ tb2b = s2.shapes.add_textbox(Inches(1), Inches(2), Inches(4), Inches(1))
173
+ tb2b.text_frame.text = '본문 내용'
174
+
175
+ prs.save(deck)
176
+ lines, _ids = build(deck)
177
+ text = '\n'.join(lines)
178
+ i1, i2 = text.index('### S1'), text.index('### S2')
179
+ block1, block2 = text[i1:i2], text[i2:]
180
+
181
+ screen1 = block1.split('🖥', 1)[1].split('🗣', 1)[0]
182
+ has1 = '목차' in screen1
183
+ print(f' {"PASS" if has1 else "FAIL"} known-positive(목차형 장, 제목 도형 하나뿐): '
184
+ f'화면에 «목차» {"있음" if has1 else "없음 — 첫 도형 유실 재발!"}')
185
+ ok &= has1
186
+
187
+ screen2 = block2.split('🖥', 1)[1].split('🗣', 1)[0]
188
+ has2 = ('본문 제목' in screen2) and ('본문 내용' in screen2)
189
+ print(f' {"PASS" if has2 else "FAIL"} known-negative(다중 도형 장, 둘 다 남아야 함): '
190
+ f'{"둘 다 있음" if has2 else "유실됨"}')
191
+ ok &= has2
192
+
193
+ print('preprep_wire SELFTEST:', 'PASS' if ok else 'FAIL — 첫 도형 유실이 재발했다')
194
+ return 0 if ok else 1
195
+
196
+
197
+ def main():
198
+ if '--self-test' in sys.argv:
199
+ return selftest()
200
+ if len(sys.argv) < 2:
201
+ print(__doc__)
202
+ return 2
203
+ deck = sys.argv[1]
204
+ out = sys.argv[sys.argv.index('--out') + 1] if '--out' in sys.argv else '_preprep'
205
+ write(deck, out)
206
+ return 0
207
+
208
+
209
+ if __name__ == '__main__':
210
+ sys.exit(main())
@@ -52,6 +52,10 @@
52
52
  아래를 보면 즉사. (DrawingML: headEnd=시작점, tailEnd=끝점 — 직관과 반대)
53
53
  - [ ] **C4. 채운 블록 화살표보다 선 + 삼각 화살촉.** 레퍼런스 덱의 어휘와 맞추면 절반은 간다.
54
54
  - [ ] **C5. 아이콘은 흰 선화 · 강조 노드에만 채움색 · 캡션은 도형 아래 작은 회색.**
55
+ - [ ] **C6. 「알고도 남긴 것」과 「몰라서 남은 것」을 갈라 적어라 (2026-09-04 실측).** 팔레트
56
+ 밖 색이나 규칙 밖 값을 **정해서** 남긴 자리가 있으면 그 자리에 이유를 적는다 — C2 와
57
+ 같은 원리를 값 전반으로 넓힌 것이다. 안 적으면 다음 감사자가 같은 것을 다시 «발견»하고,
58
+ 지운 값이 다른 번호로 되살아나기도 한다(실측: 지운 색이 다른 색번호로 되살아나 있었다).
55
59
 
56
60
  ## D. 언어 — 청중이 실제 쓰는 말
57
61
 
@@ -44,6 +44,15 @@ surfaces:
44
44
  path: "../scripts/gen_fig_s13b.py"
45
45
  kind: figure_source
46
46
  canonical_for: [도해 문구]
47
+ # 🟥 2026-09-05 신설 — 타입 있는 도해 JSON(archify 스키마)을 표면으로 세운다. figure_source 처럼
48
+ # 그림 «소스»의 문자열이 L1/L5 에 잡히고, 추가로 L12 가 옆의 굽기 영수증(*.receipt.json,
49
+ # diagram_from_json.py 가 쓴다)으로 «지금 JSON 의 그림인가 · validate 통과 · 해상도 · viewBox 폭»을 본다.
50
+ # render 를 안 적으면 UNMEASURED(0 아님). 손으로 넣은 PNG(영수증 없음)도 UNMEASURED.
51
+ - id: fig_s44
52
+ path: "figures/s44_methods_make_gate.workflow.json"
53
+ kind: diagram_source
54
+ render: "figures/s44_methods_make_gate.png"
55
+ canonical_for: [도해 문구]
47
56
  - id: wiring
48
57
  path: "../scripts/ifkakao_build_deck.py"
49
58
  kind: note_map
@@ -139,6 +148,15 @@ progressions:
139
148
  - anchor: "더 늘려도"
140
149
  - anchor: "입장을 바꾸면"
141
150
 
151
+ # ── L12 diagram — 타입 JSON 도해의 굽기 정합 (2026-09-05) ─────────────────────────
152
+ # 값의 근거: 장표 26.67in = 1920pt → 2× = 3840px. viewBox 폭은 archify 글자 배율을 정한다
153
+ # (pt = unit × 1728/폭: 720 이면 라벨 29pt·부제 19pt, 덱 최소 18pt 충족 / 845 면 부제 16pt 미달).
154
+ # 🟥 덱이 다르면 min_px 는 그 덱 폭의 2× 로, max_viewbox_w 는 그 덱 최소 글자로 다시 정한다.
155
+ diagram:
156
+ min_px: 3840
157
+ max_viewbox_w: 720
158
+ min_pad: 0.02
159
+
142
160
  # ── L10 adjacent-dup — 인접 장이 같은 문장을 다시 읽나 ────────────────────────
143
161
  # 🟥 구조는 범용이고 **임계는 발표마다 다르다** — 그래서 갈라 놓는다.
144
162
  # 미기재면 계기가 «재기만 하고 판정 안 한다» 고 스스로 적는다(조용히 통과가 아니다).
@@ -0,0 +1,184 @@
1
+ #!/usr/bin/env python3
2
+ """R1~R5 · P1/P3 self-test — known-pair 계량 + 실물 재현.
3
+
4
+ exit 0 = 전부 통과 · 1 = 실패 있음 · 2 = 픽스처 생성 불가(python-pptx 부재 등, UNMEASURED)
5
+
6
+ 구성:
7
+ ① known-pair — `fixtures/mk_slide_fixtures.py` 로 R1·R2·R4·R5·P1·P3 를 테스트 시점에 만든다
8
+ (R3 는 예외 — 실물 구조 2장을 줄인 `fixtures/fixture_R3_{positive,negative}.pptx` 를 쓴다.
9
+ 선두 줄 판별이 절대좌표·srgbClr 런 구조에 기대므로, 검출기 가정대로 «생성»한 픽스처는
10
+ 검출기 자신을 검증 못 한다 — 자기참조. `tracks-meta` 원장이 지목한 방침)
11
+ ② --baseline 델타 모드 — 편집 전/후 한 쌍에서 새 어긋남 1건만 잡히는지
12
+ ③ 실물 재현 — 실제 백업 pptx(환경변수 PREPREP_REAL_CORPUS 가 가리키는 디렉터리, 있을 때만)에 돌려 원장이 인용한 실사고 4+1건이
13
+ 그대로 뜨는지 확인한다. 그 코퍼스가 없는 머신에서는 SKIP(«통과»로 세지 않는다).
14
+ """
15
+ import os
16
+ import sys
17
+
18
+ HERE = os.path.dirname(os.path.abspath(__file__))
19
+ sys.path.insert(0, HERE)
20
+ sys.path.insert(0, os.path.join(HERE, 'fixtures'))
21
+
22
+ PASS, FAIL, SKIP = 0, 0, 0
23
+
24
+
25
+ def ok(msg):
26
+ global PASS
27
+ PASS += 1
28
+ print(' ✅', msg)
29
+
30
+
31
+ def ng(msg):
32
+ global FAIL
33
+ FAIL += 1
34
+ print(' ❌', msg)
35
+
36
+
37
+ def sk(msg):
38
+ global SKIP
39
+ SKIP += 1
40
+ print(' ⏭ ', msg, '— SKIPPED (PASS 아님)')
41
+
42
+
43
+ def codes_of(deck_path, LSR):
44
+ deck = LSR.Deck(deck_path)
45
+ F = LSR.analyze(deck)
46
+ return F, {k for k, _, _ in F}
47
+
48
+
49
+ def run_known_pairs(fx_dir):
50
+ print('\n[1] known-pair — R1·R2·R4·R5·P1·P3 (생성 픽스처) + R3 (실물 픽스처)')
51
+ import lane_slide_relations as LSR
52
+ import lane_geometry as LG
53
+
54
+ # R1
55
+ _, cp = codes_of(os.path.join(fx_dir, 'r1_pos.pptx'), LSR)
56
+ _, cn = codes_of(os.path.join(fx_dir, 'r1_neg.pptx'), LSR)
57
+ ok('R1 known-positive 검출') if 'R1' in cp else ng(f'R1 known-positive 미검출 ({cp})')
58
+ ok('R1 known-negative 무검출') if 'R1' not in cn else ng(f'R1 known-negative에서 오탐 ({cn})')
59
+
60
+ # R2
61
+ _, cp = codes_of(os.path.join(fx_dir, 'r2_pos.pptx'), LSR)
62
+ _, cn = codes_of(os.path.join(fx_dir, 'r2_neg.pptx'), LSR)
63
+ ok('R2 known-positive 검출') if 'R2' in cp else ng(f'R2 known-positive 미검출 ({cp})')
64
+ ok('R2 known-negative 무검출') if 'R2' not in cn else ng(f'R2 known-negative에서 오탐 ({cn})')
65
+
66
+ # R3 — 실물 픽스처 (자기참조 회피)
67
+ r3p = os.path.join(HERE, 'fixtures', 'fixture_R3_positive.pptx')
68
+ r3n = os.path.join(HERE, 'fixtures', 'fixture_R3_negative.pptx')
69
+ if os.path.exists(r3p) and os.path.exists(r3n):
70
+ _, cp = codes_of(r3p, LSR)
71
+ _, cn = codes_of(r3n, LSR)
72
+ ok('R3 known-positive(실물 2장) 검출') if 'R3' in cp else ng(f'R3 known-positive 미검출 ({cp})')
73
+ ok('R3 known-negative(실물 2장) 무검출') if 'R3' not in cn else ng(f'R3 known-negative에서 오탐 ({cn})')
74
+ else:
75
+ sk('R3 known-pair — fixture_R3_{positive,negative}.pptx 없음')
76
+
77
+ # R4
78
+ Fp, cp = codes_of(os.path.join(fx_dir, 'r4_pos.pptx'), LSR)
79
+ Fn, cn = codes_of(os.path.join(fx_dir, 'r4_neg.pptx'), LSR)
80
+ ok('R4 known-positive 검출(아웃라이어 500자)') if 'R4' in cp else ng(f'R4 known-positive 미검출 ({cp})')
81
+ ok('R4 known-negative 무검출(동값 분포)') if 'R4' not in cn else ng(f'R4 known-negative에서 오탐 ({cn})')
82
+
83
+ # R5
84
+ Fp, cp = codes_of(os.path.join(fx_dir, 'r5_pos.pptx'), LSR)
85
+ Fn, cn = codes_of(os.path.join(fx_dir, 'r5_neg.pptx'), LSR)
86
+ ok('R5 known-positive 검출(비율 30.0)') if 'R5' in cp else ng(f'R5 known-positive 미검출 ({cp})')
87
+ ok('R5 known-negative 무검출(동일 비율)') if 'R5' not in cn else ng(f'R5 known-negative에서 오탐 ({cn})')
88
+
89
+ # P1 — 3팔(임계 안 소폭 이동은 뜼다 · 임계 밖 큰 이동은 안 뜼다 · 무이동은 안 뜼다)
90
+ lines, compared = LG.collect_lines(os.path.join(fx_dir, 'p1.pptx'), 200000, 63500)
91
+ p1_lines = [l for l in lines if 'gate_bar' in l]
92
+ ok(f'P1 대조된 도형-쌍 {compared} > 0 (비교 안함 아님)') if compared > 0 else ng('P1 compared=0 — UNMEASURED')
93
+ ok(f'P1 후보 정확히 1건(임계 안 이동만)') if len(p1_lines) == 1 else ng(f'P1 후보 {len(p1_lines)}건 (want 1) — {p1_lines}')
94
+ if p1_lines:
95
+ ok('P1 유일 후보가 정확히 (1→2p) 소폭 이동 — 큰 이동(2→3p)·무이동(3→4p)은 안 뜬다') \
96
+ if '1p→2' in p1_lines[0].replace(' ', '') else ng(f'P1 후보가 엉뚱한 쌍을 가리킨다 {p1_lines}')
97
+
98
+ # P3
99
+ lines3, _ = LG.collect_lines(os.path.join(fx_dir, 'p3.pptx'), 200000, 63500)
100
+ p3_lines = [l for l in lines3 if 'stub' in l]
101
+ ok('P3 후보 정확히 1건(임계 안 틀/겹침만)') if len(p3_lines) == 1 else ng(f'P3 후보 {len(p3_lines)}건 (want 1) — {p3_lines}')
102
+
103
+
104
+ def run_baseline_delta(fx_dir):
105
+ print('\n[2] --baseline 델타 모드 — 편집 전/후 한 쌍')
106
+ import lane_geometry as LG
107
+ new, gone, cmp_b, cmp_c = LG.delta(os.path.join(fx_dir, 'geo_base.pptx'),
108
+ os.path.join(fx_dir, 'geo_edited.pptx'), 200000, 63500)
109
+ ok('델타 새 어긋남 정확히 1건') if len(new) == 1 else ng(f'델타 새 어긋남 {len(new)}건 (want 1)')
110
+ ok('델타 사라진 어긋남 0건(깨끗한 기준본)') if len(gone) == 0 else ng(f'델타 사라진 어긋남 {len(gone)}건 (want 0)')
111
+ ok(f'기준본 대조 {cmp_b} > 0 · 현재본 대조 {cmp_c} > 0') if cmp_b > 0 and cmp_c > 0 \
112
+ else ng(f'기준본/현재본 대조 0 있음 — UNMEASURED (cmp_b={cmp_b}, cmp_c={cmp_c})')
113
+ # scan(cfg, root) 래퍼로도 동일하게 도는지 확인 (preprep.py 가 실제로 부르는 경로)
114
+ cfg = {'surfaces_by_id': {'built_deck': {'path': os.path.join(fx_dir, 'geo_edited.pptx')}},
115
+ 'geometry': {'baseline': os.path.join(fx_dir, 'geo_base.pptx')}}
116
+ fP, nP = LG.scan(cfg, '.')
117
+ ok('scan(cfg, root) 래퍼가 델타 모드로 도고 새 어긋남 1건을 notes 에 낸다') \
118
+ if any('새 어긋남 1건' in n for n in nP) else ng(f'scan() 델타 출력 이상 — {nP}')
119
+
120
+
121
+ REAL_BASE = os.environ.get('PREPREP_REAL_CORPUS', '') # 기본값 없음 — 다른 머신에서 거짓 SKIP 방지(SKIP != PASS)
122
+ REAL_TARGETS = [
123
+ ('ifkakao26_slides_v1.3_pre-engine-strip_20260904_210857.pptx', 'R1', 40,
124
+ '선두 줄이 «그러나»로 뒤집는데'),
125
+ ('ifkakao26_slides_v1.3_pre-40p_20260904_213452.pptx', 'R2', 41,
126
+ '«이 셋/세 가지»라 부르는데 번호가 없다'),
127
+ ('ifkakao26_slides_v1.3_pre-38trim_20260904_210428.pptx', 'R4', 38,
128
+ '화면 250자'),
129
+ ('ifkakao26_slides_v1.3_pre-40p_20260904_213452.pptx', 'R5', 112,
130
+ '29.2자/초'),
131
+ ]
132
+
133
+
134
+ def run_real_corpus():
135
+ print('\n[3] 실물 재현 (PREPREP_REAL_CORPUS 백업, 있을 때만) — 원장이 인용한 실사고 4+1건')
136
+ if not os.path.isdir(REAL_BASE):
137
+ sk(f'실물 코퍼스 미지정/미존재({REAL_BASE or "PREPREP_REAL_CORPUS unset"}) — SKIP, 통과 아님')
138
+ return
139
+ import lane_slide_relations as LSR
140
+ import lane_geometry as LG
141
+ for fname, code, want_page, want_sub in REAL_TARGETS:
142
+ p = os.path.join(REAL_BASE, fname)
143
+ if not os.path.exists(p):
144
+ sk(f'{fname} 없음 — 재현 건너뜀')
145
+ continue
146
+ deck = LSR.Deck(p)
147
+ F = LSR.analyze(deck)
148
+ hit = [x for x in F if x[0] == code and abs(x[1] - want_page) <= 1 and want_sub in x[2]]
149
+ ok(f'{code} 실물 재현 — {fname} 약 {want_page}p — {hit[0][2][:60] if hit else ""}') \
150
+ if hit else ng(f'{code} 실물 미재현 — {fname}')
151
+
152
+ # P1/P3 36p (보너스 확인 — 원장이 명시적으로 요구한 4건에는 없지만 신호 자체의 원적 사건)
153
+ align36 = os.path.join(REAL_BASE, 'ifkakao26_slides_v1.3_pre-align36_222442.pptx')
154
+ if os.path.exists(align36):
155
+ lines, compared = LG.collect_lines(align36, 200000, 63500)
156
+ hit_p1 = any('s5017' in l and '35000' in l and '35p' in l.replace(' ', '') for l in lines)
157
+ hit_p3 = any('s5016' in l and 's5017' in l and '35000' in l for l in lines)
158
+ ok('P1 실물 재현 — 35p→36p s5017 x -35,000') if hit_p1 else ng('P1 실물 미재현')
159
+ ok('P3 실물 재현 — 36p s5016→s5017 겹침 35,000') if hit_p3 else ng('P3 실물 미재현')
160
+ else:
161
+ sk('ifkakao26_slides_v1.3_pre-align36_222442.pptx 없음 — P1/P3 보너스 재현 건너뜀')
162
+
163
+
164
+ def main():
165
+ try:
166
+ import mk_slide_fixtures
167
+ except ImportError as e:
168
+ print(f'❌ python-pptx 또는 픽스처 모듈 부재 — UNMEASURED: {e}')
169
+ return 2
170
+ import tempfile
171
+ fx_dir = tempfile.mkdtemp(prefix='preprep_lanes_rp_')
172
+ mk_slide_fixtures.build_all(fx_dir)
173
+ print(f'픽스처 생성: {fx_dir}')
174
+
175
+ run_known_pairs(fx_dir)
176
+ run_baseline_delta(fx_dir)
177
+ run_real_corpus()
178
+
179
+ print(f'\n결과: {PASS} passed · {FAIL} failed · {SKIP} skipped (skip != pass)')
180
+ return 1 if FAIL else 0
181
+
182
+
183
+ if __name__ == '__main__':
184
+ sys.exit(main())
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fh-meta",
3
- "version": "3.0.0",
3
+ "version": "3.1.0",
4
4
  "engines": {
5
5
  "claudeCode": ">=1.0.0"
6
6
  },
@@ -1,5 +1,47 @@
1
1
  # forge-harness (fh-meta) Changelog
2
2
 
3
+ ### [3.1.0] — 2026-09-06 — QP 플러그인 · `oracle:` 마커 채널 · preprep 도해 굽기 · FH 전체 지도 발행 · fh-gate GitHub Action
4
+
5
+ ### 왜 minor 인가
6
+ CLAUDE.md §④-b — 새 자산(fh-qp 플러그인 · `oracle:` 채널 · preprep 도해) + 새 게이트 레인 다수 + 행동을 바꾸는 교리(3층 정본 개정). 🟡 **major 로 읽을 여지는 있었다**: fh-qp 는 «제품 QA» 라는 capability *class* 가 FH 에 처음 생긴 것(정책 ⓒ)이다. 다만 폴백 1차판 · 선택 설치 · 기존 게이트 무변경이라 minor 로 뒀다. 정체성 등급은 무변경(다섯 🟢 유지)이라 major ⓑ 사유도 없다.
7
+
8
+ ### 🟥 BREAKING (gate): 마커 `oracle:` 근사키 차단 (#642)
9
+ **무엇이 이제 막히나**: `oracle_type:` · `Oracle:` · `oracle :` 처럼 «oracle 로 시작하는데 정확히 `oracle:` 이 아닌 줄」과, enum 6종 밖의 값. 종전엔 근사키가 조용히 무시돼 «오라클을 적었다고 믿는데 아무것도 안 적힌» 상태가 났다.
10
+ **처방**: `oracle: <known-pair|metamorphic|back-to-back|a-b|human|none — 근거>` 한 줄로 고치거나, 줄을 지운다(**부재는 통과**한다 — 이 필드는 옵셔널이다).
11
+
12
+ ### 🟥 BREAKING (gate): preprep 레인 L12 diagram (#654)
13
+ **무엇이 이제 막히나** — 도해 표면(`kind: diagram_source`)을 **선언한** 덱에서만, 그리고 아래 여덟 가지에서만 rc=1 이 난다: 선언된 PNG 가 디스크에 없다 · 영수증 파싱 실패 · JSON 지문이 영수증과 다르다(고치고 다시 안 구웠다) · showcase validate 통과 기록 없음 · PNG 헤더 읽기 실패 · PNG 폭이 하한 미만 · viewBox 폭이 상한 초과(글자 배율 미달) · 여백이 하한 미만. **도해 표면을 선언하지 않은 덱은 무영향.**
14
+ 🟥 **영수증이 «없는» 것은 차단이 아니다 — `UNMEASURED` 로 기록된다**(`lane_diagram.py:62` 가 `notes` 로 보내고 `preprep.py:903` 은 `findings` 가 있을 때만 rc=1). 손으로 넣은 그림은 이 레인이 «못 보는» 것이지 «틀렸다»가 아니고, 그 둘을 접지 않는 것이 이 레인의 설계다.
15
+ **처방**: 위 여덟 중 하나에 걸렸으면 `diagram_from_json.py` 로 다시 굽거나 표면 선언을 지운다. `UNMEASURED` 는 고칠 것이 아니라 «안 쟀다」는 기록이다.
16
+ ⚠️ 이 문단의 초판은 「영수증 없는 그림은 차단」이라고 적었다 — 과대 주장이었고, 릴리스 전 cross-family 감사(codex)가 소스에서 잡았다. 자력 적발 0.
17
+
18
+ ### 새로 실리는 것
19
+ - **fh-qp (QP · Quality Platform)** — qasp PAR 의 범용판. 도메인 상수 0, MCP 폴백 1차(Playwright 웹 · computer-use 데스크톱), typed capability 슬롯(미등록). 스킬 4 · `qp_tools.sh` · 레인 29 · 챔버 런 #18 EMIT. 선택 설치: `claude plugin install -s user fh-qp@forge-harness` (#653)
20
+ - **fh-gate GitHub Action** — `action.yml`. typed exit 7값을 boolean 으로 접지 않는 매핑, 모르는 코드는 `UNKNOWN`(fail-closed), 출력 `reviewed` 로 «안 돌았다 ≠ 통과» 를 분리. `fail-on` 기본이 PASS·PENDING 만 통과. 레인 28. 사용례가 `uses: chrono-meta/forge-harness@v3.1.0` 을 가리키므로 **이 태그가 그 자산의 사용 조건**이다 (#658)
21
+ - **마커 옵셔널 `oracle:` 채널** — TR 29119-11 오라클 유형 닫힌 enum 6, 훅은 형식만 검사. 레인 39 · 배선 W6 (#642)
22
+ - **마커 옵셔널 `affected:` 채널** — 「이 변경이 건드리는 것 + 열린 질문」 한 줄. 자리표시자만 있는 값은 차단(제로 영향은 명제상 없다) (#624)
23
+ - **preprep — «도해는 타입 JSON 에서 굽는다»** + 레인 L12 + 앵커 K1~K7. 첫 실사용 121장 덱 (#654). L13 슬라이드 참조 레인(장 번호 참조가 실제 장을 가리키는가) (#662)
24
+ - **FH 전체 지도** — `docs/map/FH_MAP.md` 5층 + archify 인터랙티브 3장, GitHub Pages 로 발행: **https://chrono-meta.github.io/forge-harness/** (#644 #645 #647 #659)
25
+ - **3층 정본 개정** — 3단 공정 = 모든 작업의 방법론 · 4대 엔진 = 출력이 나오는 코어 · 정체성 = 맞물려 나타나는 능력(5대 = 단련된 실물+등급) (#643)
26
+ - **ISO/IEC AI 표준 crosswalk** — 42119-2/3.2/7/8 · TR 29119-11 · 25059 · 42001 Annex A · 5338 · 23894 · TS 8200 (#641) · `docs/STANDARDS_ALIGNMENT.md`
27
+ - **live-eval** — `/prompt-regression` 의 라이브 짝(프로브 12 · 격리 클론 · 4값 채점 · launchd 템플릿) (#625), launchd PATH 에 `timeout(1)` 부재로 12/12 FAILED-TO-RUN 하던 것 수리 (#632)
28
+ - **worktree_reclaim.sh** — 워크트리 제거 «전» gitignored `tracks/**` 회수(목록 파일 먼저 → 복사+검증, 제거는 사람) (#639)
29
+ - **push-zone 게이트** — 비-소유 github.com 원격 push 를 pre-push 에서 차단 (#636) · **outbound query 가드** PreToolUse(WebSearch|WebFetch) (#637) · **residency 폐포 스캔**을 `crossfamily:` grounds 의 `residency=` 토큰으로 (#633)
30
+ - **sim 러너 MCP 격리** — `--strict-mcp-config` 기본. 헤드리스 팔이 운영자 사용자 스코프 MCP 를 상속하던 것 차단 (#651)
31
+ - **pipe_verdict_guard R3** — heredoc 뒤 «리다이렉션만 있는 줄»(zsh NULLCMD `cat` 이 stdin 파이프를 영원히 읽는 형태) 검출. «push 멈춤» 9건의 실제 원인이었다 (#618) · R2 오탐 수리 (#661)
32
+ - 문서: 모델 등급 × 기대 역량 (#635) · `docs/USE_CASES.md` · 카드 규율의 역방향 상주화(«안 닫힌 것이 사라지면 유실») (#656)
33
+
34
+ ### 같이 고친 것
35
+ - `directional_diff_gate.sh` `has_nul` 이중 정의 — 셀프테스트가 자기 사본만 재고 프로덕션 파손에 초록이던 결함 (#634)
36
+ - 챔버 step 5 가 2026-08-17 교리 어휘(CURATED · NOT-APPLICABLE)를 받는다 — **18일간 러너가 교리를 거부하고 있었다** (#620)
37
+ - 헤드리스 런에서 Skill 툴 무음 거부(`--allowedTools`) + 그 플래그가 variadic 이라 뒤따르는 프롬프트를 먹던 인자 순서 (#612 #631)
38
+ - `fh_audit_check.zsh` 빈 허브에서 zsh 미매치 glob 오류 (#613)
39
+ - (허브 오너 노트 · npm 미출하) `sync-to-be.sh` 목적지 가드 — 비-git 디렉토리·외부 레포 서브디렉토리 companion 은 rc=12 거부 (#646)
40
+
41
+ ### 알려진 한계 (이 릴리스 시점)
42
+ - `docs/map/**` 는 `files[]` 밖이라 **npm 배포물에 안 실린다** — GitHub Pages 표면에만 있다.
43
+ - live-eval 문턱 `0.80` 은 **보정 주간(~2026-09-11) 상수**다. rc 는 나오지만 판정에 쓰지 않는다.
44
+
3
45
  ### [3.0.0] — 2026-09-04 — 정체성 다섯 전부 🟢(identity 1.0 을 이 번호가 나른다) · 릴리스 트랙 통일 · 훅 사실 줄 · 게이트 무음 접힘 수리 · 6축 단련
4
46
 
5
47
  ### 왜 major 인가
@@ -7,7 +49,7 @@ CLAUDE.md §④-b major ⓑ — **정체성 다섯이 «전부» 🟢**(2026-09-
7
49
  운영자 결정(2026-09-04): `identity-v1.0.0` 은 정체성 트랙의 마지막 태그, **이후 한 번호** — 이 3.0.0 이 identity 1.0 을 같이 나른다. 🧭 접두 태그는 더 만들지 않는다. 등급의 정본은 여전히 `ship_readiness_gate.md` 의 셀이고 번호는 나르기만 한다.
8
50
 
9
51
  ### 🟥 BREAKING (gate): pre-commit [Pointers] 게이트가 워킹트리가 아니라 **인덱스**를 읽는다 (#602)
10
- **무엇이 이제 막히나**: `.md` 를 스테이징한 뒤 디스크에서 지우면(또는 더 고치면) 종전엔 `[ -f ] || continue` 로 Detail-pointer 검사가 통째로 빠졌다 — 깨진 `**Detail**: See §X` 포인터가 조용히 커밋됐다. 이제 `git show ":$f"` 로 스테이징된 바이트를 검사하고, 인덱스에서 못 읽으면 ❌ 로 막는다. **처방**: 스테이징한 것이 곧 커밋되는 것이다 — 디스크 파일을 믿지 마라. 레인 `scripts/test_precommit_pointer_index_lanes.sh`(일회용 클론) 가 옛 훅에선 정확히 P1 만 실패함을 보인다.
52
+ **무엇이 이제 막히나**: `.md` 를 스테이징한 뒤 디스크에서 지우면(또는 더 고치면) 종전엔 `[ -f ] || continue` 로 Detail-pointer 검사가 통째로 빠졌다 — 깨진 Detail 포인터(`**Detail**` + `See <path> §X` 형태)가 조용히 커밋됐다. 이제 `git show ":$f"` 로 스테이징된 바이트를 검사하고, 인덱스에서 못 읽으면 ❌ 로 막는다. **처방**: 스테이징한 것이 곧 커밋되는 것이다 — 디스크 파일을 믿지 마라. 레인 `scripts/test_precommit_pointer_index_lanes.sh`(일회용 클론) 가 옛 훅에선 정확히 P1 만 실패함을 보인다.
11
53
 
12
54
  ### 🟥 BREAKING (gate): selfcheck 가 명시경로 게이트 인프라(bin/fh-gate·fh-run·fh-goal 등) 부재를 FAIL 로 센다 (#602)
13
55
  종전엔 glob 미매칭과 같은 `continue` 로 무음. glob 미매칭 스킵은 그대로. **처방**: 그 파일들이 없어졌으면 files[] 와 selfcheck 목록을 같이 고쳐라.
@@ -193,6 +193,63 @@ diversity vs the orchestrator** (orchestrator = Claude/opus → recruit GPT or G
193
193
  succeeded where reading-files-itself hung) — or degrade to single-session with a recorded note.
194
194
  This is the sidecar twin of `mcp-circuit-breaker` (stop a stuck external call instead of hanging on it).
195
195
 
196
+ ## Step 4.5 — Residency screen (mandatory, immediately before ANY external-family send)
197
+
198
+ **Every** payload this skill sends outward — the diff, the marker (Step 5's payload), the prompt
199
+ text itself — crosses a family boundary the moment a codex/agy/gemini sidecar is recruited.
200
+ "Sanitize before external-family dispatch" used to be a prose reminder with no consumer reading it
201
+ back (`[[feedback_half_externalization_slot_without_consumer]]` shape — a slot that exists but
202
+ nothing downstream checks). This step makes it mechanical:
203
+
204
+ ```bash
205
+ python3 scripts/residency_closure_scan.py --files src/a.py docs/b.md # every file you are about to send, as separate args (zsh: ${=var}, never an unquoted "$var")
206
+ ```
207
+
208
+ `--files` (not the default closure-walk) because what matters is **what you are actually
209
+ sending**, not this skill's own dependency graph. Read the exit code, not the words:
210
+
211
+ | Exit | Meaning | What to do |
212
+ |---|---|---|
213
+ | 0 | CLEAN — no organizational identifier found **within the scanner's own stated limits** (see its header — this is a screening result, not a safety guarantee) | Send. Record `residency=CLEAN(files=N)` in the crossfamily grounds (Step 6). |
214
+ | 1 | TAINTED — at least one payload file matched a residency pattern | Do not send as-is. Either strip/redact the tainted file(s) from the payload and re-scan, or route to an in-boundary panel member. If it truly cannot be cleaned, record `residency=TAINTED(N files, stripped=no)` and do NOT dispatch — the crossfamily value becomes a `DEGRADED_*` rung, never `panel(...)`. |
215
+ | 10 | HARNESS_ERROR — the scan itself could not run (commonly: no operator-private `.claude/rules/.residency-patterns` override on this machine — the shipped `.defaults` file alone is deliberately insufficient to claim CLEAN, per the scanner's own header) | Do not send. Record `residency=NOT_SCANNED(<reason>)` — same as exit 1's outcome (no dispatch), different reason (*could not screen* vs *screened and found something*). |
216
+
217
+ 🟥 **On a fresh install with no operator pattern file, this WILL be exit 10 every time** — that is
218
+ the scanner's own documented fail-closed default (`residency_closure_scan.py`'s header: "오버라이드
219
+ 파일이 없으면 exit 10 이다"), not a bug introduced here. It means: until an operator configures
220
+ their own residency patterns, this skill cannot claim a panel dispatch was screened, so it honestly
221
+ degrades to a `DEGRADED_*` rung with `residency=NOT_SCANNED(...)` instead of silently sending. That
222
+ is not a new block on fresh installs — the crossfamily field already tolerates `DEGRADED_*` without
223
+ hard-failing the commit; it is one more honest reason to be in that state, not a novel one.
224
+
225
+ **Where the result lands**: `templates/.git-hooks/pre-commit`'s `validate_crossfamily_leg` now
226
+ requires a `residency=CLEAN(...)` token inside the grounds of any `panel(...)` value (grace date
227
+ `RESIDENCY_TOKEN_GRACE_DATE`, no retroactivity on markers dated earlier), and accepts an optional,
228
+ format-checked `residency=(CLEAN|TAINTED|NOT_SCANNED)(...)` token on `DEGRADED_*`/`UNKNOWN`/
229
+ `declined`. Fixtures: `scripts/test_marker_crossfamily_lanes.sh` (`r1`–`r13`). This is the SAME
230
+ typed field Step 6 already emits into — one channel, not a second marker line.
231
+
232
+ 🟥 **A stripped file is invisible to the reviewer, and a reviewer's default read of invisible is
233
+ "absent," not "redacted"** (governor dogfood, 2026-09-05, same-day live use of this Step): two
234
+ TAINTED files were stripped from a real payload and the CLEAN remainder sent cross-family — the
235
+ reviewer came back with a false-positive S-finding, *"runner wiring is missing,"* because the
236
+ wiring actually lived in one of the two files that had just been stripped. Stripping fixes
237
+ residency; it does nothing about the reviewer's next inference, and silence there reads as a hole.
238
+ **So**: whenever a payload is sent with `stripped > 0`, the review PROMPT itself must name the
239
+ omission explicitly — the list of what was left out, and one line per file on what it is
240
+ responsible for (not its content, just its role) — so the reviewer can discount that surface
241
+ instead of flagging it as missing. Record whether this was done: when the token carries
242
+ `stripped=M` with `M > 0`, the same grounds line adds one phrase on whether the omission list
243
+ reached the review prompt, e.g. `residency=CLEAN(files=3, stripped=2) · omission-list: given to
244
+ reviewer`. This is evidence-of-doing, not proof — same shape as Step 5's "quote one marker line it
245
+ checked," and it does not become a new hard-blocking hook check (channel discipline stays a
246
+ grounds phrase here, per the same Mechanization Boundary this whole field already respects).
247
+
248
+ ⚠️ **Scope, same as everywhere else in this file**: CLEAN is a screening result, not a safety
249
+ guarantee (dynamic imports, non-Python artifacts, encoding tricks, and TOCTOU are named, unclosed
250
+ gaps in the scanner's own header). Recording `residency=CLEAN(...)` asserts the scan ran and found
251
+ nothing **within those limits** — it does not assert the payload is safe in some absolute sense.
252
+
196
253
  ## Step 5 — Role split + source-grounded acceptance (S-2)
197
254
 
198
255
  CC = **governor**, terminal verdict, source-closes. Sidecars = decorrelated **challengers** feeding
@@ -202,6 +259,11 @@ re-checks** it against the artifact (phantom-quench back-trace) before accepting
202
259
  source-grounded is **dropped, not judged**. No sidecar-only verdict (no weak-local-judge regression).
203
260
  Local 4090 = **canary tier** (evidence-of, never terminal verdict).
204
261
 
262
+ **Report shape (2026-09-05)**: when the findings are written up (PR body · marker `crossfamily:` expansion · chamber record),
263
+ use `templates/RED_TEAM_REPORT.md` — the five sections mirror ISO/IEC AWI TS 42119-7 (risk identification · objectives & attack
264
+ vectors · method · findings with reproducing input · life-cycle integration). It is a name tag on what this skill already
265
+ produces, not a new check; crosswalk: `knowledge/shared/harness-core/iso_ai_standards_crosswalk.md §2`.
266
+
205
267
  🟥 **What you SEND decides which axis you get — put the MARKER in the payload, not just the diff**
206
268
  (2026-08-19). Sidecars have been receiving the *diff* only. That buys a review of the **code** and
207
269
  buys nothing on the **record**: the Axes 2+3 marker is self-attested, and this repo has already
@@ -238,8 +300,10 @@ floor-tier sims), which is why it is stated here instead of left to the reader t
238
300
 
239
301
  ⚠️ **Prose, not a check** — measured recurrence is 2, below this repo's own N≥3 bar
240
302
  (`[[feedback_mechanize_at_repetition_prose_before]]`). On the third occurrence, mechanize it here.
241
- ⚠️ **Residency still governs**: a marker can name company assets. Sanitize before any external-family
242
- dispatch, exactly as with the diff — the marker is not exempt because it is metadata.
303
+ ⚠️ **Residency still governs**: a marker can name company assets. Run Step 4.5's screen against the
304
+ marker file too, not just the diff — the marker is not exempt because it is metadata. (This line was
305
+ pure prose until 2026-09-05; Step 4.5 above is what closes it — the scan itself still carries the
306
+ named, unclosed gaps in its own header, so "screened" is not "guaranteed safe".)
243
307
 
244
308
  *Origin*: sister-asset read of `raphaelchristi/harness-evolver`'s `harness-critic` agent, whose whole
245
309
  role is auditing the **evaluator** rather than the artifact. Its detection signatures (score jumps,
@@ -290,6 +354,22 @@ rejected at commit time.
290
354
  | — | change is not load-bearing — decorrelation not required | `single-family` | — |
291
355
  | — | **panel never probed** | `UNKNOWN` | **required** |
292
356
 
357
+ 🟥 **Rungs 1–2's `panel(<families>)` additionally require a `residency=CLEAN(...)` token inside the
358
+ same grounds string, since 2026-09-05** (Step 4.5 above; `pre-commit`'s `validate_crossfamily_leg`
359
+ blocks a bare `panel(...)` with no such token on a marker dated on/after
360
+ `RESIDENCY_TOKEN_GRACE_DATE`). Full grammar:
361
+
362
+ ```
363
+ crossfamily: panel(codex) — residency=CLEAN(files=7) · R1..R2, 4 findings
364
+ crossfamily: DEGRADED_SINGLE_FAMILY — residency=TAINTED(2 files, stripped=no) → not sent, probed codex/agy, 0 reachable
365
+ crossfamily: DEGRADED_SINGLE_FAMILY — residency=NOT_SCANNED(no operator pattern file) → not sent, probed codex/agy, 0 reachable
366
+ ```
367
+
368
+ The token is **optional** on `DEGRADED_*`/`UNKNOWN`/`declined` (format-checked only when present —
369
+ those rungs never claimed anything left the boundary, so nothing to require) and **rejected as a
370
+ contradiction** if `panel(...)` carries `residency=TAINTED(` or `residency=NOT_SCANNED(` on the same
371
+ line — a sent payload and an unscreened/tainted one cannot both be true.
372
+
293
373
  **The three degrade values are the load-bearing split**: *could not* (`DEGRADED_SINGLE_FAMILY`) ·
294
374
  *did not* (`DEGRADED_PANEL_UNUSED`) · *did not look* (`UNKNOWN`). Free prose merges all three, and
295
375
  each merge hides a different thing — an unrun probe renders as a zero finding
@@ -388,6 +468,11 @@ at N per run (no loop).
388
468
  is NOT accepted on a load-bearing change — that block only runs there, so "decorrelation not
389
469
  required" is a contradiction, and as a no-ack pass it was a free bypass of the lane.
390
470
  *[mandatory-pass — enforced by pre-commit, fixtures `scripts/test_marker_crossfamily_lanes.sh`]*
471
+ - Step 4.5's residency screen ran against the actual send list before any `panel(...)` claim, and
472
+ its result is recorded as a `residency=CLEAN|TAINTED|NOT_SCANNED(...)` token inside the same
473
+ `crossfamily:` grounds — required when the value is `panel(...)`, format-checked when present on
474
+ a degrade value. *[mandatory-pass — enforced by pre-commit since RESIDENCY_TOKEN_GRACE_DATE,
475
+ fixtures `scripts/test_marker_crossfamily_lanes.sh` `r1`–`r13`]*
391
476
  - Panel members are review-capable: embedding/reranker/OCR/safeguard classes are excluded **before**
392
477
  family-diversity is counted, never after. *[mandatory-pass — same fixtures, ordering anchor c4/c5]*
393
478
  - Paid recruit was per-run spend-gated (or `paid_auto` set); free local tier may auto-fire. *[mandatory-pass]*