@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,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,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
|
|
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.
|
|
242
|
-
|
|
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]*
|