@chrono-meta/fh-gate 3.0.0 โ†’ 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/.claude/regression/probes_live.yaml +137 -0
  2. package/.claude/rules/.residency-patterns.defaults +7 -0
  3. package/.claude/rules/fh_4axis_gate.md +50 -1
  4. package/.claude-plugin/marketplace.json +8 -2
  5. package/AGENTS.md +27 -0
  6. package/CATALOG.md +17 -0
  7. package/CLAUDE.md +12 -2
  8. package/README.ja.md +51 -7
  9. package/README.ko.md +48 -7
  10. package/README.md +37 -5
  11. package/README.zh.md +45 -8
  12. package/docs/STANDARDS_ALIGNMENT.md +61 -0
  13. package/docs/USER_GUIDE.md +3 -0
  14. package/docs/USE_CASES.md +50 -0
  15. package/docs/model_tier_expectations.md +60 -0
  16. package/knowledge/shared/harness-core/fh_three_layer_canon.md +30 -10
  17. package/knowledge/shared/harness-core/field_verdict_crossfamily_gate.md +8 -0
  18. package/knowledge/shared/harness-core/harness_incubator_doctrine.md +9 -0
  19. package/knowledge/shared/harness-core/iso_ai_standards_crosswalk.md +139 -0
  20. package/knowledge/shared/harness-core/measurement-integrity-checklist.md +20 -0
  21. package/knowledge/shared/learnings/subagent_invocations_log.yaml +441 -4
  22. package/package.json +40 -2
  23. package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
  24. package/plugins/fh-commons/skills/preprep/README.md +4 -1
  25. package/plugins/fh-commons/skills/preprep/SKILL.md +99 -3
  26. package/plugins/fh-commons/skills/preprep/diagram_from_json.py +154 -0
  27. package/plugins/fh-commons/skills/preprep/fixtures/fixture_R3_negative.pptx +0 -0
  28. package/plugins/fh-commons/skills/preprep/fixtures/fixture_R3_positive.pptx +0 -0
  29. package/plugins/fh-commons/skills/preprep/fixtures/mk_slide_fixtures.py +179 -0
  30. package/plugins/fh-commons/skills/preprep/interslide_deps.py +98 -9
  31. package/plugins/fh-commons/skills/preprep/lane_adjacent_dup.py +4 -1
  32. package/plugins/fh-commons/skills/preprep/lane_diagram.py +95 -0
  33. package/plugins/fh-commons/skills/preprep/lane_geometry.py +181 -0
  34. package/plugins/fh-commons/skills/preprep/lane_promise.py +4 -1
  35. package/plugins/fh-commons/skills/preprep/lane_slide_refs.py +134 -0
  36. package/plugins/fh-commons/skills/preprep/lane_slide_relations.py +300 -0
  37. package/plugins/fh-commons/skills/preprep/preprep.py +129 -8
  38. package/plugins/fh-commons/skills/preprep/preprep_wire.py +210 -0
  39. package/plugins/fh-commons/skills/preprep/presentation_checklist.md +4 -0
  40. package/plugins/fh-commons/skills/preprep/surfaces.example.yaml +18 -0
  41. package/plugins/fh-commons/skills/preprep/test_preprep_lanes_rp.py +184 -0
  42. package/plugins/fh-meta/.claude-plugin/plugin.json +1 -1
  43. package/plugins/fh-meta/CHANGELOG.md +43 -1
  44. package/plugins/fh-meta/skills/auto-decorrelation/SKILL.md +87 -2
  45. package/plugins/fh-meta/skills/frontier-digest/SKILL.md +2 -2
  46. package/plugins/fh-meta/skills/frontier-digest/SKILL_detail.md +25 -5
  47. package/plugins/fh-meta/skills/hub-cc-pr-reviewer/SKILL.md +11 -0
  48. package/plugins/fh-meta/skills/hub-cc-pr-reviewer/SKILL_detail.md +3 -0
  49. package/plugins/fh-qp/.claude-plugin/plugin.json +22 -0
  50. package/plugins/fh-qp/README.md +71 -0
  51. package/plugins/fh-qp/fixtures/evidence_known_clean.txt +3 -0
  52. package/plugins/fh-qp/fixtures/evidence_known_dirty.txt +6 -0
  53. package/plugins/fh-qp/fixtures/reach_known_partial.tsv +4 -0
  54. package/plugins/fh-qp/fixtures/reach_known_reached.tsv +4 -0
  55. package/plugins/fh-qp/fixtures/reach_known_wall.tsv +3 -0
  56. package/plugins/fh-qp/fixtures/verdicts_known_bad_branch.tsv +2 -0
  57. package/plugins/fh-qp/fixtures/verdicts_known_bad_vacuous_machine.tsv +2 -0
  58. package/plugins/fh-qp/fixtures/verdicts_known_good.tsv +4 -0
  59. package/plugins/fh-qp/fixtures/verdicts_verify_only.tsv +3 -0
  60. package/plugins/fh-qp/qp_profile.example.yaml +29 -0
  61. package/plugins/fh-qp/scripts/qp_tools.sh +216 -0
  62. package/plugins/fh-qp/skills/qp/SKILL.md +65 -0
  63. package/plugins/fh-qp/skills/qp-plan/SKILL.md +42 -0
  64. package/plugins/fh-qp/skills/qp-regress/SKILL.md +45 -0
  65. package/plugins/fh-qp/skills/qp-run/SKILL.md +49 -0
  66. package/scripts/chamber_run.sh +14 -5
  67. package/scripts/com.forge-harness.live-eval.plist +84 -0
  68. package/scripts/compaction_probe.sh +9 -35
  69. package/scripts/directional_diff_gate.sh +14 -2
  70. package/scripts/frontier_digest_autopilot.sh +4 -1
  71. package/scripts/map_postprocess.py +90 -0
  72. package/scripts/outbound_query_guard.sh +131 -0
  73. package/scripts/outbound_query_hook.sh +373 -0
  74. package/scripts/package_coverage_check.sh +55 -16
  75. package/scripts/pipe_verdict_guard.sh +41 -1
  76. package/scripts/probe_live_eval.sh +240 -0
  77. package/scripts/probe_live_eval_lib.py +579 -0
  78. package/scripts/proposal_hook.sh +120 -17
  79. package/scripts/push_zone_check.sh +78 -0
  80. package/scripts/residency_closure_scan.py +252 -0
  81. package/scripts/selfcheck.sh +41 -1
  82. package/scripts/session_close_check.sh +100 -0
  83. package/scripts/sim_isolated_run.sh +98 -2
  84. package/scripts/test_action_yml_lanes.sh +97 -0
  85. package/scripts/test_fh_qp_lanes.sh +105 -0
  86. package/scripts/test_gate_two_verdicts_lanes.sh +139 -0
  87. package/scripts/test_map_postprocess_lanes.sh +143 -0
  88. package/scripts/test_marker_affected_lanes.sh +93 -0
  89. package/scripts/test_marker_crossfamily_lanes.sh +90 -6
  90. package/scripts/test_marker_oracle_lanes.sh +136 -0
  91. package/scripts/test_outbound_query_hook_lanes.sh +433 -0
  92. package/scripts/test_outbound_query_lanes.sh +87 -0
  93. package/scripts/test_pipe_verdict_guard_lanes.sh +26 -0
  94. package/scripts/test_preprep_diagram_lanes.sh +87 -0
  95. package/scripts/test_preprep_drift_anchor.sh +3 -3
  96. package/scripts/test_preprep_slide_refs_lanes.sh +169 -0
  97. package/scripts/test_probe_live_eval_lanes.sh +437 -0
  98. package/scripts/test_proposal_hook_lanes.sh +22 -1
  99. package/scripts/test_push_zone_lanes.sh +304 -0
  100. package/scripts/test_residency_closure_lanes.sh +70 -0
  101. package/scripts/test_sim_isolated_run_lanes.sh +119 -0
  102. package/scripts/test_utterance_intake_lanes.sh +414 -0
  103. package/scripts/test_worktree_reclaim_lanes.sh +70 -0
  104. package/scripts/transcript_utterances.py +222 -0
  105. package/scripts/utterance_intake.sh +424 -0
  106. package/scripts/validate_yaml.sh +27 -0
  107. package/scripts/worktree_reclaim.sh +95 -0
  108. package/templates/.git-hooks/pre-commit +353 -1
  109. package/templates/.git-hooks/pre-push +91 -0
  110. package/templates/RED_TEAM_REPORT.md +49 -0
  111. package/templates/settings.PreToolUse.snippet.json +65 -1
@@ -22,18 +22,46 @@ usage: interslide_deps.py <draft.md> <builder.py> [--all]
22
22
  import sys, re, ast, os
23
23
 
24
24
  # ์–ดํœ˜๋Š” **์‹ค๋ฌผ์—์„œ ๋ฝ‘์•˜๋‹ค**(pre-์ž๋ฆฝํ™” S9 ์›๋ฌธ). ์ง€์–ด๋‚ด์ง€ ์•Š์•˜๋‹ค.
25
- SCREEN_REUSE = ('๊ทธ๋Œ€๋กœ', '๊ฐ™์€ ํ™”๋ฉด', '์ด์–ด์„œ', 'ํ™”๋ฉด ์œ ์ง€', '์™ผ์ชฝ ๊ทธ๋Œ€๋กœ', '์˜ค๋ฅธ์ชฝ ๊ทธ๋Œ€๋กœ')
25
+ # ๐ŸŸฅ 2026-09-04 ์ˆ˜๋ฆฌ(์‹ ํ˜ธ ยง4) โ€” ๋ฐ”๊นฅ ์ž๋ฆฌ์— ์žˆ๋˜ **๋งจ๋ชธ ใ€Œ๊ทธ๋Œ€๋กœใ€๋ฅผ ๋บ๋‹ค**. 121์žฅ ์‹ค๋ฌผ ์ฝ”ํผ์Šค์—์„œ
26
+ # ใ€Œ๊ฒฝ๊ณ ๊ฐ€ ๋– ๋„ ๊ทธ๋Œ€๋กœ ์Šน์ธํ•œ๋‹คใ€ยทใ€Œ์ œ์ถœ๋ณธ ๊ทธ๋Œ€๋กœ ๋จธ์ง€ใ€์ฒ˜๋Ÿผ **ํ‰๋ฒ”ํ•œ ๋ถ€์‚ฌ**๋กœ ํ›จ์”ฌ ๋” ์ž์ฃผ ์“ฐ์—ฌ,
27
+ # L8 UNRESOLVED 9๊ฑด ์ค‘ ๋‹ค์ˆ˜๊ฐ€ ์ด ๋‚ฑ๋ง ํ•˜๋‚˜๊ฐ€ ๋งŒ๋“ค์—ˆ๋‹ค(์†๊ฒ€์ฆ โ†’ ์ „๋ถ€ ์˜คํƒ). ๋งจ๋ชธ์„ ๋นผ๊ณ 
28
+ # **ํ™”๋ฉด์— ๊ฒฐ๋ฐ•๋œ ํ‘œ๊ธฐ๋งŒ** ๋‚จ๊ธด๋‹ค โ€” ใ€Œ๊ทธ๋Œ€๋กœใ€ ์•ž์— ํ™”๋ฉด์„ ๊ฐ€๋ฆฌํ‚ค๋Š” ๋ง์ด ์—†์œผ๋ฉด ํ™”๋ฉด ์žฌ์‚ฌ์šฉ์ด
29
+ # ์•„๋‹ˆ๋ผ ๊ทธ๋ƒฅ ๋ถ€์‚ฌ๋‹ค. [[feedback_regex_bind_not_add.md]] ์™€ ๊ฐ™์€ ์›๋ฆฌ(๊ฒฐ๋ฐ•, ์–ดํœ˜ ์ถ”๊ฐ€๊ฐ€ ์•„๋‹ˆ๋ผ).
30
+ SCREEN_REUSE = ('๊ฐ™์€ ํ™”๋ฉด', '์ด์–ด์„œ', 'ํ™”๋ฉด ์œ ์ง€',
31
+ 'ํ™”๋ฉด ๊ทธ๋Œ€๋กœ', '์ด ํ™”๋ฉด ๊ทธ๋Œ€๋กœ', '์™ผ์ชฝ ๊ทธ๋Œ€๋กœ', '์˜ค๋ฅธ์ชฝ ๊ทธ๋Œ€๋กœ')
26
32
  DIRECTION = ('์˜ค๋ฅธ์ชฝ', '์™ผ์ชฝ', '์œ„์—์„œ', '์•„๋ž˜', '์˜†')
