@chrono-meta/fh-gate 3.1.4 → 3.2.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/rules/fh_4axis_gate.md +1 -1
- package/.claude-plugin/marketplace.json +3 -3
- package/AGENTS.md +6 -0
- package/CLAUDE.md +50 -6
- 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 +1 -1
- 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 +46 -0
- package/knowledge/shared/harness-core/governance_engineering_definition.md +89 -0
- package/knowledge/shared/learnings/subagent_invocations_log.yaml +68 -0
- package/knowledge/shared/rules/auto_project_mapping.md +1 -1
- package/package.json +11 -1
- package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
- 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 +69 -0
- package/plugins/fh-qp/.claude-plugin/plugin.json +1 -1
- package/scripts/doc_claim_triad_scan.py +303 -0
- package/scripts/finding_fleet.sh +180 -0
- package/scripts/finding_pipeline.sh +213 -0
- package/scripts/finding_verifier.sh +144 -0
- package/scripts/finding_verify.py +283 -0
- package/scripts/gate_pathspec_check.sh +1 -0
- package/scripts/gate_shape_scan.sh +106 -0
- package/scripts/selfcheck.sh +50 -0
- package/scripts/test_doc_claim_triad_lanes.sh +124 -0
- package/scripts/test_finding_pipeline_lanes.sh +459 -0
- package/scripts/test_gate_shape_scan_lanes.sh +36 -0
- package/scripts/test_heavy_classifier_lanes.sh +13 -3
- package/scripts/test_preprep_font_lanes.sh +87 -0
- package/templates/.git-hooks/pre-commit +6 -5
- package/templates/PRE-PUBLISH-CHECKLIST.md +29 -0
|
@@ -164,3 +164,20 @@ diagram:
|
|
|
164
164
|
adjacent_dup:
|
|
165
165
|
max_dup_chars: 40 # 인접 두 절의 중복 자수가 이보다 크면 낸다. 지우면 «측정만»
|
|
166
166
|
|
|
167
|
+
|
|
168
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
169
|
+
# L15 font — 서체가 배포 템플릿이 정한 집합을 벗어났나 (2026-09-11 신설).
|
|
170
|
+
#
|
|
171
|
+
# 🟥 `template` 이 이 레인의 본체다. 그게 없으면 **마스터에 상속된 서체와 저자가 찍은 서체를
|
|
172
|
+
# 구분할 수 없고**, 순진한 판정은 상속분을 위반으로 센다(실측 오탐 85건). 선언이 없으면
|
|
173
|
+
# 레인은 판정을 «하지 않는» 게 아니라 «구분 못 한다»고 노트에 적는다.
|
|
174
|
+
# 🟥 `allow` 를 비우면 template 의 slides/ 분포에서 자동 유도한다 — 「내 체크리스트가 아니라
|
|
175
|
+
# 그 템플릿 자신의 분포가 기준」. 가족 이름만 적으면 굵기 변형(Bold/Light)이 함께 덮인다.
|
|
176
|
+
# fonts:
|
|
177
|
+
# template: "../brand/official_template.pptx" # 배포받은 원본 템플릿
|
|
178
|
+
# allow: # 생략 가능 (template 에서 유도)
|
|
179
|
+
# - "Pretendard"
|
|
180
|
+
# intended: # 「알고도 남긴 것」 — why 없으면 다음 감사자가 «고친다»
|
|
181
|
+
# - slides: [37]
|
|
182
|
+
# fonts: ["Roboto Mono"]
|
|
183
|
+
# why: "터미널 데모 — 비례 서체면 열 정렬이 무너진다"
|
|
@@ -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,74 @@
|
|
|
1
1
|
# forge-harness (fh-meta) Changelog
|
|
2
2
|
|
|
3
|
+
### [3.2.0] — 2026-09-09 — 판정 파이프라인이 실제로 돌고, 축에 이름이 붙었다
|
|
4
|
+
|
|
5
|
+
**BREAKING (gate):** `scripts/**/*.py` 가 이제 HEAVY 다 — 마커 없는 파이썬 단독 커밋이 막힌다.
|
|
6
|
+
처방 = `.sh` 와 동일: Axes 2–3 마커 + edit_manifest 항목.
|
|
7
|
+
🟥 **소비자 install 도 같다** — 설치된 훅은 «그 레포의» `scripts/**/*.py` 전부를 HEAVY 로 본다.
|
|
8
|
+
FH 와 무관한 파이썬 변경도 업그레이드 뒤엔 마커 없이 커밋이 막힌다. 그게 싫으면 `.sh` 와 같은
|
|
9
|
+
자리에서 같은 방법으로 좁혀라(훅 정규식 한 줄).
|
|
10
|
+
그 전까지 **출하되는 파이썬 8개**(측정 시점 7 + 3.1.4 이후 `doc_claim_triad_scan.py` —
|
|
11
|
+
`residency_closure_scan.py` · `memory_link_check.py` · `probe_live_eval_lib.py` 등)가 게이트를
|
|
12
|
+
한 번도 안 탔다. 훅 정규식 `^scripts/.*\.sh$` → `^scripts/.*\.(sh|py)$`. 레인
|
|
13
|
+
`test_heavy_classifier_lanes.sh` 가 이 구멍을 `uncovered` 로 **핀해 두고** 있었다 — 뒤집었다.
|
|
14
|
+
|
|
15
|
+
**① typed-finding 파이프라인이 배선됐다 (#686 → #690).** #686 이 fleet 과 verify 를 남겼는데
|
|
16
|
+
**둘을 잇는 것이 없었다** — `finding_verify.py` 의 `--verifier` 계약을 만족하는 명령이 레인 안의
|
|
17
|
+
하드코딩 스텁뿐이라, 실물 findings 에는 항상 `status=UNVERIFIED (rc=3)` 로만 돌았다.
|
|
18
|
+
새 자산 둘: `scripts/finding_verifier.sh`(검증기/감사기 래퍼 — `--target` 필수, 소스가 프롬프트에
|
|
19
|
+
도달했는지 호출 전 확인) · `scripts/finding_pipeline.sh`(드라이버).
|
|
20
|
+
🟥 **두 갈래 «분할»이 이 배선의 하중선이다** — verify 는 자기 계열 산출을 판정하지 않고
|
|
21
|
+
`unverified` 로 스탬프하는데 그 unverified 는 **생존자로 센다**. 한 번만 돌리면 절반이 판정 안 된 채
|
|
22
|
+
초록이 된다. 그래서 producer 별로 갈라 서로를 판정시킨다.
|
|
23
|
+
**명명된 잔여**: 계열이 둘뿐이면 드롭 감사자가 항상 생산자(`audit_role: appeal`)다 — 두 계열 런의
|
|
24
|
+
`AUDITED` 를 «독립 감사»로 읽지 마라.
|
|
25
|
+
|
|
26
|
+
**② 부분 감사 fail-open 수리 (소비자 영향 있음).** 3.1.4 까지의 `finding_verify.py` 는 감사자가
|
|
27
|
+
**무관한 id 하나만** 답해도 `audited=0 drop_audit=AUDITED rc=0` 을 냈다(빈 감사는 이미 4를 냈으므로
|
|
28
|
+
구멍은 «부분» 쪽이었다). 이제 `PARTIAL` 상태 + **rc=4** 다 — **부분 감사는 감사가 아니다.**
|
|
29
|
+
⚠️ 종전에 rc=0 이던 실행이 rc=4 가 될 수 있다. 의도된 조임이다.
|
|
30
|
+
|
|
31
|
+
**②-b 「생산자 미상」이 「검증됨」으로 통과하던 구멍 (소비자 영향 있음).** `finding_verify.py` 가
|
|
32
|
+
스스로 «이 파일이 강제하는 유일한 속성» 이라 선언한 **자기검증 금지**가 **옵셔널 필드에 걸려**
|
|
33
|
+
있었다: `producer_family` 를 빼면 검사가 통째로 건너뛰고, 같은 계열이 자기 발견을 승인하면서
|
|
34
|
+
`status=VERIFIED rc=0` 이 나왔다. **알려진 쌍으로 재현했다** — 같은 표·같은 검증자, 필드 유무만
|
|
35
|
+
다르게: 있으면 `unverified=1 rc=3`, 없으면 `confirmed=1 rc=0`.
|
|
36
|
+
🟥 배선된 경로는 안 뚫렸다(`finding_pipeline.sh` 가 라우팅을 거부하고 `finding_fleet.sh` 는 항상
|
|
37
|
+
찍는다) — 그러나 이 스크립트는 자기 CLI 를 출하하고 손으로 만든 표는 지원되는 입력이다.
|
|
38
|
+
이제 **부재 = `unverified`** 다. 부재는 «검증자가 저자가 아님»을 증명하지 못하므로, 자기검증과
|
|
39
|
+
같은 처분을 받는다.
|
|
40
|
+
**BREAKING (gate):** `producer_family` 없는 표를 `finding_verify.py` 에 직접 넣던 실행은 이제
|
|
41
|
+
rc=3(UNVERIFIED) 이다 — 처방 = 표에 `producer_family` 를 실어라(fleet 은 이미 싣는다).
|
|
42
|
+
레인 40 → **43**: L36(부재→차단) · L37(동일 계열→차단) · L38(**다른 계열→정상 통과**, 과차단
|
|
43
|
+
아님을 박는다) + L9 되돌림 프로브를 새 가드에 재고정하고 «앵커가 움직였다» 와 «앵커가 장식이다»
|
|
44
|
+
를 **다른 메시지로** 가른다(초판은 둘을 한 문장으로 냈고, 오늘 실제로 첫째가 나는데 둘째라고
|
|
45
|
+
말했다).
|
|
46
|
+
|
|
47
|
+
**③ fleet 스키마에 `defeater` 요구.** 없으면 리뷰 비교의 «반증 조건» 축이 구조적으로 측정 불가다.
|
|
48
|
+
첫 실사용에서 실제 모델이 3/3 비공허·관측가능한 값을 채웠다.
|
|
49
|
+
|
|
50
|
+
**④ 정체성 — Core Axis 에 목적 축 (#691).** 하네스 엔지니어링(수단) 옆에 **거버넌스
|
|
51
|
+
엔지니어링(목적)** 을 명명한다: *수치를 목표로 움직이되, 그 수치가 게이트를 열지는 않는다.*
|
|
52
|
+
슬로건이 아니라 실측이 있다 — 같은 8케이스에 다섯 리뷰 갈래가 **2.7 %–13.6 %** 주장 오류율을 냈고,
|
|
53
|
+
**전부 리뷰 표면에서는 쓸 만하고 전부 비가역 표면에서는 못 쓴다.** 넣기 전에 플로어 티어 4팔로 쟀다
|
|
54
|
+
(도달성 ARM 3/3 · CTRL 0/3 · 비회귀 양쪽 3/3 거부, 반증 조건 미발동).
|
|
55
|
+
정본 `knowledge/shared/harness-core/governance_engineering_definition.md`.
|
|
56
|
+
|
|
57
|
+
**⑤ 진입점 동기화 (#689).** `AGENTS.md` 항목 6 이 CLAUDE.md 로 **위임만** 하고 있었다 —
|
|
58
|
+
CLAUDE.md 를 안 싣는 런타임에는 그 규율이 전달되지 않는데, 하필 그 런타임 자신이 판정 엔진인
|
|
59
|
+
경우가 많다. 오류예산 교리를 그 자리에 실었다.
|
|
60
|
+
|
|
61
|
+
**⑥ 발행 기록 갱신 (#692).** arXiv **v2 가 2026-09-09 등재**됐다(내용 v1.2.2 · §6.7 신설 ·
|
|
62
|
+
제목 `Evidence` → `a Test of` · 초록 결론 → 가설). **그리고 같은 날 Zenodo v1.2.2 를 발행해
|
|
63
|
+
갈라졌던 두 예치를 맞췄다** — DOI `10.5281/zenodo.22674575`, 기계 필드로 `isIdenticalTo
|
|
64
|
+
arXiv:2609.04218`. 🟥 **확인은 폼이 아니라 발행 «후» 레코드 API 로 했다** — 저자·라이선스·키워드·
|
|
65
|
+
초록·파일 md5·버전 총수를 발행 후와 메타데이터 편집 후 각각 재독했고 전부 불변이었다.
|
|
66
|
+
#692 가 출하한 문서들은 «갈렸다» 상태를 적고 있었으므로 이 릴리스에서 5개 파일을 함께 갱신한다
|
|
67
|
+
(README ×4 · `docs/OUTPUT_EVIDENCE.md`).
|
|
68
|
+
|
|
69
|
+
**⑦ 새 게이트 자산** — `gate_shape_scan.sh`(#683, 매핑 안 된 판정 코드에도 게이트 발화) ·
|
|
70
|
+
`doc_claim_triad_scan.py` · 대응 레인 3종.
|
|
71
|
+
|
|
3
72
|
### [3.1.4] — 2026-09-07 — 논문이 arXiv 에 걸렸고, 「published」는 과대주장이었다
|
|
4
73
|
|
|
5
74
|
3.1.3 과 **같은 사유**다: 소비자가 받는 문서가 더 이상 참이 아니게 됐다.
|