@chrono-meta/fh-gate 3.2.0 → 3.5.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 (106) hide show
  1. package/.claude/registry/agent_cards.json +1 -1
  2. package/.claude/rules/fh_4axis_gate.md +25 -0
  3. package/.claude-plugin/marketplace.json +9 -3
  4. package/AGENTS.md +2 -2
  5. package/CATALOG.md +4 -4
  6. package/CHEATSHEET.md +1 -1
  7. package/CLAUDE.md +4 -4
  8. package/README.md +2 -2
  9. package/docs/OUTPUT_EVIDENCE.md +1 -1
  10. package/docs/STANDARDS_ALIGNMENT.md +1 -1
  11. package/docs/codex-compat.md +2 -2
  12. package/knowledge/shared/harness-core/agents_md_runtime_details.md +2 -2
  13. package/knowledge/shared/harness-core/field_verdict_crossfamily_gate.md +30 -3
  14. package/knowledge/shared/harness-core/iso_ai_standards_crosswalk.md +1 -1
  15. package/knowledge/shared/harness-core/skill_quality_rubric.md +1 -1
  16. package/knowledge/shared/learnings/subagent_invocations_log.yaml +120 -4
  17. package/knowledge/shared/rules/modes_and_value.md +2 -2
  18. package/package.json +9 -1
  19. package/plugins/fh-commons/.claude-plugin/plugin.json +2 -2
  20. package/plugins/fh-commons/README.md +38 -0
  21. package/plugins/fh-meta/.claude-plugin/plugin.json +1 -1
  22. package/plugins/fh-meta/CHANGELOG.md +117 -0
  23. package/plugins/fh-meta/skills/agent-composer/SKILL.md +2 -2
  24. package/plugins/fh-meta/skills/auto-decorrelation/SKILL.md +40 -0
  25. package/plugins/fh-meta/skills/frontier-digest/SKILL.md +1 -1
  26. package/plugins/fh-meta/skills/frontier-digest/SKILL_detail.md +43 -5
  27. package/plugins/fh-meta/skills/{hub-cc-pr-reviewer → harness-pr-reviewer}/SKILL.md +75 -3
  28. package/plugins/fh-meta/skills/{hub-cc-pr-reviewer → harness-pr-reviewer}/SKILL_detail.md +2 -2
  29. package/plugins/fh-meta/skills/harvest-loop/SKILL_detail.md +2 -2
  30. package/plugins/fh-meta/skills/install-doctor/SKILL.md +1 -1
  31. package/plugins/fh-meta/skills/install-wizard/SKILL.md +1 -1
  32. package/plugins/fh-meta/skills/meta-prompt-builder/SKILL.md +1 -1
  33. package/plugins/fh-meta/skills/pipeline-conductor/SKILL.md +1 -1
  34. package/plugins/fh-meta/skills/plugin-recommender/SKILL.md +1 -1
  35. package/plugins/fh-meta/skills/sim-conductor/SKILL.md +1 -1
  36. package/plugins/fh-preprep/.claude-plugin/plugin.json +22 -0
  37. package/plugins/fh-preprep/README.md +24 -0
  38. package/plugins/{fh-commons → fh-preprep}/skills/preprep/SKILL.md +75 -3
  39. package/plugins/{fh-commons → fh-preprep}/skills/preprep/fixtures/mk_slide_fixtures.py +152 -1
  40. package/plugins/fh-preprep/skills/preprep/lane_attr_consistency.py +370 -0
  41. package/plugins/fh-preprep/skills/preprep/lane_geometry.py +339 -0
  42. package/plugins/fh-preprep/skills/preprep/lane_screen_parity.py +232 -0
  43. package/plugins/fh-preprep/skills/preprep/oox.py +305 -0
  44. package/plugins/{fh-commons → fh-preprep}/skills/preprep/preprep.py +52 -4
  45. package/plugins/fh-preprep/skills/preprep/safe_install.py +369 -0
  46. package/plugins/{fh-commons → fh-preprep}/skills/preprep/surfaces.example.yaml +29 -0
  47. package/plugins/fh-preprep/skills/preprep/test_preprep_lanes_rp.py +1417 -0
  48. package/plugins/fh-preprep/skills/preprep/usage_ledger.py +191 -0
  49. package/plugins/fh-qp/.claude-plugin/plugin.json +1 -1
  50. package/scripts/fh-run.sh +13 -0
  51. package/scripts/field_canon_preload.sh +1 -1
  52. package/scripts/finding_fleet.sh +391 -13
  53. package/scripts/finding_pipeline.sh +370 -11
  54. package/scripts/finding_verifier.sh +31 -2
  55. package/scripts/finding_verify.py +198 -13
  56. package/scripts/frontier_digest_autopilot.sh +3 -3
  57. package/scripts/gate_shape_scan.sh +16 -2
  58. package/scripts/selfcheck.sh +12 -8
  59. package/scripts/test_fh_run_resolver_lanes.sh +116 -0
  60. package/scripts/test_files_manifest_shipping_lanes.sh +31 -0
  61. package/scripts/test_finding_pipeline_lanes.sh +1556 -4
  62. package/scripts/test_gate_shape_scan_lanes.sh +11 -0
  63. package/scripts/test_marker_crossfamily_lanes.sh +75 -3
  64. package/scripts/test_marker_standpoint_lanes.sh +31 -6
  65. package/scripts/test_preprep_adjacent_dup_lanes.sh +1 -1
  66. package/scripts/test_preprep_diagram_lanes.sh +1 -1
  67. package/scripts/test_preprep_drift_anchor.sh +27 -12
  68. package/scripts/test_preprep_drift_anchor_lanes.sh +4 -2
  69. package/scripts/test_preprep_font_lanes.sh +7 -7
  70. package/scripts/test_preprep_progression_lanes.sh +1 -1
  71. package/scripts/test_preprep_promise_lanes.sh +1 -1
  72. package/scripts/test_preprep_retired_lanes.sh +45 -2
  73. package/scripts/test_preprep_slide_refs_lanes.sh +5 -5
  74. package/scripts/test_usage_ledger_lanes.sh +243 -0
  75. package/scripts/usage_report.py +211 -0
  76. package/templates/.git-hooks/pre-commit +152 -6
  77. package/templates/local_fh_context.md +1 -1
  78. package/templates/regression_guard.sh +1 -1
  79. package/plugins/fh-commons/skills/preprep/lane_geometry.py +0 -181
  80. package/plugins/fh-commons/skills/preprep/test_preprep_lanes_rp.py +0 -184
  81. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/README.md +0 -0
  82. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/canon_terms.example.yaml +0 -0
  83. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/diagram_from_json.py +0 -0
  84. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/fixtures/adjdup_known_negative.md +0 -0
  85. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/fixtures/adjdup_known_positive.md +0 -0
  86. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/fixtures/fixture_R3_negative.pptx +0 -0
  87. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/fixtures/fixture_R3_positive.pptx +0 -0
  88. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/fixtures/font_revert_probe.py +0 -0
  89. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/fixtures/progression_known_negative.md +0 -0
  90. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/fixtures/progression_known_positive.md +0 -0
  91. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/fixtures/promise_known_negative.md +0 -0
  92. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/fixtures/promise_known_positive.md +0 -0
  93. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/interslide_deps.py +0 -0
  94. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/jargon_terms.example.yaml +0 -0
  95. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/lane_adjacent_dup.py +0 -0
  96. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/lane_diagram.py +0 -0
  97. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/lane_font.py +0 -0
  98. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/lane_progression.py +0 -0
  99. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/lane_promise.py +0 -0
  100. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/lane_slide_refs.py +0 -0
  101. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/lane_slide_relations.py +0 -0
  102. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/ooxml/c1_baseline.txt +0 -0
  103. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/ooxml/gate.py +0 -0
  104. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/preprep_wire.py +0 -0
  105. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/presentation_checklist.md +0 -0
  106. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/test_lane_font.py +0 -0
