@chrono-meta/fh-gate 3.1.4 → 3.4.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/registry/agent_cards.json +1 -1
- package/.claude/rules/fh_4axis_gate.md +26 -1
- package/.claude-plugin/marketplace.json +3 -3
- package/AGENTS.md +8 -2
- package/CATALOG.md +4 -4
- package/CHEATSHEET.md +1 -1
- package/CLAUDE.md +54 -10
- package/README.ja.md +3 -3
- package/README.ko.md +3 -3
- package/README.md +3 -3
- package/README.zh.md +3 -3
- package/docs/OUTPUT_EVIDENCE.md +2 -2
- package/docs/STANDARDS_ALIGNMENT.md +1 -1
- package/docs/codex-compat.md +1 -1
- package/knowledge/shared/harness-core/agents_md_runtime_details.md +2 -2
- package/knowledge/shared/harness-core/claude_md_gate_details.md +26 -0
- package/knowledge/shared/harness-core/fh_three_layer_canon.md +1 -1
- package/knowledge/shared/harness-core/field_verdict_crossfamily_gate.md +76 -3
- package/knowledge/shared/harness-core/governance_engineering_definition.md +89 -0
- package/knowledge/shared/harness-core/iso_ai_standards_crosswalk.md +1 -1
- package/knowledge/shared/harness-core/skill_quality_rubric.md +1 -1
- package/knowledge/shared/learnings/subagent_invocations_log.yaml +99 -0
- package/knowledge/shared/rules/auto_project_mapping.md +1 -1
- package/knowledge/shared/rules/modes_and_value.md +2 -2
- package/package.json +12 -1
- package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-commons/README.md +38 -0
- package/plugins/fh-commons/skills/preprep/SKILL.md +23 -0
- package/plugins/fh-commons/skills/preprep/fixtures/font_revert_probe.py +92 -0
- package/plugins/fh-commons/skills/preprep/lane_font.py +462 -0
- package/plugins/fh-commons/skills/preprep/preprep.py +16 -1
- package/plugins/fh-commons/skills/preprep/surfaces.example.yaml +17 -0
- package/plugins/fh-commons/skills/preprep/test_lane_font.py +452 -0
- package/plugins/fh-meta/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-meta/CHANGELOG.md +164 -0
- package/plugins/fh-meta/skills/agent-composer/SKILL.md +2 -2
- package/plugins/fh-meta/skills/auto-decorrelation/SKILL.md +40 -0
- package/plugins/fh-meta/skills/frontier-digest/SKILL.md +1 -1
- package/plugins/fh-meta/skills/frontier-digest/SKILL_detail.md +43 -5
- package/plugins/fh-meta/skills/{hub-cc-pr-reviewer → harness-pr-reviewer}/SKILL.md +75 -3
- package/plugins/fh-meta/skills/{hub-cc-pr-reviewer → harness-pr-reviewer}/SKILL_detail.md +2 -2
- package/plugins/fh-meta/skills/harvest-loop/SKILL_detail.md +2 -2
- package/plugins/fh-meta/skills/install-doctor/SKILL.md +1 -1
- package/plugins/fh-meta/skills/install-wizard/SKILL.md +1 -1
- package/plugins/fh-meta/skills/meta-prompt-builder/SKILL.md +1 -1
- package/plugins/fh-meta/skills/pipeline-conductor/SKILL.md +1 -1
- package/plugins/fh-meta/skills/plugin-recommender/SKILL.md +1 -1
- package/plugins/fh-meta/skills/sim-conductor/SKILL.md +1 -1
- package/plugins/fh-qp/.claude-plugin/plugin.json +1 -1
- package/scripts/doc_claim_triad_scan.py +303 -0
- package/scripts/finding_fleet.sh +558 -0
- package/scripts/finding_pipeline.sh +572 -0
- package/scripts/finding_verifier.sh +173 -0
- package/scripts/finding_verify.py +468 -0
- package/scripts/frontier_digest_autopilot.sh +3 -3
- package/scripts/gate_pathspec_check.sh +1 -0
- package/scripts/gate_shape_scan.sh +120 -0
- package/scripts/selfcheck.sh +50 -0
- package/scripts/test_doc_claim_triad_lanes.sh +124 -0
- package/scripts/test_finding_pipeline_lanes.sh +2011 -0
- package/scripts/test_gate_shape_scan_lanes.sh +47 -0
- package/scripts/test_heavy_classifier_lanes.sh +13 -3
- package/scripts/test_marker_crossfamily_lanes.sh +75 -3
- package/scripts/test_marker_standpoint_lanes.sh +31 -6
- package/scripts/test_preprep_font_lanes.sh +87 -0
- package/templates/.git-hooks/pre-commit +158 -11
- package/templates/PRE-PUBLISH-CHECKLIST.md +29 -0
- package/templates/local_fh_context.md +1 -1
- package/templates/regression_guard.sh +1 -1
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""L15 font self-test — known-pair + 뮤턴트.
|
|
3
|
+
|
|
4
|
+
exit 0 = 전부 통과 · 1 = 실패 있음 · 2 = 픽스처 생성 불가 (UNMEASURED, 통과 아님)
|
|
5
|
+
|
|
6
|
+
🟥 이 레인이 반드시 증명해야 하는 것은 «이탈을 잡는다»가 아니라 **«상속분을 안 잡는다»** 다.
|
|
7
|
+
순진한 구현은 마스터에 상속된 서체를 위반으로 세고, 실측 코퍼스에서 그 오탐이 85건이었다.
|
|
8
|
+
그래서 known-negative 는 «서체가 하나뿐인 덱»이 아니라 **«마스터에 이질 서체가 있지만
|
|
9
|
+
템플릿과 동일한 덱»** 이다 — 그게 실제로 틀렸던 자리다.
|
|
10
|
+
|
|
11
|
+
K1 known-negative 마스터에 Helvetica 상속 + slides 는 허용 서체만 → finding 0
|
|
12
|
+
K2 known-positive K1 과 한 글자만 다르다: 한 런의 서체를 이탈로 바꿈 → finding 1
|
|
13
|
+
K3 뮤턴트(마스터) 마스터를 템플릿과 다르게 만든다 → 갈라짐이 보고된다
|
|
14
|
+
K4 테마 이탈 테마 자체가 허용 밖 → 테마 1건만, 장마다 아님
|
|
15
|
+
K5 선언 면제 K2 의 그 이탈을 fonts.intended 로 선언 → finding 0, 노트에 «면제됨»
|
|
16
|
+
K6 미선언 degrade template·allow 둘 다 없음 → NOT_CONFIGURED, PASS 아님
|
|
17
|
+
K7 빈 추출 degrade slides 에 서체 토큰 0 → UNMEASURED, 「이탈 없음」 아님
|
|
18
|
+
"""
|
|
19
|
+
import os
|
|
20
|
+
import re
|
|
21
|
+
import sys
|
|
22
|
+
import shutil
|
|
23
|
+
import zipfile
|
|
24
|
+
import tempfile
|
|
25
|
+
|
|
26
|
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
27
|
+
sys.path.insert(0, HERE)
|
|
28
|
+
import lane_font # noqa: E402
|
|
29
|
+
|
|
30
|
+
PASS = FAIL = 0
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def ok(m):
|
|
34
|
+
global PASS
|
|
35
|
+
PASS += 1
|
|
36
|
+
print(' ✅', m)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def bad(m):
|
|
40
|
+
global FAIL
|
|
41
|
+
FAIL += 1
|
|
42
|
+
print(' ❌', m)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# ── 최소 pptx 를 손으로 짓는다 (python-pptx 없이 — 의존을 늘리지 않는다) ────────────
|
|
46
|
+
CT = ('<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
|
47
|
+
'<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">'
|
|
48
|
+
'<Default Extension="xml" ContentType="application/xml"/>'
|
|
49
|
+
'<Override PartName="/ppt/presentation.xml" ContentType="application/vnd.openxmlformats-'
|
|
50
|
+
'officedocument.presentationml.presentation.main+xml"/></Types>')
|
|
51
|
+
PRES = ('<p:presentation xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" '
|
|
52
|
+
'xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">'
|
|
53
|
+
'<p:sldIdLst><p:sldId id="256" r:id="rId1"/></p:sldIdLst></p:presentation>')
|
|
54
|
+
PRES_RELS = ('<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'
|
|
55
|
+
'<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/'
|
|
56
|
+
'2006/relationships/slide" Target="slides/slide1.xml"/></Relationships>')
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def theme_with_fallback(font, fb='SimSun'):
|
|
60
|
+
"""Office 테마가 흔히 들고 있는 «보조 스크립트 폴백» 이 섞인 테마."""
|
|
61
|
+
return ('<a:theme xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">'
|
|
62
|
+
'<a:themeElements><a:fontScheme name="x">'
|
|
63
|
+
f'<a:majorFont><a:latin typeface="{font}"/><a:ea typeface="{font}"/>'
|
|
64
|
+
f'<a:font script="Hans" typeface="{fb}"/></a:majorFont>'
|
|
65
|
+
f'<a:minorFont><a:latin typeface="{font}"/><a:ea typeface="{font}"/>'
|
|
66
|
+
f'<a:font script="Hans" typeface="{fb}"/></a:minorFont>'
|
|
67
|
+
'</a:fontScheme></a:themeElements></a:theme>')
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def run_attrs_first(text, font):
|
|
71
|
+
"""`<a:latin pitchFamily="34" charset="0" typeface="X"/>` — PowerPoint 가 실제로 쓰는 형태."""
|
|
72
|
+
return (f'<a:r><a:rPr lang="ko-KR">'
|
|
73
|
+
f'<a:latin pitchFamily="34" charset="0" typeface="{font}"/></a:rPr>'
|
|
74
|
+
f'<a:t>{text}</a:t></a:r>')
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# 🟥 Target 이 Id 보다 «앞에» 오는 rels. OOXML 이 허용하고, 가정하면 rels 가 통째로 빈 dict 가
|
|
78
|
+
# 되어 장 순서가 파일명 순으로 조용히 대체된다(장 번호 어긋남 → 면제가 엉뚱한 장에 걸린다).
|
|
79
|
+
PRES_RELS_REVERSED = (
|
|
80
|
+
'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'
|
|
81
|
+
'<Relationship Target="slides/slide1.xml" Id="rId1" '
|
|
82
|
+
'Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide"/>'
|
|
83
|
+
'</Relationships>')
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def para_default_nested(font):
|
|
87
|
+
"""`defRPr` 에 **자식이 있는** 형태 — 비탐욕 정규식이 여기서 끊겼다."""
|
|
88
|
+
return ('<a:pPr><a:defRPr sz="1800"><a:solidFill><a:srgbClr val="000000"/></a:solidFill>'
|
|
89
|
+
f'<a:latin typeface="{font}"/></a:defRPr></a:pPr>')
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def theme(font):
|
|
93
|
+
return ('<a:theme xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">'
|
|
94
|
+
'<a:themeElements><a:fontScheme name="x">'
|
|
95
|
+
f'<a:majorFont><a:latin typeface="{font}"/><a:ea typeface="{font}"/></a:majorFont>'
|
|
96
|
+
f'<a:minorFont><a:latin typeface="{font}"/><a:ea typeface="{font}"/></a:minorFont>'
|
|
97
|
+
'</a:fontScheme></a:themeElements></a:theme>')
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def run(text, font=None):
|
|
101
|
+
rpr = f'<a:rPr lang="ko-KR"><a:latin typeface="{font}"/></a:rPr>' if font else '<a:rPr lang="ko-KR"/>'
|
|
102
|
+
return f'<a:r>{rpr}<a:t>{text}</a:t></a:r>'
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def fld(text, font):
|
|
106
|
+
"""`<a:fld>` — 장 번호·바닥글. 런이 아니지만 글자가 찍힌다."""
|
|
107
|
+
return (f'<a:fld id="x" type="slidenum"><a:rPr lang="ko-KR">'
|
|
108
|
+
f'<a:latin typeface="{font}"/></a:rPr><a:t>{text}</a:t></a:fld>')
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def para_default(font):
|
|
112
|
+
"""`defRPr` — 박스 전체를 선택해 바꾸면 런이 아니라 여기 남는다."""
|
|
113
|
+
return f'<a:pPr><a:defRPr><a:latin typeface="{font}"/></a:defRPr></a:pPr>'
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def end_para(font):
|
|
117
|
+
"""`endParaRPr` — 빈 문단의 끝 서식. **렌더되는 글자가 없다** → 이탈이 아니다."""
|
|
118
|
+
return f'<a:endParaRPr lang="ko-KR"><a:latin typeface="{font}"/></a:endParaRPr>'
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def empty_run(font):
|
|
122
|
+
"""글자를 지우고 남은 찌꺼기 런 — 렌더되지 않으므로 이탈이 아니다."""
|
|
123
|
+
return f'<a:r><a:rPr lang="ko-KR"><a:latin typeface="{font}"/></a:rPr><a:t></a:t></a:r>'
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def slide(runs):
|
|
127
|
+
return ('<p:sld xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" '
|
|
128
|
+
'xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">'
|
|
129
|
+
'<p:cSld><p:spTree><p:sp><p:nvSpPr><p:cNvPr id="2" name="body"/></p:nvSpPr>'
|
|
130
|
+
f'<p:txBody><a:p>{"".join(runs)}</a:p></p:txBody></p:sp></p:spTree></p:cSld></p:sld>')
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def master(font):
|
|
134
|
+
"""마스터에 «이질 서체»를 넣는다 — 실사고가 정확히 이 형태였다(Helvetica Neue 85건)."""
|
|
135
|
+
return ('<p:sldMaster xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" '
|
|
136
|
+
'xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">'
|
|
137
|
+
f'<p:txStyles><a:lvl1pPr><a:defRPr><a:latin typeface="{font}"/></a:defRPr>'
|
|
138
|
+
'</a:lvl1pPr></p:txStyles></p:sldMaster>')
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
# 🟥 `r:id` 가 `id` 보다 **앞에** 오는 형태. OOXML 이 허용하고, 초판 정규식은 여기서
|
|
142
|
+
# 빈 리스트를 냈다(예외도 안 나서 폴백조차 안 탔다 → 0건 PASS).
|
|
143
|
+
PRES_RID_FIRST = (
|
|
144
|
+
'<p:presentation xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" '
|
|
145
|
+
'xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">'
|
|
146
|
+
'<p:sldIdLst><p:sldId r:id="rId1" id="256"/></p:sldIdLst></p:presentation>')
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def build(path, slide_runs, master_font='Helvetica Neue', theme_font='Brand Display Bold',
|
|
150
|
+
theme_xml=None, pres_xml=None, layout_xml=None, rels_xml=None):
|
|
151
|
+
with zipfile.ZipFile(path, 'w', zipfile.ZIP_DEFLATED) as z:
|
|
152
|
+
z.writestr('[Content_Types].xml', CT)
|
|
153
|
+
z.writestr('ppt/presentation.xml', pres_xml or PRES)
|
|
154
|
+
z.writestr('ppt/_rels/presentation.xml.rels', rels_xml or PRES_RELS)
|
|
155
|
+
z.writestr('ppt/slides/slide1.xml', slide(slide_runs))
|
|
156
|
+
z.writestr('ppt/slideMasters/slideMaster1.xml', master(master_font))
|
|
157
|
+
z.writestr('ppt/theme/theme1.xml', theme_xml or theme(theme_font))
|
|
158
|
+
if layout_xml is not None:
|
|
159
|
+
z.writestr('ppt/slideLayouts/slideLayout1.xml', layout_xml)
|
|
160
|
+
return path
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
# 🟥 장 순서를 실제로 재려면 **2장 이상**이어야 한다. 1장짜리로는 파일명 순 폴백과
|
|
164
|
+
# 정상 경로가 같은 답을 내서 «초록인 이유»를 구분할 수 없다 — R3-4 레인이 그래서
|
|
165
|
+
# 엉뚱한 이유로 초록이었고, 4라운드가 그 뒤의 진짜 결함(r:id 파싱)을 잡았다.
|
|
166
|
+
PRES_2 = ('<p:presentation xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" '
|
|
167
|
+
'xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">'
|
|
168
|
+
'<p:sldIdLst><p:sldId id="256" r:id="rId2"/><p:sldId id="257" r:id="rId1"/>'
|
|
169
|
+
'</p:sldIdLst></p:presentation>')
|
|
170
|
+
# rId2→slide1, rId1→slide2 : 즉 «발표 1장 = slide1.xml, 2장 = slide2.xml» 이 아니라 그 반대가
|
|
171
|
+
# 되도록 일부러 꼬아 둔다. 파일명 순 폴백이면 순서가 뒤집혀 면제가 엉뚱한 장에 걸린다.
|
|
172
|
+
PRES_RELS_2 = ('<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'
|
|
173
|
+
'<Relationship Id="rId2" Target="slides/slide1.xml" '
|
|
174
|
+
'Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide"/>'
|
|
175
|
+
'<Relationship Id="rId1" Target="slides/slide2.xml" '
|
|
176
|
+
'Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide"/>'
|
|
177
|
+
'</Relationships>')
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def build2(path, runs1, runs2, tpl_theme='Brand Display Bold'):
|
|
181
|
+
"""2장 덱. 발표순 1장=slide1, 2장=slide2 (rId 로 그렇게 묶는다)."""
|
|
182
|
+
with zipfile.ZipFile(path, 'w', zipfile.ZIP_DEFLATED) as z:
|
|
183
|
+
z.writestr('[Content_Types].xml', CT)
|
|
184
|
+
z.writestr('ppt/presentation.xml', PRES_2)
|
|
185
|
+
z.writestr('ppt/_rels/presentation.xml.rels', PRES_RELS_2)
|
|
186
|
+
z.writestr('ppt/slides/slide1.xml', slide(runs1))
|
|
187
|
+
z.writestr('ppt/slides/slide2.xml', slide(runs2))
|
|
188
|
+
z.writestr('ppt/slideMasters/slideMaster1.xml', master('Helvetica Neue'))
|
|
189
|
+
z.writestr('ppt/theme/theme1.xml', theme(tpl_theme))
|
|
190
|
+
return path
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def sym_run(font):
|
|
194
|
+
"""`<a:sym>` — 기호 글꼴 슬롯. 실물 덱에 157개 있었다."""
|
|
195
|
+
return (f'<a:r><a:rPr lang="ko-KR"><a:sym typeface="{font}"/></a:rPr>'
|
|
196
|
+
f'<a:t>기호</a:t></a:r>')
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def run_xmlspace(text, font):
|
|
200
|
+
"""`<a:t xml:space="preserve">` — 앞뒤 공백이 있으면 PowerPoint 가 붙인다."""
|
|
201
|
+
return (f'<a:r><a:rPr lang="ko-KR"><a:latin typeface="{font}"/></a:rPr>'
|
|
202
|
+
f'<a:t xml:space="preserve"> {text} </a:t></a:r>')
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def cfg_for(deck, tpl=None, allow=None, intended=None):
|
|
206
|
+
c = {'surfaces_by_id': {'built_deck': {'path': os.path.basename(deck)}}, 'fonts': {}}
|
|
207
|
+
if tpl:
|
|
208
|
+
c['fonts']['template'] = os.path.basename(tpl)
|
|
209
|
+
if allow:
|
|
210
|
+
c['fonts']['allow'] = allow
|
|
211
|
+
if intended:
|
|
212
|
+
c['fonts']['intended'] = intended
|
|
213
|
+
return c
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def main():
|
|
217
|
+
d = tempfile.mkdtemp(prefix='lanefont_')
|
|
218
|
+
try:
|
|
219
|
+
GOOD = ['Brand Display Bold', 'Brand Text Regular']
|
|
220
|
+
tpl = build(os.path.join(d, 'tpl.pptx'),
|
|
221
|
+
[run('템플릿 본문', 'Brand Display Bold')])
|
|
222
|
+
|
|
223
|
+
# K1 — 마스터 상속 이질 서체가 있어도 저자 이탈은 0
|
|
224
|
+
k1 = build(os.path.join(d, 'k1.pptx'), [run('본문', 'Brand Display Bold')])
|
|
225
|
+
f, n = lane_font.scan(cfg_for(k1, tpl, GOOD), d)
|
|
226
|
+
(ok if not f else bad)(f'K1 known-negative: 상속 Helvetica 있어도 finding {len(f)} (0 이어야)')
|
|
227
|
+
(ok if any('상속 확인' in x for x in n) else bad)('K1: 상속 확인 노트가 나온다')
|
|
228
|
+
|
|
229
|
+
# K2 — 한 런만 이탈시킨다 (K1 과 단일 변수)
|
|
230
|
+
k2 = build(os.path.join(d, 'k2.pptx'),
|
|
231
|
+
[run('본문', 'Brand Display Bold'), run('코드', 'Comic Sans MS')])
|
|
232
|
+
f2, _ = lane_font.scan(cfg_for(k2, tpl, GOOD), d)
|
|
233
|
+
(ok if len(f2) == 1 and 'Comic Sans MS' in f2[0][3]
|
|
234
|
+
else bad)(f'K2 known-positive: 이탈 1건 잡힌다 (실제 {len(f2)})')
|
|
235
|
+
|
|
236
|
+
# K3 — 마스터를 템플릿과 다르게: 갈라짐이 보고돼야 한다
|
|
237
|
+
k3 = build(os.path.join(d, 'k3.pptx'), [run('본문', 'Brand Display Bold')],
|
|
238
|
+
master_font='Papyrus')
|
|
239
|
+
_, n3 = lane_font.scan(cfg_for(k3, tpl, GOOD), d)
|
|
240
|
+
(ok if any('갈라진 부품' in x for x in n3)
|
|
241
|
+
else bad)('K3 뮤턴트: 템플릿에서 갈라진 마스터가 보고된다')
|
|
242
|
+
|
|
243
|
+
# K4 — 테마 자체가 이탈. 장마다가 아니라 테마 1건으로
|
|
244
|
+
k4 = build(os.path.join(d, 'k4.pptx'), [run('본문', 'Brand Display Bold')],
|
|
245
|
+
theme_font='Papyrus')
|
|
246
|
+
f4, _ = lane_font.scan(cfg_for(k4, tpl, GOOD), d)
|
|
247
|
+
(ok if [x for x in f4 if x[1] == 'font-theme']
|
|
248
|
+
else bad)('K4: 테마 이탈이 테마 1건으로 보고된다')
|
|
249
|
+
|
|
250
|
+
# K5 — 선언 면제
|
|
251
|
+
f5, n5 = lane_font.scan(
|
|
252
|
+
cfg_for(k2, tpl, GOOD,
|
|
253
|
+
intended=[{'fonts': ['Comic Sans MS'], 'why': '코드 화면 고정폭'}]), d)
|
|
254
|
+
(ok if not f5 and any('면제됨' in x for x in n5)
|
|
255
|
+
else bad)(f'K5: 선언 면제되면 finding 0 + «면제됨» 노트 (실제 {len(f5)})')
|
|
256
|
+
|
|
257
|
+
# K6 — 미선언 degrade
|
|
258
|
+
f6, n6 = lane_font.scan({'surfaces_by_id': {'built_deck': {'path': 'k1.pptx'}}}, d)
|
|
259
|
+
(ok if not f6 and any('NOT_CONFIGURED' in x for x in n6)
|
|
260
|
+
else bad)('K6: template·allow 미선언 → NOT_CONFIGURED (PASS 아님)')
|
|
261
|
+
|
|
262
|
+
# K7 — 서체 토큰 0 → UNMEASURED
|
|
263
|
+
k7 = build(os.path.join(d, 'k7.pptx'), [run('본문')])
|
|
264
|
+
f7, n7 = lane_font.scan(cfg_for(k7, tpl, GOOD), d)
|
|
265
|
+
(ok if not f7 and any('UNMEASURED' in x for x in n7)
|
|
266
|
+
else bad)('K7: 서체 토큰 0 → UNMEASURED («이탈 없음» 아님)')
|
|
267
|
+
|
|
268
|
+
# K8 — 가족 단위 허용: 「Brand Display」 선언이 Bold 변형을 덮는다
|
|
269
|
+
f8, _ = lane_font.scan(cfg_for(k1, tpl, ['Brand Display']), d)
|
|
270
|
+
(ok if not f8 else bad)('K8: 가족 이름만 선언해도 굵기 변형이 허용된다')
|
|
271
|
+
|
|
272
|
+
# K8b — 역방향 누수: allow=["Arial Black"] 이 tf="Arial" 을 열면 안 된다
|
|
273
|
+
(ok if not lane_font._allowed('Arial', ['Arial Black'])
|
|
274
|
+
else bad)('K8b: 허용은 «가족→변형» 한 방향뿐 (역방향 누수 없음)')
|
|
275
|
+
|
|
276
|
+
# ── 아래 넷은 cross-family(agy 3.1 Pro) 지적으로 닫은 구멍이다 ──────────
|
|
277
|
+
# K9 [S1] 마스터를 변조해 금지 서체를 꽂으면 — 노트가 아니라 finding 이어야 한다
|
|
278
|
+
k9 = build(os.path.join(d, 'k9.pptx'), [run('본문', 'Brand Display Bold')],
|
|
279
|
+
master_font='Comic Sans MS')
|
|
280
|
+
f9, _ = lane_font.scan(cfg_for(k9, tpl, GOOD), d)
|
|
281
|
+
(ok if [x for x in f9 if x[1] == 'font-inherited-drift']
|
|
282
|
+
else bad)(f'K9 [S1]: 변조된 마스터의 금지 서체가 finding 으로 뜬다 (실제 {len(f9)}건)')
|
|
283
|
+
|
|
284
|
+
# K10 [S2] 박스 전체 서체 변경(런이 아니라 defRPr 에 남는다)
|
|
285
|
+
k10 = build(os.path.join(d, 'k10.pptx'),
|
|
286
|
+
[para_default('Comic Sans MS') + run('본문', 'Brand Display Bold')])
|
|
287
|
+
f10, _ = lane_font.scan(cfg_for(k10, tpl, GOOD), d)
|
|
288
|
+
(ok if [x for x in f10 if 'Comic Sans' in x[3]]
|
|
289
|
+
else bad)('K10 [S2]: 단락 기본서식(defRPr)의 이탈이 잡힌다')
|
|
290
|
+
|
|
291
|
+
# K11 [S3] `<a:fld>` 안의 이탈
|
|
292
|
+
k11 = build(os.path.join(d, 'k11.pptx'),
|
|
293
|
+
[run('본문', 'Brand Display Bold'), fld('12', 'Comic Sans MS')])
|
|
294
|
+
f11, _ = lane_font.scan(cfg_for(k11, tpl, GOOD), d)
|
|
295
|
+
(ok if [x for x in f11 if 'Comic Sans' in x[3]]
|
|
296
|
+
else bad)('K11 [S3]: 장 번호 필드(a:fld)의 이탈이 잡힌다')
|
|
297
|
+
|
|
298
|
+
# K12 [A3+오탐] 렌더 안 되는 둘은 잡으면 안 된다 — 빈 런 · endParaRPr
|
|
299
|
+
k12 = build(os.path.join(d, 'k12.pptx'),
|
|
300
|
+
[run('본문', 'Brand Display Bold'), empty_run('Comic Sans MS'),
|
|
301
|
+
end_para('Papyrus')])
|
|
302
|
+
f12, _ = lane_font.scan(cfg_for(k12, tpl, GOOD), d)
|
|
303
|
+
(ok if not f12
|
|
304
|
+
else bad)(f'K12 [A3]: 빈 런·endParaRPr 는 렌더 안 되므로 이탈 아님 (실제 {len(f12)}건)')
|
|
305
|
+
|
|
306
|
+
# K13 [R2-3, 1라운드 A2 되돌림] 템플릿 마스터가 쓰는 서체여도 **finding 은 남는다**.
|
|
307
|
+
# 억제하면 저자가 Office 기본값(Arial·Calibri)으로 본문을 찍어도 종료코드가 안 움직인다.
|
|
308
|
+
k13 = build(os.path.join(d, 'k13.pptx'),
|
|
309
|
+
[run('본문', 'Brand Display Bold'), run('각주', 'Helvetica Neue')])
|
|
310
|
+
f13, n13 = lane_font.scan(cfg_for(k13, tpl, GOOD), d)
|
|
311
|
+
(ok if len(f13) == 1 and '템플릿 자신의 마스터' in f13[0][4]
|
|
312
|
+
else bad)(f'K13 [R2-3]: 템플릿도 쓰는 이름이어도 finding 유지 + 맥락 주석 (실제 {len(f13)}건)')
|
|
313
|
+
|
|
314
|
+
# ── R2: 2라운드(agy 3.8 Flash High)가 새로 문 여섯 ──────────────────────
|
|
315
|
+
# R2-1 [S] 자식 있는 defRPr — 비탐욕 정규식이 첫 self-closing 에서 끊겼다
|
|
316
|
+
r1 = build(os.path.join(d, 'r1.pptx'),
|
|
317
|
+
[para_default_nested('Comic Sans MS') + run('본문', 'Brand Display Bold')])
|
|
318
|
+
fr1, _ = lane_font.scan(cfg_for(r1, tpl, GOOD), d)
|
|
319
|
+
(ok if [x for x in fr1 if 'Comic Sans' in x[3]]
|
|
320
|
+
else bad)('R2-1 [S]: 자식 있는 defRPr 의 서체를 조용히 버리지 않는다')
|
|
321
|
+
|
|
322
|
+
# R2-2 [S] `r:id` 가 앞에 오는 presentation.xml → 0장 = UNMEASURED, PASS 아님
|
|
323
|
+
r2 = build(os.path.join(d, 'r2.pptx'), [run('코드', 'Comic Sans MS')],
|
|
324
|
+
pres_xml=PRES_RID_FIRST)
|
|
325
|
+
fr2, _ = lane_font.scan(cfg_for(r2, tpl, GOOD), d)
|
|
326
|
+
(ok if [x for x in fr2 if 'Comic Sans' in x[3]]
|
|
327
|
+
else bad)('R2-2 [S]: 속성 순서가 달라도 장을 읽는다(빈 order 로 조용히 통과 안 함)')
|
|
328
|
+
|
|
329
|
+
# R2-4 [A] 테마의 보조 스크립트 폴백(SimSun)은 위반이 아니다
|
|
330
|
+
r4 = build(os.path.join(d, 'r4.pptx'), [run('본문', 'Brand Display Bold')],
|
|
331
|
+
theme_xml=theme_with_fallback('Brand Display Bold'))
|
|
332
|
+
fr4, _ = lane_font.scan(cfg_for(r4, tpl, GOOD), d)
|
|
333
|
+
(ok if not [x for x in fr4 if x[1] == 'font-theme']
|
|
334
|
+
else bad)('R2-4 [A]: 테마 script 폴백(SimSun)을 위반으로 세지 않는다')
|
|
335
|
+
|
|
336
|
+
# R2-5 [A] 면제도 가족 단위 — "Roboto Mono" 선언이 "Roboto Mono Bold" 를 덮는다
|
|
337
|
+
r5 = build(os.path.join(d, 'r5.pptx'),
|
|
338
|
+
[run('본문', 'Brand Display Bold'), run('$ git', 'Roboto Mono Bold')])
|
|
339
|
+
fr5, _ = lane_font.scan(
|
|
340
|
+
cfg_for(r5, tpl, GOOD,
|
|
341
|
+
intended=[{'fonts': ['Roboto Mono'], 'why': '코드 화면'}]), d)
|
|
342
|
+
(ok if not fr5 else bad)(f'R2-5 [A]: 면제도 가족 단위 (실제 {len(fr5)}건)')
|
|
343
|
+
|
|
344
|
+
# R2-6 [A] 레이아웃이 갈라져도 «원래 있던» 서체는 이탈이 아니다 — 새로 들인 것만
|
|
345
|
+
LAY_T = ('<p:sldLayout xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" '
|
|
346
|
+
'xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">'
|
|
347
|
+
'<a:latin typeface="Helvetica Neue"/></p:sldLayout>')
|
|
348
|
+
LAY_D = LAY_T.replace('</p:sldLayout>', '<a:ext cx="1"/></p:sldLayout>') # 크기만 바뀜
|
|
349
|
+
tpl6 = build(os.path.join(d, 'tpl6.pptx'), [run('본문', 'Brand Display Bold')],
|
|
350
|
+
layout_xml=LAY_T)
|
|
351
|
+
r6 = build(os.path.join(d, 'r6.pptx'), [run('본문', 'Brand Display Bold')],
|
|
352
|
+
layout_xml=LAY_D)
|
|
353
|
+
fr6, _ = lane_font.scan(cfg_for(r6, tpl6, GOOD), d)
|
|
354
|
+
(ok if not [x for x in fr6 if x[1] == 'font-inherited-drift']
|
|
355
|
+
else bad)(f'R2-6 [A]: 갈라진 부품의 «원래» 서체는 이탈 아님 (실제 {len(fr6)}건)')
|
|
356
|
+
|
|
357
|
+
# ── R3: 3라운드가 문 넷 ────────────────────────────────────────────────
|
|
358
|
+
# R3-2 [S] 속성이 typeface 앞에 오면 서체를 통째로 놓쳤다
|
|
359
|
+
r3a = build(os.path.join(d, 'r3a.pptx'),
|
|
360
|
+
[run('본문', 'Brand Display Bold'), run_attrs_first('코드', 'Comic Sans MS')])
|
|
361
|
+
f3a, _ = lane_font.scan(cfg_for(r3a, tpl, GOOD), d)
|
|
362
|
+
(ok if [x for x in f3a if 'Comic Sans' in x[3]]
|
|
363
|
+
else bad)('R3-2 [S]: 속성 순서와 무관하게 서체를 읽는다')
|
|
364
|
+
|
|
365
|
+
# R3-4 [S/A] rels 의 Id/Target 순서가 뒤집혀도 장 순서를 읽는다
|
|
366
|
+
r3b = build(os.path.join(d, 'r3b.pptx'), [run('코드', 'Comic Sans MS')],
|
|
367
|
+
rels_xml=PRES_RELS_REVERSED)
|
|
368
|
+
f3b, n3b = lane_font.scan(cfg_for(r3b, tpl, GOOD), d)
|
|
369
|
+
(ok if [x for x in f3b if 'slides 1' in x[2]]
|
|
370
|
+
else bad)(f'R3-4 [S]: rels 속성 순서가 달라도 장 번호가 맞다 ({[x[2] for x in f3b]})')
|
|
371
|
+
|
|
372
|
+
# R3-3 [A] 테마 이탈도 선언 면제를 존중한다
|
|
373
|
+
r3c = build(os.path.join(d, 'r3c.pptx'), [run('본문', 'Brand Display Bold')],
|
|
374
|
+
theme_font='Papyrus')
|
|
375
|
+
f3c, _ = lane_font.scan(
|
|
376
|
+
cfg_for(r3c, tpl, GOOD, intended=[{'fonts': ['Papyrus'], 'why': '테마 기본값 승인'}]), d)
|
|
377
|
+
(ok if not [x for x in f3c if x[1] == 'font-theme']
|
|
378
|
+
else bad)('R3-3 [A]: 테마 이탈도 fonts.intended 를 존중한다')
|
|
379
|
+
|
|
380
|
+
# R3-5 [A] 갈라진 부품의 endParaRPr 은 렌더 안 되므로 이탈이 아니다
|
|
381
|
+
LAY_T2 = ('<p:sldLayout xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" '
|
|
382
|
+
'xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">'
|
|
383
|
+
'<a:latin typeface="Helvetica Neue"/></p:sldLayout>')
|
|
384
|
+
LAY_D2 = LAY_T2.replace('</p:sldLayout>',
|
|
385
|
+
'<a:p><a:endParaRPr><a:latin typeface="Papyrus"/></a:endParaRPr>'
|
|
386
|
+
'</a:p></p:sldLayout>')
|
|
387
|
+
tpl3 = build(os.path.join(d, 'tpl3.pptx'), [run('본문', 'Brand Display Bold')],
|
|
388
|
+
layout_xml=LAY_T2)
|
|
389
|
+
r3d = build(os.path.join(d, 'r3d.pptx'), [run('본문', 'Brand Display Bold')],
|
|
390
|
+
layout_xml=LAY_D2)
|
|
391
|
+
f3d, _ = lane_font.scan(cfg_for(r3d, tpl3, GOOD), d)
|
|
392
|
+
(ok if not [x for x in f3d if x[1] == 'font-inherited-drift']
|
|
393
|
+
else bad)('R3-5 [A]: 갈라진 부품의 endParaRPr 은 이탈이 아니다')
|
|
394
|
+
|
|
395
|
+
# ── R4: 4라운드가 문 넷 (그중 하나는 R3 수리가 심은 결함이다) ──────────────
|
|
396
|
+
# R4-1 [S] `r:id` 네임스페이스 접두사 — 장 순서가 파일명 순으로 조용히 떨어졌다.
|
|
397
|
+
# 2장 덱으로 «순서»를 실제로 검증한다(1장짜리로는 폴백과 구분 불가 → R3-4 의 교훈)
|
|
398
|
+
r41 = build2(os.path.join(d, 'r41.pptx'),
|
|
399
|
+
[run('본문', 'Brand Display Bold')],
|
|
400
|
+
[run('코드', 'Comic Sans MS')])
|
|
401
|
+
f41, _ = lane_font.scan(cfg_for(r41, tpl, GOOD), d)
|
|
402
|
+
(ok if [x for x in f41 if 'slides 2' in x[2]]
|
|
403
|
+
else bad)(f'R4-1 [S]: r:id 를 읽어 발표순 2장으로 잡는다 (실제 {[x[2] for x in f41]})')
|
|
404
|
+
# 컨트롤 — 면제를 «2장»에 걸면 먹고, 1장에 걸면 안 먹어야 한다
|
|
405
|
+
f41b, _ = lane_font.scan(
|
|
406
|
+
cfg_for(r41, tpl, GOOD, intended=[{'slides': [2], 'fonts': ['Comic Sans MS'],
|
|
407
|
+
'why': '2장 면제'}]), d)
|
|
408
|
+
f41c, _ = lane_font.scan(
|
|
409
|
+
cfg_for(r41, tpl, GOOD, intended=[{'slides': [1], 'fonts': ['Comic Sans MS'],
|
|
410
|
+
'why': '1장 면제'}]), d)
|
|
411
|
+
(ok if not f41b and f41c
|
|
412
|
+
else bad)(f'R4-1 컨트롤: 면제가 «발표순» 장 번호를 따른다 (2장면제={len(f41b)} 1장면제={len(f41c)})')
|
|
413
|
+
|
|
414
|
+
# R4-2 [S] `<a:t xml:space="preserve">` 를 못 읽어 런을 통째로 건너뛰었다
|
|
415
|
+
r42 = build(os.path.join(d, 'r42.pptx'),
|
|
416
|
+
[run('본문', 'Brand Display Bold'), run_xmlspace('코드', 'Comic Sans MS')])
|
|
417
|
+
f42, _ = lane_font.scan(cfg_for(r42, tpl, GOOD), d)
|
|
418
|
+
(ok if [x for x in f42 if 'Comic Sans' in x[3]]
|
|
419
|
+
else bad)('R4-2 [S]: xml:space 속성이 붙은 a:t 도 읽는다')
|
|
420
|
+
|
|
421
|
+
# R4-3 [S] 템플릿의 endParaRPr 잔재가 허용 집합으로 «승격»되면 안 된다
|
|
422
|
+
tpl43 = build(os.path.join(d, 'tpl43.pptx'),
|
|
423
|
+
[run('본문', 'Brand Display Bold'), end_para('Comic Sans MS')])
|
|
424
|
+
r43 = build(os.path.join(d, 'r43.pptx'),
|
|
425
|
+
[run('본문', 'Brand Display Bold'), run('코드', 'Comic Sans MS')])
|
|
426
|
+
f43, _ = lane_font.scan(cfg_for(r43, tpl43), d) # allow 미선언 → 템플릿에서 유도
|
|
427
|
+
(ok if [x for x in f43 if 'Comic Sans' in x[3]]
|
|
428
|
+
else bad)('R4-3 [S]: 템플릿의 렌더 안 되는 끝 서식은 허용으로 승격되지 않는다')
|
|
429
|
+
|
|
430
|
+
# R4-4 [S] `<a:sym>` 기호 글꼴 슬롯
|
|
431
|
+
r44 = build(os.path.join(d, 'r44.pptx'),
|
|
432
|
+
[run('본문', 'Brand Display Bold'), sym_run('Wingdings')])
|
|
433
|
+
f44, _ = lane_font.scan(cfg_for(r44, tpl, GOOD), d)
|
|
434
|
+
(ok if [x for x in f44 if 'Wingdings' in x[3]]
|
|
435
|
+
else bad)('R4-4 [S]: a:sym 기호 슬롯의 이탈도 잡는다')
|
|
436
|
+
|
|
437
|
+
# R2-6b 대조 — 갈라진 부품에 «새로» 들인 서체는 여전히 잡힌다 (과교정 방지)
|
|
438
|
+
LAY_NEW = LAY_T.replace('</p:sldLayout>', '<a:latin typeface="Papyrus"/></p:sldLayout>')
|
|
439
|
+
r6b = build(os.path.join(d, 'r6b.pptx'), [run('본문', 'Brand Display Bold')],
|
|
440
|
+
layout_xml=LAY_NEW)
|
|
441
|
+
fr6b, _ = lane_font.scan(cfg_for(r6b, tpl6, GOOD), d)
|
|
442
|
+
(ok if [x for x in fr6b if x[1] == 'font-inherited-drift']
|
|
443
|
+
else bad)('R2-6b 과교정 방지: 갈라진 부품에 새로 들인 서체는 잡는다')
|
|
444
|
+
finally:
|
|
445
|
+
shutil.rmtree(d, ignore_errors=True)
|
|
446
|
+
|
|
447
|
+
print(f'\nL15 font self-test — PASS {PASS} · FAIL {FAIL}')
|
|
448
|
+
return 1 if FAIL else 0
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
if __name__ == '__main__':
|
|
452
|
+
sys.exit(main())
|
|
@@ -1,5 +1,169 @@
|
|
|
1
1
|
# forge-harness (fh-meta) Changelog
|
|
2
2
|
|
|
3
|
+
### [3.4.0] — 2026-09-12 — 탈상관의 «강한 축» 을 기계로 옮긴다 (외부 실측 기인)
|
|
4
|
+
|
|
5
|
+
**BREAKING (gate):** 마커 두 자리가 조인다. 둘 다 **grace date 로 소급하지 않는다** — 그 날짜 이전
|
|
6
|
+
파일명의 마커는 종전대로다.
|
|
7
|
+
① `crossfamily: panel(<families>)` 는 이제 grounds 에 **`evidence=SHARED|INDEPENDENT|MIXED(...)`**
|
|
8
|
+
토큰을 요구한다(`EVIDENCE_TOKEN_GRACE_DATE=2026-09-12`). 처방 = 한 토큰 추가:
|
|
9
|
+
`crossfamily: panel(codex) — residency=CLEAN(files=3) · evidence=SHARED(same staged diff to both) · …`
|
|
10
|
+
② `standpoint:` 의 `tier2`/`tier2b`/`tier3` grounds 검사가 **advisory → 차단**
|
|
11
|
+
(`STANDPOINT_GROUNDS_GRACE_DATE=2026-09-12`). 처방 = 돌린 **명령과 본 출력을 명명**하거나,
|
|
12
|
+
실행을 안 했으면 정직하게 `tier1b`(grounds 미검사)로 내린다.
|
|
13
|
+
🟥 **소비자 install 도 같다** — 설치된 훅은 그 레포의 마커를 같은 기준으로 본다.
|
|
14
|
+
|
|
15
|
+
**왜 지금인가 — 근거가 내부 관찰에서 외부 실측으로 바뀌었다.** `arXiv:2609.10969`
|
|
16
|
+
(*Engineering Reliable Commit Gates for Agentic AI: Cost-Aware Verification Portfolios under
|
|
17
|
+
Common-Mode Data Failures*, 2026-09-10, cs.SE)이 탈상관 두 축을 **고정 호출예산 2×2**로 분리해
|
|
18
|
+
48 템플릿 · **2,880 시나리오**에서 쟀다: 「같은 증거를 읽는 교차-모델 투표」는 위험 제안의 **62.9 %**
|
|
19
|
+
를 승인하고 「독립 출처」는 **22.9 %** — 출처 효과 **40.9 %p** 대 모델 다양성 **11.3 %p**, **3.6 배**.
|
|
20
|
+
즉 FH 가 **하드 차단하던 축이 약한 쪽**이었고, 강한 쪽의 grounds 는 advisory 였다. 그 불균형을 뒤집는다.
|
|
21
|
+
|
|
22
|
+
**`SHARED` 는 합법이다.** 결함은 «같은 diff 를 읽은 패널» 이 틀렸다는 것이 아니라 그 사실이 **기록에
|
|
23
|
+
적힐 수 없었다**는 것 — 그런 런이 이 enum 의 가장 강한 값으로 남았다. 차단되는 것은: 어느 쪽인지 안
|
|
24
|
+
적은 `panel(...)` · 닫힌 셋 밖의 값 · 형식 위반/중복 토큰 · `SHARED`/`MIXED` 의 공허한 본문
|
|
25
|
+
(`INDEPENDENT` 는 자기서술적이라 본문 검사 없음 — 정직한 답을 과차단하면 override 를 훈련시킨다).
|
|
26
|
+
|
|
27
|
+
🟥 **막는 것은 «기록의 형태» 이고 «진위» 가 아니다.** 저자가 명령을 명명했는지는 기록의 속성이지만,
|
|
28
|
+
그 실행이 진짜였는지는 이 게이트가 보지 않는다 — §Mechanization Boundary 가 사람에게 남긴 의도된 잔여다.
|
|
29
|
+
|
|
30
|
+
**함께 — 도구 쪽 정직성 (`auto-decorrelation` Step 4.6 신설).** 🟥 **이 스킬의 기본형이 곧 약한 팔이다**:
|
|
31
|
+
Step 4 가 payload 하나를 만들어 팬아웃하므로 평범하게 돌린 패널은 구조적으로 62.9 % 팔이다. 그 사실을
|
|
32
|
+
스킬 본문에 적고, 토큰 값을 «의도가 아니라 실제 디스패치 형태로» 정하게 했다. **독립 출처 모드는 없다** —
|
|
33
|
+
비용 구조·residency 경계·«독립» 의 정의가 전부 미결이라 설계 문제로 이름만 남겼다
|
|
34
|
+
(`tracks/_meta/fh_signal_2026-09-12_independent-source-axis.md`, CHAMBER-CANDIDATE).
|
|
35
|
+
|
|
36
|
+
**frontier-digest — 목록 경로의 ID↔제목 짝짓기.** 429 폴백 경로에서 **위치로 짝지으면 안 된다**:
|
|
37
|
+
2026-09-12 실측에 목록이 **ID 51개 · 제목 50개**를 내서 두 병렬 추출이 중간부터 밀렸고, 그 뒤 짝은
|
|
38
|
+
**형식은 멀쩩한데 전부 틀렸다**(`2609.10550` 외 2건). 한 항목을 **한 단위로** 읽고, 그게 확인되기 전까지
|
|
39
|
+
**항목별 `abs/{id}` 재해소가 의무**이며 제목 불일치는 **버린다**. 훅은 없다(파싱이 세션 안에서 일어난다).
|
|
40
|
+
|
|
41
|
+
🟥 **이 게이트의 초판이 여섯 군데 틀렸고, 다른 계열이 그걸 찾았다 (자력 적발 0).**
|
|
42
|
+
같은 diff 를 `agy`(gemini-3.8-flash-high)에 보냈고 여섯 건 전부 **실행 프로브로 재현한 뒤** 고쳤다.
|
|
43
|
+
방향이 중요하다 — **넷은 과차단**이었다. 새 게이트가 advisory 가 아니라 차단이 된 순간, 과차단은
|
|
44
|
+
경고 한 줄이 아니라 `--no-verify` 를 훈련시키는 결함이 된다:
|
|
45
|
+
|
|
46
|
+
| # | 부류 | 초판 동작 | 수리 |
|
|
47
|
+
|:-:|---|---|---|
|
|
48
|
+
| ① | **과차단 S** | `evidence=MIXED(codex(diff), gemini(repo))` 의 본문이 안쪽 `)` 에서 잘려 «공허» 로 판정 → 정당한 기록 하드 차단 | 중첩 **한 겹** 허용(두 겹은 여전히 형식 위반) |
|
|
49
|
+
| ② | **fail-open S** | `evidence=INDEPENDENT()` 와 `INDEPENDENT(<what>)` 가 **통과** — «길이 미검사» 를 «본문 없어도 됨» 으로 접었다 | 빈 본문·자리표시자는 **세 값 모두** 차단, 길이 바만 SHARED/MIXED |
|
|
50
|
+
| ③ | **과차단 S** | `standpoint:` 키워드에 뒤따르는 공백이 박혀 `cargo check` · `python scripts/eval.py` · `ran: ./ci.sh` 가 전부 차단 | 경계를 비-알파벳으로 + 흔한 러너 추가 + `30/30`·`rc=`·`./path` 도 실행 증거로 |
|
|
51
|
+
| ④ | **fail-open S** | `EVIDENCE_TOKEN_GRACE_DATE` 미주입 시 grace 가 **조용히 꺼져** pre-grace 마커를 전부 차단 | 상수를 mdate 검사 **앞에서** `:?` 로 평가 → 크게 죽는다 |
|
|
52
|
+
| ⑤ | **fail-open A** | pre-grace 마커의 `evidence=PARTIAL(...)` 이 조용히 통과 | 부재는 면제, **오작성은 면제 아님**(residency 와 같은 약속) |
|
|
53
|
+
| ⑦ | **과차단 A** | `wc -w` 가 `same-diff` 도 `동일diff` 도 1 낱말로 세어 정당한 **한국어** 기록을 차단 | 길이 바를 **문자 수**로 |
|
|
54
|
+
|
|
55
|
+
⑥(가드 단축평가 — `[ -n "$mdate" ] && [ ... ${VAR:?} ]` 에서 mdate 가 비면 `:?` 가 **아예 안 돈다**)도
|
|
56
|
+
같이 고쳤다. 🟥 **가드가 있다고 적어두고 안 도는 것은 장식이다**(`[[feedback_anchor_can_be_decorative]]`).
|
|
57
|
+
|
|
58
|
+
**레인**: `test_marker_crossfamily_lanes.sh` **82 픽스처**(신규 `e1`–`e15`, 위 여섯 건이 각각 컨트롤과 짝) ·
|
|
59
|
+
`test_marker_standpoint_lanes.sh` `N8`~`N8i`. 되돌림 프로브 두 종 — evidence 요구를 떼면
|
|
60
|
+
**정확히 6개**(e1·e5·e6·e7·e8·e8c)만 적색, standpoint 차단을 떼면 **N8 하나만** 적색.
|
|
61
|
+
🟥 두 레인 러너가 새 grace 상수를 주입하지 않으면 기존 픽스처가 통째로 오판하는 자리였다(standpoint 쪽은
|
|
62
|
+
실제로 깨졌다) — 러너에 주입을 넣고, 훅에는 `:?` 가드를 박아 부분 추출이 **조용히 통과하지 않게** 했다.
|
|
63
|
+
|
|
64
|
+
### [3.3.0] — 2026-09-12 — 짝지음(paired) 측정: 한 round-1 을 두 분기가 나눠 먹는다
|
|
65
|
+
|
|
66
|
+
**① 새 자산 — `--r1-only` · `--reuse-r1 <dir>` (`finding_fleet.sh` · `finding_pipeline.sh`).**
|
|
67
|
+
round-1 을 **한 번** 돌려 두고, 그 **같은** round-1 에서 round-2 를 두 번 갈라 돌린다
|
|
68
|
+
(`--reuse-r1 <dir> --round2` / `--round2-blind`). 왜 필요했나: «round-2 에서 상대 계열의 목록을
|
|
69
|
+
보여주는가» 를 A/B 하려고 두 팔을 독립 실행했더니 **round-1 기저부터 달랐다**(146 vs 136, 7.4 %) —
|
|
70
|
+
즉 한 변수 비교가 아니었다. 이 플래그들은 그 교란을 설득이 아니라 **설계**로 없앤다.
|
|
71
|
+
부수 효과로 더 싸다 — round-1 을 두 번 돌리지 않는다(멤버콜 −25 %).
|
|
72
|
+
|
|
73
|
+
기계 보증(레인 `test_finding_pipeline_lanes.sh` L115~L124, 122 → **132**):
|
|
74
|
+
두 분기의 `findings_r1.jsonl` 이 공유 소스와 **바이트 동일** · 파이프라인이 r1 **sha256** 을 두 분기
|
|
75
|
+
로그에 찍어 사후 대조 가능 · **빈 r1 재사용 거부**(미측정을 0 으로 렌더하지 않는다) · 옛 round-2
|
|
76
|
+
잔재 제거 · 소스==out 거부 · `--reuse-r1` 단독 거부 · `--r1-only` 은 검증을 안 돌리므로 **씨앗 선언을
|
|
77
|
+
거부**한다(선언된 컨트롤이 돈 적 없이 «통과» 로 읽히는 것을 막는다).
|
|
78
|
+
|
|
79
|
+
**② fail-open 수리 (`--r1-only`, cross-family 적발).** `--r1-only` 이 **빈 round-1 을 «사용 가능»
|
|
80
|
+
으로 통과**시켰다. 기전: `grep -c . f || echo 0` 은 빈 파일에서 «`0` 을 찍고 rc=1» 이라 폴백이 줄을
|
|
81
|
+
하나 더 붙여 `_n1="0\n0"` 이 되고, `[ "$_n1" -eq 0 ]` 이 **구문오류로 거짓**이 되어 `R1_UNUSABLE`
|
|
82
|
+
분기를 건너뛴다. ⚠️ **새 경로라 종전 동작 변화는 없다** — 기존 팔(플래그 미사용)의 경로는 불변이고
|
|
83
|
+
레인 122개가 그대로 통과한다. 질문은 «폴백을 붙였나» 가 아니라 **«실패 시 stdout 이 비는가»** 다.
|
|
84
|
+
|
|
85
|
+
**③ 출하 누락 수리 — `plugins/fh-commons/README.md` 를 `files[]` 에 명시.** npm 11 은 중첩 README 를
|
|
86
|
+
암묵 포함했는데 **npm 12 는 뺀다**(실측: `npx npm@latest pack --dry-run` 에서 0건). 발행 경로가
|
|
87
|
+
npm@latest 이므로 그대로 두면 3.2.0 에서 이미 빠져 나갔을 파일이다.
|
|
88
|
+
|
|
89
|
+
**자릿수 근거**: 새 capability **instance** 가 아니라 새 플래그 둘 + 레인 10 + 출하 파일 1 —
|
|
90
|
+
§Version-Digit-Policy 의 minor(새 자산 · 새 게이트 레인)다. **`BREAKING (gate):` 없음** — 소비자의
|
|
91
|
+
게이트 수용을 깨지 않는다(새 플래그는 opt-in, 기존 호출 경로 불변).
|
|
92
|
+
|
|
93
|
+
**④ harness-pr-reviewer (개명 + Axis 2·3 의무 배선) — 이 릴리스가 싣는다.**
|
|
94
|
+
- **RENAME** `hub-cc-pr-reviewer` → `harness-pr-reviewer` (운영자 결정 2026-09-12): 실제 쓰임이 «현장 하네스가 자기 PR 을 FH 리뷰 능력으로 검증하는 standalone 창구» 라서. 옛 이름 발화·참조는 그대로 라우팅(별칭), 리다이렉트 스텁 없음(phantom-quench 선례).
|
|
95
|
+
- Step 3.5 «Axis 2·3 의무 디스패치 레인» — 트리거 3항(8-matrix ❌ · load-bearing 경로 grep · 머지 권고 요청)을 PR 에서 계산, Axis 2 = auto-decorrelation 재사용 + `crossfamily:` 닫힌 enum, Axis 3 = phantom-quench(N/A 는 `--name-only` 로), 판정형 질문 reps≥3. Self-Catch 는 «판정이 아니라 cue». 플로어 sim: 현행 0/3 → 수정 3/3 (pmh-dev #77).
|
|
96
|
+
|
|
97
|
+
**⑤ `gate_shape_scan.sh` 여러 줄 docstring 안쪽 오탐 수리 + 판정형 질문 reps≥3 교리** (pmh-dev #76 역수확, #699): `"""`/`'''` 짝 상태를 추적해 docstring 내부를 판정 어휘 스캔에서 뺀다(selftest 8 → 10, 레인 9 → 11, L8b 되돌림 프로브 포함).
|
|
98
|
+
### [3.2.0] — 2026-09-09 — 판정 파이프라인이 실제로 돌고, 축에 이름이 붙었다
|
|
99
|
+
|
|
100
|
+
**BREAKING (gate):** `scripts/**/*.py` 가 이제 HEAVY 다 — 마커 없는 파이썬 단독 커밋이 막힌다.
|
|
101
|
+
처방 = `.sh` 와 동일: Axes 2–3 마커 + edit_manifest 항목.
|
|
102
|
+
🟥 **소비자 install 도 같다** — 설치된 훅은 «그 레포의» `scripts/**/*.py` 전부를 HEAVY 로 본다.
|
|
103
|
+
FH 와 무관한 파이썬 변경도 업그레이드 뒤엔 마커 없이 커밋이 막힌다. 그게 싫으면 `.sh` 와 같은
|
|
104
|
+
자리에서 같은 방법으로 좁혀라(훅 정규식 한 줄).
|
|
105
|
+
그 전까지 **출하되는 파이썬 8개**(측정 시점 7 + 3.1.4 이후 `doc_claim_triad_scan.py` —
|
|
106
|
+
`residency_closure_scan.py` · `memory_link_check.py` · `probe_live_eval_lib.py` 등)가 게이트를
|
|
107
|
+
한 번도 안 탔다. 훅 정규식 `^scripts/.*\.sh$` → `^scripts/.*\.(sh|py)$`. 레인
|
|
108
|
+
`test_heavy_classifier_lanes.sh` 가 이 구멍을 `uncovered` 로 **핀해 두고** 있었다 — 뒤집었다.
|
|
109
|
+
|
|
110
|
+
**① typed-finding 파이프라인이 배선됐다 (#686 → #690).** #686 이 fleet 과 verify 를 남겼는데
|
|
111
|
+
**둘을 잇는 것이 없었다** — `finding_verify.py` 의 `--verifier` 계약을 만족하는 명령이 레인 안의
|
|
112
|
+
하드코딩 스텁뿐이라, 실물 findings 에는 항상 `status=UNVERIFIED (rc=3)` 로만 돌았다.
|
|
113
|
+
새 자산 둘: `scripts/finding_verifier.sh`(검증기/감사기 래퍼 — `--target` 필수, 소스가 프롬프트에
|
|
114
|
+
도달했는지 호출 전 확인) · `scripts/finding_pipeline.sh`(드라이버).
|
|
115
|
+
🟥 **두 갈래 «분할»이 이 배선의 하중선이다** — verify 는 자기 계열 산출을 판정하지 않고
|
|
116
|
+
`unverified` 로 스탬프하는데 그 unverified 는 **생존자로 센다**. 한 번만 돌리면 절반이 판정 안 된 채
|
|
117
|
+
초록이 된다. 그래서 producer 별로 갈라 서로를 판정시킨다.
|
|
118
|
+
**명명된 잔여**: 계열이 둘뿐이면 드롭 감사자가 항상 생산자(`audit_role: appeal`)다 — 두 계열 런의
|
|
119
|
+
`AUDITED` 를 «독립 감사»로 읽지 마라.
|
|
120
|
+
|
|
121
|
+
**② 부분 감사 fail-open 수리 (소비자 영향 있음).** 3.1.4 까지의 `finding_verify.py` 는 감사자가
|
|
122
|
+
**무관한 id 하나만** 답해도 `audited=0 drop_audit=AUDITED rc=0` 을 냈다(빈 감사는 이미 4를 냈으므로
|
|
123
|
+
구멍은 «부분» 쪽이었다). 이제 `PARTIAL` 상태 + **rc=4** 다 — **부분 감사는 감사가 아니다.**
|
|
124
|
+
⚠️ 종전에 rc=0 이던 실행이 rc=4 가 될 수 있다. 의도된 조임이다.
|
|
125
|
+
|
|
126
|
+
**②-b 「생산자 미상」이 「검증됨」으로 통과하던 구멍 (소비자 영향 있음).** `finding_verify.py` 가
|
|
127
|
+
스스로 «이 파일이 강제하는 유일한 속성» 이라 선언한 **자기검증 금지**가 **옵셔널 필드에 걸려**
|
|
128
|
+
있었다: `producer_family` 를 빼면 검사가 통째로 건너뛰고, 같은 계열이 자기 발견을 승인하면서
|
|
129
|
+
`status=VERIFIED rc=0` 이 나왔다. **알려진 쌍으로 재현했다** — 같은 표·같은 검증자, 필드 유무만
|
|
130
|
+
다르게: 있으면 `unverified=1 rc=3`, 없으면 `confirmed=1 rc=0`.
|
|
131
|
+
🟥 배선된 경로는 안 뚫렸다(`finding_pipeline.sh` 가 라우팅을 거부하고 `finding_fleet.sh` 는 항상
|
|
132
|
+
찍는다) — 그러나 이 스크립트는 자기 CLI 를 출하하고 손으로 만든 표는 지원되는 입력이다.
|
|
133
|
+
이제 **부재 = `unverified`** 다. 부재는 «검증자가 저자가 아님»을 증명하지 못하므로, 자기검증과
|
|
134
|
+
같은 처분을 받는다.
|
|
135
|
+
**BREAKING (gate):** `producer_family` 없는 표를 `finding_verify.py` 에 직접 넣던 실행은 이제
|
|
136
|
+
rc=3(UNVERIFIED) 이다 — 처방 = 표에 `producer_family` 를 실어라(fleet 은 이미 싣는다).
|
|
137
|
+
레인 40 → **43**: L36(부재→차단) · L37(동일 계열→차단) · L38(**다른 계열→정상 통과**, 과차단
|
|
138
|
+
아님을 박는다) + L9 되돌림 프로브를 새 가드에 재고정하고 «앵커가 움직였다» 와 «앵커가 장식이다»
|
|
139
|
+
를 **다른 메시지로** 가른다(초판은 둘을 한 문장으로 냈고, 오늘 실제로 첫째가 나는데 둘째라고
|
|
140
|
+
말했다).
|
|
141
|
+
|
|
142
|
+
**③ fleet 스키마에 `defeater` 요구.** 없으면 리뷰 비교의 «반증 조건» 축이 구조적으로 측정 불가다.
|
|
143
|
+
첫 실사용에서 실제 모델이 3/3 비공허·관측가능한 값을 채웠다.
|
|
144
|
+
|
|
145
|
+
**④ 정체성 — Core Axis 에 목적 축 (#691).** 하네스 엔지니어링(수단) 옆에 **거버넌스
|
|
146
|
+
엔지니어링(목적)** 을 명명한다: *수치를 목표로 움직이되, 그 수치가 게이트를 열지는 않는다.*
|
|
147
|
+
슬로건이 아니라 실측이 있다 — 같은 8케이스에 다섯 리뷰 갈래가 **2.7 %–13.6 %** 주장 오류율을 냈고,
|
|
148
|
+
**전부 리뷰 표면에서는 쓸 만하고 전부 비가역 표면에서는 못 쓴다.** 넣기 전에 플로어 티어 4팔로 쟀다
|
|
149
|
+
(도달성 ARM 3/3 · CTRL 0/3 · 비회귀 양쪽 3/3 거부, 반증 조건 미발동).
|
|
150
|
+
정본 `knowledge/shared/harness-core/governance_engineering_definition.md`.
|
|
151
|
+
|
|
152
|
+
**⑤ 진입점 동기화 (#689).** `AGENTS.md` 항목 6 이 CLAUDE.md 로 **위임만** 하고 있었다 —
|
|
153
|
+
CLAUDE.md 를 안 싣는 런타임에는 그 규율이 전달되지 않는데, 하필 그 런타임 자신이 판정 엔진인
|
|
154
|
+
경우가 많다. 오류예산 교리를 그 자리에 실었다.
|
|
155
|
+
|
|
156
|
+
**⑥ 발행 기록 갱신 (#692).** arXiv **v2 가 2026-09-09 등재**됐다(내용 v1.2.2 · §6.7 신설 ·
|
|
157
|
+
제목 `Evidence` → `a Test of` · 초록 결론 → 가설). **그리고 같은 날 Zenodo v1.2.2 를 발행해
|
|
158
|
+
갈라졌던 두 예치를 맞췄다** — DOI `10.5281/zenodo.22674575`, 기계 필드로 `isIdenticalTo
|
|
159
|
+
arXiv:2609.04218`. 🟥 **확인은 폼이 아니라 발행 «후» 레코드 API 로 했다** — 저자·라이선스·키워드·
|
|
160
|
+
초록·파일 md5·버전 총수를 발행 후와 메타데이터 편집 후 각각 재독했고 전부 불변이었다.
|
|
161
|
+
#692 가 출하한 문서들은 «갈렸다» 상태를 적고 있었으므로 이 릴리스에서 5개 파일을 함께 갱신한다
|
|
162
|
+
(README ×4 · `docs/OUTPUT_EVIDENCE.md`).
|
|
163
|
+
|
|
164
|
+
**⑦ 새 게이트 자산** — `gate_shape_scan.sh`(#683, 매핑 안 된 판정 코드에도 게이트 발화) ·
|
|
165
|
+
`doc_claim_triad_scan.py` · 대응 레인 3종.
|
|
166
|
+
|
|
3
167
|
### [3.1.4] — 2026-09-07 — 논문이 arXiv 에 걸렸고, 「published」는 과대주장이었다
|
|
4
168
|
|
|
5
169
|
3.1.3 과 **같은 사유**다: 소비자가 받는 문서가 더 이상 참이 아니게 됐다.
|