27
33
  VISUAL_VERB = ('๋ณด์…จ', '๋ณด์‹ ', '๋ณด์‹œ๋Š”', '๋ณด์‹œ๋ฉด', '๋ณด์˜€')
28
34
  RECALL = ('๋ฐฉ๊ธˆ', '์•„๊นŒ', '์กฐ๊ธˆ ์ „', '์•ž์„œ', '์•ž์—์„œ', '์ง€๊ธˆ๊นŒ์ง€')
29
35
  SOFT_ONLY = ('๋ง์”€๋“œ๋ฆฐ', '๋ง์”€๋“œ๋ ธ', '์„ค๋ช…๋“œ๋ฆฐ', '๊ทธ ์„ธ', '์•ž์˜')
30
36
 
31
- def units(src):
32
- """(sid, ํ—ค๋”ฉ, ๋ณธ๋ฌธ) ์ˆœ์„œ๋Œ€๋กœ. ๐Ÿšซ ์€ํ‡ด ํ‘œ๊ธฐ ์ ˆ์€ ยซ์ฃฝ์€ ๊ฒƒยป์œผ๋กœ ํ‘œ์‹œํ•ด ๊ฐ™์ด ๋‚ธ๋‹ค."""
37
+ _DEFAULT_UNIT_PATTERN = r'^### (S[0-9]+(?:-[a-z])?)\s*ยท'
38
+
39
+
40
+ def units(src, pattern=None):
41
+ """(sid, ํ—ค๋”ฉ, ๋ณธ๋ฌธ) ์ˆœ์„œ๋Œ€๋กœ. ๐Ÿšซ ์€ํ‡ด ํ‘œ๊ธฐ ์ ˆ์€ ยซ์ฃฝ์€ ๊ฒƒยป์œผ๋กœ ํ‘œ์‹œํ•ด ๊ฐ™์ด ๋‚ธ๋‹ค.
42
+
43
+ ๐ŸŸฅ 2026-09-04 ์ˆ˜๋ฆฌ(์‹ ํ˜ธ ยง3-1) โ€” ์ดˆํŒ์€ ์ ˆ ํ—ค๋”ฉ ์ •๊ทœ์‹์„ **์—ฌ๊ธฐ ํ•˜๋“œ์ฝ”๋”ฉ**ํ–ˆ๋‹ค. ๊ฐ™์€
44
+ `surfaces.yaml unit_pattern` ์„ `preprep.py manuscript_ids()` ๋Š” ์ด๋ฏธ ์ฝ๋Š”๋ฐ ์ด ํ•จ์ˆ˜๋Š”
45
+ ์•ˆ ์ฝ์–ด์„œ, ยซ์ ˆ์„ ๋ฌด์—‡์œผ๋กœ ๋ณด๋‚˜ยป์— ๋Œ€ํ•œ ๊ด€๋Œ€ํ•จ์ด **๋‘ ๊ณณ์—์„œ ๋”ฐ๋กœ ๊ฐˆ๋ ธ๋‹ค**
46
+ ([[feedback_divergent_leniency_duplicate_normalizers]]). `pattern` ์ธ์ž๋กœ ๋ฐ›๋Š”๋‹ค โ€”
47
+ ๋ฏธ์ง€์ •์ด๋ฉด ์ด ์ฝ”ํผ์Šค์˜ ๊ธฐ์กด ํŒจํ„ด์œผ๋กœ ํ•˜์œ„ํ˜ธํ™˜.
48
+ ๐ŸŸฅ ์ •๊ทœ์‹์„ ๋‹ค์‹œ ์ง“์ง€ ์•Š๋Š”๋‹ค โ€” `manuscript_ids()`(preprep.py)์™€ **๊ฐ™์€ head/slice ๊ธฐ๋ฒ•**
49
+ (ํ—ค๋”ฉ ์œ„์น˜๋ฅผ ์ „๋ถ€ ์ฐพ๊ณ  ๋‹ค์Œ ํ—ค๋”ฉ ์‹œ์ž‘ ์ „๊นŒ์ง€๋ฅผ ๋ณธ๋ฌธ์œผ๋กœ ์Šฌ๋ผ์ด์Šค)์„ ์“ด๋‹ค. lookahead ๋กœ
50
+ "๋‹ค์Œ ํ—ค๋”ฉ" ์„ ๋‹ค์‹œ ์ •์˜ํ•˜๋ฉด ๊ทธ ์ž์ฒด๊ฐ€ ์„ธ ๋ฒˆ์งธ ๊ฐˆ๋ž˜๊ฐ€ ๋œ๋‹ค.
51
+ `pattern`: ์ ˆ ํ—ค๋”ฉ์„ ์—ฌ๋Š” ์ •๊ทœ์‹, **์บก์ฒ˜ ๊ทธ๋ฃน ํ•˜๋‚˜**(์ ˆ id)๋งŒ ๊ฐ€์ง„๋‹ค
52
+ (์˜ˆ: r'^###\\s+(S[0-9]+(?:-[a-z])?)\\s*ยท').
53
+ """
54
+ pat = pattern or _DEFAULT_UNIT_PATTERN
33
55
  out = []
34
- for m in re.finditer(r'^### (S[0-9]+(?:-[a-z])?)\s*ยท([^\n]*)\n(.*?)(?=^### |\Z)', src, re.M | re.S):
35
- retired = any(t in m.group(2) for t in ('๐Ÿšซ', '์ œ๊ฑฐ๋จ', 'ํ๊ธฐ'))
36
- out.append((m.group(1), m.group(2).strip(), m.group(3), retired))
56
+ heads = list(re.finditer(pat + r'([^\n]*)', src, re.M))
57
+ for i, m in enumerate(heads):
58
+ end = heads[i + 1].start() if i + 1 < len(heads) else len(src)
59
+ head_line = m.group(2)
60
+ body = src[m.end():end]
61
+ if body.startswith('\n'):
62
+ body = body[1:] # ํ—ค๋”ฉ ์ค„ ์ž์‹ ์˜ ๊ฐœํ–‰ โ€” ์˜› ๊ตฌํ˜„์€ ๋ณธ๋ฌธ์— ์•ˆ ๋‹ด์•˜๋‹ค(๋™์น˜ ์œ ์ง€)
63
+ retired = any(t in head_line for t in ('๐Ÿšซ', '์ œ๊ฑฐ๋จ', 'ํ๊ธฐ'))
64
+ out.append((m.group(1), head_line.strip(), body, retired))
37
65
  return out
38
66
 
39
67
  # ๐ŸŸฅ ๋Ÿฐ #14 ์—์„œ ์ด๋ฏธ ํ•œ ๋ฒˆ ๊ฒช์€ ๊ฒฐํ•จ์ด ์—ฌ๊ธฐ์„œ ์žฌํ˜„๋๋‹ค: ์›๊ณ ์˜ **์ฃผ์„ ๋ธ”๋ก**๋„ `>` ๋ผ์„œ
@@ -107,7 +135,12 @@ def referents(line):
107
135
  t = re.sub(DEICTIC_PREFIX, '', m.group(1).strip(' .,ยทใ€Œใ€ยซยป')).strip()
108
136
  # ์ง€์‹œ์–ด๋งŒ ๋ฒ—๊ธฐ๋ฉด ๋‚จ๋Š” ๋ง์ด ๋„ˆ๋ฌด ์งง๊ฑฐ๋‚˜ ํ”ํ•˜๋ฉด ๋Œ€์ƒ์ด ๋ชป ๋œ๋‹ค
109
137
  if len(t) >= 2: cands.append(t)
110
- for m in re.finditer(r'[๊ทธ์ €]\s*([๊ฐ€-ํžฃA-Za-z][๊ฐ€-ํžฃA-Za-z0-9 ]{1,12})', line):
138
+ # ๐ŸŸฅ 2026-09-04 ์ˆ˜๋ฆฌ(์‹ ํ˜ธ ยง4) โ€” ์ดˆํŒ์€ `[๊ทธ์ €]` ๋กœ **ใ€Œ์ €ใ€๋„ ์ง€์‹œ์–ด**๋กœ ์žก์•˜๋‹ค. ํ•œ๊ตญ์–ด
139
+ # ์กด๋Œ“๋ง 1์ธ์นญ ใ€Œ์ €ํฌใ€๊ฐ€ ใ€Œ์ € + ํฌโ€ฆใ€๋กœ ๋ฌผ๋ ค ๋Œ€์ƒ์–ด๊ฐ€ ยซํฌ ํ•˜๋„ค์Šค์˜ ํ™œ์šฉ ๊ตฌ์กฐ์ž…ยป ๊ฐ™์€
140
+ # ํŒŒํŽธ์ด ๋๋‹ค(์‹ค์ธก: S25). ใ€Œ์ €ใ€๋Š” ์ง€์‹œ์–ด(์ €๊ฒƒ ยท ์ € ์‚ฌ๋žŒ)์™€ 1์ธ์นญ ๊ฒธ์–‘์–ด(์ € ยท ์ €ํฌ)๋ฅผ
141
+ # **๊ฐ™์€ ๊ธ€์ž๋กœ ๊ฒธํ•˜๋Š” ๋‹ค์˜์–ด**๋ผ ์ด ์ •๊ทœ์‹ ์ž๋ฆฌ์—์„œ ์›๋ฆฌ์ ์œผ๋กœ ๊ฐˆ๋ฆฌ์ง€ ์•Š๋Š”๋‹ค โ€” ์ง€์‹œ์–ด
142
+ # ์ชฝ๋งŒ ์›ํ•˜๋ฉด ใ€Œ๊ทธใ€๋งŒ ๋‚จ๊ธฐ๊ณ  ใ€Œ์ €ใ€๋Š” ๋บ€๋‹ค(๋†“์น˜๋Š” ์ง€์‹œ์–ด๋ณด๋‹ค ์กด๋Œ“๋ง ์˜คํƒ์ด ๋” ํ”ํ•˜๋‹ค).
143
+ for m in re.finditer(r'๊ทธ\s*([๊ฐ€-ํžฃA-Za-z][๊ฐ€-ํžฃA-Za-z0-9 ]{1,12})', line):
111
144
  cands.append(m.group(1).strip())