@@ -805,8 +805,25 @@ validate_defense_leg() { # $1 = marker path — fires ONLY at the floor tiers (s
805
805
  # would block every in-flight branch and teach `--no-verify`, which disarms the Destructive-Op
806
806
  # gate sharing this hook.
807
807
  RESIDENCY_TOKEN_GRACE_DATE="2026-09-05"
808
+ # 🟥 `evidence=` — panel(...) 이 «무엇을 받았는지» 를 적게 한다. 2026-09-12 신설, 근거는 외부 실측이다:
809
+ # arXiv:2609.10969 (48 템플릿 · 2,880 시나리오 · **고정예산 2×2**) 에서 «같은 증거를 읽는 교차-모델
810
+ # 투표» 는 위험 제안의 **62.9 %** 를 승인하고 «독립 출처» 는 **22.9 %** — 출처 효과 40.9 %p 대
811
+ # 모델 다양성 11.3 %p, **3.6 배**. 즉 이 훅이 이미 하드 차단하는 축(계열)이 약한 쪽이고,
812
+ # `panel(codex, gemini)` 가 **같은 diff 를 읽은 패널**이면 그것은 62.9 % 팔인데 기록은 강한 값으로
813
+ # 남는다. 그 구분을 닫는 토큰이다. 🟥 새 필드도 새 게이트도 만들지 않는다 — residency= 와 같은
814
+ # 자리(crossfamily 의 grounds) 에 같은 문법으로 얹는다.
815
+ EVIDENCE_TOKEN_GRACE_DATE="2026-09-12"
816
+ # 🟥 `standpoint:` tier2+ grounds — 2026-09-12 부로 **advisory 에서 차단으로**. 근거가 바뀌었다:
817
+ # 이 carve-out 은 증거가 FH 자기 관찰 n=4+8 뿐일 때 합리적이었다. arXiv:2609.10969 이 같은 비교를
818
+ # n=2,880 에서 **40.9 %p 대 11.3 %p** 로 재면서, 「강한 축의 grounds 를 advisory 로 두고 약한 축을
819
+ # 하드 차단한다」가 더는 균형이 아니게 됐다. §Mechanization Boundary 검사는 여전히 통과한다 —
820
+ # «저자가 실행한 명령과 본 출력을 명명했는가» 는 **기록의 속성**(존재·비공허·귀속가능)이지 결론이 아니다.
821
+ # 🟥 정직한 범위: 이 게이트는 **기록의 형태**만 막는다. 그 실행이 진짜였는지는 여전히 안 본다 —
822
+ # 게임 가능성은 §Mechanization Boundary 가 사람에게 남긴 의도된 잔여이고 이 변경이 닫는 구멍이 아니다.
823
+ STANDPOINT_GROUNDS_GRACE_DATE="2026-09-12"
808
824
  validate_crossfamily_leg() { # $1 = marker path
809
825
  local m="$1" line val reason fams mdate _res_active _res_tokens _res_ntok _res_wellformed _res_kind
826
+ local _ev_active _ev_tokens _ev_ntok _ev_full _ev_nfull _ev_wellformed _ev_kind _ev_body _ev_grace _ev_strip
810
827
  if [ ! -f "$m" ] || ! grep -qE '^[[:space:]]*crossfamily:[[:space:]]*[^[:space:]]' "$m"; then
811
828
  echo " ❌ FAIL — load-bearing file staged with no 'crossfamily:' line in the Axes 2-3 marker."
812
829
  echo " Verdict/gate/irreversible-surface code shares the author's blind spot with a"
@@ -869,6 +886,31 @@ validate_crossfamily_leg() { # $1 = marker path
869
886
  _res_kind=""
870
887
  [ "$_res_wellformed" -eq 1 ] && _res_kind=$(printf '%s' "$_res_full" | sed -E 's/^residency=([A-Za-z_]*)\(.*$/\1/')
871
888
 
889
+ # evidence= token — same grammar and same presence-first rule as residency= above. REQUIRED for
890
+ # panel(...) on markers dated >= EVIDENCE_TOKEN_GRACE_DATE; format-only-if-present elsewhere.
891
+ # 🟥 상수를 «먼저» 평가한다(cross-family agy 적발). 이걸 mdate 검사 뒤에 두면 두 결함이 같이 난다:
892
+ # ⓐ 미주입 시 `[ "$mdate" \< "" ]` 이 거짓이 되어 **grace 가 통째로 꺼지고** pre-grace 마커가
893
+ # 전부 막힌다(실측 확인) ⓑ mdate 가 비면 단축평가로 `:?` 가드가 아예 안 돈다.
894
+ _ev_grace="${EVIDENCE_TOKEN_GRACE_DATE:?EVIDENCE_TOKEN_GRACE_DATE unset — partial extraction of this hook? refusing to guess a grace date}"
895
+ _ev_active=1
896
+ if [ -n "$mdate" ] && [ "$mdate" \< "$_ev_grace" ]; then _ev_active=0; fi
897
+ _ev_tokens=$(printf '%s' "$reason" | grep -oE 'evidence=[^[:space:]]*' 2>/dev/null || true)
898
+ _ev_ntok=0
899
+ [ -n "$_ev_tokens" ] && _ev_ntok=$(printf '%s\n' "$_ev_tokens" | grep -c .)
900
+ # 🟥 `[^)]*` 는 중첩 괄호에서 안쪽 `)` 에 끊긴다 — `evidence=MIXED(codex(diff), gemini(repo))` 의
901
+ # 본문이 `codex(diff` 로 잘려 «공허» 판정을 받고 **정당한 기록이 하드 차단**됐다(실측).
902
+ # 한 겹 중첩을 허용한다(두 겹은 여전히 형식 위반 — 무한 중첩을 정규식으로 쫓지 않는다).
903
+ _ev_full=$(printf '%s' "$reason" | grep -oE 'evidence=(SHARED|INDEPENDENT|MIXED)\(([^()]|\([^()]*\))*\)' 2>/dev/null || true)
904
+ _ev_nfull=0
905
+ [ -n "$_ev_full" ] && _ev_nfull=$(printf '%s\n' "$_ev_full" | grep -c .)
906
+ _ev_wellformed=0
907
+ [ "$_ev_ntok" -eq 1 ] && [ "$_ev_nfull" -eq 1 ] && _ev_wellformed=1
908
+ _ev_kind=""; _ev_body=""
909
+ if [ "$_ev_wellformed" -eq 1 ]; then
910
+ _ev_kind=$(printf '%s' "$_ev_full" | sed -E 's/^evidence=([A-Z]*)\(.*$/\1/')
911
+ _ev_body=$(printf '%s' "$_ev_full" | sed -E 's/^evidence=[A-Z]*\(//; s/\)$//')
912
+ fi
913
+
872
914
  case "$val" in
873
915
  declined)
874
916
  # `declined` means ONE thing: the OPERATOR declined sidecars (UAP), a chosen floor. It was
@@ -1022,6 +1064,81 @@ validate_crossfamily_leg() { # $1 = marker path
1022
1064
  return 1
1023
1065
  fi
1024
1066
  fi
1067
+ # evidence= — WHAT the panel received. Same four checks in the same order as residency=
1068
+ # (duplicate → absent → shape → meaning). Why this exists at all: arXiv:2609.10969 measured
1069
+ # the two axes against each other at fixed call budget (48 templates, 2,880 scenarios) and a
1070
+ # cross-model vote over SHARED evidence approved 62.9% of unsafe proposals vs 22.9% with an
1071
+ # independent source — 40.9pp vs 11.3pp for model diversity. A `panel(codex, gemini)` that
1072
+ # read the same diff is that 62.9% arm while recording as this enum's STRONG value. The token
1073
+ # makes the distinction sayable; it does not make shared evidence illegal.
1074
+ # 🟥 grace 여부와 무관하게, 토큰이 «있으면» 형식은 본다 — residency 가 `DEGRADED_*` 에서 하는
1075
+ # 약속과 같은 모양이다. 이게 없으면 pre-grace 마커의 `evidence=PARTIAL(...)` 이 조용히
1076
+ # 통과한다(실측). 부재는 grace 가 면제하지만, **잘못 쓴 것은 면제하지 않는다.**
1077
+ if [ "$_ev_active" -eq 0 ] && [ "$_ev_ntok" -gt 0 ] && [ "$_ev_wellformed" -ne 1 ]; then
1078
+ echo " ❌ FAIL — 'evidence=' token present but malformed (pre-grace marker: the token is"
1079
+ echo " OPTIONAL here, but a token that IS written must be well-formed):"
1080
+ printf '%s\n' "$_ev_tokens" | sed 's/^/ /'
1081
+ echo " Closed set evidence=(SHARED|INDEPENDENT|MIXED)(...) with a closing paren."
1082
+ return 1
1083
+ fi
1084
+ if [ "$_ev_active" -eq 1 ]; then
1085
+ if [ "$_ev_ntok" -gt 1 ]; then
1086
+ echo " ❌ FAIL — grounds carries MORE THAN ONE 'evidence=' token."
1087
+ printf '%s\n' "$_ev_tokens" | sed 's/^/ /'
1088
+ echo " Same trap as two crossfamily: lines — the reader takes the first. Leave one."
1089
+ return 1
1090
+ fi
1091
+ if [ "$_ev_ntok" -eq 0 ]; then
1092
+ echo " ❌ FAIL — panel(...) with no 'evidence=' token in grounds."
1093
+ echo " State WHAT the panel received, because that axis carries ~3.6x the effect of"
1094
+ echo " family diversity (arXiv:2609.10969: shared evidence approves 62.9% of unsafe"
1095
+ echo " proposals vs 22.9% independent; 40.9pp vs 11.3pp). One of:"
1096
+ echo " evidence=SHARED(<what every family read — e.g. the same diff>)"
1097
+ echo " evidence=INDEPENDENT(<what each got separately — own checkout, own run>)"
1098
+ echo " evidence=MIXED(<which member got which>)"
1099
+ echo " e.g. crossfamily: panel($fams) — residency=CLEAN(files=3) · evidence=SHARED(same"
1100
+ echo " staged diff to both) · R1..R2, 4 findings"
1101
+ echo " 🟥 SHARED is a legal, common answer — it is not a failure. Recording it as if"
1102
+ echo " it were INDEPENDENT is the failure this token exists to stop."
1103
+ return 1
1104
+ fi
1105
+ if [ "$_ev_wellformed" -ne 1 ]; then
1106
+ echo " ❌ FAIL — 'evidence=' token present but not one well-formed"
1107
+ echo " evidence=(SHARED|INDEPENDENT|MIXED)(...) token:"
1108
+ printf '%s\n' "$_ev_tokens" | sed 's/^/ /'
1109
+ echo " Closed set with a closing paren — same shape as residency=, same reason: a"
1110
+ echo " malformed token must be malformed, never invisible."
1111
+ return 1
1112
+ fi
1113
+ # Meaning check, and it is deliberately only on SHARED/MIXED: those are the values whose
1114
+ # whole content is "which evidence did they actually share", so an empty body says nothing.
1115
+ # INDEPENDENT is self-describing; over-blocking it would train the override.
1116
+ # 🟥 본문 검사, 두 층 — cross-family agy 가 초판의 두 결함을 다 지목했다:
1117
+ # ⓐ **빈 본문과 자리표시자는 세 값 모두 차단**한다. 초판은 INDEPENDENT 를 통째로 면제해서
1118
+ # `evidence=INDEPENDENT()` 와 `evidence=INDEPENDENT(<what>)` 가 **통과했다**(실측 fail-open).
1119
+ # «자기서술적이라 본문 검사 면제» 는 «본문이 없어도 된다» 와 다른 말이었는데 초판이 접었다.
1120
+ # ⓑ 길이 바는 **문자 수**로 센다. `wc -w` 는 `same-diff` 도 `동일diff` 도 1 낱말로 세서
1121
+ # 정당한 한국어/하이픈 기록을 막았다(실측) — 한국어로 근거를 쓰는 이 레포에서 이건 상습 과차단이다.
1122
+ _ev_strip=$(printf '%s' "$_ev_body" | tr -d '[:space:]')
1123
+ if [ -z "$_ev_strip" ] \
1124
+ || printf '%s' "$_ev_body" | grep -qE '^[[:space:]]*(TBD|tbd|N/?A|n/?a|없음|미정|-+|\?+|<[^>]*>)[[:space:]]*$'; then
1125
+ echo " ❌ FAIL — evidence=$_ev_kind(...) has an EMPTY or placeholder body: '$_ev_body'"
1126
+ echo " All three values need a body — 'self-describing' meant it is not length-checked,"
1127
+ echo " never that it can be blank. Name what each member actually received."
1128
+ return 1
1129
+ fi
1130
+ case "$_ev_kind" in
1131
+ SHARED|MIXED)
1132
+ # 문자 수 기준(공백 제외). 4자 미만이면 «diff» 같은 한 토큰이고, 그건 어느 산출물인지
1133
+ # 말하지 않는다. 🟥 낱말 수로 세지 않는다 — CJK 와 하이픈 복합어가 전부 1 낱말이 된다.
1134
+ if [ "$(printf '%s' "$_ev_strip" | wc -m | tr -d ' ')" -lt 6 ]; then
1135
+ echo " ❌ FAIL — evidence=$_ev_kind(...) body too thin to identify the artifact: '$_ev_body'"
1136
+ echo " Name it (\"same staged diff\", \"one findings.jsonl\", \"동일 프롬프트 파일\")."
1137
+ echo " A bare $_ev_kind(diff) records the weak arm as if it were documented."
1138
+ return 1
1139
+ fi ;;
1140
+ esac
1141
+ fi
1025
1142
  echo " ✅ cross-family leg: $val${reason:+ — $(printf '%s' "$reason" | cut -c1-60)}" ;;
1026
1143
  DEGRADED_SINGLE_FAMILY|DEGRADED_PANEL_UNUSED|UNKNOWN)
1027
1144
  # A degrade is allowed; being silent about WHICH degrade, or about its grounds, is not.
@@ -1129,6 +1246,7 @@ validate_crossfamily_leg() { # $1 = marker path
1129
1246
  # and 0 over-blocks. Anchor: scripts/test_marker_standpoint_lanes.sh
1130
1247
  validate_standpoint_leg() { # $1 = marker path
1131
1248
  local m="$1" line val reason n quoted=""
1249
+ local _sp_mdate _sp_block _sp_grace
1132
1250
  n=$(grep -cE '^[[:space:]]*standpoint:' "$m" 2>/dev/null); n=${n:-0}
1133
1251
  if [ "$n" -eq 0 ]; then
1134
1252
  echo " ❌ FAIL — load-bearing file staged with no 'standpoint:' line in the Axes 2-3 marker."
@@ -1174,13 +1292,41 @@ validate_standpoint_leg() { # $1 = marker path
1174
1292
  tier1b\(*\)|tier2\(*\)|tier2b\(*\)|tier3\(*\))
1175
1293
  case "$val" in
1176
1294
  tier2\(*\)|tier2b\(*\)|tier3\(*\))
1295
+ # 🟥 키워드에 «뒤따르는 공백» 을 박아두면 정당한 기록이 막힌다 — cross-family agy 적발,
1296
+ # 실측 확인: `cargo check` · `python scripts/eval.py` · `ran: ./ci.sh` 가 전부 BLOCK 이었다.
1297
+ # advisory 일 때는 경고 한 줄이었지만 **차단이 된 순간 이건 override 를 훈련시키는 결함**이다.
1298
+ # ⇒ ⓐ 경계를 «비-알파벳» 으로 바꾸고(콜론·세미콜론·줄끝 허용) ⓑ 흔한 러너를 넣고
1299
+ # ⓒ «출력» 의 형태(`30/30`, `rc=`, `./path`)도 실행의 증거로 받는다.
1300
+ # ⚠️ 방향: 이건 **느슨하게** 만드는 변경이다. 막고 싶은 것은 «명령도 출력도 안 적은 줄» 이고,
1301
+ # 길이 20 자 하한은 그대로 남는다.
1177
1302
  if [ "${#reason}" -lt 20 ] || ! printf '%s' "$reason" \
1178
- | grep -qiE 'ran |run |실행|돌렸|호출|invok|call|rc=|exit |output|출력|suite|스위트|test|bash |grep |npm |pytest|command|커맨드|arm'; then
1179
- echo " ⚠️ standpoint leg: $val recorded, but the grounds do not NAME an execution."
1180
- echo " ${val%%(*} asserts code RAN in the target. The discriminator is mechanical:"
1181
- echo " name the command you ran and the output you saw. If you only read files,"
1182
- echo " the honest rung is tier1b — weaker on purpose, so the owed arm stays visible."
1183
- echo " Advisory by design (see the named residual above this function)."
1303
+ | grep -qiE '(^|[^[:alpha:]])(ran|run|runs|exec|invoke[ds]?|call|calls|called|exit|output|suite|test|tests|tested|bash|sh|zsh|grep|npm|pnpm|yarn|pytest|python|python3|cargo|make|go|gradle|mvn|dotnet|node|command)([^[:alpha:]]|$)|실행|돌렸|돌린|돌려|호출|출력|스위트|커맨드|rc=|[0-9]+/[0-9]+|\./'; then
1304
+ # Grace: markers dated before STANDPOINT_GROUNDS_GRACE_DATE keep the old advisory
1305
+ # behaviour (no retro-blocking), same pattern as the residency=/evidence= tokens.
1306
+ _sp_mdate=$(printf '%s' "$m" | sed -nE 's/.*_([0-9]{4}-[0-9]{2}-[0-9]{2})\.marker$/\1/p')
1307
+ _sp_block=1
1308
+ # Unreadable date NOT exempt. The hook stamps TODAY into every marker filename it
1309
+ # creates, so an unparseable date is an unexpected shape, not evidence of age.
1310
+ # 🟥 상수를 «먼저» 평가한다 — 초판은 `[ -n "$_sp_mdate" ] && [ ... ${VAR:?} ]` 였고,
1311
+ # mdate 가 비면 단축평가로 `:?` 가드가 **아예 안 돌았다**(agy 적발). 가드가 있다고
1312
+ # 적어놓고 안 도는 것은 장식이다([[feedback_anchor_can_be_decorative]]).
1313
+ _sp_grace="${STANDPOINT_GROUNDS_GRACE_DATE:?STANDPOINT_GROUNDS_GRACE_DATE unset — partial extraction of this hook? refusing to guess a grace date}"
1314
+ if [ -n "$_sp_mdate" ] && [ "$_sp_mdate" \< "$_sp_grace" ]; then _sp_block=0; fi
1315
+ if [ "$_sp_block" -eq 1 ]; then
1316
+ echo " ❌ FAIL — standpoint leg: $val — the grounds do not NAME an execution."
1317
+ echo " ${val%%(*} asserts code RAN in the target. The discriminator is mechanical:"
1318
+ echo " name the command you ran and the output you saw. If you only read files,"
1319
+ echo " the honest rung is tier1b — weaker on purpose, so the owed arm stays visible."
1320
+ echo " standpoint: tier2(<harness>) — ran \`bash scripts/x.sh\` there, output: 30/30 PASS"
1321
+ echo " 🟥 BLOCKING since 2026-09-12 (was advisory). Why it changed:"
1322
+ echo " arXiv:2609.10969 measured evidence-source diversity at 40.9pp against 11.3pp"
1323
+ echo " for model diversity (n=2,880, fixed call budget) — this is the STRONGER axis,"
1324
+ echo " and leaving its grounds advisory while hard-blocking the weaker one was not a"
1325
+ echo " balance. Honest scope: this blocks on the RECORD's shape, never on whether the"
1326
+ echo " run was real."
1327
+ return 1
1328
+ fi
1329
+ echo " ⚠️ standpoint leg: $val — grounds name no execution (pre-$STANDPOINT_GROUNDS_GRACE_DATE marker, advisory)."
1184
1330
  else
1185
1331
  echo " ✅ standpoint leg: $val$quoted"
1186
1332
  fi ;;
@@ -11,7 +11,7 @@ description: forge-harness path and skill list pointer — local only, do not co
11
11
  **forge-harness path**: `~/path/to/forge-harness` (replace with your actual install path)
12
12
  **Session records**: `{FH_ROOT}/tracks/_meta/`
13
13
 
14
- **Available skills (fh-meta, 35)**: agent-composer · dialogue-harvest · fh · apex-review · asset-placement-gate · auto-decorrelation · context-doctor · contention-layer · corpus-grounding-expander · cross-ecosystem-synergy-detection · deep-clarify · edit-manifest · field-harvest · frontier-digest · goal-quench · harness-doctor · harvest-loop · hub-cc-pr-reviewer · install-doctor · install-wizard · marketplace-gate · memory-hygiene · meta-prompt-builder · persona-roster-expander · phantom-quench · pipeline-conductor · plugin-recommender · prompt-regression · public-surface-audit · return-path-gate · sim-conductor · salience-splitter · steel-quench · verify-bidirectional · video-ingest
14
+ **Available skills (fh-meta, 35)**: agent-composer · dialogue-harvest · fh · apex-review · asset-placement-gate · auto-decorrelation · context-doctor · contention-layer · corpus-grounding-expander · cross-ecosystem-synergy-detection · deep-clarify · edit-manifest · field-harvest · frontier-digest · goal-quench · harness-doctor · harvest-loop · harness-pr-reviewer · install-doctor · install-wizard · marketplace-gate · memory-hygiene · meta-prompt-builder · persona-roster-expander · phantom-quench · pipeline-conductor · plugin-recommender · prompt-regression · public-surface-audit · return-path-gate · sim-conductor · salience-splitter · steel-quench · verify-bidirectional · video-ingest
15
15
 
16
16
  **Available skills (fh-commons)**: convergence-loop · deliberation · mcp-circuit-breaker · token-budget-gate
17
17
 
@@ -15,7 +15,7 @@
15
15
  # 구분하려면 stdout 의 `REGRESSION_GUARD_RESULT=skip` 을 보라. 종료코드만 보는 호출자는
16
16
  # 미검사를 통과로 읽는다(2026-07-22: pre-commit 이 정확히 그랬고, AGENTS.md 변경이
17
17
  # 그 경로로 '✅ PASS' 를 받고 지나갔다).
18
- # 배선 현황: pre-commit ✅ / harness-doctor · harvest-loop · hub-cc-pr-reviewer ·
18
+ # 배선 현황: pre-commit ✅ / harness-doctor · harvest-loop · harness-pr-reviewer ·
19
19
  # self_evolution_routine = **미배선(종료코드만 판정)** — 알려진 잔여.
20
20
  #
21
21
  # PR mode rationale: using 'main' as BASE_REF for a PR branch includes changes from OTHER
@@ -1,181 +0,0 @@
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
@@ -1,184 +0,0 @@
1
- #!/usr/bin/env python3
2
- """R1~R5 · P1/P3 self-test — known-pair 계량 + 실물 재현.
3
-
4
- exit 0 = 전부 통과 · 1 = 실패 있음 · 2 = 픽스처 생성 불가(python-pptx 부재 등, UNMEASURED)
5
-
6
- 구성:
7
- ① known-pair — `fixtures/mk_slide_fixtures.py` 로 R1·R2·R4·R5·P1·P3 를 테스트 시점에 만든다
8
- (R3 는 예외 — 실물 구조 2장을 줄인 `fixtures/fixture_R3_{positive,negative}.pptx` 를 쓴다.
9
- 선두 줄 판별이 절대좌표·srgbClr 런 구조에 기대므로, 검출기 가정대로 «생성»한 픽스처는
10
- 검출기 자신을 검증 못 한다 — 자기참조. `tracks-meta` 원장이 지목한 방침)
11
- ② --baseline 델타 모드 — 편집 전/후 한 쌍에서 새 어긋남 1건만 잡히는지
12
- ③ 실물 재현 — 실제 백업 pptx(환경변수 PREPREP_REAL_CORPUS 가 가리키는 디렉터리, 있을 때만)에 돌려 원장이 인용한 실사고 4+1건이
13
- 그대로 뜨는지 확인한다. 그 코퍼스가 없는 머신에서는 SKIP(«통과»로 세지 않는다).
14
- """
15
- import os
16
- import sys
17
-
18
- HERE = os.path.dirname(os.path.abspath(__file__))
19
- sys.path.insert(0, HERE)
20
- sys.path.insert(0, os.path.join(HERE, 'fixtures'))
21
-
22
- PASS, FAIL, SKIP = 0, 0, 0
23
-
24
-
25
- def ok(msg):
26
- global PASS
27
- PASS += 1
28
- print(' ✅', msg)
29
-
30
-
31
- def ng(msg):
32
- global FAIL
33
- FAIL += 1
34
- print(' ❌', msg)
35
-
36
-
37
- def sk(msg):
38
- global SKIP
39
- SKIP += 1
40
- print(' ⏭ ', msg, '— SKIPPED (PASS 아님)')
41
-
42
-
43
- def codes_of(deck_path, LSR):
44
- deck = LSR.Deck(deck_path)
45
- F = LSR.analyze(deck)
46
- return F, {k for k, _, _ in F}
47
-
48
-
49
- def run_known_pairs(fx_dir):
50
- print('\n[1] known-pair — R1·R2·R4·R5·P1·P3 (생성 픽스처) + R3 (실물 픽스처)')
51
- import lane_slide_relations as LSR
52
- import lane_geometry as LG
53
-
54
- # R1
55
- _, cp = codes_of(os.path.join(fx_dir, 'r1_pos.pptx'), LSR)
56
- _, cn = codes_of(os.path.join(fx_dir, 'r1_neg.pptx'), LSR)
57
- ok('R1 known-positive 검출') if 'R1' in cp else ng(f'R1 known-positive 미검출 ({cp})')
58
- ok('R1 known-negative 무검출') if 'R1' not in cn else ng(f'R1 known-negative에서 오탐 ({cn})')
59
-
60
- # R2
61
- _, cp = codes_of(os.path.join(fx_dir, 'r2_pos.pptx'), LSR)
62
- _, cn = codes_of(os.path.join(fx_dir, 'r2_neg.pptx'), LSR)
63
- ok('R2 known-positive 검출') if 'R2' in cp else ng(f'R2 known-positive 미검출 ({cp})')
64
- ok('R2 known-negative 무검출') if 'R2' not in cn else ng(f'R2 known-negative에서 오탐 ({cn})')
65
-
66
- # R3 — 실물 픽스처 (자기참조 회피)
67
- r3p = os.path.join(HERE, 'fixtures', 'fixture_R3_positive.pptx')
68
- r3n = os.path.join(HERE, 'fixtures', 'fixture_R3_negative.pptx')
69
- if os.path.exists(r3p) and os.path.exists(r3n):
70
- _, cp = codes_of(r3p, LSR)
71
- _, cn = codes_of(r3n, LSR)
72
- ok('R3 known-positive(실물 2장) 검출') if 'R3' in cp else ng(f'R3 known-positive 미검출 ({cp})')
73
- ok('R3 known-negative(실물 2장) 무검출') if 'R3' not in cn else ng(f'R3 known-negative에서 오탐 ({cn})')
74
- else:
75
- sk('R3 known-pair — fixture_R3_{positive,negative}.pptx 없음')
76
-
77
- # R4
78
- Fp, cp = codes_of(os.path.join(fx_dir, 'r4_pos.pptx'), LSR)
79
- Fn, cn = codes_of(os.path.join(fx_dir, 'r4_neg.pptx'), LSR)
80
- ok('R4 known-positive 검출(아웃라이어 500자)') if 'R4' in cp else ng(f'R4 known-positive 미검출 ({cp})')
81
- ok('R4 known-negative 무검출(동값 분포)') if 'R4' not in cn else ng(f'R4 known-negative에서 오탐 ({cn})')
82
-
83
- # R5
84
- Fp, cp = codes_of(os.path.join(fx_dir, 'r5_pos.pptx'), LSR)
85
- Fn, cn = codes_of(os.path.join(fx_dir, 'r5_neg.pptx'), LSR)
86
- ok('R5 known-positive 검출(비율 30.0)') if 'R5' in cp else ng(f'R5 known-positive 미검출 ({cp})')
87
- ok('R5 known-negative 무검출(동일 비율)') if 'R5' not in cn else ng(f'R5 known-negative에서 오탐 ({cn})')
88
-
89
- # P1 — 3팔(임계 안 소폭 이동은 뜼다 · 임계 밖 큰 이동은 안 뜼다 · 무이동은 안 뜼다)
90
- lines, compared = LG.collect_lines(os.path.join(fx_dir, 'p1.pptx'), 200000, 63500)
91
- p1_lines = [l for l in lines if 'gate_bar' in l]
92
- ok(f'P1 대조된 도형-쌍 {compared} > 0 (비교 안함 아님)') if compared > 0 else ng('P1 compared=0 — UNMEASURED')
93
- ok(f'P1 후보 정확히 1건(임계 안 이동만)') if len(p1_lines) == 1 else ng(f'P1 후보 {len(p1_lines)}건 (want 1) — {p1_lines}')
94
- if p1_lines:
95
- ok('P1 유일 후보가 정확히 (1→2p) 소폭 이동 — 큰 이동(2→3p)·무이동(3→4p)은 안 뜬다') \
96
- if '1p→2' in p1_lines[0].replace(' ', '') else ng(f'P1 후보가 엉뚱한 쌍을 가리킨다 {p1_lines}')
97
-
98
- # P3
99
- lines3, _ = LG.collect_lines(os.path.join(fx_dir, 'p3.pptx'), 200000, 63500)
100
- p3_lines = [l for l in lines3 if 'stub' in l]
101
- ok('P3 후보 정확히 1건(임계 안 틀/겹침만)') if len(p3_lines) == 1 else ng(f'P3 후보 {len(p3_lines)}건 (want 1) — {p3_lines}')
102
-
103
-
104
- def run_baseline_delta(fx_dir):
105
- print('\n[2] --baseline 델타 모드 — 편집 전/후 한 쌍')
106
- import lane_geometry as LG
107
- new, gone, cmp_b, cmp_c = LG.delta(os.path.join(fx_dir, 'geo_base.pptx'),
108
- os.path.join(fx_dir, 'geo_edited.pptx'), 200000, 63500)
109
- ok('델타 새 어긋남 정확히 1건') if len(new) == 1 else ng(f'델타 새 어긋남 {len(new)}건 (want 1)')
110
- ok('델타 사라진 어긋남 0건(깨끗한 기준본)') if len(gone) == 0 else ng(f'델타 사라진 어긋남 {len(gone)}건 (want 0)')
111
- ok(f'기준본 대조 {cmp_b} > 0 · 현재본 대조 {cmp_c} > 0') if cmp_b > 0 and cmp_c > 0 \
112
- else ng(f'기준본/현재본 대조 0 있음 — UNMEASURED (cmp_b={cmp_b}, cmp_c={cmp_c})')
113
- # scan(cfg, root) 래퍼로도 동일하게 도는지 확인 (preprep.py 가 실제로 부르는 경로)
114
- cfg = {'surfaces_by_id': {'built_deck': {'path': os.path.join(fx_dir, 'geo_edited.pptx')}},
115
- 'geometry': {'baseline': os.path.join(fx_dir, 'geo_base.pptx')}}
116
- fP, nP = LG.scan(cfg, '.')
117
- ok('scan(cfg, root) 래퍼가 델타 모드로 도고 새 어긋남 1건을 notes 에 낸다') \
118
- if any('새 어긋남 1건' in n for n in nP) else ng(f'scan() 델타 출력 이상 — {nP}')
119
-
120
-
121
- REAL_BASE = os.environ.get('PREPREP_REAL_CORPUS', '') # 기본값 없음 — 다른 머신에서 거짓 SKIP 방지(SKIP != PASS)
122
- REAL_TARGETS = [
123
- ('ifkakao26_slides_v1.3_pre-engine-strip_20260904_210857.pptx', 'R1', 40,
124
- '선두 줄이 «그러나»로 뒤집는데'),
125
- ('ifkakao26_slides_v1.3_pre-40p_20260904_213452.pptx', 'R2', 41,
126
- '«이 셋/세 가지»라 부르는데 번호가 없다'),
127
- ('ifkakao26_slides_v1.3_pre-38trim_20260904_210428.pptx', 'R4', 38,
128
- '화면 250자'),
129
- ('ifkakao26_slides_v1.3_pre-40p_20260904_213452.pptx', 'R5', 112,
130
- '29.2자/초'),
131
- ]
132
-
133
-
134
- def run_real_corpus():
135
- print('\n[3] 실물 재현 (PREPREP_REAL_CORPUS 백업, 있을 때만) — 원장이 인용한 실사고 4+1건')
136
- if not os.path.isdir(REAL_BASE):
137
- sk(f'실물 코퍼스 미지정/미존재({REAL_BASE or "PREPREP_REAL_CORPUS unset"}) — SKIP, 통과 아님')
138
- return
139
- import lane_slide_relations as LSR
140
- import lane_geometry as LG
141
- for fname, code, want_page, want_sub in REAL_TARGETS:
142
- p = os.path.join(REAL_BASE, fname)
143
- if not os.path.exists(p):
144
- sk(f'{fname} 없음 — 재현 건너뜀')
145
- continue
146
- deck = LSR.Deck(p)
147
- F = LSR.analyze(deck)
148
- hit = [x for x in F if x[0] == code and abs(x[1] - want_page) <= 1 and want_sub in x[2]]
149
- ok(f'{code} 실물 재현 — {fname} 약 {want_page}p — {hit[0][2][:60] if hit else ""}') \
150
- if hit else ng(f'{code} 실물 미재현 — {fname}')
151
-
152
- # P1/P3 36p (보너스 확인 — 원장이 명시적으로 요구한 4건에는 없지만 신호 자체의 원적 사건)
153
- align36 = os.path.join(REAL_BASE, 'ifkakao26_slides_v1.3_pre-align36_222442.pptx')
154
- if os.path.exists(align36):
155
- lines, compared = LG.collect_lines(align36, 200000, 63500)
156
- hit_p1 = any('s5017' in l and '35000' in l and '35p' in l.replace(' ', '') for l in lines)
157
- hit_p3 = any('s5016' in l and 's5017' in l and '35000' in l for l in lines)
158
- ok('P1 실물 재현 — 35p→36p s5017 x -35,000') if hit_p1 else ng('P1 실물 미재현')
159
- ok('P3 실물 재현 — 36p s5016→s5017 겹침 35,000') if hit_p3 else ng('P3 실물 미재현')
160
- else:
161
- sk('ifkakao26_slides_v1.3_pre-align36_222442.pptx 없음 — P1/P3 보너스 재현 건너뜀')
162
-
163
-
164
- def main():
165
- try:
166
- import mk_slide_fixtures
167
- except ImportError as e:
168
- print(f'❌ python-pptx 또는 픽스처 모듈 부재 — UNMEASURED: {e}')
169
- return 2
170
- import tempfile
171
- fx_dir = tempfile.mkdtemp(prefix='preprep_lanes_rp_')
172
- mk_slide_fixtures.build_all(fx_dir)
173
- print(f'픽스처 생성: {fx_dir}')
174
-
175
- run_known_pairs(fx_dir)
176
- run_baseline_delta(fx_dir)
177
- run_real_corpus()
178
-
179
- print(f'\n결과: {PASS} passed · {FAIL} failed · {SKIP} skipped (skip != pass)')
180
- return 1 if FAIL else 0
181
-
182
-
183
- if __name__ == '__main__':
184
- sys.exit(main())