@chrono-meta/fh-gate 3.0.0 → 3.1.1

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 +451 -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 +76 -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 +160 -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.1",
4
4
  "engines": {
5
5
  "claudeCode": ">=1.0.0"
6
6
  },
@@ -1,5 +1,80 @@
1
1
  # forge-harness (fh-meta) Changelog
2
2
 
3
+ ### [3.1.1] — 2026-09-07 — fh-gate Action 의 기본 `level` 이 게이트가 안 받는 값이었다
4
+
5
+ ### 🟥 무엇이 깨져 있었나
6
+ `action.yml` 의 `level` 기본값이 **`'standard'`** 였는데, `scripts/fh-gate.sh:275` 는 `quick` 과
7
+ `full` 만 받고 나머지는 **ARG_ERROR(11)** 로 거절한다. 그리고 `fail-on` 기본 집합에 `ARG_ERROR`
8
+ 가 들어 있다. ⇒ **`uses: chrono-meta/forge-harness@v3.1.0` 을 `level:` 없이 쓰면 스텝이 실패했고,
9
+ 에러 메시지는 사용자가 설정한 적 없는 인자를 지목했다.**
10
+
11
+ 발행된 3.1.0 타르볼로 실측(소스를 읽은 게 아니다):
12
+ ```
13
+ level=standard → rc=11 ERROR: gate level must be 'quick' or 'full' (got: standard)
14
+ level=quick → rc=12 (DRY_RUN, 정상)
15
+ level=full → rc=12 (DRY_RUN, 정상)
16
+ ```
17
+
18
+ ### 어떻게 잡혔나
19
+ 릴리스 마커에 `standpoint: DEGRADED_NOT_RUN — 소비자 install 팔을 안 돌렸다` 라고 적어 두고,
20
+ 발행 직후 그 팔을 실제로 돌렸다. **읽어서는 안 나오고 돌려야 나오는 자리**였다.
21
+ 레인 28개가 이미 있었지만 전부 **종료코드 매핑**만 봤고 「입력 기본값이 CLI 가 받는 값인가」는
22
+ 아무도 안 봤다.
23
+
24
+ ### 고친 것
25
+ - `action.yml` `level` 기본값 `'standard'` → **`'quick'`**, description 도 «다른 값은 안 받는다»로
26
+ - **레인 D0~D2 신설** — `action.yml` 의 기본값과 `fh-gate.sh` 의 허용 집합을 **둘 다 실물에서 추출**해
27
+ 대조한다(어느 쪽도 하드코딩 안 한다 — 그러면 같은 방식으로 또 드리프트한다).
28
+ D0 = 허용 집합 추출 성공(계기 생사) · D1 = 기본값이 그 안에 있나 · D2 = **컨트롤**(같은 검사가
29
+ `'standard'` 를 거절하는지 — 아니면 D1 이 아무것도 증명 못 한다). fail-before 실증: 기본값을
30
+ `standard` 로 되돌리면 **정확히 D1 만** 적색(30 passed · 1 failed), 복원하면 31/0.
31
+ - README 4판의 사용례를 `@v3.1.1` 로
32
+
33
+ ⚠️ **`@v3.1.0` 태그는 그대로 둔다**(태그는 안 움직인다). 그 태그를 쓰는 워크플로는 `level: quick`
34
+ 을 명시하면 정상 동작한다.
35
+
36
+ ### [3.1.0] — 2026-09-06 — QP 플러그인 · `oracle:` 마커 채널 · preprep 도해 굽기 · FH 전체 지도 발행 · fh-gate GitHub Action
37
+
38
+ ### 왜 minor 인가
39
+ CLAUDE.md §④-b — 새 자산(fh-qp 플러그인 · `oracle:` 채널 · preprep 도해) + 새 게이트 레인 다수 + 행동을 바꾸는 교리(3층 정본 개정). 🟡 **major 로 읽을 여지는 있었다**: fh-qp 는 «제품 QA» 라는 capability *class* 가 FH 에 처음 생긴 것(정책 ⓒ)이다. 다만 폴백 1차판 · 선택 설치 · 기존 게이트 무변경이라 minor 로 뒀다. 정체성 등급은 무변경(다섯 🟢 유지)이라 major ⓑ 사유도 없다.
40
+
41
+ ### 🟥 BREAKING (gate): 마커 `oracle:` 근사키 차단 (#642)
42
+ **무엇이 이제 막히나**: `oracle_type:` · `Oracle:` · `oracle :` 처럼 «oracle 로 시작하는데 정확히 `oracle:` 이 아닌 줄」과, enum 6종 밖의 값. 종전엔 근사키가 조용히 무시돼 «오라클을 적었다고 믿는데 아무것도 안 적힌» 상태가 났다.
43
+ **처방**: `oracle: <known-pair|metamorphic|back-to-back|a-b|human|none — 근거>` 한 줄로 고치거나, 줄을 지운다(**부재는 통과**한다 — 이 필드는 옵셔널이다).
44
+
45
+ ### 🟥 BREAKING (gate): preprep 레인 L12 diagram (#654)
46
+ **무엇이 이제 막히나** — 도해 표면(`kind: diagram_source`)을 **선언한** 덱에서만, 그리고 아래 여덟 가지에서만 rc=1 이 난다: 선언된 PNG 가 디스크에 없다 · 영수증 파싱 실패 · JSON 지문이 영수증과 다르다(고치고 다시 안 구웠다) · showcase validate 통과 기록 없음 · PNG 헤더 읽기 실패 · PNG 폭이 하한 미만 · viewBox 폭이 상한 초과(글자 배율 미달) · 여백이 하한 미만. **도해 표면을 선언하지 않은 덱은 무영향.**
47
+ 🟥 **영수증이 «없는» 것은 차단이 아니다 — `UNMEASURED` 로 기록된다**(`lane_diagram.py:62` 가 `notes` 로 보내고 `preprep.py:903` 은 `findings` 가 있을 때만 rc=1). 손으로 넣은 그림은 이 레인이 «못 보는» 것이지 «틀렸다»가 아니고, 그 둘을 접지 않는 것이 이 레인의 설계다.
48
+ **처방**: 위 여덟 중 하나에 걸렸으면 `diagram_from_json.py` 로 다시 굽거나 표면 선언을 지운다. `UNMEASURED` 는 고칠 것이 아니라 «안 쟀다」는 기록이다.
49
+ ⚠️ 이 문단의 초판은 「영수증 없는 그림은 차단」이라고 적었다 — 과대 주장이었고, 릴리스 전 cross-family 감사(codex)가 소스에서 잡았다. 자력 적발 0.
50
+
51
+ ### 새로 실리는 것
52
+ - **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)
53
+ - **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)
54
+ - **마커 옵셔널 `oracle:` 채널** — TR 29119-11 오라클 유형 닫힌 enum 6, 훅은 형식만 검사. 레인 39 · 배선 W6 (#642)
55
+ - **마커 옵셔널 `affected:` 채널** — 「이 변경이 건드리는 것 + 열린 질문」 한 줄. 자리표시자만 있는 값은 차단(제로 영향은 명제상 없다) (#624)
56
+ - **preprep — «도해는 타입 JSON 에서 굽는다»** + 레인 L12 + 앵커 K1~K7. 첫 실사용 121장 덱 (#654). L13 슬라이드 참조 레인(장 번호 참조가 실제 장을 가리키는가) (#662)
57
+ - **FH 전체 지도** — `docs/map/FH_MAP.md` 5층 + archify 인터랙티브 3장, GitHub Pages 로 발행: **https://chrono-meta.github.io/forge-harness/** (#644 #645 #647 #659)
58
+ - **3층 정본 개정** — 3단 공정 = 모든 작업의 방법론 · 4대 엔진 = 출력이 나오는 코어 · 정체성 = 맞물려 나타나는 능력(5대 = 단련된 실물+등급) (#643)
59
+ - **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`
60
+ - **live-eval** — `/prompt-regression` 의 라이브 짝(프로브 12 · 격리 클론 · 4값 채점 · launchd 템플릿) (#625), launchd PATH 에 `timeout(1)` 부재로 12/12 FAILED-TO-RUN 하던 것 수리 (#632)
61
+ - **worktree_reclaim.sh** — 워크트리 제거 «전» gitignored `tracks/**` 회수(목록 파일 먼저 → 복사+검증, 제거는 사람) (#639)
62
+ - **push-zone 게이트** — 비-소유 github.com 원격 push 를 pre-push 에서 차단 (#636) · **outbound query 가드** PreToolUse(WebSearch|WebFetch) (#637) · **residency 폐포 스캔**을 `crossfamily:` grounds 의 `residency=` 토큰으로 (#633)
63
+ - **sim 러너 MCP 격리** — `--strict-mcp-config` 기본. 헤드리스 팔이 운영자 사용자 스코프 MCP 를 상속하던 것 차단 (#651)
64
+ - **pipe_verdict_guard R3** — heredoc 뒤 «리다이렉션만 있는 줄»(zsh NULLCMD `cat` 이 stdin 파이프를 영원히 읽는 형태) 검출. «push 멈춤» 9건의 실제 원인이었다 (#618) · R2 오탐 수리 (#661)
65
+ - 문서: 모델 등급 × 기대 역량 (#635) · `docs/USE_CASES.md` · 카드 규율의 역방향 상주화(«안 닫힌 것이 사라지면 유실») (#656)
66
+
67
+ ### 같이 고친 것
68
+ - `directional_diff_gate.sh` `has_nul` 이중 정의 — 셀프테스트가 자기 사본만 재고 프로덕션 파손에 초록이던 결함 (#634)
69
+ - 챔버 step 5 가 2026-08-17 교리 어휘(CURATED · NOT-APPLICABLE)를 받는다 — **18일간 러너가 교리를 거부하고 있었다** (#620)
70
+ - 헤드리스 런에서 Skill 툴 무음 거부(`--allowedTools`) + 그 플래그가 variadic 이라 뒤따르는 프롬프트를 먹던 인자 순서 (#612 #631)
71
+ - `fh_audit_check.zsh` 빈 허브에서 zsh 미매치 glob 오류 (#613)
72
+ - (허브 오너 노트 · npm 미출하) `sync-to-be.sh` 목적지 가드 — 비-git 디렉토리·외부 레포 서브디렉토리 companion 은 rc=12 거부 (#646)
73
+
74
+ ### 알려진 한계 (이 릴리스 시점)
75
+ - `docs/map/**` 는 `files[]` 밖이라 **npm 배포물에 안 실린다** — GitHub Pages 표면에만 있다.
76
+ - live-eval 문턱 `0.80` 은 **보정 주간(~2026-09-11) 상수**다. rc 는 나오지만 판정에 쓰지 않는다.
77
+
3
78
  ### [3.0.0] — 2026-09-04 — 정체성 다섯 전부 🟢(identity 1.0 을 이 번호가 나른다) · 릴리스 트랙 통일 · 훅 사실 줄 · 게이트 무음 접힘 수리 · 6축 단련
4
79
 
5
80
  ### 왜 major 인가
@@ -7,7 +82,7 @@ CLAUDE.md §④-b major ⓑ — **정체성 다섯이 «전부» 🟢**(2026-09-
7
82
  운영자 결정(2026-09-04): `identity-v1.0.0` 은 정체성 트랙의 마지막 태그, **이후 한 번호** — 이 3.0.0 이 identity 1.0 을 같이 나른다. 🧭 접두 태그는 더 만들지 않는다. 등급의 정본은 여전히 `ship_readiness_gate.md` 의 셀이고 번호는 나르기만 한다.
8
83
 
9
84
  ### 🟥 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 만 실패함을 보인다.
85
+ **무엇이 이제 막히나**: `.md` 를 스테이징한 뒤 디스크에서 지우면(또는 더 고치면) 종전엔 `[ -f ] || continue` 로 Detail-pointer 검사가 통째로 빠졌다 — 깨진 Detail 포인터(`**Detail**` + `See <path> §X` 형태)가 조용히 커밋됐다. 이제 `git show ":$f"` 로 스테이징된 바이트를 검사하고, 인덱스에서 못 읽으면 ❌ 로 막는다. **처방**: 스테이징한 것이 곧 커밋되는 것이다 — 디스크 파일을 믿지 마라. 레인 `scripts/test_precommit_pointer_index_lanes.sh`(일회용 클론) 가 옛 훅에선 정확히 P1 만 실패함을 보인다.
11
86
 
12
87
  ### 🟥 BREAKING (gate): selfcheck 가 명시경로 게이트 인프라(bin/fh-gate·fh-run·fh-goal 등) 부재를 FAIL 로 센다 (#602)
13
88
  종전엔 glob 미매칭과 같은 `continue` 로 무음. glob 미매칭 스킵은 그대로. **처방**: 그 파일들이 없어졌으면 files[] 와 selfcheck 목록을 같이 고쳐라.