112
145
  return [c for c in dict.fromkeys(cands) if c not in COMMON and len(c) >= 2]
113
146
 
@@ -141,15 +174,16 @@ def wired_ids(builder):
141
174
  return {v[0] for v in ast.literal_eval(src[start:i+1]).values() if v[0]}
142
175
  raise RuntimeError('NOTE_MAP ๊ด„ํ˜ธ ๋ถˆ๊ท ํ˜•')
143
176
 
144
- def analyze(draft, builder, show_all=False):
177
+ def analyze(draft, builder, show_all=False, pattern=None):
145
178
  """๊ณ„์‚ฐ๋ถ€ โ€” ์ถœ๋ ฅํ•˜์ง€ ์•Š๋Š”๋‹ค. ๋ ˆ์ธ ๋ฐฐ์„ ์ด ์ด๊ฑธ ๋ถ€๋ฅธ๋‹ค(main ์€ ์ด๊ฑธ ยซ์ฐ๊ธฐ๋งŒยป ํ•œ๋‹ค).
146
179
 
147
180
  ๐ŸŸฅ ๋กœ์ง์„ ๋ณต์ œํ•˜์ง€ ์•Š์œผ๋ ค๊ณ  ๋บ€ ๊ฒƒ์ด๋‹ค. CLI ์™€ ๋ ˆ์ธ์ด ๊ฐ์ž ๊ณ„์‚ฐํ•˜๋ฉด
148
181
  ๊ด€๋Œ€ํ•จ์ด ๊ฐˆ๋ ค์„œ ํ•œ์ชฝ๋งŒ ํ†ต๊ณผํ•˜๋Š” ์ž…๋ ฅ์ด ์กฐ์šฉํžˆ ์ƒ๊ธด๋‹ค.
182
+ `pattern`: ์›๊ณ  ํ‘œ๋ฉด์˜ `unit_pattern` ์„ ์–ธ(์žˆ์œผ๋ฉด) โ€” units() ๋กœ ๊ทธ๋Œ€๋กœ ์ „๋‹ฌํ•œ๋‹ค.
149
183
  ๋ฐ˜ํ™˜: dict(error, n_units, n_retired, n_wired, edges, broken, unresolved)
150
184
  """
151
185
  try:
152
- us = units(open(draft, encoding='utf-8').read())
186
+ us = units(open(draft, encoding='utf-8').read(), pattern=pattern)
153
187
  wired = wired_ids(builder)
154
188
  except Exception as e:
155
189
  return {'error': f'{e}', 'edges': [], 'broken': [], 'unresolved': []}
@@ -188,7 +222,62 @@ def analyze(draft, builder, show_all=False):
188
222
  'n_wired': len(wired), 'edges': edges, 'broken': broken, 'unresolved': unresolved}
189
223
 
190
224
 
225
+ def selftest():
226
+ """known-pair โ€” ์‹ ํ˜ธ ยง3-1ยทยง4 ์ˆ˜๋ฆฌ๊ฐ€ ์‹ค์ œ๋กœ ์ง์„ ๊ฐ€๋ฅด๋Š”์ง€. ์•ˆ ๊ฐ€๋ฅด๋ฉด ์žฅ์‹์ด๋‹ค."""
227
+ ok = True
228
+
229
+ # โ”€โ”€ L8 ยง4-a: ใ€Œ์ €ใ€๋ฅผ ์ง€์‹œ์–ด ์ •๊ทœ์‹์—์„œ ๋บ๋‹ค โ€” ์กด๋Œ“๋ง ใ€Œ์ €ํฌใ€์˜คํƒ ์žฌ๋ฐœ ๋ฐฉ์ง€ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
230
+ cases_ref = [
231
+ ('known-positive(์กด๋Œ“๋ง ใ€Œ์ €ํฌใ€ โ€” ์ง€์‹œ ๋Œ€์ƒ์ด๋ฉด ์•ˆ ๋œ๋‹ค)',
232
+ '์ €ํฌ ํ•˜๋„ค์Šค์˜ ํ™œ์šฉ ๊ตฌ์กฐ์ž…๋‹ˆ๋‹ค . ๋ณด์‹œ๋Š” ๊ฒƒ์ฒ˜๋Ÿผ ์„ธ ์ธต์ž…๋‹ˆ๋‹ค .', False),
233
+ ('known-negative(์ง„์งœ ์ง€์‹œ์–ด ใ€Œ๊ทธใ€ โ€” ์—ฌ์ „ํžˆ ์žกํ˜€์•ผ ํ•œ๋‹ค)',
234
+ '๊ทธ ํ•˜๋„ค์Šค๋Š” ์ด๋ ‡๊ฒŒ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค .', True),
235
+ ]
236
+ for name, line, want_hit in cases_ref:
237
+ got = referents(line)
238
+ hit = bool(got) == want_hit
239
+ ok &= hit
240
+ print(f' {"PASS" if hit else "FAIL"} referents {name}: {got}')
241
+
242
+ # โ”€โ”€ L8 ยง4-b: SCREEN_REUSE ๋งจ๋ชธ ใ€Œ๊ทธ๋Œ€๋กœใ€๋ฅผ ๋บ๋‹ค โ€” ํ™”๋ฉด ๊ฒฐ๋ฐ• ํ‘œ๊ธฐ๋งŒ ๋‚จ๋Š”๋‹ค โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
243
+ cases_scr = [
244
+ ('known-positive(ํ‰๋ฒ”ํ•œ ๋ถ€์‚ฌ โ€” ํ™”๋ฉด ์žฌ์‚ฌ์šฉ ์•„๋‹ˆ๋‹ค)', '๊ฒฝ๊ณ ๊ฐ€ ๋– ๋„ ๊ทธ๋Œ€๋กœ ์Šน์ธํ•œ๋‹ค', False),
245
+ ('known-positive(๋‹ค๋ฅธ ๋ฌธ๋งฅ์˜ ๋ถ€์‚ฌ)', '์ œ์ถœ๋ณธ ๊ทธ๋Œ€๋กœ ๋จธ์ง€', False),
246
+ ('known-negative(ํ™”๋ฉด ๊ฒฐ๋ฐ• โ€” ์—ฌ์ „ํžˆ ์žกํ˜€์•ผ ํ•œ๋‹ค)', '์™ผ์ชฝ ๊ทธ๋Œ€๋กœ', True),
247
+ ('known-negative(ํ™”๋ฉด ๊ฒฐ๋ฐ• ๋ณ€ํ˜•)', '์ด ํ™”๋ฉด ๊ทธ๋Œ€๋กœ ๋‘๋ฉด ๋ฉ๋‹ˆ๋‹ค', True),
248
+ ]
249
+ for name, line, want_hit in cases_scr:
250
+ got = any(c in line for c in SCREEN_REUSE)
251
+ hit = got == want_hit
252
+ ok &= hit
253
+ print(f' {"PASS" if hit else "FAIL"} SCREEN_REUSE {name}: hit={got}')
254
+
255
+ # โ”€โ”€ ยง3-1: units() ๊ฐ€ pattern ์ธ์ž๋ฅผ ๋ฐ›๊ณ , ๋ฏธ์ง€์ • ์‹œ ํ•˜์œ„ํ˜ธํ™˜ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
256
+ src_default = '### S1 ยท ์ œ๋ชฉ\n๐Ÿ—ฃ\n> ๋ณธ๋ฌธ\n\n### S2 ยท ๋‘˜์งธ\n๐Ÿ—ฃ\n> ๋ณธ๋ฌธ2\n'
257
+ default_ids = [u[0] for u in units(src_default)]
258
+ ok3 = default_ids == ['S1', 'S2']
259
+ ok &= ok3
260
+ print(f' {"PASS" if ok3 else "FAIL"} units() ๊ธฐ๋ณธ ํŒจํ„ด ํ•˜์œ„ํ˜ธํ™˜: {default_ids}')
261
+
262
+ src_custom = '### U1 ยท ์ œ๋ชฉ\n๐Ÿ—ฃ\n> ๋ณธ๋ฌธ\n\n### U2 ยท ๋‘˜์งธ\n๐Ÿ—ฃ\n> ๋ณธ๋ฌธ2\n'
263
+ custom_ids = [u[0] for u in units(src_custom, pattern=r'^###\s+(U[0-9]+)\s*ยท')]
264
+ ok4 = custom_ids == ['U1', 'U2']
265
+ ok &= ok4
266
+ print(f' {"PASS" if ok4 else "FAIL"} units() ์ปค์Šคํ…€ unit_pattern ์ˆ˜์šฉ: {custom_ids}')
267
+ # ๊ธฐ๋ณธ ํŒจํ„ด์œผ๋กœ๋Š” U1/U2 ์ฝ”ํผ์Šค๋ฅผ ๋ชป ์ฝ์–ด์•ผ ํ•œ๋‹ค โ€” ยซํ•ญ์ƒ ํ†ต๊ณผยปํ•˜๋Š” ๊ณ„๊ธฐ๊ฐ€ ์•„๋‹˜์„ ํ™•์ธ
268
+ default_on_custom = units(src_custom)
269
+ ok5 = default_on_custom == []
270
+ ok &= ok5
271
+ print(f' {"PASS" if ok5 else "FAIL"} ๊ธฐ๋ณธ ํŒจํ„ด์€ U-์ ‘๋‘๋ฅผ ๋ชป ์ฝ๋Š”๋‹ค(๊ณ„๊ธฐ๊ฐ€ ์ง„์งœ pattern ์„ ์”€): '
272
+ f'{len(default_on_custom)}๊ฑด')
273
+
274
+ print('interslide_deps SELFTEST:', 'PASS' if ok else 'FAIL โ€” ์ด ๊ณ„๊ธฐ์˜ ์ถœ๋ ฅ์„ ์“ฐ์ง€ ๋งˆ๋ผ')
275
+ return 0 if ok else 1
276
+
277
+
191
278
  def main():
279
+ if '--self-test' in sys.argv:
280
+ return selftest()
192
281
  if len(sys.argv) < 3: print(__doc__); return 2
193
282
  draft, builder = sys.argv[1], sys.argv[2]
194
283
  show_all = '--all' in sys.argv
@@ -47,7 +47,10 @@ def scan(cfg, texts, surf_meta, mod):
47
47
  findings, notes = [], []
48
48
  for sid in sorted(spoken):
49
49
  text = texts[sid][0]
50
- us = [u for u in mod.units(text) if not u[3]] # ์€ํ‡ด ์ ˆ ์ œ์™ธ
50
+ # ๐ŸŸฅ 2026-09-04 โ€” units() ๊ฐ€ pattern ์„ ๋ฐ›๋Š”๋‹ค. ์ด ํ‘œ๋ฉด์˜ unit_pattern ์„ ์–ธ์„ ๋„˜๊ธด๋‹ค
51
+ # (interslide_deps ์‹ ํ˜ธ ยง3-1 ๊ณผ ๊ฐ™์€ ์ˆ˜๋ฆฌ โ€” ๋‘ ๊ณณ์ด ๊ฐ์ž ๊ด€๋Œ€ํ•จ์„ ๊ฐ–์ง€ ์•Š๊ฒŒ ํ•œ๋‹ค).
52
+ _pat = surf_meta.get(sid, {}).get('unit_pattern')
53
+ us = [u for u in mod.units(text, pattern=_pat) if not u[3]] # ์€ํ‡ด ์ ˆ ์ œ์™ธ
51
54
  if len(us) < 2:
