@chrono-meta/fh-gate 2.15.1 → 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 +88 -1
- package/.claude-plugin/marketplace.json +8 -2
- package/AGENTS.md +27 -0
- package/CATALOG.md +17 -0
- package/CLAUDE.md +13 -3
- package/README.ja.md +51 -7
- package/README.ko.md +48 -7
- package/README.md +43 -6
- 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 +77 -10
- package/knowledge/shared/harness-core/field_verdict_crossfamily_gate.md +18 -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 +35 -1
- package/knowledge/shared/harness-core/ship_readiness_gate.md +19 -2
- package/knowledge/shared/learnings/subagent_invocations_log.yaml +489 -3
- package/package.json +48 -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 +67 -0
- 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/backtick_guard.sh +194 -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/context_continuity_score.sh +49 -7
- package/scripts/directional_diff_gate.sh +14 -2
- package/scripts/fh-gate.sh +3 -3
- package/scripts/files_manifest_shipping_check.sh +19 -0
- package/scripts/frontier_digest_autopilot.sh +4 -1
- package/scripts/gate_pathspec_check.sh +1 -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 +79 -18
- 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 +192 -0
- package/scripts/public_surface_scan_files.sh +11 -2
- package/scripts/push_zone_check.sh +78 -0
- package/scripts/residency_closure_scan.py +252 -0
- package/scripts/revert_probe.sh +250 -0
- package/scripts/selfcheck.sh +106 -3
- package/scripts/session_close_check.sh +100 -0
- package/scripts/sim_isolated_run.sh +195 -9
- package/scripts/test_action_yml_lanes.sh +97 -0
- package/scripts/test_backtick_guard_lanes.sh +115 -0
- package/scripts/test_degrade_scan_shell_probes.sh +7 -7
- package/scripts/test_fh_qp_lanes.sh +105 -0
- package/scripts/test_files_manifest_shipping_lanes.sh +5 -5
- package/scripts/test_gate_two_verdicts_lanes.sh +139 -0
- package/scripts/test_heavy_classifier_lanes.sh +1 -1
- package/scripts/test_lane_runner_lanes.sh +59 -33
- package/scripts/test_map_postprocess_lanes.sh +143 -0
- package/scripts/test_mapped_tracks_lanes.sh +1 -1
- 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_marker_soul_check_lanes.sh +24 -0
- package/scripts/test_node_check_lanes.sh +34 -34
- package/scripts/test_outbound_query_hook_lanes.sh +433 -0
- package/scripts/test_outbound_query_lanes.sh +87 -0
- package/scripts/test_package_coverage_lanes.sh +53 -27
- package/scripts/test_pipe_verdict_guard_lanes.sh +31 -5
- package/scripts/test_precommit_pointer_index_lanes.sh +33 -0
- package/scripts/test_preprep_diagram_lanes.sh +87 -0
- package/scripts/test_preprep_drift_anchor.sh +16 -7
- package/scripts/test_preprep_drift_anchor_lanes.sh +23 -0
- 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 +57 -0
- package/scripts/test_push_zone_lanes.sh +304 -0
- package/scripts/test_residency_closure_lanes.sh +70 -0
- package/scripts/test_revert_probe_lanes.sh +146 -0
- package/scripts/test_session_close_lanes.sh +3 -5
- package/scripts/test_sim_isolated_run_lanes.sh +136 -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/utterance_landing_check.sh +2 -2
- package/scripts/validate_yaml.sh +27 -0
- package/scripts/worktree_reclaim.sh +95 -0
- package/templates/.git-hooks/pre-commit +380 -5
- package/templates/.git-hooks/pre-push +91 -0
- package/templates/RED_TEAM_REPORT.md +49 -0
- package/templates/settings.PreToolUse.snippet.json +101 -1
- package/plugins/fh-commons/README.md +0 -38
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chrono-meta/fh-gate",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"description": "FH runtime adapters — run FH governance, skills, and agents via Claude or Codex with machine-parseable gates.",
|
|
5
5
|
"homepage": "https://github.com/chrono-meta/forge-harness#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -107,6 +107,9 @@
|
|
|
107
107
|
"docs/IDENTITIES.md",
|
|
108
108
|
"docs/GATE_DAY.md",
|
|
109
109
|
"docs/platform_sustainability.md",
|
|
110
|
+
"docs/model_tier_expectations.md",
|
|
111
|
+
"docs/USE_CASES.md",
|
|
112
|
+
"docs/STANDARDS_ALIGNMENT.md",
|
|
110
113
|
"knowledge/shared/GLOSSARY.md",
|
|
111
114
|
"knowledge/shared/patterns",
|
|
112
115
|
"knowledge/shared/plugin-catalog",
|
|
@@ -129,10 +132,15 @@
|
|
|
129
132
|
"scripts/test_marker_floor_lanes.sh",
|
|
130
133
|
"scripts/test_marker_defense_lanes.sh",
|
|
131
134
|
"scripts/test_marker_crossfamily_lanes.sh",
|
|
135
|
+
"scripts/residency_closure_scan.py",
|
|
136
|
+
"scripts/test_residency_closure_lanes.sh",
|
|
132
137
|
"scripts/test_marker_standpoint_lanes.sh",
|
|
133
138
|
"scripts/test_marker_thirdparty_lanes.sh",
|
|
134
139
|
"scripts/test_marker_axes_run_lanes.sh",
|
|
135
140
|
"scripts/test_marker_soul_check_lanes.sh",
|
|
141
|
+
"scripts/test_marker_affected_lanes.sh",
|
|
142
|
+
"scripts/test_marker_oracle_lanes.sh",
|
|
143
|
+
"scripts/test_gate_two_verdicts_lanes.sh",
|
|
136
144
|
"scripts/test_heavy_classifier_lanes.sh",
|
|
137
145
|
"scripts/residency_admission_check.sh",
|
|
138
146
|
"scripts/test_residency_admission_lanes.sh",
|
|
@@ -167,6 +175,7 @@
|
|
|
167
175
|
"scripts/test_leak_scan_control_lanes.sh",
|
|
168
176
|
"templates/predelete_check.sh",
|
|
169
177
|
"templates/PRE-PUBLISH-CHECKLIST.md",
|
|
178
|
+
"templates/RED_TEAM_REPORT.md",
|
|
170
179
|
"scripts/degrade_direction_scan.sh",
|
|
171
180
|
"scripts/degrade_probe_capability.sh",
|
|
172
181
|
"scripts/psa_probe_capability.sh",
|
|
@@ -178,6 +187,8 @@
|
|
|
178
187
|
"scripts/prepush_guard_check.sh",
|
|
179
188
|
"templates/predelete_check.test.sh",
|
|
180
189
|
"scripts/test_prepush_destructive_lanes.sh",
|
|
190
|
+
"scripts/push_zone_check.sh",
|
|
191
|
+
"scripts/test_push_zone_lanes.sh",
|
|
181
192
|
"scripts/fixture_guard_lib.sh",
|
|
182
193
|
"scripts/shell_name_boundary_scan.sh",
|
|
183
194
|
"scripts/test_verdict_watermark_lanes.sh",
|
|
@@ -202,6 +213,7 @@
|
|
|
202
213
|
"scripts/test_prepush_stdin_integrity.sh",
|
|
203
214
|
"scripts/public_surface_scan_files.sh",
|
|
204
215
|
".claude/rules/.public-surface-patterns.defaults",
|
|
216
|
+
".claude/rules/.residency-patterns.defaults",
|
|
205
217
|
"plugins/fh-meta/.claude-plugin/plugin.json",
|
|
206
218
|
"plugins/fh-meta/skills",
|
|
207
219
|
"plugins/fh-meta/agents",
|
|
@@ -226,10 +238,22 @@
|
|
|
226
238
|
"scripts/test_sidecar_calibrate_lanes.sh",
|
|
227
239
|
"scripts/pipe_verdict_guard.sh",
|
|
228
240
|
"scripts/test_pipe_verdict_guard_lanes.sh",
|
|
241
|
+
"scripts/backtick_guard.sh",
|
|
242
|
+
"scripts/test_backtick_guard_lanes.sh",
|
|
229
243
|
"scripts/test_destructive_pre_gate_lanes.sh",
|
|
230
244
|
"scripts/destructive_pre_gate.sh",
|
|
231
245
|
"scripts/test_stale_clone_guard_lanes.sh",
|
|
232
246
|
"scripts/stale_clone_guard.sh",
|
|
247
|
+
"scripts/proposal_hook.sh",
|
|
248
|
+
"scripts/test_proposal_hook_lanes.sh",
|
|
249
|
+
"scripts/outbound_query_hook.sh",
|
|
250
|
+
"scripts/test_outbound_query_hook_lanes.sh",
|
|
251
|
+
"scripts/outbound_query_guard.sh",
|
|
252
|
+
"scripts/test_outbound_query_lanes.sh",
|
|
253
|
+
"scripts/test_revert_probe_lanes.sh",
|
|
254
|
+
"scripts/revert_probe.sh",
|
|
255
|
+
"scripts/test_preprep_drift_anchor_lanes.sh",
|
|
256
|
+
"scripts/test_precommit_pointer_index_lanes.sh",
|
|
233
257
|
"templates/settings.PreToolUse.snippet.json",
|
|
234
258
|
"scripts/halffix_propagation_scan.sh",
|
|
235
259
|
"scripts/test_halffix_lanes.sh",
|
|
@@ -274,8 +298,30 @@
|
|
|
274
298
|
"scripts/test_preprep_adjacent_dup_lanes.sh",
|
|
275
299
|
"scripts/test_preprep_promise_lanes.sh",
|
|
276
300
|
"scripts/test_mapped_tracks_lanes.sh",
|
|
301
|
+
"scripts/map_postprocess.py",
|
|
302
|
+
"scripts/test_map_postprocess_lanes.sh",
|
|
277
303
|
"!**/__pycache__",
|
|
278
304
|
"!**/__pycache__/**",
|
|
279
|
-
"!**/*.pyc"
|
|
305
|
+
"!**/*.pyc",
|
|
306
|
+
"scripts/probe_live_eval.sh",
|
|
307
|
+
"scripts/probe_live_eval_lib.py",
|
|
308
|
+
"scripts/test_probe_live_eval_lanes.sh",
|
|
309
|
+
".claude/regression/probes_live.yaml",
|
|
310
|
+
"scripts/com.forge-harness.live-eval.plist",
|
|
311
|
+
"scripts/utterance_intake.sh",
|
|
312
|
+
"scripts/transcript_utterances.py",
|
|
313
|
+
"scripts/test_utterance_intake_lanes.sh",
|
|
314
|
+
"scripts/worktree_reclaim.sh",
|
|
315
|
+
"scripts/test_worktree_reclaim_lanes.sh",
|
|
316
|
+
"plugins/fh-qp/.claude-plugin/plugin.json",
|
|
317
|
+
"plugins/fh-qp/README.md",
|
|
318
|
+
"plugins/fh-qp/qp_profile.example.yaml",
|
|
319
|
+
"plugins/fh-qp/skills",
|
|
320
|
+
"plugins/fh-qp/scripts",
|
|
321
|
+
"plugins/fh-qp/fixtures",
|
|
322
|
+
"scripts/test_fh_qp_lanes.sh",
|
|
323
|
+
"scripts/test_preprep_diagram_lanes.sh",
|
|
324
|
+
"scripts/test_preprep_slide_refs_lanes.sh",
|
|
325
|
+
"scripts/test_action_yml_lanes.sh"
|
|
280
326
|
]
|
|
281
327
|
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
8
|
mkdir -p ~/preprep && cd ~/preprep
|
|
9
|
-
cp <이 디렉터리>/{preprep.py,interslide_deps.py,lane_progression.py,lane_adjacent_dup.py,lane_promise.py} .
|
|
9
|
+
cp <이 디렉터리>/{preprep.py,interslide_deps.py,lane_progression.py,lane_adjacent_dup.py,lane_promise.py,lane_diagram.py,diagram_from_json.py} .
|
|
10
10
|
cp -r <이 디렉터리>/ooxml . # 덱(pptx)을 다룬다면. 아니면 생략
|
|
11
11
|
cp <이 디렉터리>/surfaces.example.yaml surfaces.yaml
|
|
12
12
|
cp <이 디렉터리>/canon_terms.example.yaml canon_terms.yaml
|
|
@@ -16,6 +16,9 @@ python3 preprep.py
|
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
의존: `pyyaml` · `python-pptx` 둘뿐이다(나머지는 표준 라이브러리). FH 도, 네트워크도 필요 없다.
|
|
19
|
+
도해를 JSON 에서 굽는 갈래(`diagram_from_json.py`)만 **선택 의존**이 둘 더 있다 — node ≥ 18 + 로컬 설치한
|
|
20
|
+
archify(`npx -y skills add tt-a1i/archify --skill archify --agent claude-code --copy --yes`, MIT) · headless Chrome.
|
|
21
|
+
없으면 그 스크립트만 exit 10 으로 죽고 레인 L12 는 영수증 없음을 UNMEASURED 로 낸다 — 나머지 레인은 그대로 돈다.
|
|
19
22
|
|
|
20
23
|
## 드리프트를 막는 법
|
|
21
24
|
|
|
@@ -66,7 +66,7 @@ model: sonnet
|
|
|
66
66
|
| **위치가 어긋나 있다** | 🟢 덮인다 | `ooxml/gate.py` ⑥ 화면 밖 도형 ⚠️ 그 검사에 붙은 사고 수치(`26p · 1,854,400 EMU`)는 **`gate.py` 자신의 인라인 주석 말고 대조할 기록이 없다** — 거짓이라는 뜻이 아니라 독립 출처가 없다는 뜻이다 |
|
|
67
67
|
| **번역투** | 🟢 덮인다 | ⑤에서 `ko-tech-writer` Step 2 |
|
|
68
68
|
| **주어가 빠져 있다** | 🔴 **미구현** (판별자는 있다) | 🟥 «패턴만으로는 못 한다»고 적었던 것을 정정한다 — 판별자가 이미 실재한다: **주어 생략의 가부는 문장이 아니라 화면이 정한다.** 그 문장이 뜨는 화면에 참조 대상이 있으면 생략이 맞고, **참조 대상이 사라지는 장에서 복원**한다. 화면 텍스트는 이미 추출되므로 이건 judged 가 아니라 **measured 후보**다. 다만 복원/유지는 4등급으로 갈린다 (목적어=복원 · 주어+신정보=복원 · **주어+구정보=그대로 둔다** · SOV 재배열) — 전부 복원하면 번역투를 **새로 만든다**. 미구현 상태다 |
|
|
69
|
-
| **화면이 너무 비어 보인다** | 🔴 **미구축** | gate.py 는 «밖으로 나갔나»를 보지 «안이 비었나»를 안 본다. 🟥 **정정 2026-09-01 — 「pptx→PDF 가 끊긴다」는 거짓이었고, 그 오진이 이 항목을 막고 있었다.** known-pair: `soffice` 부재는 **참**(`which soffice` → not found · LibreOffice.app 없음) · `pdftoppm` 실재는 **참**(`/opt/homebrew/bin/pdftoppm`) — **그런데 PowerPoint AppleScript `save as PDF` 로 120장 덱이 실제로 12회 렌더됐다**(`/tmp/v11*.pdf` 12개, 최신본 120페이지 `%PDF-1.3`, 2026-09-01 실측). ⇒ 끊긴 것은 **「LibreOffice 경로」**지 **「pptx→PDF」**가 아니다. 🟥 **미구축 사유가 바뀐다**: 「렌더가 안 된다」(장애물 실재) → **「기준선을 아직 안 쟀다」**(장애물 없음). 두 상태는 다르고, 전자는 «못 한다»로 후자는 «안 했다»로 읽힌다. [[feedback_rule_misdescribes_its_own_machine]] 기준선 형태는 정해져 있다: **레퍼런스 덱을 실측해 장 유형별 화면 자수 기준선을 세우고 미달 장을 센다** — 값은 발표마다 다시 재야 한다 |
|
|
69
|
+
| **화면이 너무 비어 보인다** | 🔴 **미구축** | gate.py 는 «밖으로 나갔나»를 보지 «안이 비었나»를 안 본다. 🟥 **정정 2026-09-01 — 「pptx→PDF 가 끊긴다」는 거짓이었고, 그 오진이 이 항목을 막고 있었다.** known-pair: `soffice` 부재는 **참**(`which soffice` → not found · LibreOffice.app 없음) · `pdftoppm` 실재는 **참**(`/opt/homebrew/bin/pdftoppm`) — **그런데 PowerPoint AppleScript `save as PDF` 로 120장 덱이 실제로 12회 렌더됐다**(`/tmp/v11*.pdf` 12개, 최신본 120페이지 `%PDF-1.3`, 2026-09-01 실측). ⇒ 끊긴 것은 **「LibreOffice 경로」**지 **「pptx→PDF」**가 아니다. 🟥 **미구축 사유가 바뀐다**: 「렌더가 안 된다」(장애물 실재) → **「기준선을 아직 안 쟀다」**(장애물 없음). 두 상태는 다르고, 전자는 «못 한다»로 후자는 «안 했다»로 읽힌다. [[feedback_rule_misdescribes_its_own_machine]] 기준선 형태는 정해져 있다: **레퍼런스 덱을 실측해 장 유형별 화면 자수 기준선을 세우고 미달 장을 센다** — 값은 발표마다 다시 재야 한다. 🟥 **정정 2026-09-04 — 기준선을 실제로 쟀다, 다만 반대편(과밀)이다.** 121장 한국어 덱 실측: 화면 자수 중앙 118 · 평균 133 · P90 207 · 최대 808. 읽는 부담(신규 글자 ÷ 발화 초) 중앙 10.0 · P90 16.9(§레인 R4·R5). ⚠️ 값은 발표마다 다시 재야 한다 — 임계를 코드에 안 박고 **그 덱 자기 분포**에서 뽑는 이유가 이것이다. 그리고 이건 게이트가 아니라 **순위표**다 — 정의상 항상 약 10%가 뜬다, 「후보 0건」을 목표로 삼으면 오독이다(§정직한 한계 참조). **「비어 보인다」쪽(하한)은 여전히 기준선이 없다** — 잰 것은 과밀(상한) 뿐이라 이 행은 아직 🔴 미구축으로 남는다 |
|
|
70
70
|
| **흐름이 매끄럽지 않다** | 🟡 **절반 덮인다** | L8 은 «지시 대상이 해소되나»를 볼 뿐 «논지가 서나»를 못 본다. 🟢 그 부분집합 하나는 이제 덮인다 — **L9 progression** 이 «선언된 단계가 화면에서 빠졌나»를 본다(압축에서 첫 칸이 떨어지는 실사고). ⚠️ «논지가 서나» 자체는 여전히 미해결이고, 되읊음 대조(리뷰어가 요약한 뜻이 저자 의도와 반대인가)는 **사람이 한다** |
|
|
71
71
|
|
|
72
72
|
🟥 **미구축 셋을 「advisory 라서 괜찮다」로 덮지 않는다.** 지금은 ④·⑤ 단계에서 **사람이** 하고
|
|
@@ -117,13 +117,30 @@ model: sonnet
|
|
|
117
117
|
뒤 단계 전부의 근거다. 여기가 비면 뒤가 전부 기본값으로 채워진다
|
|
118
118
|
② 레퍼런스 「참고하려는 것이 있나」를 **먼저 묻는다**(ko-tech-writer §Step 1 과 같은 규율:
|
|
119
119
|
표본 없이 톤을 단정하지 않는다). 행사 공식 장표가이드 · 좋다고 본 남의 덱 ·
|
|
120
|
-
조직 템플릿. 있으면 /corpus-grounding-expander 로 그 실물을 근거로
|
|
120
|
+
조직 템플릿. 있으면 /corpus-grounding-expander 로 그 실물을 근거로 세운다.
|
|
121
|
+
🟥 «위반»은 원문 **문장**을 인용할 수 있을 때만 낸다 — 스와치·템플릿 실측값과의
|
|
122
|
+
차이는 «위반»이 아니라 «차이»로만 낸다(§방법론 ⓖ, 숫자가 없는 원문에서 「위반」을
|
|
123
|
+
냈다가 정정하고, 스와치만 근거로 「위반」을 냈다가 철회한 실사고)
|
|
121
124
|
③ 프런티어 ②가 비었거나 더 넓히고 싶을 때만. persona-innovator(Mode F) · /frontier-digest
|
|
122
125
|
로 세련된 방법을 찾아온다. 🟥 찾아온 것은 **후보**지 결정이 아니다
|
|
123
126
|
④ 구성 기승전결. 하네스는 배치안을 «몇 개» 내고 사람이 고른다.
|
|
124
127
|
🟥 하나만 내면 그건 제안이 아니라 결정이다
|
|
125
128
|
⑤ 제작·퇴고 마이크로매니징. 문체는 /ko-tech-writer 로 — 레지스터 · 실물 캘리브레이션 ·
|
|
126
|
-
번역투 제거 · 수치 게이트 · 지각 QA
|
|
129
|
+
번역투 제거 · 수치 게이트 · 지각 QA.
|
|
130
|
+
🟥 **도해를 건드렸으면 렌더한다** — 연결선 잔해 · 도형 종류 이탈 · 빌드 프레임
|
|
131
|
+
텍스트 소실 · 부연-본문 밝기 경쟁 넷 다 렌더 없이는 구조적으로 안 보였다(§방법론 ⓒ).
|
|
132
|
+
🟥 **장을 뺄 때는 뒤 장의 전제인지 먼저 본다** — 지운 장이 다음 장 부제의
|
|
133
|
+
접속사(「그러나」 류)가 받는 말이었던 실사고가 있다(§방법론 ⓔ)
|
|
134
|
+
🔷 **도해는 타입 있는 JSON 에서 굽는다**(2026-09-05 신설, 5단계 안의 갈래이지 6단계가
|
|
135
|
+
아니다). `python3 diagram_from_json.py <spec.json> --out <png> --css <덱 토큰.css>
|
|
136
|
+
--no-legend` — 외부 렌더러 archify(MIT, 로컬 설치·소스 복사 0)의 showcase validate 를
|
|
137
|
+
**못 통과하면 굽기를 거부**하고, 통과하면 deliver → 뷰어 크롬 없는 SVG 단독 PNG(≥3840px)
|
|
138
|
+
→ 굽기 영수증. 덱 자기 규약(배경·강조색·폰트)은 archify CSS 변수 override 로 꽂는다 —
|
|
139
|
+
archify 색을 덱에 가져오는 게 아니라 **덱 규약을 렌더러에 꽂는다**(§방법론 ⓓ). 가이드 뷰
|
|
140
|
+
인터랙션은 장표에 없으므로 `--focus id,…` 빌드 프레임으로 번역한다(§B3 좌표 고정).
|
|
141
|
+
🟥 archify 는 글자 크기 토큰이 없어 **viewBox 폭이 글자 pt 를 정한다**(720 → 라벨 29pt·
|
|
142
|
+
부제 19pt / 1137 → 12pt) — 그래서 폭 상한이 굽기 거부 조건이다. 대응표·차이·취향 판정
|
|
143
|
+
항목: `tracks-meta/dispatch/2026-09-05_archify-preprep/DESIGN_TOKENS.md`(운영자 로컬)
|
|
127
144
|
─────────────────────────────────────────────────────────────────
|
|
128
145
|
바닥 `python3 preprep.py` — 아래 레인. 걸리면 ⑤ 로 되돌아간다
|
|
129
146
|
```
|
|
@@ -164,12 +181,28 @@ python3 "$SKILL/preprep.py"
|
|
|
164
181
|
| **L11 promise** | **앞에서 예고한 것이 뒤에서 상환되나** — 후보를 나열하고 «예고 N 회 vs 뒤에서 M 회»를 센다. 실사고: 「세 번」 예고에 상환 1건 | 🟥 advisory · **판정은 사람** |
|
|
165
182
|
| **L10 adjacent-dup** | **인접 장이 같은 문장을 다시 읽나** — 쪼갤 때 뒤 프레임에 원본을 통째로 남기면 앞 장에서 읽은 문장을 또 읽는다. 실측 117자 중 82자(70%) | 차단 (임계는 사람이 적는다. 미기재면 측정만) |
|
|
166
183
|
| **L9 progression** | **선언된 단계 중 하나가 화면에서 빠졌나** — 실사고: ①②③④ 중 ①이 화면에서 떨어져 리뷰어가 논지를 **정반대로** 읽었다. 개별 문장은 전부 옳았다 | 차단 (선언된 것만 본다) |
|
|
184
|
+
| **L12 diagram** | **타입 JSON 도해가 «지금 JSON» 에서 validate 를 거쳐 구워졌나** — `kind: diagram_source` 표면의 굽기 영수증(`*.receipt.json`)으로 JSON 지문 일치 · showcase validate ok · PNG 실제 폭(IHDR 직독) ≥ 3840 · viewBox 폭 ≤ 720 · 여백. 실사고 후보 셋: JSON 고치고 PNG 안 구움 · 미검증 JSON 손렌더 · 픽셀은 충분한데 글자 12pt | 차단 (선언된 표면만 · 영수증 없는 손그림은 **UNMEASURED**, 0 아님) |
|
|
167
185
|
| **G ooxml** | pptx 구조 9종 — 관계 참조 무결성 · 죽은 Content_Types Override · **화면 밖 도형** · 열린 도형 채움 · 선 굵기 토큰 래칫 | 차단 (`python3 ooxml/gate.py <풀린 pptx 트리>`) |
|
|
186
|
+
| **R1 orphan-connective** | 선두 줄이 «그러나/그래서»로 뒤집는데 그 문장의 주어가 **직전 장 어디에도 없다** | 🟥 advisory · **실물 known-positive**(실제 백업에서 뜬 결함) |
|
|
187
|
+
| **R2 enum-dropped** | 앞 장이 ①②③ 으로 센 것을, 이 장이 «이 셋 중…» 이라 부르며 **번호 없이** 재편한다 — 1:1 대응이 안 보인다 | 🟥 advisory · **실물 known-positive** |
|
|
188
|
+
| **R3 lead-term-unbacked** | 선두 줄이 **강조색으로 선언한 낱말**이 그 장 다른 어디에도 없다 — 화면이 자기 부제를 못 받친다 | 🟥 advisory · **합성 뮤턴트 known-pair**(자연 발생본이 파일로 안 남아 실물 2장을 줄여 만든 픽스처로만 계량됨 — R1·R2·P1·P3 과 증거 등급이 다르다) |
|
|
189
|
+
| **R4 screen-heavy** | 화면 자수가 **이 덱 자기 분포**(P90)의 상위권 — 한 장에 너무 많이 얹었다 | 🟥 advisory · **순위표**(게이트가 아니다 — 늘 자기 덱의 상위 약 10%가 뜬다, 「후보 0」이 목표가 아니다) |
|
|
190
|
+
| **R5 read-load** | **직전 장 대비 새로 뜬 글자** ÷ 발화 초 — 화면은 늘었는데 말은 짧다. 무대 지시([간지]/[진행])는 발화로 안 센다 | 🟥 advisory · **순위표**(R4 와 같은 성질) |
|
|
191
|
+
| **P1 build-jitter** | 같은 이름 도형이 **연속한 두 장**에서 조금 다른 자리에 있다 — 넘길 때 튄다 | 🟥 advisory · **실물 known-positive** — 🟥 대조된 도형-쌍이 0 건이면 「튐 없음」이 아니라 **UNMEASURED**(이름 중복 도형은 대조 못 해 통째로 뺀다) |
|
|
192
|
+
| **P3 adjacency** | 한 도형의 **오른쪽 끝**과 다른 도형의 **왼쪽 끝**이 맞닿으려다 어긋났다 | 🟥 advisory · **실물 known-positive** · 🟥 절대 목록으로 읽지 마라(기저 오탐 76건 실측) — `surfaces.yaml` 의 `geometry.baseline`(이전 판 pptx)을 주면 **편집 전/후 델타**로 돈다. **P2(near-miss, 같은 종류 모서리만 대조)는 미채택** — 구조적으로 실제 결함(다른 종류 모서리의 인접)을 못 잡아서다 |
|
|
168
193
|
|
|
169
194
|
🟥 **L8 이 advisory 인 이유**: 두 계기의 오탐을 **최저비용으로 무마하는 길이
|
|
170
195
|
「그 문장을 지우는 것」**이라, 종료코드에 태우면 원고를 나쁜 방향으로 미는 압력이 된다.
|
|
171
196
|
후보를 내고 판정은 사람이 한다. 오탐 다수가 정상이다.
|
|
172
197
|
|
|
198
|
+
🟥 **R1~R5·P1·P3 이 전부 advisory 인 이유**: L8·L11 과 같다 — 오탐의 최저비용 무마가 원고를
|
|
199
|
+
망가뜨리는 방향(문장 삭제·형식 왜곡)이라 종료코드에 태우지 않는다. `preprep.py` 배선도 findings 를
|
|
200
|
+
버리고 notes 로만 낸다(L8·L11 관례). `--lane R1,R2,R3,R4,R5,P1,P3`(또는 그룹 `R`·`P`)로 이 일곱만
|
|
201
|
+
골라 돌릴 수 있다 — 🟥 **L1~L11 은 아직 이 `--lane` 필터를 안 탄다**(늘 전부 돈다). 정본:
|
|
202
|
+
`tracks-meta/fh_signal_2026-09-04_preprep-evolution.md` §1·§2 · 코드: `lane_slide_relations.py` ·
|
|
203
|
+
`lane_geometry.py` · 픽스처: `fixtures/mk_slide_fixtures.py`(테스트 시 생성) +
|
|
204
|
+
`fixtures/fixture_R3_{positive,negative}.pptx`(실물 2장) · self-test: `test_preprep_lanes_rp.py`.
|
|
205
|
+
|
|
173
206
|
## 문체는 레인이 아니다 — 레퍼런스와 스킬이 한다
|
|
174
207
|
|
|
175
208
|
초판에는 `ko-tech-writer` Step 2 를 레인으로 복제한 **L9** 가 있었다. **뺐다.** 이유가 둘이다.
|
|
@@ -198,6 +231,68 @@ python3 "$SKILL/preprep.py"
|
|
|
198
231
|
- **생성면 ↔ 사람 편집면** — 재생성은 사람 손질을 전량 덮어쓴다. 실측: 발표자 노트 **20곳**이
|
|
199
232
|
소리 없이 사라졌고 빌드는 성공했으며 원고↔빌드 대조는 «원고대로»였다. L7 의 자리다.
|
|
200
233
|
|
|
234
|
+
## 방법론 — 실사용 하루치에서 역산 (2026-09-04)
|
|
235
|
+
|
|
236
|
+
121장 한국어 발표 덱 한 벌을 하루 동안 실사용하며 나온 판단 축이다. 코드가 된 것은 §레인의
|
|
237
|
+
신규 레인들이고, 아래는 코드만으로는 안 닫히는(또는 아직 코드가 아닌) 것들이다.
|
|
238
|
+
|
|
239
|
+
### ⓐ 장을 지워도 시간은 안 줄어든다
|
|
240
|
+
장 삭제만으로는 발화 시간이 0초 줄었다. 시간은 **대사**에 있고, 대사는 **화면 글자**에
|
|
241
|
+
묶여 있다(화면에 적혀 있으면 안 말할 수가 없다). 실제로 줄어든 것은 화면 자수를 깎았을 때뿐이다.
|
|
242
|
+
⇒ **분량 정리의 1순위 대상은 «장 수»가 아니라 «화면 글자»다.**
|
|
243
|
+
|
|
244
|
+
### ⓑ 전역 축은 한 장짜리 렌더로 판정하면 안 된다
|
|
245
|
+
색을 여러 면 바꾸고 한 장씩 렌더해서 「개선」이라 판정했는데, 전체를 이어서 넘겨 본 판정은
|
|
246
|
+
전달력 저하로 되돌렸다. 두 판정은 다른 것을 본다 — 한 장 안의 대비와, 여러 장을 흐르는 동안
|
|
247
|
+
「여기가 문제다」가 꽂히는 일관성은 같은 속성이 아니다. **후자는 한 장짜리 렌더에 구조적으로
|
|
248
|
+
안 나타난다.**
|
|
249
|
+
⇒ 색·타이포 같은 전역 축은 **연속 구간을 이어서** 보거나 사람이 넘겨 봐야 한다.
|
|
250
|
+
⇒ 반대 방향도 같이: 한 장에서 보인 것을 **전역으로 밀지도 마라** — 다른 장은 이미 다른
|
|
251
|
+
요소(예: 테두리 있는 카드)가 같은 문제를 막고 있을 수 있다.
|
|
252
|
+
|
|
253
|
+
### ⓒ 렌더 없이는 구조적으로 안 보이는 것들
|
|
254
|
+
같은 하루에 렌더 없이는 놓쳤던 것 넷 — 옛 도해의 연결선 잔해(도형만 지우고 선을 안 지움) ·
|
|
255
|
+
도형 종류가 덱 규약과 다름 · 빌드 프레임 전환에서 텍스트가 사라지는 자리 · 부연이 본문과
|
|
256
|
+
같은 밝기로 경쟁하는 자리. ⇒ **도해를 건드렸으면 렌더한다** — ⑤ 단계 규율.
|
|
257
|
+
|
|
258
|
+
### ⓓ 새 도형을 넣기 전에 «같은 역할의 기존 도형»을 세라
|
|
259
|
+
새로 넣을 도형이 덱 전체와 혼자 달랐던 사고는, 덱에 이미 그 역할의 규약이 백 회 넘게
|
|
260
|
+
있었기 때문이었다 — 색도 형태도. 희귀한 값(전체 중 여섯 번뿐)이라도 **그 여섯 번이 전부
|
|
261
|
+
같은 역할**이면 그게 답이다. ⇒ **덱 자기 규약을 먼저 추출**한다. 지어내는 것은 마지막 수단.
|
|
262
|
+
|
|
263
|
+
### ⓔ 장을 뺄 때 «그 장이 받는 말이었나»를 본다
|
|
264
|
+
한 장을 지우자 다음 장 부제의 「그러나」가 받을 말을 잃었다. 삭제 전 **뒤 장의 전제인지**
|
|
265
|
+
확인한다 — ⑤ 단계 규율.
|
|
266
|
+
|
|
267
|
+
### ⓕ 「알고도 남긴 것」과 「몰라서 남은 것」을 갈라 적어라
|
|
268
|
+
팔레트 밖 색을 정해서 남긴 자리가 있었다. 그렇게 적지 않으면 다음 감사자가 다시 «발견»한다
|
|
269
|
+
(실측: 지운 색이 다른 색번호로 되살아나 있었다). 체크리스트 항목: `presentation_checklist.md` C6.
|
|
270
|
+
|
|
271
|
+
### ⓖ 외부 가이드를 인용할 때 — «정의»는 «금지문»이 아니다
|
|
272
|
+
같은 레퍼런스를 두고 두 판정이 갈렸다: 폰트 크기 「위반」을 유보했는데 원문에 **숫자가 한
|
|
273
|
+
개도 없어서** 위반이 아닌 것으로 확정됐고, 색 「위반」은 스와치(정의)를 근거로 올렸다가
|
|
274
|
+
**철회**했다 — 색에 대한 유일한 **문장**은 「같은 위계에 같은 색으로 통일」이라 오히려 만족하고
|
|
275
|
+
있었다. ⇒ 레퍼런스에서 «위반»을 낼 때는 **원문 문장을 인용**하고, 스와치·템플릿 실측값은
|
|
276
|
+
«차이»로만 낸다 — ② 단계 규율.
|
|
277
|
+
|
|
278
|
+
## 정직한 한계 (2026-09-04 실측분, 인용 전에 읽어라)
|
|
279
|
+
|
|
280
|
+
§레인의 신규 텍스트 레인(orphan-connective · enum-dropped · lead-term-unbacked · screen-heavy ·
|
|
281
|
+
read-load)과 신규 기하 레인(build-jitter · adjacency)에 대한 캘리브레이션 잔여다. **이 절
|
|
282
|
+
없이 그 레인들의 수치를 인용하면 과계상이다.**
|
|
283
|
+
|
|
284
|
+
- **reps=1.** 텍스트·기하 신규 레인은 모두 known-positive 한 팔씩이다. 이 저장소 자기 바(≥3)
|
|
285
|
+
미달.
|
|
286
|
+
- **lead-term-unbacked 의 known-positive 는 합성 뮤턴트다**(자연 발생본이 파일로 안 남았다).
|
|
287
|
+
나머지는 실제 백업에서 떴으니 **증거 등급이 다르다 — 묶어 말하지 마라.**
|
|
288
|
+
- **오탐 0 은 n=1 코퍼스에서의 수치다.** 다른 덱에 옮기면 임계(선두 줄 판별 · 강조색 비교 ·
|
|
289
|
+
활용 어미 제외 · 자기분포 P90)를 다시 재야 한다.
|
|
290
|
+
- **enum-dropped 는 표에서 한 칸 어긋난 프레임에서 뜬다** — 같은 표의 이웃 프레임이라 부류는
|
|
291
|
+
맞지만 쪽수가 어긋난다.
|
|
292
|
+
- **adjacency 기저 오탐이 76건.** 절대 목록으로 쓰면 안 된다 — 편집 전/후 델타 용법이 전제다.
|
|
293
|
+
- **L8 오탐 진단(«저희»·«그대로»)은 이 한 덱에서 나왔다.** 다른 한국어 코퍼스에서 같은 비율인지는
|
|
294
|
+
미측정.
|
|
295
|
+
|
|
201
296
|
## Done When
|
|
202
297
|
|
|
203
298
|
| 조건 | check-class |
|
|
@@ -207,6 +302,7 @@ python3 "$SKILL/preprep.py"
|
|
|
207
302
|
| **레퍼런스를 물었다** — 참고할 실물이 있나를 묻고, 있으면 그것을 근거로 세웠다. 없으면 «없다»고 기록했다(안 물은 것과 구분) | mandatory-pass |
|
|
208
303
|
| 단일 소스 드리프트 앵커가 통과한다 (`scripts/test_preprep_drift_anchor.sh`) | measured |
|
|
209
304
|
| 발견 목록을 사람이 판정했고, advisory 후보 중 **남긴 것에는 남긴 이유**가 적혔다 | judged — 적대 짝: 그 판정을 저자가 아닌 사람이 다시 읽는다(격리 그라운딩). 자기 판정으로 대체 금지 |
|
|
305
|
+
| **도해를 건드렸으면 렌더 결과를 첨부했다**(§방법론 ⓒ) | judged — 적대 짝: 렌더 파일이 실제로 존재하는지(경로·확장자·이번 편집 이후의 mtime)는 별도로 measured 로 확인한다. 「첨부했다」는 자기신고만으론 인정하지 않는다 |
|
|
210
306
|
|
|
211
307
|
## 이 하네스가 못 하는 것
|
|
212
308
|
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""diagram_from_json.py — 타입 있는 JSON 한 장 → 검증된 다이어그램 PNG(장표용) + 굽기 영수증.
|
|
3
|
+
|
|
4
|
+
⑤ 제작·퇴고 단계의 «도해» 갈래. 도해를 영점부터 그리는 시행착오를 줄이는 자리다(SKILL.md §제작 흐름).
|
|
5
|
+
|
|
6
|
+
파이프라인 (한 번에, 중간 산출은 전부 남긴다):
|
|
7
|
+
JSON ──archify validate(showcase)──▶ 못 통과 = **굽기 거부(exit 1)**, PNG 를 안 만든다
|
|
8
|
+
──archify deliver───────────▶ <out>.html (뷰어 포함 정본 산출)
|
|
9
|
+
──SVG 단독 추출 + 토큰 CSS──▶ headless Chrome 스크린샷 <out>.png (뷰어 크롬 0)
|
|
10
|
+
──해상도 하한·여백·viewBox 폭 검사──▶ <out>.receipt.json (레인 L12 가 읽는 채널)
|
|
11
|
+
|
|
12
|
+
🟥 archify 소스는 복사하지 않는다 — 외부 렌더러(MIT)를 **쓴다**. 위치는 --archify 또는
|
|
13
|
+
$PREPREP_ARCHIFY_BIN, 없으면 cwd 기준 .claude/skills/archify/bin/archify.mjs.
|
|
14
|
+
🟥 해상도 하한: 장표 폭 26.67in(1920pt) 기준 2× = 3840px. 미달이면 굽기 거부.
|
|
15
|
+
🟥 viewBox 폭 상한(기본 720): archify 는 글자 크기 토큰이 없어 **viewBox 폭이 pt 를 정한다**
|
|
16
|
+
(pt = unit × 1728/viewBox폭). 720 이면 라벨 29pt·부제 19pt, 1137 이면 12pt 로 안 읽힌다
|
|
17
|
+
(tracks-meta/dispatch/2026-09-05_archify-preprep/DESIGN_TOKENS.md §3). 넘으면 거부 — --max-viewbox-w 로 조정.
|
|
18
|
+
🟥 여백(--pad): 그림 가장자리 비율. 0 이면 장표 가장자리와 선이 붙는다.
|
|
19
|
+
|
|
20
|
+
exit 0 굽음 · 1 거부(validate 실패 · 해상도 · viewBox · deliver 실패) · 10 계기 오류(node/archify/Chrome 부재)
|
|
21
|
+
usage: diagram_from_json.py <spec.json> --out <name.png> [--css tokens.css] [--theme dark|light]
|
|
22
|
+
[--focus id,id] [--no-legend] [--size 3840x2160] [--pad 0.03] [--min-px 3840] [--max-viewbox-w 720] [--archify BIN]
|
|
23
|
+
"""
|
|
24
|
+
import argparse, hashlib, json, os, re, shutil, struct, subprocess, sys, tempfile, zlib
|
|
25
|
+
|
|
26
|
+
def sha256_file(p):
|
|
27
|
+
h = hashlib.sha256()
|
|
28
|
+
with open(p, 'rb') as f: h.update(f.read())
|
|
29
|
+
return h.hexdigest()
|
|
30
|
+
|
|
31
|
+
def png_size(p):
|
|
32
|
+
with open(p, 'rb') as f: head = f.read(24)
|
|
33
|
+
if head[:8] != b'\x89PNG\r\n\x1a\n': raise ValueError('PNG 아님')
|
|
34
|
+
w, h = struct.unpack('>II', head[16:24]); return w, h
|
|
35
|
+
|
|
36
|
+
def run_json(cmd, env):
|
|
37
|
+
r = subprocess.run(cmd, capture_output=True, text=True, env=env)
|
|
38
|
+
try: return json.loads(r.stdout), r
|
|
39
|
+
except Exception: return None, r
|
|
40
|
+
|
|
41
|
+
def main():
|
|
42
|
+
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
43
|
+
ap.add_argument('spec'); ap.add_argument('--out', required=True, help='산출 PNG 경로(.png). HTML·영수증은 같은 이름으로 옆에')
|
|
44
|
+
ap.add_argument('--css', default=None, help='디자인 토큰 override CSS(archify CSS 변수 재정의)')
|
|
45
|
+
ap.add_argument('--theme', default='dark', choices=['dark', 'light'])
|
|
46
|
+
ap.add_argument('--focus', default=None, help='빌드 프레임: 켤 노드 id(콤마). 나머지는 흐림 — 좌표는 불변(§B3)')
|
|
47
|
+
ap.add_argument('--dim', type=float, default=0.22)
|
|
48
|
+
ap.add_argument('--opaque', action='store_true', help='배경을 칠한다(기본은 투명 — 장표 배경이 그대로 비친다. 색 관리 차이로 «패널»이 보이던 실측 결함의 처방)')
|
|
49
|
+
ap.add_argument('--strip-lane-prefix', action='store_true', help='레인 제목의 «01 / »·«EX / » 접두를 뺀다(archify 고정 어휘 — 장표에선 뜻이 없다). 글자만 짧아지고 기하는 불변')
|
|
50
|
+
ap.add_argument('--no-crop', action='store_true', help='내용 경계로 자르지 않는다(기본은 자른다 — archify 가 범례 행·채널 여백을 남겨 장표에서 그림이 작아진다)')
|
|
51
|
+
ap.add_argument('--no-legend', action='store_true', help='SVG 범례 행 숨김(영어 고정이라 D1 위반)')
|
|
52
|
+
ap.add_argument('--size', default='auto', help="'auto' = 폭 3840 · 높이는 SVG 비율대로(레터박스 0 — 장표에 넣을 때 글자 배율이 안 죽는다) 또는 WxH"); ap.add_argument('--pad', type=float, default=0.03)
|
|
53
|
+
ap.add_argument('--min-px', type=int, default=3840); ap.add_argument('--max-viewbox-w', type=int, default=720)
|
|
54
|
+
ap.add_argument('--archify', default=os.environ.get('PREPREP_ARCHIFY_BIN', '.claude/skills/archify/bin/archify.mjs'))
|
|
55
|
+
ap.add_argument('--chrome', default=os.environ.get('PREPREP_CHROME', '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'))
|
|
56
|
+
a = ap.parse_args()
|
|
57
|
+
|
|
58
|
+
if not os.path.exists(a.spec): print('HARNESS ERROR: spec 없음', a.spec); return 10
|
|
59
|
+
if not os.path.exists(a.archify): print('HARNESS ERROR: archify 없음 —', a.archify, '(npx -y skills add tt-a1i/archify --skill archify --agent claude-code --copy --yes)'); return 10
|
|
60
|
+
if shutil.which('node') is None: print('HARNESS ERROR: node 없음'); return 10
|
|
61
|
+
if not os.path.exists(a.chrome) and shutil.which(a.chrome) is None: print('HARNESS ERROR: Chrome 없음 —', a.chrome); return 10
|
|
62
|
+
env = dict(os.environ, ARCHIFY_UPDATE_CHECK_DISABLED='1') # 외부 통신 차단
|
|
63
|
+
spec = json.load(open(a.spec, encoding='utf-8'))
|
|
64
|
+
dtype = spec.get('diagram_type')
|
|
65
|
+
if dtype not in ('architecture', 'workflow', 'sequence', 'dataflow', 'lifecycle'):
|
|
66
|
+
print('REFUSE: diagram_type 없음/미지원:', dtype); return 1
|
|
67
|
+
out_png = os.path.abspath(a.out); stem = re.sub(r'\.png$', '', out_png)
|
|
68
|
+
out_html, out_receipt = stem + '.html', stem + '.receipt.json'
|
|
69
|
+
|
|
70
|
+
# ① validate — showcase 9 검사 전부. 못 통과하면 여기서 끝(PNG 안 만든다)
|
|
71
|
+
v, r = run_json(['node', a.archify, 'validate', dtype, a.spec, '--quality', 'showcase', '--json'], env)
|
|
72
|
+
if not v or not v.get('ok'):
|
|
73
|
+
print('REFUSE: archify validate 실패 — 굽지 않는다')
|
|
74
|
+
for d in (v or {}).get('diagnostics') or (((v or {}).get('composition') or {}).get('diagnostics') or []):
|
|
75
|
+
print(' ·', d.get('code'), d.get('message', '')[:300])
|
|
76
|
+
if not v: print(' ·', (r.stderr or r.stdout)[-600:])
|
|
77
|
+
return 1
|
|
78
|
+
comp = v.get('composition') or {}
|
|
79
|
+
if (comp.get('summary') or {}).get('errors', 0) or (comp.get('summary') or {}).get('warnings', 0):
|
|
80
|
+
print('REFUSE: showcase 구성 오류/경고 있음', comp.get('summary')); return 1
|
|
81
|
+
# ② deliver
|
|
82
|
+
d, r = run_json(['node', a.archify, 'deliver', dtype, a.spec, out_html, '--quality', 'showcase', '--json'], env)
|
|
83
|
+
if not d or not d.get('ok'):
|
|
84
|
+
print('REFUSE: deliver 실패', (r.stderr or r.stdout)[-600:]); return 1
|
|
85
|
+
html = open(out_html, encoding='utf-8').read()
|
|
86
|
+
svgs = re.findall(r'<svg[\s\S]*?</svg>', html)
|
|
87
|
+
if len(svgs) != 1: print('REFUSE: <svg> 블록이 1개가 아님:', len(svgs)); return 1
|
|
88
|
+
vb = re.search(r'viewBox="([^"]+)"', svgs[0]); vbw = float(vb.group(1).split()[2]) if vb else None
|
|
89
|
+
if vbw is None or vbw > a.max_viewbox_w:
|
|
90
|
+
print(f'REFUSE: viewBox 폭 {vbw} > 상한 {a.max_viewbox_w} — 글자가 {12*1728/(vbw or 1):.0f}pt 로 떨어진다. 노드/레인을 줄이거나 --max-viewbox-w 를 근거와 함께 올려라')
|
|
91
|
+
return 1
|
|
92
|
+
# ③ SVG 단독 PNG
|
|
93
|
+
styles = ''.join(re.findall(r'<style[^>]*>[\s\S]*?</style>', html))
|
|
94
|
+
svg_out = svgs[0]
|
|
95
|
+
if a.strip_lane_prefix:
|
|
96
|
+
# 레인 제목 <text …font-size="10"…>01 / 사람</text> — 접두만 지운다. 텍스트가 짧아질 뿐 어떤 좌표도 안 바뀐다.
|
|
97
|
+
svg_out = re.sub(r'(<text[^>]*font-size="10"[^>]*>)(?:\d{2}|EX) / ', r'\1', svg_out)
|
|
98
|
+
vbh = float(vb.group(1).split()[3])
|
|
99
|
+
if a.size.lower() == 'auto':
|
|
100
|
+
# 🟥 16:9 고정 캔버스에 1.9:1 도해를 넣으면 레터박스가 생겨 장표에서 그림이 작아진다(실측:
|
|
101
|
+
# 후보 1차가 라벨 18pt 로 떨어졌다). 캔버스를 SVG 비율에 맞춰 여백 0 으로 굽는다.
|
|
102
|
+
W = max(a.min_px, 3840); H = int(round(W * vbh / vbw))
|
|
103
|
+
else:
|
|
104
|
+
W, H = map(int, a.size.lower().split('x'))
|
|
105
|
+
focus_css = ''
|
|
106
|
+
if a.no_legend: focus_css += '[data-legend],[data-legend-kind]{display:none !important}\n'
|
|
107
|
+
if a.focus:
|
|
108
|
+
ids = [x.strip() for x in a.focus.split(',') if x.strip()]
|
|
109
|
+
keep_n = ','.join(f'g[data-node-id="{i}"]' for i in ids)
|
|
110
|
+
keep_e = ','.join(f'[data-edge-from="{x}"][data-edge-to="{y}"]' for x in ids for y in ids if x != y)
|
|
111
|
+
focus_css += f'g[data-node-id],[data-edge-from]{{opacity:{a.dim}}}\n{keep_n}{{opacity:1}}\n' + (f'{keep_e}{{opacity:1}}\n' if keep_e else '')
|
|
112
|
+
token_css = ('<style>' + open(a.css, encoding='utf-8').read() + '</style>') if a.css else ''
|
|
113
|
+
bg_css = '' if a.opaque else 'html,body{background:transparent !important}'
|
|
114
|
+
doc = (f'<!doctype html><html lang="ko" data-theme="{a.theme}" data-preset="classic"><head><meta charset="utf-8">{styles}'
|
|
115
|
+
f'<style>{focus_css}html,body{{margin:0;padding:0;width:{W}px;height:{H}px;overflow:hidden}}{bg_css}'
|
|
116
|
+
f'body{{display:flex;align-items:center;justify-content:center}}'
|
|
117
|
+
f'.stage{{width:{W*(1-2*a.pad):.0f}px;height:{H*(1-2*a.pad):.0f}px;display:flex;align-items:center;justify-content:center}}'
|
|
118
|
+
f'.stage>svg{{width:100%;height:100%;max-width:100%;max-height:100%}}</style>{token_css}</head>'
|
|
119
|
+
f'<body><div class="stage">{svg_out}</div></body></html>')
|
|
120
|
+
tmpd = tempfile.mkdtemp(prefix='preprep_diagram_'); src = os.path.join(tmpd, 'svg_only.html')
|
|
121
|
+
open(src, 'w', encoding='utf-8').write(doc)
|
|
122
|
+
subprocess.run([a.chrome, '--headless=new', '--disable-gpu', '--hide-scrollbars', '--no-first-run'] +
|
|
123
|
+
([] if a.opaque else ['--default-background-color=00000000']) +
|
|
124
|
+
[f'--window-size={W},{H}', f'--screenshot={out_png}', 'file://' + src], capture_output=True, text=True, timeout=180)
|
|
125
|
+
shutil.rmtree(tmpd, ignore_errors=True)
|
|
126
|
+
if not os.path.exists(out_png): print('HARNESS ERROR: Chrome 스크린샷 없음'); return 10
|
|
127
|
+
cropped = None
|
|
128
|
+
if not a.no_crop and not a.opaque:
|
|
129
|
+
try:
|
|
130
|
+
from PIL import Image
|
|
131
|
+
im = Image.open(out_png).convert('RGBA'); bb = im.getbbox() # 알파 0 이 아닌 픽셀의 경계
|
|
132
|
+
if bb:
|
|
133
|
+
m = int(round(a.pad * W)); bb2 = (max(0, bb[0]-m), max(0, bb[1]-m), min(W, bb[2]+m), min(H, bb[3]+m))
|
|
134
|
+
im.crop(bb2).save(out_png); cropped = bb2
|
|
135
|
+
except ImportError:
|
|
136
|
+
cropped = 'PIL 없음 — 자르지 못함(UNMEASURED)'
|
|
137
|
+
pw, ph = png_size(out_png)
|
|
138
|
+
if pw < a.min_px and not cropped:
|
|
139
|
+
os.remove(out_png); print(f'REFUSE: PNG 폭 {pw} < 하한 {a.min_px} — 지웠다'); return 1
|
|
140
|
+
if cropped and W < a.min_px:
|
|
141
|
+
os.remove(out_png); print(f'REFUSE: 캔버스 폭 {W} < 하한 {a.min_px} — 지웠다'); return 1
|
|
142
|
+
# ④ 영수증 — L12 가 읽는 채널. JSON 지문으로 «지금 JSON 의 그림인가»를 묶는다
|
|
143
|
+
receipt = {'schema': 'preprep.diagram.receipt/1', 'spec': os.path.abspath(a.spec), 'spec_sha256': sha256_file(a.spec),
|
|
144
|
+
'diagram_type': dtype, 'validate': {'ok': True, 'profile': 'showcase', 'checks': len(v.get('checks', [])),
|
|
145
|
+
'composition': comp.get('summary')}, 'deliver': {'artifact_sha256': (d.get('artifact') or {}).get('sha256')},
|
|
146
|
+
'html': out_html, 'png': out_png, 'png_size': [pw, ph], 'viewbox_w': vbw, 'viewbox_h': vbh, 'canvas': a.size, 'transparent': not a.opaque, 'crop': cropped, 'pad': a.pad, 'theme': a.theme,
|
|
147
|
+
'css': os.path.abspath(a.css) if a.css else None, 'focus': a.focus, 'legend_hidden': bool(a.no_legend), 'lane_prefix_stripped': bool(a.strip_lane_prefix),
|
|
148
|
+
'text_pt_estimate': {'label12': round(12*1728/vbw, 1), 'sublabel8': round(8*1728/vbw, 1)}}
|
|
149
|
+
json.dump(receipt, open(out_receipt, 'w', encoding='utf-8'), ensure_ascii=False, indent=2)
|
|
150
|
+
print(f'OK {out_png} {pw}x{ph} viewBox_w={vbw:.0f} (라벨≈{receipt["text_pt_estimate"]["label12"]}pt · 부제≈{receipt["text_pt_estimate"]["sublabel8"]}pt) 영수증 {out_receipt}')
|
|
151
|
+
return 0
|
|
152
|
+
|
|
153
|
+
if __name__ == '__main__':
|
|
154
|
+
sys.exit(main())
|