52
55
  notes.append(f'L10 adjacent-dup : {sid} โ€” ์ ˆ {len(us)}๊ฐœ, ์ธ์ ‘ ์Œ ์—†์Œ (0 ์•„๋‹˜)')
53
56
  continue
@@ -0,0 +1,95 @@
1
+ """L12 diagram โ€” **๋„ํ•ด๊ฐ€ ยซ์ง€๊ธˆ JSONยป ์—์„œ ๊ฒ€์ฆ์„ ๊ฑฐ์ณ ๊ตฌ์›Œ์กŒ๋‚˜.**
2
+
3
+ ## ์กด์žฌ ๊ทผ๊ฑฐ (2026-09-05, archify ร— preprep ๊ฒฐํ•ฉ ์‹ค์ธก)
4
+
5
+ ๋„ํ•ด๋ฅผ ํƒ€์ž… ์žˆ๋Š” JSON โ†’ ์™ธ๋ถ€ ๋ Œ๋”๋Ÿฌ(archify) โ†’ PNG โ†’ pptx ๋กœ ๋„ฃ๋Š” ๊ฒฝ๋กœ๊ฐ€ ๋šซ๋ ธ๋‹ค. ๊ทธ ๊ฒฝ๋กœ์˜ ์‚ฌ๊ฐ์€
6
+ ์…‹์ด๊ณ , ์…‹ ๋‹ค ยซPNG ๊ฐ€ ์žˆ๋‹คยป๋กœ๋Š” ์•ˆ ๋ณด์ธ๋‹ค:
7
+ โ‘  JSON ์„ ๊ณ ์ณค๋Š”๋ฐ PNG ๋Š” ์˜› ๊ฒƒ(์žฌ์ƒ์„ฑ ์•ˆ ํ•จ) โ€” ๋นŒ๋“œ๋Š” ์„ฑ๊ณต, ๊ทธ๋ฆผ์€ ๊ฑฐ์ง“
8
+ โ‘ก validate ๋ฅผ ๋ชป ํ†ต๊ณผํ•œ JSON ์„ ์†์œผ๋กœ ๋ Œ๋”ํ•ด ๋„ฃ์Œ โ€” ๊ฒน์นจยท๋ผ๋ฒจ ๋งˆ์Šคํ‚น์ด ํ™”๋ฉด์—
9
+ โ‘ข ํ”ฝ์…€์€ ์ถฉ๋ถ„ํ•œ๋ฐ viewBox ๊ฐ€ ๋„“์–ด ๊ธ€์ž๊ฐ€ 12pt ๋กœ ๋–จ์–ด์ง โ€” ยซํ•ด์ƒ๋„ OKยป ์ธ๋ฐ ๋ชป ์ฝ๋Š”๋‹ค
10
+
11
+ ## ๋ฌด์—‡์„ ์žฌ๊ณ  ๋ฌด์—‡์„ ์•ˆ ์žฌ๋‚˜
12
+
13
+ - ์žฐ๋‹ค โ€” `kind: diagram_source` ํ‘œ๋ฉด๋งˆ๋‹ค `render:` PNG ์˜†์˜ **๊ตฝ๊ธฐ ์˜์ˆ˜์ฆ**(`*.receipt.json`,
14
+ `diagram_from_json.py` ๊ฐ€ ์“ด๋‹ค)์„ ์ฝ์–ด: ์˜์ˆ˜์ฆ ์กด์žฌ ยท JSON sha ์ผ์น˜ ยท validate ok(showcase) ยท
15
+ PNG ์‹ค์ œ ํญ(IHDR ์„ ์ง์ ‘ ์ฝ๋Š”๋‹ค โ€” ์˜์ˆ˜์ฆ ์ž๊ธฐ์‹ ๊ณ ๊ฐ€ ์•„๋‹ˆ๋‹ค) โ‰ฅ min_px ยท viewBox ํญ โ‰ค max ยท
16
+ pad โ‰ฅ min.
17
+ - ์•ˆ ์žฐ๋‹ค โ€” ๊ทธ๋ฆผ์ด ยซ์ข‹์€๊ฐ€ยป. ๊ทธ๊ฑด ยง๋ฐฉ๋ฒ•๋ก  โ“’ ยซ๋„ํ•ด๋ฅผ ๊ฑด๋“œ๋ ธ์œผ๋ฉด ๋ Œ๋”ํ•œ๋‹คยป ์˜ ์‚ฌ๋žŒ ๋ชซ์ด๋‹ค.
18
+ - ๐ŸŸฅ ์˜์ˆ˜์ฆ์ด ์—†๋Š” PNG(์†์œผ๋กœ ๋„ฃ์€ ๊ทธ๋ฆผ)๋Š” **UNMEASURED ๋กœ ๋‚ธ๋‹ค** โ€” 0 ์ด ์•„๋‹ˆ๊ณ , ํ†ต๊ณผ๋„ ์•„๋‹ˆ๋‹ค.
19
+ ์ด ๋ ˆ์ธ์€ ยซ์ƒ์„ฑ๊ธฐ๋ฅผ ๊ฑฐ์นœ ๊ทธ๋ฆผยป๋งŒ ๋ณธ๋‹ค. ์‚ฌ๊ฐ์€ ์ข์•„์ง€์ง€ ์ข์•„์ง€์ง€ ๋‹ซํžˆ์ง€ ์•Š๋Š”๋‹ค(SKILL.md ยง๊ฐˆ๋ผ ์ ๋Š” ๊ฒƒ).
20
+
21
+ ## ์ฐจ๋‹จ์ธ ์ด์œ 
22
+ ์„ ์–ธ๋œ ํ‘œ๋ฉด์— ํ•œํ•ด ยซ์ง€๋ฌธ์ด ๋‹ค๋ฅด๋‹ค / validate ๊ฐ€ ์•„๋‹ˆ๋‹ค / ํญ์ด ๋ชจ์ž๋ž€๋‹คยป ๋Š” ์• ๋งคํ•˜์ง€ ์•Š์€ ์‚ฌ์‹ค์ด๋‹ค.
23
+ ์˜คํƒ์˜ ์ตœ์ €๋น„์šฉ ๋ฌด๋งˆ๋Š” ยซ๋‹ค์‹œ ๊ตฝ๋Š” ๊ฒƒยป์ด๋ผ ์›๊ณ ๋ฅผ ๋ง๊ฐ€๋œจ๋ฆฌ๋Š” ๋ฐฉํ–ฅ์ด ์•„๋‹ˆ๋‹ค.
24
+ """
25
+ import hashlib, json, os, struct
26
+
27
+
28
+ def _sha(p):
29
+ h = hashlib.sha256(); h.update(open(p, 'rb').read()); return h.hexdigest()
30
+
31
+
32
+ def _png_w(p):
33
+ with open(p, 'rb') as f: head = f.read(24)
34
+ if head[:8] != b'\x89PNG\r\n\x1a\n': raise ValueError('PNG ์•„๋‹˜')
35
+ return struct.unpack('>II', head[16:24])[0]
36
+
37
+
38
+ def _resolve(root, p):
39
+ return os.path.normpath(os.path.join(root, os.path.expanduser(p)))
40
+
41
+
42
+ def scan(cfg, root):
43
+ """(findings, notes). findings ํŠœํ”Œ = (sid, kind, ref, lit, why) โ€” preprep.py ์˜ USE ๊ณ„์—ด."""
44
+ surfs = [s for s in cfg.get('surfaces', []) if s.get('kind') == 'diagram_source']
45
+ if not surfs:
46
+ return [], ['L12 diagram : diagram_source ํ‘œ๋ฉด ์—†์Œ โ€” NOT_CONFIGURED (0 ์•„๋‹˜)']
47
+ spec = cfg.get('diagram') or {}
48
+ min_px = int(spec.get('min_px', 3840)); max_vb = float(spec.get('max_viewbox_w', 720)); min_pad = float(spec.get('min_pad', 0.02))
49
+ findings, notes = [], []
50
+ n_ok = n_unm = 0
51
+ for s in surfs:
52
+ sid = s['id']; jp = _resolve(root, s['path'])
53
+ if not os.path.exists(jp):
54
+ notes.append(f'L12 diagram : {sid} JSON ์—†์Œ({jp}) โ€” UNMEASURED (0 ์•„๋‹˜)'); n_unm += 1; continue
55
+ rp_png = s.get('render')
56
+ if not rp_png:
57
+ notes.append(f'L12 diagram : {sid} render(PNG) ๋ฏธ์„ ์–ธ โ€” UNMEASURED (0 ์•„๋‹˜)'); n_unm += 1; continue
58
+ png = _resolve(root, rp_png); rec = png[:-4] + '.receipt.json' if png.endswith('.png') else png + '.receipt.json'
59
+ if not os.path.exists(png):
60
+ findings.append((sid, 'diagram', 'render', os.path.basename(png), 'L12 ์„ ์–ธ๋œ PNG ๊ฐ€ ๋””์Šคํฌ์— ์—†๋‹ค โ€” ๊ตฝ์ง€ ์•Š์•˜๊ฑฐ๋‚˜ ๊ฒฝ๋กœ๊ฐ€ ํ‹€๋ ธ๋‹ค')); continue
61
+ if not os.path.exists(rec):
62
+ notes.append(f'L12 diagram : {sid} ๊ตฝ๊ธฐ ์˜์ˆ˜์ฆ ์—†์Œ({os.path.basename(rec)}) โ€” ์†์œผ๋กœ ๋„ฃ์€ ๊ทธ๋ฆผ์€ ์ด ๋ ˆ์ธ์ด ๋ชป ๋ณธ๋‹ค. UNMEASURED (0 ์•„๋‹˜)'); n_unm += 1; continue
63
+ try: r = json.load(open(rec, encoding='utf-8'))
64
+ except Exception as e:
65
+ findings.append((sid, 'diagram', 'receipt', os.path.basename(rec), f'L12 ์˜์ˆ˜์ฆ ํŒŒ์‹ฑ ์‹คํŒจ({type(e).__name__}) โ€” ์ฑ„๋„์ด ๊นจ์กŒ๋‹ค')); continue
66
+ bad = False
67
+ if r.get('spec_sha256') != _sha(jp):
68
+ findings.append((sid, 'diagram', 'stale', os.path.basename(png), 'L12 JSON ์ง€๋ฌธ์ด ์˜์ˆ˜์ฆ๊ณผ ๋‹ค๋ฅด๋‹ค โ€” JSON ์„ ๊ณ ์นœ ๋’ค ๋‹ค์‹œ ๊ตฝ์ง€ ์•Š์•˜๋‹ค')); bad = True
69
+ if not (r.get('validate') or {}).get('ok') or (r.get('validate') or {}).get('profile') != 'showcase':
70
+ findings.append((sid, 'diagram', 'validate', os.path.basename(png), 'L12 showcase validate ํ†ต๊ณผ ๊ธฐ๋ก์ด ์—†๋‹ค โ€” ๊ฒ€์ฆ ์•ˆ ๋œ ๊ทธ๋ฆผ')); bad = True
71
+ try: w = _png_w(png)
72
+ except Exception as e:
73
+ findings.append((sid, 'diagram', 'png', os.path.basename(png), f'L12 PNG ํ—ค๋” ์ฝ๊ธฐ ์‹คํŒจ({e})')); continue
74
+ if w < min_px:
75
+ # ์ž๋ฅธ(crop) PNG ๋Š” ์บ”๋ฒ„์Šค๋ณด๋‹ค ์ข๋‹ค โ€” ์˜์ˆ˜์ฆ์˜ png_size ๊ฐ€ ์•„๋‹ˆ๋ผ ยซcrop ์ „ ์บ”๋ฒ„์Šคยป๊ฐ€ ํ•˜ํ•œ์„ ๋„˜๊ฒผ๋Š”์ง€ ๋ณธ๋‹ค.
76
+ # ๐ŸŸฅ ์บ”๋ฒ„์Šค ํญ์€ ์˜์ˆ˜์ฆ ์ž๊ธฐ์‹ ๊ณ ๋‹ค. IHDR ์‹ค์ธก(w)๊ณผ ํ•จ๊ป˜ ๋‘˜ ๋‹ค ์ ์–ด ๊ฐ์‚ฌ์ž๊ฐ€ ๊ฐ€๋ฅด๊ฒŒ ํ•œ๋‹ค.
77
+ canvas_w = None
78
+ cv = r.get('canvas')
79
+ if isinstance(cv, str) and 'x' in cv.lower() and cv.lower() != 'auto':
80
+ canvas_w = int(cv.lower().split('x')[0])
81
+ elif r.get('crop'):
82
+ canvas_w = min_px if r.get('png_size') else None # auto ์บ”๋ฒ„์Šค๋Š” ์Šคํฌ๋ฆฝํŠธ๊ฐ€ ํ•˜ํ•œ ์ด์ƒ์œผ๋กœ๋งŒ ๋งŒ๋“ ๋‹ค
83
+ if not (r.get('crop') and canvas_w and canvas_w >= min_px):
84
+ findings.append((sid, 'diagram', 'resolution', os.path.basename(png), f'L12 PNG ํญ {w} < ํ•˜ํ•œ {min_px} (์žฅํ‘œ 2ร—)')); bad = True
85
+ else:
86
+ notes.append(f'L12 diagram : {sid} ์ž๋ฅธ PNG ํญ {w}(IHDR) โ€” ์บ”๋ฒ„์Šค {canvas_w}(์˜์ˆ˜์ฆ ์ž๊ธฐ์‹ ๊ณ ) ๋กœ ํ•˜ํ•œ ํŒ์ •')
87
+ vbw = r.get('viewbox_w')
88
+ if vbw is None or float(vbw) > max_vb:
89
+ findings.append((sid, 'diagram', 'viewbox', os.path.basename(png), f'L12 viewBox ํญ {vbw} > ์ƒํ•œ {max_vb:.0f} โ€” ๊ธ€์ž ๋ฐฐ์œจ ๋ฏธ๋‹ฌ(๋ผ๋ฒจโ‰ˆ{12*1728/float(vbw or 1):.0f}pt)')); bad = True
90
+ if float(r.get('pad', 0)) < min_pad:
91
+ findings.append((sid, 'diagram', 'pad', os.path.basename(png), f'L12 ์—ฌ๋ฐฑ {r.get("pad")} < ํ•˜ํ•œ {min_pad}')); bad = True
92
+ if not bad: n_ok += 1
93
+ notes.append(f'L12 diagram : ํ‘œ๋ฉด {len(surfs)} ยท ํ†ต๊ณผ {n_ok} ยท UNMEASURED {n_unm} ยท ๋ฐœ๊ฒฌ {len(findings)} '
94
+ f'(ํ•˜ํ•œ {min_px}px ยท viewBoxโ‰ค{max_vb:.0f} ยท padโ‰ฅ{min_pad})')
95
+ return findings, notes
@@ -0,0 +1,181 @@
1
+ #!/usr/bin/env python3
2
+ """P1ยทP3 โ€” ๋„ํ˜• ๋ฐฐ์น˜ ๊ฒ€์‚ฌ ๋‘˜. the private companion store's origin script (lane_geometry.py(2026-09-04,
3
+ 36p ์‚ฌ๊ณ ์—์„œ ์—ญ์‚ฐ)๋ฅผ preprep ๊ด€๋ก€๋กœ ์ด์‹ํ•œ ๊ฒƒ โ€” ๋กœ์ง์€ ๊ทธ๋Œ€๋กœ๋‹ค.
4
+ ์ •๋ณธ: `tracks-meta/fh_signal_2026-09-04_preprep-evolution.md` ยง2.
5
+
6
+ P1 build-jitter ๊ฐ™์€ ์ด๋ฆ„์˜ ๋„ํ˜•์ด ยซ์—ฐ์†ํ•œ ๋‘ ์žฅยป์—์„œ ์กฐ๊ธˆ ๋‹ค๋ฅธ ์ž๋ฆฌ์— ์žˆ๋‹ค
7
+ โ†’ ๋„˜๊ธธ ๋•Œ ํŠ„๋‹ค. ์‹ค์‚ฌ๊ณ : ๋…ธ๋ž€ ๊ฒŒ์ดํŠธ ๋ง‰๋Œ€๊ฐ€ ์ง์ „ ์žฅ ๋Œ€๋น„ 35,000 EMU ๋ฐ€๋ ค
8
+ ํšŒ์ƒ‰ ์—ฐ๊ฒฐ์„ ๊ณผ ๊ฒน์ณค๋‹ค. ํฐ ์ฐจ์ด๋Š” ยซ์˜๋„ํ•œ ์ด๋™ยป์ด๋ผ ์ œ์™ธํ•œ๋‹ค.
9
+ P3 adjacency ํ•œ ๋„ํ˜•์˜ ยซ์˜ค๋ฅธ์ชฝ ๋ยป๊ณผ ๋‹ค๋ฅธ ๋„ํ˜•์˜ ยซ์™ผ์ชฝ ๋ยป์ด ๋งž๋‹ฟ์œผ๋ ค๋‹ค ์–ด๊ธ‹๋‚ฌ๋‹ค
10
+ โ†’ ์‹ค์ œ ๊ฒฐํ•จ์ด ์ด ํ˜•ํƒœ์˜€๋‹ค(ํšŒ์ƒ‰ ์Šคํ… ๋ โ†” ๋ง‰๋Œ€ ์‹œ์ž‘, 35,000 ๊ฒน์นจ).
11
+
12
+ ๐ŸŸฅ **P2(near-miss, ๊ฐ™์€ ์ข…๋ฅ˜ ๋ชจ์„œ๋ฆฌ๋ผ๋ฆฌ๋งŒ ๋Œ€์กฐ)๋Š” ๋ฏธํฌํ•จ์ด๋‹ค โ€” ์ฑ„ํƒํ•˜์ง€ ์•Š๋Š”๋‹ค.** ์‹ค์‚ฌ๊ณ ๊ฐ€
13
+ ๋ชป ์žกํžˆ๋Š” ์ด์œ ๊ฐ€ ์„ค๊ณ„ ๊ทผ๊ฑฐ์˜€๋‹ค: ์‹ค์ œ ๊ฒฐํ•จ์€ ใ€ŒํšŒ์ƒ‰ ์Šคํ… ์˜ค๋ฅธ์ชฝ ๋ โ†” ๋ง‰๋Œ€ ์™ผ์ชฝ ๋ใ€์ด๋ผ๋Š”
14
+ **๋‹ค๋ฅธ ์ข…๋ฅ˜** ๋ชจ์„œ๋ฆฌ์˜ ์ธ์ ‘ ๊ด€๊ณ„์ธ๋ฐ, P2 ๋Š” ์™ผโ†”์™ผยท์˜ค๋ฅธโ†”์˜ค๋ฅธ๋งŒ ๋ด์„œ ๊ตฌ์กฐ์ ์œผ๋กœ ๋ชป ์žก๋Š”๋‹ค.
15
+ ๊ทธ ๊ด€๊ณ„๋Š” P3 ๊ฐ€ ์ด๋ฏธ ๋ฎ๋Š”๋‹ค. ์ƒˆ ๋ ˆ์ธ์„ ์งค ๋•Œ ยซ๊ฐ™์€ ์ข…๋ฅ˜๋ผ๋ฆฌยป๊ฐ€ ๊ธฐ๋ณธ๊ฐ’์ด๋ผ๋Š” ๊ฒƒ์„ ์˜์‹ฌํ•˜๋ผ.
16
+
17
+ ๐ŸŸฅ ๋‘˜ ๋‹ค advisory. ์ž„๊ณ„๋Š” ์‚ฌ๋žŒ์ด ์ •ํ•œ๋‹ค(๊ธฐ๋ณธ JIT=200,000 EMU ยท ALN=63,500 EMU=5pt ยท
18
+ 1pt=12,700 EMU). `surfaces.yaml` ์˜ `geometry.jitter_emu` / `geometry.align_emu` ๋กœ ๋ฐ”๊พผ๋‹ค.
19
+ ๐ŸŸฅ ยซ์ฐจ์ด 0ยป ์€ ์ •์ƒ์ด๊ณ  ยซ์ฐจ์ด ํผยป ๋„ ์ •์ƒ์ด๋‹ค. ์ด ๊ฒ€์‚ฌ๊ฐ€ ๋ณด๋Š” ๊ฒƒ์€ ๊ทธ ์‚ฌ์ด๋ฟ์ด๋‹ค.
20
+
21
+ ## ๐ŸŸฅ ๋ชฉ๋ก์œผ๋กœ ์ฝ์ง€ ๋งˆ๋ผ โ€” ๋ธํƒ€๋กœ ์ฝ์–ด๋ผ
22
+
23
+ ์‹ค์ธก: P3 ๋Š” ์› ์ฝ”ํผ์Šค์—์„œ ๊ธฐ์ € ์˜คํƒ์ด 76๊ฑด์ด๋‹ค(์•„์ด์ฝ˜ ๋ฌด๋ฆฌ ๋‚ด๋ถ€ ยท ๋งต ๋ผ๋ฒจ์˜ ยซ์˜๋„๋œยป ๊ฒน์นจ).
24
+ ๊ทธ ๋ชฉ๋ก์—์„œ ์ง„์งœ๋ฅผ ๊ณจ๋ผ๋‚ผ ๋ฐฉ๋ฒ•์ด ์—†๋‹ค. ๊ทธ๋Ÿฐ๋ฐ ํŽธ์ง‘ ์ „/ํ›„๋ฅผ ๋Œ€์กฐํ•˜๋ฉด **์ฐจ์ด๊ฐ€ ์ •ํ™•ํžˆ ๊ทธ
25
+ ํ•œ ์ค„**์ด์—ˆ๋‹ค. โ‡’ ์ ˆ๋Œ€ ๋ชฉ๋ก์€ ํšŒ๊ท€ ๋„๊ตฌ๋กœ ๋ชป ์“ฐ๊ณ  **ํŽธ์ง‘ ์ „ํ›„ ๋ธํƒ€๋Š” ์ •ํ™•ํ•˜๋‹ค.**
26
+ `surfaces.yaml` ์˜ `geometry.baseline`(์ด์ „ ํŒ pptx ๊ฒฝ๋กœ)์ด ์žˆ์œผ๋ฉด ์ด ๋ ˆ์ธ์€ ๋ธํƒ€ ๋ชจ๋“œ๋กœ ๋ˆ๋‹ค
27
+ โ€” ใ€Œ์ง€๊ธˆ ๋ช‡ ๊ฑด์ด๋ƒใ€๊ฐ€ ์•„๋‹ˆ๋ผ ยซ๋‚ด๊ฐ€ ๋ฐฉ๊ธˆ ๋ญ˜ ์–ด๊ธ‹๋ƒˆ๋ƒยป๋ฅผ ๋ฌป๋Š” ํšŒ๊ท€ ๋„๊ตฌ๋‹ค.
28
+
29
+ ## ๐ŸŸฅ P1 ์ด ยซ๋น„๊ต ์•ˆ ํ•จยป์„ ยซํА ์—†์Œยป์œผ๋กœ ์†์ผ ์ˆ˜ ์žˆ๋Š” ์ž๋ฆฌ (๊ณ„๊ธฐ ๊ฒฐํ•จ, ๋ฏธ๋ฆฌ ์ ๋Š”๋‹ค)
30
+
31
+ `shapes()` ๋Š” **ํ•œ ์žฅ ์•ˆ์—์„œ ์ด๋ฆ„์ด ๊ฒน์น˜๋Š” ๋„ํ˜•์„ ํ†ต์งธ๋กœ ๋ฒ„๋ฆฐ๋‹ค**(`len(v) == 1` ๋งŒ ๋‚จ๊ธด๋‹ค) โ€”
32
+ ๊ฒน์น˜๋Š” ์ด๋ฆ„๋ผ๋ฆฌ๋Š” ๋Œ€์กฐํ•  ๋Œ€์ƒ์„ ํ•˜๋‚˜๋กœ ๋ชป ์ •ํ•˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. python-pptx ๊ธฐ๋ณธ ์ด๋ฆ„
33
+ (`TextBox 1` ๋ฅ˜)์€ ์žฅ๋งˆ๋‹ค ์ƒˆ๋กœ ๋งค๊ฒจ์ ธ **์ด๋ฆ„์ด ์šฐ์—ฐํžˆ ์•ˆ ๊ฒน์น  ์ˆ˜ ์žˆ๊ณ **, ๋Œ€์กฐ ์ž์ฒด๊ฐ€ 0๊ฑด์ด
34
+ ๋˜๋Š” ์ž…๋ ฅ์—์„œ๋„ ใ€Œํ›„๋ณด 0๊ฑดใ€์ด ๋˜‘๊ฐ™์ด ์ถœ๋ ฅ๋œ๋‹ค โ€” ๊ทธ 0 ์€ **ใ€ŒํА ์—†์Œใ€์ด ์•„๋‹ˆ๋ผ ใ€Œ๋น„๊ต ์•ˆ ํ•จใ€**
35
+ ์ด๋‹ค(์ด ์ €์žฅ์†Œ๊ฐ€ ์ด๋ฆ„ ๋ถ™์ธ ใ€Œ๋ ˆ์ธ์ด ์ดˆ๋ก์ธ ์ด์œ ๋Š” ์…‹ใ€์˜ โ‘ก). โ‡’ `scan()` ์€ **๋น„๊ต๋œ ๋„ํ˜•-์Œ
36
+ ์ˆ˜**๋ฅผ ํ•ญ์ƒ ๊ฐ™์ด ๋‚ธ๋‹ค โ€” 0 ์ด๋ฉด UNMEASURED ๋กœ ์ฝ์–ด์•ผ ํ•œ๋‹ค๋Š” ๋œป์ด๋‹ค.
37
+ """
38
+ import zipfile, re, os, collections
39
+
40
+ EMU_PT = 12700
41
+
42
+
43
+ def shapes(z, sn):
44
+ """ํ•œ ์Šฌ๋ผ์ด๋“œ์˜ ๋„ํ˜•์„ {์ด๋ฆ„: (x,y,cx,cy,text)} ๋กœ. ์ด๋ฆ„ ์ค‘๋ณต์€ ๋Œ€์กฐ ๋ชป ํ•˜๋‹ˆ ๋บ€๋‹ค."""
45
+ x = z.read('ppt/slides/slide%d.xml' % sn).decode('utf-8')
46
+ out = {}
47
+ for m in re.finditer(r'<p:(sp|cxnSp)>.*?</p:\1>', x, re.S):
48
+ b = m.group(0)
49
+ nm = re.search(r'name="([^"]*)"', b)
50
+ o = re.search(r'<a:off x="(-?\d+)" y="(-?\d+)"/><a:ext cx="(-?\d+)" cy="(-?\d+)"', b)
51
+ if not (nm and o):
52
+ continue
53
+ t = re.sub(r'\s+', ' ', ' '.join(re.findall(r'<a:t>([^<]*)</a:t>', b))).strip()
54
+ out.setdefault(nm.group(1), []).append(
55
+ (int(o.group(1)), int(o.group(2)), int(o.group(3)), int(o.group(4)), t))
56
+ return {k: v[0] for k, v in out.items() if len(v) == 1}
57
+
58
+
59
+ def _order(z):
60
+ rels = dict(re.findall(r'Id="([^"]+)"[^>]*Target="slides/slide(\d+)\.xml"',
61
+ z.read('ppt/_rels/presentation.xml.rels').decode('utf-8')))
62
+ lst = re.search(r'<p:sldIdLst>.*?</p:sldIdLst>',
63
+ z.read('ppt/presentation.xml').decode('utf-8'), re.S).group(0)
64
+ return [int(rels[r]) for r in re.findall(r'<p:sldId id="\d+" r:id="([^"]+)"/>', lst)]
65
+
66
+
67
+ def load_slides(path):
68
+ z = zipfile.ZipFile(path)
69
+ return [shapes(z, sn) for sn in _order(z)]
70
+
71
+
72
+ def p1_lines(S, JIT):
73
+ """build-jitter ํ›„๋ณด. ๋ฐ˜ํ™˜: (lines, compared) โ€” compared=0 ์ด๋ฉด ๋Œ€์กฐ ๋Œ€์ƒ์ด ์•„์˜ˆ ์—†์—ˆ๋‹ค๋Š”
74
+ ๋œป์ด๋ผ ยซํА 0๊ฑดยป๊ณผ ยซUNMEASUREDใ€๋ฅผ ๊ฐˆ๋ผ์•ผ ํ•œ๋‹ค."""
75
+ lines, compared = [], 0
76
+ for i in range(1, len(S)):
77
+ for nm, cur in S[i].items():
78
+ prv = S[i - 1].get(nm)
79
+ if not prv:
80
+ continue
81
+ compared += 1
82
+ d = [cur[k] - prv[k] for k in range(4)]
83
+ mx = max(abs(v) for v in d)
84
+ if 0 < mx <= JIT:
85
+ lab = ['x', 'y', 'cx', 'cy']
86
+ moved = ' ยท '.join(f'{lab[k]} {d[k]:+d}' for k in range(4) if d[k])
87
+ lines.append(f" {i:>3}pโ†’{i+1:<3}p {nm[:16]:16} {moved} ({mx/EMU_PT:.2f}pt) "
88
+ f"ยซ{cur[4][:26]}ยป")
89
+ return lines, compared
90
+
91
+
92
+ def p3_lines(S, ALN):
93
+ """adjacency ํ›„๋ณด โ€” A ์˜ค๋ฅธ์ชฝ ๋ โ†” B ์™ผ์ชฝ ๋, ์„ธ๋กœ๋กœ ๊ฒน์น˜๋Š” ๋ ์— ์žˆ์„ ๋•Œ๋งŒ."""
94
+ lines = []
95
+ for i, sh in enumerate(S):
96
+ items = [(nm, v) for nm, v in sh.items() if v[2] > 0 and v[3] > 0]
97
+ for a in range(len(items)):
98
+ for b in range(len(items)):
99
+ if a == b:
100
+ continue
101
+ na, va = items[a]
102
+ nb, vb = items[b]
103
+ oy = min(va[1] + va[3], vb[1] + vb[3]) - max(va[1], vb[1])
104
+ if oy <= 0:
105
+ continue
106
+ gap = vb[0] - (va[0] + va[2]) # A ์˜ค๋ฅธ์ชฝ โ†’ B ์™ผ์ชฝ
107
+ if 0 < abs(gap) <= ALN:
108
+ kind = '๊ฒน์นจ' if gap < 0 else 'ํ‹ˆ'
109
+ lines.append(f" {i+1:>3}p {na[:14]:14} ์˜ค๋ฅธ๋ โ†’ {nb[:14]:14} ์™ผ๋ "
110
+ f"{kind} {abs(gap):>6} EMU ({abs(gap)/EMU_PT:.2f}pt)")
111
+ return lines
112
+
113
+
114
+ def collect_lines(path, JIT, ALN):
115
+ """(lines, compared) โ€” P1+P3 ํ•ฉ์นœ ํ›„๋ณด ์ค„๊ณผ, P1 ์ด ์‹ค์ œ๋กœ ๋Œ€์กฐํ•œ ๋„ํ˜•-์Œ ์ˆ˜."""
116
+ S = load_slides(path)
117
+ l1, compared = p1_lines(S, JIT)
118
+ l3 = p3_lines(S, ALN)
119
+ return l1 + l3, compared
120
+
121
+
122
+ def delta(base_path, cur_path, JIT, ALN):
123
+ """ํŽธ์ง‘ ์ „/ํ›„ ๋ธํƒ€ โ€” ์ ˆ๋Œ€ ๋ชฉ๋ก์ด ์•„๋‹ˆ๋ผ **์ฐจ์ด**๋กœ ์ฝ๋Š” ์šฉ๋ฒ•(์ •ํ™•ํ–ˆ๋˜ ๊ฒƒ). P3 ๋Š” SYS ๋ฅผ ์•ˆ ์“ฐ๋‹ˆ
124
+ ์–‘์ชฝ์— ๊ฐ™์€ ๊ทœ์น™์„ ๊ทธ๋Œ€๋กœ ์ ์šฉํ•˜๋ฉด ๋œ๋‹ค(๊ณ„๊ธฐ ์ž๊ธฐ๊ฒฐํ•จ์ด P2 ์ „์šฉ์ด๋ผ ์—ฌ๊ธฐ ์•ˆ ์˜ฎ๋Š”๋‹ค)."""
125
+ bl, cmp_b = collect_lines(base_path, JIT, ALN)
126
+ cur, cmp_c = collect_lines(cur_path, JIT, ALN)
127
+ b = set(bl)
128
+ new = [l for l in cur if l not in b]
129
+ gone = [l for l in bl if l not in set(cur)]
130
+ return new, gone, cmp_b, cmp_c
131
+
132
+
133
+ def _resolve(root, p):
134
+ return os.path.normpath(os.path.join(root, os.path.expanduser(p)))
135
+
136
+
137
+ def scan(cfg, root):
138
+ """preprep.py ๊ฐ€ ๋ถ€๋ฅด๋Š” ๋ ˆ์ธ ์ง„์ž…์ . (findings, notes) โ€” ๐ŸŸฅ **advisory ๊ณ ์ •**:
139
+ ํ˜ธ์ถœ์ž๋Š” findings ๋ฅผ ๋ฒ„๋ฆฌ๊ณ  notes ๋งŒ ์ทจํ•œ๋‹ค(L8ยทL11 ๊ด€๋ก€)."""
140
+ deck_s = (cfg.get('surfaces_by_id') or {}).get('built_deck')
141
+ if not deck_s:
142
+ return [], ['P1/P3 geometry : built_deck ๋ฏธ์„ ์–ธ โ€” NOT_CONFIGURED (0 ์•„๋‹˜)']
143
+ deck = _resolve(root, deck_s['path'])
144
+ if not os.path.exists(deck):
145
+ return [], [f'P1/P3 geometry : built_deck ์‹ค๋ฌผ ์—†์Œ({deck}) โ€” UNMEASURED (0 ์•„๋‹˜)']
146
+ spec = cfg.get('geometry') or {}
147
+ JIT = int(spec.get('jitter_emu', 200000))
148
+ ALN = int(spec.get('align_emu', 63500))
149
+ base_p = spec.get('baseline')
150
+ notes = []
151
+
152
+ if base_p:
153
+ base = _resolve(root, base_p)
154
+ if not os.path.exists(base):
155
+ return [], [f'P1/P3 geometry : baseline ์‹ค๋ฌผ ์—†์Œ({base}) โ€” UNMEASURED (0 ์•„๋‹˜)']
156
+ try:
157
+ new, gone, cmp_b, cmp_c = delta(base, deck, JIT, ALN)
158
+ except Exception as e:
159
+ return [], [f'P1/P3 geometry : ๊ณ„๊ธฐ ์˜ค๋ฅ˜({type(e).__name__}: {e}) โ€” UNMEASURED (0 ์•„๋‹˜)']
160
+ notes.append(f'P1/P3 geometry(๋ธํƒ€) : ๊ธฐ์ค€ {os.path.basename(base)} ๋Œ€๋น„ '
161
+ f'์ƒˆ ์–ด๊ธ‹๋‚จ {len(new)}๊ฑด ยท ์‚ฌ๋ผ์ง„ ์–ด๊ธ‹๋‚จ {len(gone)}๊ฑด ๐ŸŸฅ advisory '
162
+ f'(P1 ๋Œ€์กฐ๋œ ๋„ํ˜•-์Œ: ๊ธฐ์ค€ {cmp_b} ยท ํ˜„์žฌ {cmp_c}'
163
+ + ('' if cmp_b and cmp_c else ' โ€” ๐ŸŸฅ 0๊ฑด์ด๋ฉด UNMEASURED, ใ€ŒํА ์—†์Œใ€์ด ์•„๋‹ˆ๋‹ค') + ')')
164
+ for l in new:
165
+ notes.append(' + ' + l.strip())
166
+ for l in gone:
167
+ notes.append(' - ' + l.strip())
168
+ return [], notes
169
+
170
+ try:
171
+ lines, compared = collect_lines(deck, JIT, ALN)
172
+ except Exception as e:
173
+ return [], [f'P1/P3 geometry : ๊ณ„๊ธฐ ์˜ค๋ฅ˜({type(e).__name__}: {e}) โ€” UNMEASURED (0 ์•„๋‹˜)']
174
+ notes.append(f'P1/P3 geometry(๋ชฉ๋ก) : ํ›„๋ณด {len(lines)}๊ฑด โ€” ๐ŸŸฅ ์ ˆ๋Œ€ ๋ชฉ๋ก์œผ๋กœ ์ฝ์ง€ ๋งˆ๋ผ, '
175
+ f'P3 ๊ธฐ์ € ์˜คํƒ์ด ์ฝ”ํผ์Šค๋งˆ๋‹ค ์ˆ˜์‹ญ ๊ฑด์ผ ์ˆ˜ ์žˆ๋‹ค(์› ์ฝ”ํผ์Šค ์‹ค์ธก 76๊ฑด). '
176
+ f'`geometry.baseline` ์„ ์“ฐ๋Š” ํŽธ์ง‘-๋ธํƒ€ ์šฉ๋ฒ•์ด ์ •ํ™•ํ•˜๋‹ค '
177
+ f'(P1 ๋Œ€์กฐ๋œ ๋„ํ˜•-์Œ {compared}'
178
+ + ('' if compared else ' โ€” ๐ŸŸฅ UNMEASURED, ใ€ŒํА ์—†์Œใ€์ด ์•„๋‹ˆ๋‹ค') + ')')
179
+ for l in lines:
180
+ notes.append(l)
181
+ return [], notes
@@ -74,7 +74,10 @@ def scan(texts, surf_meta, max_report=8, mod=None):
74
74
  text = texts[sid][0]
75
75
  if mod is not None:
76
76
  speech = []
77
- for _uid, _h, body, _r in mod.units(text):
77
+ # ๐ŸŸฅ 2026-09-04 โ€” units() ๊ฐ€ ์ด์ œ pattern ์„ ๋ฐ›๋Š”๋‹ค. ์ด ํ‘œ๋ฉด์˜ surfaces.yaml
78
+ # unit_pattern ์„ ์–ธ์„ ๊ทธ๋Œ€๋กœ ๋„˜๊ธด๋‹ค(interslide_deps ์‹ ํ˜ธ ยง3-1 ๊ณผ ๊ฐ™์€ ์ˆ˜๋ฆฌ).
79
+ _pat = surf_meta.get(sid, {}).get('unit_pattern')
80
+ for _uid, _h, body, _r in mod.units(text, pattern=_pat):
78
81
  _sc, sp = mod.blocks(body)
79
82
  speech.append(sp if isinstance(sp, str) else '\n'.join(sp))
80
83
  text = '\n'.join(speech) or text
@@ -0,0 +1,134 @@
1
+ #!/usr/bin/env python3
2
+ """L13 โ€” ยซN ํŽ˜์ด์ง€ยป ๋ฆฌํ„ฐ๋Ÿด ์žฅ ๋ฒˆํ˜ธ ์ฐธ์กฐ๊ฐ€ ์‹ค์žฌํ•˜๋Š” ์žฅ์„ ๊ฐ€๋ฆฌํ‚ค๋‚˜ (2026-09-06 ์‹ ์„ค).
3
+
4
+ WHY: ์žฅ์„ ํ•˜๋‚˜ ๋นผ๋ฉด ๊ทธ ๋’ค ๋ฒˆํ˜ธ๊ฐ€ ํ†ต์งธ๋กœ ๋‹น๊ฒจ์ง„๋‹ค. preprep ์˜ ์ด์›ƒ ๋ ˆ์ธ๋“ค์€ **๋ง๋กœ ๊ฐ€๋ฆฌํ‚ค๋Š”
5
+ ๊ฒƒ**๋งŒ ๋ณธ๋‹ค โ€” L5/L8 ์€ ํ™”๋ฉด ์žฌ์‚ฌ์šฉยท์‹œ๊ฐ ์ง€์‹œ๋ฅผ, R1~R5 ๋Š” ์ ‘์†์–ดยท์†Œ์œ ๊ถŒ์„. **์ˆซ์ž๋กœ ๊ฐ€๋ฆฌํ‚ค๋Š”
6
+ ๊ฒƒ์€ ์–ด๋А ๋ ˆ์ธ๋„ ์•ˆ ๋ดค๋‹ค.** ์‹ค์‚ฌ๊ณ  ๊ณ„๊ธฐ: if(kakao) ๋ฑ์—์„œ 42์žฅ์„ ๋นผ์ž 43~121 ์ด ํ•œ ์นธ์”ฉ
7
+ ๋‹น๊ฒจ์กŒ๊ณ , ใ€Œ๊นจ์ง„ ์ฐธ์กฐ๊ฐ€ ์–ด๋””๋ƒใ€๋ฅผ ์†์œผ๋กœ ํ›‘์–ด์•ผ ํ–ˆ๋‹ค(2026-09-06, ์šด์˜์ž ์ง€์ ).
8
+
9
+ ๐ŸŸฅ ์ด ๋ ˆ์ธ์˜ ๋‚œ์ ์€ ๊ฒ€์ถœ์ด ์•„๋‹ˆ๋ผ **๋ถ„๋ฅ˜**๋‹ค. ๊ฐ™์€ ยซ์ˆซ์ž+๋‹จ์œ„ยป๊ฐ€ ์„ธ ๊ฐ€์ง€ ๋‹ค๋ฅธ ๊ฒƒ์„ ๊ฐ€๋ฆฌํ‚จ๋‹ค โ€”
10
+ ์‹ค์ธก ์ฝ”ํผ์Šค(๊ทธ ๋ฑ 121์žฅ)์—์„œ ์…‹์ด **์ „๋ถ€** ๋‚˜์™”๋‹ค:
11
+
12
+ SLIDE ใ€Œ40p ๋ถ€์ œ๊ฐ€ โ€ฆใ€ โ†’ ์ง„์งœ ์žฅ ์ฐธ์กฐ. ์ด๊ฒƒ๋งŒ ๊ฒ€์‚ฌ ๋Œ€์ƒ์ด๋‹ค
13
+ SECTION ใ€Œ3์žฅ์—์„œ ๊ฒŒ์ดํŠธ๋ถ€ํ„ฐใ€ โ†’ ์Šฌ๋ผ์ด๋“œ๊ฐ€ ์•„๋‹ˆ๋ผ **์„น์…˜**(๊ทธ ๋ฑ์˜ sec-guide ใ€Œ3. ์–ด๋–ป๊ฒŒ ๋งŒ๋“ค์—ˆ๋‚˜ใ€)
14
+ CITATION ใ€ŒarXiv:2606.05647 (4p)ใ€ โ†’ ์ธ์šฉ **๋…ผ๋ฌธ ์ชฝ์ˆ˜**
15
+
16
+ ์…‹์„ ์•ˆ ๊ฐ€๋ฅด๋ฉด ์˜คํƒ ๋ฉ์–ด๋ฆฌ๊ฐ€ ๋œ๋‹ค. ๊ทธ๋ž˜์„œ ํŒ๋ณ„์ž๋Š” ์ฝ”ํผ์Šค์—์„œ ๊ธฐ๊ณ„๋กœ ๋ฝ‘๋Š”๋‹ค:
17
+ ยท SECTION โ€” ๋ฑ์˜ ์„น์…˜ ๋ผ๋ฒจ(ใ€ŒN. ์ œ๋ชฉใ€)์—์„œ N ์ง‘ํ•ฉ์„ ๋ชจ์œผ๊ณ , ใ€ŒN์žฅใ€ ํ˜•ํƒœ์ด๋ฉด์„œ ๊ทธ ์ง‘ํ•ฉ์—
18
+ ์†ํ•˜๋ฉด ์„น์…˜์ด๋‹ค. ๋ผ๋ฒจ์ด ์—†์œผ๋ฉด ์ด ๋ถ„๊ธฐ๋Š” **๋น„ํ™œ์„ฑ**์ด๊ณ  ๊ทธ ์‚ฌ์‹ค์„ ๋…ธํŠธ์— ์ ๋Š”๋‹ค.
19
+ ยท CITATION โ€” ๊ฐ™์€ ์ค„์— ์ธ์šฉ ํ‘œ์ง€(arXiv/doi/ใ€Žใ€/pp./4์ž๋ฆฌ ์—ฐ๋„)๊ฐ€ ์žˆ์œผ๋ฉด ์ธ์šฉ ์ชฝ์ˆ˜๋‹ค.
20
+
21
+ ๐ŸŸฅ **ํŒ์ •์€ ยซ์กด์žฌยป๋งŒ ํ•œ๋‹ค** โ€” ยซ๊ทธ ๋ฒˆํ˜ธ๊ฐ€ ๋งž๋Š” ์žฅ์„ ๊ฐ€๋ฆฌํ‚ค๋‚˜ยป๋Š” ์˜๋ฏธ ํŒ๋‹จ์ด๋ผ ์•ˆ ๋ณธ๋‹ค
22
+ (ยงMechanization Boundary: ๊ธฐ๋ก์˜ ์†์„ฑ๋งŒ ๋‹จ์–ธํ•œ๋‹ค). ๊ทธ๋ž˜์„œ finding ์€ **๋ฒ”์œ„ ๋ฐ– ์ฐธ์กฐ**
23
+ ํ•˜๋‚˜๋ฟ์ด๊ณ , ์ž๊ธฐ ์ž์‹  ์ฐธ์กฐยท์ •์ƒ ์ฐธ์กฐ๋Š” ๋…ธํŠธ๋กœ๋งŒ ๋‚˜๊ฐ„๋‹ค.
24
+
25
+ ๐ŸŸฅ **์ฐธ์กฐ 0๊ฑด์€ ยซํ†ต๊ณผยป๊ฐ€ ์•„๋‹ˆ๋‹ค** โ€” ์ถ”์ถœ์ด ์ฃฝ์–ด๋„ 0์ด ๋‚˜์˜จ๋‹ค. 0์ด๋ฉด UNMEASURED ๋กœ ์ ๋Š”๋‹ค.
26
+ """
27
+ import os
28
+ import re
29
+
30
+ # ใ€Œ40pใ€ ใ€Œ40 ํŽ˜์ด์ง€ใ€ ใ€Œ40์ชฝใ€ ใ€Œ3์žฅ์—์„œใ€โ€ฆ
31
+ REF = re.compile(r'(?<![0-9A-Za-z])(\d{1,3})\s*(p\b|ํŽ˜์ด์ง€|์ชฝ(?!์ˆ˜)|์žฅ(?=์—์„œ|์˜|์„|๋ถ€ํ„ฐ|์—))')
32
+ # ๐ŸŸฅ `\s+` ๊ฐ€ ํ•˜์ค‘์„ ์ง„๋‹ค โ€” ์ดˆํŒ์€ `\s*` ์˜€๊ณ  ์‹ค์ธก ์ฝ”ํผ์Šค์˜ ยซ62.5%ยป ๋ฅผ ยซ62๋ฒˆ ์„น์…˜ยป์œผ๋กœ
33
+ # ๋จน์—ˆ๋‹ค. ๊ทธ๋Ÿฌ๋ฉด ์ง„์งœ ใ€Œ62์žฅใ€ ์ฐธ์กฐ๊ฐ€ ์กฐ์šฉํžˆ SECTION ์œผ๋กœ ๋ถ„๋ฅ˜๋ผ ์‚ฌ๋ผ์ง„๋‹ค.
34
+ # ์„น์…˜ ๋ผ๋ฒจ์€ ์  ๋’ค์— ๊ณต๋ฐฑ์ด ์žˆ๋‹ค(ใ€Œ3. ์–ด๋–ป๊ฒŒ ๋งŒ๋“ค์—ˆ๋‚˜ใ€). ์†Œ์ˆ˜์ ์€ ์—†๋‹ค.
35
+ SECTION_LABEL = re.compile(r'^\s*(\d{1,2})\.\s+\S')
36
+ SECTION_MAX = 20 # ์„น์…˜์ด 20์„ ๋„˜๋Š” ๋ฑ์€ ์—†๋‹ค โ€” ๋„˜์œผ๋ฉด ๋ผ๋ฒจ์ด ์•„๋‹ˆ๋‹ค
37
+ SECTION_MIN_SLIDES = 2 # ์„น์…˜ ๋ผ๋ฒจ์€ ์—ฌ๋Ÿฌ ์žฅ์— ๋ฐ˜๋ณต๋œ๋‹ค(ํ•œ ์žฅ์งœ๋ฆฌ๋Š” ๋ผ๋ฒจ์ด ์•„๋‹ˆ๋‹ค)
38
+ CITE = re.compile(r'arXiv|doi|ใ€Ž|ใ€|pp\.|\b(19|20)\d{2}\b', re.I)
39
+
40
+
41
+ def _texts(slide):
42
+ scr, note = [], ''
43
+ for sh in slide.shapes:
44
+ if sh.has_text_frame:
45
+ t = ' '.join(r.text for p in sh.text_frame.paragraphs for r in p.runs)
46
+ if t.strip():
47
+ scr.append(t)
48
+ if slide.has_notes_slide:
49
+ note = slide.notes_slide.notes_text_frame.text or ''
50
+ return scr, note
51
+
52
+
53
+ def section_numbers(prs):
54
+ """์„น์…˜ ๋ผ๋ฒจ(ใ€ŒN. ์ œ๋ชฉใ€)์—์„œ N ์„ ๋ชจ์€๋‹ค. ๋ชป ๋ชจ์œผ๋ฉด ๋นˆ ์ง‘ํ•ฉ โ€” ๊ทธ ์‚ฌ์‹ค์ด ๋…ธํŠธ์— ์ ํžŒ๋‹ค.
55
+
56
+ ์„ธ ์กฐ๊ฑด์„ **ํ•จ๊ป˜** ๊ฑด๋‹ค: ์  ๋’ค ๊ณต๋ฐฑ(์†Œ์ˆ˜์  ๋ฐฐ์ œ) ยท N โ‰ค SECTION_MAX ยท ์—ฌ๋Ÿฌ ์žฅ์— ๋ฐ˜๋ณต.
57
+ ํ•˜๋‚˜๋งŒ ๊ฑธ๋ฉด ์‹ค์ธก์—์„œ ๋šซ๋ฆฐ๋‹ค(์œ„ ์ฃผ์„์˜ 62.5% ์‚ฌ๋ก€).
58
+ """
59
+ seen = {}
60
+ for i, s in enumerate(prs.slides, 1):
61
+ for sh in s.shapes:
62
+ if not sh.has_text_frame:
63
+ continue
64
+ for p in sh.text_frame.paragraphs:
65
+ line = ''.join(r.text for r in p.runs).strip()
66
+ m = SECTION_LABEL.match(line)
67
+ if m and len(line) < 40:
68
+ n = int(m.group(1))
69
+ if n <= SECTION_MAX:
70
+ seen.setdefault(n, set()).add(i)
71
+ return {n for n, slides in seen.items() if len(slides) >= SECTION_MIN_SLIDES}
72
+
73
+
74
+ def classify(num, unit, line, secs):
75
+ if CITE.search(line):
76
+ return 'CITATION'
77
+ if unit.startswith('์žฅ') and num in secs:
78
+ return 'SECTION'
79
+ return 'SLIDE'
80
+
81
+
82
+ def analyze(prs):
83
+ total = len(prs.slides)
84
+ secs = section_numbers(prs)
85
+ rows = []
86
+ for i, s in enumerate(prs.slides, 1):
87
+ scr, note = _texts(s)
88
+ for where, chunks in (('ํ™”๋ฉด', scr), ('๋Œ€๋ณธ', note.split('\n'))):
89
+ for line in chunks:
90
+ for m in REF.finditer(line):
91
+ n, unit = int(m.group(1)), m.group(2)
92
+ kind = classify(n, unit, line, secs)
93
+ rows.append((i, where, n, unit, kind, line.strip()[:70]))
94
+ return total, secs, rows
95
+
96
+
97
+ def scan(cfg, root):
98
+ deck_s = (cfg.get('surfaces_by_id') or {}).get('built_deck')
99
+ if not deck_s:
100
+ return [], ['L13 slide-refs : built_deck ๋ฏธ์„ ์–ธ โ€” NOT_CONFIGURED (0 ์•„๋‹˜)']
101
+ path = os.path.normpath(os.path.join(root, os.path.expanduser(deck_s['path'])))
102
+ if not os.path.exists(path):
103
+ return [], [f'L13 slide-refs : built_deck ์‹ค๋ฌผ ์—†์Œ({path}) โ€” UNMEASURED (0 ์•„๋‹˜)']
104
+ try:
105
+ from pptx import Presentation
106
+ prs = Presentation(path)
107
+ total, secs, rows = analyze(prs)
108
+ except Exception as e:
109
+ return [], [f'L13 slide-refs : ๊ณ„๊ธฐ ์˜ค๋ฅ˜({type(e).__name__}: {e}) โ€” UNMEASURED (0 ์•„๋‹˜)']
110
+
111
+ per = {}
112
+ for _, _, _, _, k, _ in rows:
113
+ per[k] = per.get(k, 0) + 1
114
+ notes = [f'L13 slide-refs : {total}์žฅ ยท ์ฐธ์กฐ {len(rows)}๊ฑด '
115
+ f'(SLIDE {per.get("SLIDE",0)} ยท SECTION {per.get("SECTION",0)} ยท CITATION {per.get("CITATION",0)}) '
116
+ f'ยท ์„น์…˜ ๋ฒˆํ˜ธ ์ง‘ํ•ฉ {sorted(secs) if secs else "์—†์Œ โ€” SECTION ๋ถ„๊ธฐ ๋น„ํ™œ์„ฑ"}']
117
+ if not rows:
118
+ notes.append(' โš ๏ธ ์ฐธ์กฐ 0๊ฑด โ€” ใ€Œ๊นจ๋—ํ•˜๋‹คใ€๊ฐ€ ์•„๋‹ˆ๋ผ **UNMEASURED** ๋‹ค(์ถ”์ถœ์ด ์ฃฝ์–ด๋„ 0์ด ๋‚˜์˜จ๋‹ค). '
119
+ '๋ฑ์— ๋ฒˆํ˜ธ ์ฐธ์กฐ๊ฐ€ ์›๋ž˜ ์—†์œผ๋ฉด ์ •์ƒ์ด๊ณ , ๊ทธ ์‚ฌ์‹ค์„ ์—ฌ๊ธฐ์„œ ํ™•์ธํ•ด๋ผ')
120
+ return [], notes
121
+
122
+ findings = []
123
+ for i, where, n, unit, kind, line in rows:
124
+ if kind != 'SLIDE':
125
+ continue
126
+ if n < 1 or n > total:
127
+ findings.append((f'{path}#s{i}', 'use',
128
+ f'L13 ์žฅ ๋ฒˆํ˜ธ ์ฐธ์กฐ๊ฐ€ ๋ฒ”์œ„ ๋ฐ–์ด๋‹ค โ€” s{i} [{where}] โ†’{n} (์ด {total}์žฅ) : {line}'))
129
+ notes.append(f' โŒ s{i:3} [{where}] โ†’{n:3} ๋ฒ”์œ„ ๋ฐ–(์ด {total}) : {line}')
130
+ elif n == i:
131
+ notes.append(f' โš ๏ธ s{i:3} [{where}] โ†’{n:3} ์ž๊ธฐ ์ž์‹  ์ฐธ์กฐ : {line}')
132
+ else:
133
+ notes.append(f' ยท s{i:3} [{where}] โ†’{n:3} : {line}')
134
+ return findings, notes