@chrono-meta/fh-gate 3.4.0 → 3.6.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 (66) hide show
  1. package/.claude-plugin/marketplace.json +9 -3
  2. package/README.md +2 -2
  3. package/docs/codex-compat.md +1 -1
  4. package/knowledge/shared/learnings/subagent_invocations_log.yaml +89 -4
  5. package/package.json +8 -1
  6. package/plugins/fh-commons/.claude-plugin/plugin.json +2 -2
  7. package/plugins/fh-meta/.claude-plugin/plugin.json +1 -1
  8. package/plugins/fh-meta/CHANGELOG.md +47 -0
  9. package/plugins/fh-preprep/.claude-plugin/plugin.json +22 -0
  10. package/plugins/fh-preprep/README.md +24 -0
  11. package/plugins/{fh-commons → fh-preprep}/skills/preprep/SKILL.md +75 -3
  12. package/plugins/{fh-commons → fh-preprep}/skills/preprep/fixtures/mk_slide_fixtures.py +152 -1
  13. package/plugins/fh-preprep/skills/preprep/lane_attr_consistency.py +370 -0
  14. package/plugins/fh-preprep/skills/preprep/lane_geometry.py +339 -0
  15. package/plugins/fh-preprep/skills/preprep/lane_screen_parity.py +232 -0
  16. package/plugins/fh-preprep/skills/preprep/oox.py +305 -0
  17. package/plugins/{fh-commons → fh-preprep}/skills/preprep/preprep.py +52 -4
  18. package/plugins/fh-preprep/skills/preprep/safe_install.py +369 -0
  19. package/plugins/{fh-commons → fh-preprep}/skills/preprep/surfaces.example.yaml +29 -0
  20. package/plugins/fh-preprep/skills/preprep/test_preprep_lanes_rp.py +1417 -0
  21. package/plugins/fh-preprep/skills/preprep/usage_ledger.py +231 -0
  22. package/plugins/fh-qp/.claude-plugin/plugin.json +1 -1
  23. package/scripts/fh-run.sh +13 -0
  24. package/scripts/field_canon_preload.sh +1 -1
  25. package/scripts/selfcheck.sh +12 -8
  26. package/scripts/test_fh_run_resolver_lanes.sh +116 -0
  27. package/scripts/test_files_manifest_shipping_lanes.sh +31 -0
  28. package/scripts/test_preprep_adjacent_dup_lanes.sh +1 -1
  29. package/scripts/test_preprep_diagram_lanes.sh +1 -1
  30. package/scripts/test_preprep_drift_anchor.sh +27 -12
  31. package/scripts/test_preprep_drift_anchor_lanes.sh +4 -2
  32. package/scripts/test_preprep_font_lanes.sh +7 -7
  33. package/scripts/test_preprep_progression_lanes.sh +1 -1
  34. package/scripts/test_preprep_promise_lanes.sh +1 -1
  35. package/scripts/test_preprep_retired_lanes.sh +45 -2
  36. package/scripts/test_preprep_slide_refs_lanes.sh +5 -5
  37. package/scripts/test_usage_ledger_lanes.sh +284 -0
  38. package/scripts/usage_report.py +211 -0
  39. package/plugins/fh-commons/skills/preprep/lane_geometry.py +0 -181
  40. package/plugins/fh-commons/skills/preprep/test_preprep_lanes_rp.py +0 -184
  41. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/README.md +0 -0
  42. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/canon_terms.example.yaml +0 -0
  43. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/diagram_from_json.py +0 -0
  44. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/fixtures/adjdup_known_negative.md +0 -0
  45. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/fixtures/adjdup_known_positive.md +0 -0
  46. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/fixtures/fixture_R3_negative.pptx +0 -0
  47. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/fixtures/fixture_R3_positive.pptx +0 -0
  48. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/fixtures/font_revert_probe.py +0 -0
  49. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/fixtures/progression_known_negative.md +0 -0
  50. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/fixtures/progression_known_positive.md +0 -0
  51. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/fixtures/promise_known_negative.md +0 -0
  52. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/fixtures/promise_known_positive.md +0 -0
  53. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/interslide_deps.py +0 -0
  54. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/jargon_terms.example.yaml +0 -0
  55. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/lane_adjacent_dup.py +0 -0
  56. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/lane_diagram.py +0 -0
  57. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/lane_font.py +0 -0
  58. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/lane_progression.py +0 -0
  59. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/lane_promise.py +0 -0
  60. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/lane_slide_refs.py +0 -0
  61. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/lane_slide_relations.py +0 -0
  62. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/ooxml/c1_baseline.txt +0 -0
  63. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/ooxml/gate.py +0 -0
  64. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/preprep_wire.py +0 -0
  65. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/presentation_checklist.md +0 -0
  66. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/test_lane_font.py +0 -0
@@ -0,0 +1,231 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """사용 원장 — 하네스가 «누구에게 얼마나 쓰이는가» 를 남긴다. 표준 라이브러리만 쓴다.
4
+
5
+ ## 🟥 옵트인이다 — `FH_USAGE_LEDGER` 가 없으면 **완전 무동작**이다.
6
+
7
+ 이 파일은 npm 으로 배포되는 하네스 안에 들어간다. 기본값으로 켜 두면 소비자가 동의한 적 없는
8
+ 계측이 남의 머신에서 돌게 되고, 그건 이 저장소가 §Operational Adaptation Loop 에서 금지하는
9
+ 형태다(동의는 «부재 ≠ 승인»). 그래서 스위치는 **경로를 직접 주는 것**이고, 경로를 주는 행위
10
+ 자체가 동의의 표시다.
11
+
12
+ ```
13
+ export FH_USAGE_LEDGER="$HOME/.local/state/fh-usage" # 이 줄이 없으면 아무 일도 안 난다
14
+ export FH_USAGE_AUTHOR=1 # 저자 본인 머신에서만
15
+ ```
16
+
17
+ ## 무엇을 적고 무엇을 안 적나 — residency 가 여기서 정해진다
18
+
19
+ 적는다 : 시각(분 단위) · 하네스 이름 · 진입점 이름(화이트리스트 형태) · **actor 해시** ·
20
+ 벽시계 ms · CPU ms · 반환 코드 · 결과 부류
21
+ 🟥 안 적는다 : **인자 · 경로 · 파일명 · 티켓 id · URL · 사용자 이름 · 호스트 이름.**
22
+ 접근이 제한된 환경에서도 돌 수 있는 하네스라 이건 취향이 아니라 경계다. 인자 한 칸을
23
+ 열어두면 그 칸으로 조직 내부 식별자가 흘러나간다 — 그래서 인자를 받는 필드가 **아예 없다**.
24
+
25
+ `actor` = sha256(salt + 사용자 + 호스트)[:12]. salt 는 원장 디렉터리 안 `.salt`(0600)에만 있고
26
+ 어디로도 안 나간다. ⚠️ **한계를 이름으로 남긴다**: 같은 사람이 머신 둘을 쓰면 **2 명**으로 세고,
27
+ 공용 머신을 여럿이 쓰면 **1 명**으로 센다. 그래서 이 값은 «사람 수»가 아니라 «머신-사용자 수»다.
28
+
29
+ ## degrade 방향 — 호스트를 절대 안 죽인다, 그러나 조용히 죽지도 않는다
30
+
31
+ 원장 쓰기가 실패해도 예외를 호스트로 올리지 않는다(관측이 대상을 죽이면 안 된다). 대신
32
+ 같은 디렉터리의 `.errors` 를 1 증가시키고, 리포터가 그 수를 **커버리지 옆에 같이 찍는다** —
33
+ 「미측정을 0 으로 렌더하는」 부류를 안 만들기 위해서다.
34
+ """
35
+ import hashlib
36
+ import json
37
+ import os
38
+ import time
39
+
40
+ _SCHEMA = 1
41
+ _ENV_DIR = "FH_USAGE_LEDGER"
42
+ _ENV_AUTHOR = "FH_USAGE_AUTHOR"
43
+ _ENV_SALT = "FH_USAGE_SALT"
44
+
45
+
46
+ _OFF = ("off", "no", "none", "0", "false", "decline", "거절")
47
+
48
+
49
+ def _dir():
50
+ d = os.environ.get(_ENV_DIR, "").strip()
51
+ if not d or d.lower() in _OFF:
52
+ return None
53
+ return d
54
+
55
+
56
+ def notice(stream=None):
57
+ """미설정이면 «아직 안 정했다» 로 보고 안내를 낸다. 정하면 조용해진다.
58
+
59
+ 🟥 **세 상태를 가른다** — 이 저장소의 동의 규율 그대로다(부재 ≠ 승인, 그리고 거절도 기록이다):
60
+ · 미설정 아직 안 정함 → 매 실행 안내 (정할 때까지 계속 뜬다)
61
+ · 경로 지정 동의 → 조용, 기록 시작
62
+ · `off` 명시적 거절 → 조용, 기록 안 함
63
+
64
+ 🟥 **안내는 stderr 로만 간다.** stdout 은 이 도구의 판정 출력이고 기계가 읽는다 —
65
+ 거기에 한 줄이라도 끼면 부르는 쪽의 파싱이 깨진다(관측이 대상을 바꾸는 것과 같은 부류).
66
+ 🟥 **문구는 참인 이유만 쓴다.** 이 변수가 없어도 도구는 **완전히 같게** 동작하고 종료코드도
67
+ 같다(레인이 직접 잰다). 그러니 «제대로 안 돈다» 는 거짓이다 — 못 보는 것은 **우리 관측**이지
68
+ 그 사람의 도구가 아니다. 거짓으로 받은 동의는 그 동의로 얻은 숫자의 값도 같이 떨어뜨린다.
69
+ """
70
+ import sys as _sys
71
+ raw = os.environ.get(_ENV_DIR, "").strip()
72
+ if raw: # 경로든 off 든 — 정했으면 조용
73
+ return False
74
+ out = stream if stream is not None else _sys.stderr
75
+ try:
76
+ out.write(
77
+ "\n[usage] 이 도구가 얼마나 쓰이는지 아직 못 재고 있습니다 — 기본값이 꺼짐이라 그렇습니다.\n"
78
+ " 켜기: export FH_USAGE_LEDGER=\"$HOME/.fh-usage\"\n"
79
+ " 끄기: export FH_USAGE_LEDGER=off (그러면 이 안내가 사라집니다)\n"
80
+ " 기록되는 것: 시각(분)·도구명·진입점·실행시간·종료코드·익명 ID.\n"
81
+ " 기록 안 되는 것: 인자·경로·파일명·티켓번호·URL·사용자명·호스트명.\n"
82
+ " ⚠️ 켜든 끄든 도구 동작과 종료코드는 **완전히 같습니다**. 못 보는 것은\n"
83
+ " 여러분의 도구가 아니라 «여러분이 어디서 막히는지» 입니다.\n\n")
84
+ out.flush()
85
+ except Exception:
86
+ return False
87
+ return True
88
+
89
+
90
+ def _salt(d):
91
+ """salt 는 env 우선, 없으면 원장 디렉터리 안 파일. 없으면 만든다(0600)."""
92
+ s = os.environ.get(_ENV_SALT, "").strip()
93
+ if s:
94
+ return s
95
+ p = os.path.join(d, ".salt")
96
+ try:
97
+ # 🟥 cross-family codex 지목(2026-09-13, B-3): `.salt` 가 FIFO 면 열기가 **블록**한다.
98
+ # 이 함수는 `observe()` 의 finally 에서 불리므로, 그 순간 관측이 호스트를 멈춰 세운다.
99
+ # 관측은 대상의 타이밍 계약도 바꾸면 안 된다 → 정규 파일이 아니면 읽지 않는다.
100
+ # (O_NONBLOCK 은 FIFO 열기가 막히는 것까지 같이 막는 두 번째 자물쇠다.)
101
+ import stat as _stat
102
+ st = os.lstat(p)
103
+ if not _stat.S_ISREG(st.st_mode):
104
+ raise OSError("salt path is not a regular file")
105
+ fd = os.open(p, os.O_RDONLY | getattr(os, "O_NONBLOCK", 0))
106
+ try:
107
+ v = os.read(fd, 4096).decode("utf-8", "replace").strip()
108
+ finally:
109
+ os.close(fd)
110
+ if v:
111
+ return v
112
+ except FileNotFoundError:
113
+ pass
114
+ except Exception:
115
+ # 정규 파일이 아니거나 읽을 수 없다 → 아래 O_EXCL 생성이 실패하고 record() 가
116
+ # 그 예외를 잡아 `.errors` 를 올린다. 행은 안 남고 호스트는 안 멈춘다(fail-closed).
117
+ pass
118
+ import secrets
119
+ v = secrets.token_hex(16)
120
+ fd = os.open(p, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
121
+ try:
122
+ os.write(fd, v.encode())
123
+ finally:
124
+ os.close(fd)
125
+ return v
126
+
127
+
128
+ def _actor(d):
129
+ raw = "%s|%s" % (os.environ.get("USER") or os.environ.get("LOGNAME") or "?",
130
+ os.uname().nodename if hasattr(os, "uname") else "?")
131
+ return hashlib.sha256((_salt(d) + "|" + raw).encode()).hexdigest()[:12]
132
+
133
+
134
+ def _entry_name(name):
135
+ """진입점 이름만 받는다. 형태를 벗어나면 «other» — 인자가 이름에 섞여 들어오는 것 차단."""
136
+ ok = name and len(name) <= 40 and all(c.isalnum() or c in "_.-" for c in name)
137
+ return name if ok else "other"
138
+
139
+
140
+ def _bump_errors(d):
141
+ try:
142
+ p = os.path.join(d, ".errors")
143
+ n = 0
144
+ try:
145
+ with open(p) as f:
146
+ n = int((f.read() or "0").strip() or 0)
147
+ except Exception:
148
+ n = 0
149
+ with open(p, "w") as f:
150
+ f.write(str(n + 1))
151
+ except Exception:
152
+ pass
153
+
154
+
155
+ def record(harness, entry, wall_ms, cpu_ms, rc, outcome, counts=None):
156
+ """원장에 한 줄. 실패해도 예외를 밖으로 안 낸다."""
157
+ d = _dir()
158
+ if not d:
159
+ return False
160
+ try:
161
+ os.makedirs(d, exist_ok=True)
162
+ row = {
163
+ "v": _SCHEMA,
164
+ # 🟥 분 단위로 자른다 — 초 단위 타임스탬프는 개인 행동 패턴을 더 세밀히 남긴다.
165
+ "ts": time.strftime("%Y-%m-%dT%H:%MZ", time.gmtime()),
166
+ "h": _entry_name(harness),
167
+ "e": _entry_name(entry),
168
+ "actor": _actor(d),
169
+ "author": 1 if os.environ.get(_ENV_AUTHOR, "").strip() in ("1", "true", "yes") else 0,
170
+ "wall_ms": int(wall_ms),
171
+ "cpu_ms": int(cpu_ms),
172
+ "rc": rc if isinstance(rc, int) else None,
173
+ "out": outcome if outcome in ("DONE", "EXIT", "ERROR") else "ERROR",
174
+ # LLM 호출이 있는 경로만 채운다. 없으면 null — 🟥 0 이 아니다.
175
+ "llm": None,
176
+ }
177
+ if isinstance(counts, dict):
178
+ row["n"] = {k: v for k, v in counts.items()
179
+ if isinstance(k, str) and isinstance(v, int) and len(k) <= 16}
180
+ path = os.path.join(d, "%s-%s.jsonl" % (row["h"], time.strftime("%Y-%m", time.gmtime())))
181
+ # append 는 O_APPEND 라 한 줄이 4 KiB 미만이면 동시 실행에서도 안 섞인다(POSIX).
182
+ # 🟥 0600 으로 만든다 — 행에 actor 해시와 사용 시각이 남으므로 기본 umask(0644)로
183
+ # 두면 같은 머신의 다른 계정이 읽는다. `.salt` 만 잠그고 원장을 열어두면 반쪽이다.
184
+ # 같은 이유로 원장 파일도 정규 파일일 때만 쓴다(FIFO 로 바꿔치기하면 append 가 막힌다).
185
+ if os.path.exists(path):
186
+ import stat as _stat
187
+ if not _stat.S_ISREG(os.lstat(path).st_mode):
188
+ raise OSError("ledger path is not a regular file")
189
+ fd = os.open(path, os.O_WRONLY | os.O_APPEND | os.O_CREAT
190
+ | getattr(os, "O_NONBLOCK", 0), 0o600)
191
+ try:
192
+ os.write(fd, (json.dumps(row, ensure_ascii=False, sort_keys=True) + "\n").encode("utf-8"))
193
+ finally:
194
+ os.close(fd)
195
+ return True
196
+ except Exception:
197
+ # 🟥 여기서 예외를 올리지 않는 것은 «관측이 대상을 죽이면 안 되기» 때문이고, 그래서
198
+ # default-toward-PASS 로 보인다. 정당화는 **조용하지 않다**는 것이다 — 실패는
199
+ # `.errors` 로 세어지고 리포터가 커버리지 옆에 같이 찍는다(안 막는 것과 안 보이는 것은 다르다).
200
+ # ⚠️ 닫히지 않은 잔여를 이름으로 남긴다: 디렉터리가 통째로 못 쓰이면 `_bump_errors` 도
201
+ # 같이 실패해서 **카운터조차 안 남는다**. 그 경우는 리포터가 쓰기 권한을 직접 보고 잡는다
202
+ # (usage_report.py `--dir` 쓰기 가능 검사). 원장 혼자서는 못 닫는다.
203
+ _bump_errors(d)
204
+ return False
205
+
206
+
207
+ def observe(harness, entry, fn, *args, **kwargs):
208
+ """`fn` 을 그대로 실행하고 반환값·예외를 **건드리지 않고** 통과시킨다.
209
+
210
+ 🟥 이 함수는 판정을 만들지도 바꾸지도 않는다. 종료코드는 `fn` 이 정하고 여기는 읽기만 한다.
211
+ 그래서 게이트 코드가 아니다 — 관측이 판정을 바꾸는 순간 그 숫자는 아무 의미가 없다.
212
+ """
213
+ notice()
214
+ t0, c0 = time.monotonic(), time.process_time()
215
+ rc, outcome = None, "ERROR"
216
+ try:
217
+ rc = fn(*args, **kwargs)
218
+ outcome = "DONE"
219
+ return rc
220
+ except SystemExit as e:
221
+ rc = e.code if isinstance(e.code, int) else None
222
+ outcome = "EXIT"
223
+ raise
224
+ except BaseException:
225
+ outcome = "ERROR"
226
+ raise
227
+ finally:
228
+ record(harness, entry,
229
+ (time.monotonic() - t0) * 1000.0,
230
+ (time.process_time() - c0) * 1000.0,
231
+ rc, outcome)
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fh-qp",
3
- "version": "3.4.0",
3
+ "version": "3.6.0",
4
4
  "engines": {
5
5
  "claudeCode": ">=1.0.0"
6
6
  },
package/scripts/fh-run.sh CHANGED
@@ -153,10 +153,23 @@ resolve_unit() {
153
153
  if [[ -n "$plugin" ]]; then
154
154
  candidates+=("$FH_ROOT/plugins/$plugin/skills/$bare/SKILL.md")
155
155
  fi
156
+ # 🟥 2026-09-13 — 종전엔 이 목록이 fh-meta·fh-commons **둘만** 하드코딩했다. 그래서
157
+ # `fh-qp` 의 스킬 4개는 **처음부터** 이름만으로 안 잡혔고(선재 갭 — `--skill qp` 가
158
+ # «unable to resolve»), preprep 을 자기 플러그인으로 승격하자 거기에 하나가 더 붙었다.
159
+ # cross-family(codex)가 그 이동에서 이걸 지목했다 — 레포 어느 검사도 이 해석 경로를
160
+ # 안 보고 있었다. ⇒ 목록을 **플러그인 전체로 일반화**한다.
161
+ # 우선순위는 보존한다: 명시 지정 → fh-meta → fh-commons → 나머지(사전순, 결정적).
162
+ # 이름이 겹칠 때 종전 해석이 안 바뀌게 하려는 것이고, 정렬은 파일시스템 순서에
163
+ # 기대지 않기 위해서다.
156
164
  candidates+=(
157
165
  "$FH_ROOT/plugins/fh-meta/skills/$bare/SKILL.md"
158
166
  "$FH_ROOT/plugins/fh-commons/skills/$bare/SKILL.md"
159
167
  )
168
+ local _p
169
+ while IFS= read -r _p; do
170
+ case "$_p" in */fh-meta/*|*/fh-commons/*) continue ;; esac
171
+ candidates+=("$_p")
172
+ done < <(ls -d "$FH_ROOT"/plugins/*/skills/"$bare"/SKILL.md 2>/dev/null | sort)
160
173
  for f in "${candidates[@]}"; do
161
174
  [[ -f "$f" ]] && printf "%s\n" "$f" && return 0
162
175
  done
@@ -149,7 +149,7 @@ skill_canon_emit() {
149
149
  SKEOF
150
150
  [ -z "$SENT_DIR" ] || : > "$SENT_DIR/skill-$_sk" 2>/dev/null || true
151
151
  }
152
- skill_canon_emit preprep "plugins/fh-commons/skills/preprep" \
152
+ skill_canon_emit preprep "plugins/fh-preprep/skills/preprep" \
153
153
  "발표" "장표" "슬라이드" "덱" "대본" "리허설" "presentation" "keynote"
154
154
 
155
155
  # 매핑된 프로젝트 = tracks/ 하위 디렉토리(언더스코어 접두는 메타라 제외)
@@ -649,6 +649,10 @@ fi
649
649
  # resolves to "SKIP (subject not in files[], and absent)" — the anchor arm is never reached.
650
650
  _LANE_TO=""; command -v timeout >/dev/null 2>&1 && _LANE_TO="timeout 300"
651
651
  for _pair in \
652
+ `# 사용 원장 (2026-09-13 신설). SUBJECT 를 원장 모듈로 잡는다 — 배선이 끊기는 자리는 preprep.py 의 __main__ 한 줄이 아니라 모듈이 사라지는 쪽이고, import 가 try/except 라 모듈이 없으면 조용히 무동작으로 돌아간다. 그 무음이 이 레인이 잡을 것이다.` \
653
+ "plugins/fh-preprep/skills/preprep/usage_ledger.py|scripts/test_usage_ledger_lanes.sh" \
654
+ `# fh-run 의 이름→스킬 해석기 (2026-09-13 신설). preprep 승격이 --skill preprep 을 조용히 깨뜨렸고, fh-qp 의 4개는 애초부터 안 닿고 있었다. 소비자 대면 경로인데 커버리지가 0이었다.` \
655
+ "scripts/fh-run.sh|scripts/test_fh_run_resolver_lanes.sh" \
652
656
  "scripts/degrade_probe_capability.sh|scripts/test_capability_entrypoint_shipping.sh" \
653
657
  "scripts/chamber_run.sh|scripts/test_chamber_run_lanes.sh" \
654
658
  "scripts/chamber_candidate_collect.sh|scripts/test_chamber_sig_lanes.sh" \
@@ -676,7 +680,7 @@ for _pair in \
676
680
  "templates/.git-hooks/pre-commit|scripts/test_gate_two_verdicts_lanes.sh" \
677
681
  `# ── fh-qp (QP) — chamber run #18 EMIT 2026-09-05: qp_tools.sh known-pair + residency lanes ──` \
678
682
  "plugins/fh-qp/scripts/qp_tools.sh|scripts/test_fh_qp_lanes.sh" \
679
- "plugins/fh-commons/skills/preprep/diagram_from_json.py|scripts/test_preprep_diagram_lanes.sh" \
683
+ "plugins/fh-preprep/skills/preprep/diagram_from_json.py|scripts/test_preprep_diagram_lanes.sh" \
680
684
  `# ── action.yml — the GitHub Action wrapper: its exit-code mapping is where a typed verdict could become a boolean ──` \
681
685
  "action.yml|scripts/test_action_yml_lanes.sh" \
682
686
  "scripts/sim_isolated_run.sh|scripts/test_sim_path_isolation_lanes.sh" \
@@ -707,13 +711,13 @@ for _pair in \
707
711
  "scripts/fh-goal.sh|scripts/test_fh_goal_change_detection_lanes.sh" \
708
712
  "scripts/utterance_skill_probe.sh|scripts/test_utterance_skill_probe_lanes.sh" \
709
713
  `# ── preprep 스킬(2026-08-29). 주체는 스킬 안의 모듈이라 scripts/ 밖이다 ──` \
710
- "plugins/fh-commons/skills/preprep/preprep.py|scripts/test_preprep_retired_lanes.sh" \
711
- "plugins/fh-commons/skills/preprep/lane_progression.py|scripts/test_preprep_progression_lanes.sh" \
712
- "plugins/fh-commons/skills/preprep/lane_adjacent_dup.py|scripts/test_preprep_adjacent_dup_lanes.sh" \
713
- "plugins/fh-commons/skills/preprep/lane_promise.py|scripts/test_preprep_promise_lanes.sh" \
714
- "plugins/fh-commons/skills/preprep/lane_slide_refs.py|scripts/test_preprep_slide_refs_lanes.sh" \
715
- "plugins/fh-commons/skills/preprep/lane_font.py|scripts/test_preprep_font_lanes.sh" \
716
- "plugins/fh-commons/skills/preprep/SKILL.md|scripts/test_preprep_drift_anchor.sh" \
714
+ "plugins/fh-preprep/skills/preprep/preprep.py|scripts/test_preprep_retired_lanes.sh" \
715
+ "plugins/fh-preprep/skills/preprep/lane_progression.py|scripts/test_preprep_progression_lanes.sh" \
716
+ "plugins/fh-preprep/skills/preprep/lane_adjacent_dup.py|scripts/test_preprep_adjacent_dup_lanes.sh" \
717
+ "plugins/fh-preprep/skills/preprep/lane_promise.py|scripts/test_preprep_promise_lanes.sh" \
718
+ "plugins/fh-preprep/skills/preprep/lane_slide_refs.py|scripts/test_preprep_slide_refs_lanes.sh" \
719
+ "plugins/fh-preprep/skills/preprep/lane_font.py|scripts/test_preprep_font_lanes.sh" \
720
+ "plugins/fh-preprep/skills/preprep/SKILL.md|scripts/test_preprep_drift_anchor.sh" \
717
721
  "scripts/test_preprep_drift_anchor.sh|scripts/test_preprep_drift_anchor_lanes.sh" \
718
722
  "scripts/field_canon_preload.sh|scripts/test_skill_canon_preload_lanes.sh" \
719
723
  `# ── round/ 회차 계기 4종(2026-09-01). 넷 다 한 스위트가 잡는다 — 주체별로 행을 둔다 ──` \
@@ -0,0 +1,116 @@
1
+ #!/usr/bin/env bash
2
+ # fh-run 의 «이름 → 스킬 파일» 해석기 앵커.
3
+ #
4
+ # ## 왜 생겼나 (2026-09-13)
5
+ #
6
+ # preprep 을 자기 플러그인으로 승격하면서 `--skill preprep` 가 **조용히 깨졌다**.
7
+ # 해석기가 `fh-meta` · `fh-commons` 둘만 하드코딩하고 있었기 때문이다. 🟥 그리고 그건
8
+ # 새 결함이 아니라 **선재 갭**이었다 — `fh-qp` 의 스킬 4개는 **처음부터** 이름만으로 안 잡혔다
9
+ # (`--skill qp` → «unable to resolve»). 레포의 어떤 검사도 이 경로를 안 보고 있었고,
10
+ # cross-family(codex) 가 그 이동 리뷰에서 지목했다.
11
+ #
12
+ # 🟥 이 레인은 **스킬을 실행하지 않는다.** `FH_DRY_RUN=1` 로 조립된 프롬프트만 뽑는다.
13
+ # (옵션 `--dry-run` 은 존재하지 않는다 — 그렇게 부르면 프롬프트로 먹혀 **실제로 돈다**.
14
+ # 첫 검증에서 그걸 겪었다.)
15
+ #
16
+ # 실행: bash scripts/test_fh_run_resolver_lanes.sh
17
+ # 🟥 이 파일의 모든 «$VAR» 는 `${VAR}` 로 쓴다 — 변수 뒤에 ASCII 가 아닌 글자(» 등)가 바로
18
+ # 붙으면 셸이 그 바이트를 **변수 이름에 먹어** `VAR?: unbound variable` 로 죽는다.
19
+ # 이 세션에 같은 결함을 두 번 냈다(한 번 고치고 새 파일에 또 썼다). 중괄호가 그 경계다.
20
+ set -uo pipefail
21
+ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
22
+ cd "$HERE" || exit 2
23
+ PASS=0; FAIL=0
24
+ ok(){ printf ' ✅ %s\n' "$1"; PASS=$((PASS+1)); }
25
+ ng(){ printf ' ❌ %s\n' "$1"; FAIL=$((FAIL+1)); }
26
+
27
+ # 해석된 파일 경로만 뽑는다. 못 찾으면 빈 문자열.
28
+ # 🟥 `--backend codex` 를 **명시**한다. 안 그러면 `fh-run` 이 해석보다 **먼저** 백엔드를
29
+ # 자동탐지하고, `codex`·`claude` 가 없는 환경(=CI 러너)에서 `exit 10` 으로 죽는다.
30
+ # 초판 레인이 그걸 빼먹어 **로컬 6/6 · CI 0/6** 이 났다 — 컨트롤(F3)까지 빨개서 «해석 전에
31
+ # 죽는다»가 드러났고 그래서 오진하지 않았다. 명시하면 `FH_DRY_RUN=1` 이 실제 호출 전에
32
+ # 돌아오므로 바이너리가 없어도 된다.
33
+ # ⚠️ 명명된 잔여: 「백엔드 부재」가 「스킬 못 찾음」을 **가린다**(순서상 먼저 죽는다).
34
+ # 더 쓸모 있는 순서는 해석이 먼저지만, 그건 이 PR 의 범위가 아니라 별건이다.
35
+ resolve(){
36
+ FH_DRY_RUN=1 timeout 60 bash "$HERE/scripts/fh-run.sh" --backend codex --skill "$1" 2>&1 \
37
+ | /usr/bin/grep -oE 'plugins/[A-Za-z0-9_-]+/skills/[A-Za-z0-9_.-]+/SKILL\.md' | head -1
38
+ }
39
+
40
+ echo "== 이름만으로 어느 플러그인이든 닿나 =="
41
+ R=$(resolve preprep)
42
+ case "$R" in
43
+ plugins/fh-preprep/skills/preprep/SKILL.md) ok "F1 preprep → $R (승격 후에도 이름으로 닿는다)" ;;
44
+ "") ng "F1 preprep 미해결 — 승격이 소비자 표면을 깼다" ;;
45
+ *) ng "F1 preprep 이 엉뚱한 곳으로: $R" ;;
46
+ esac
47
+
48
+ R=$(resolve qp)
49
+ case "$R" in
50
+ plugins/fh-qp/skills/qp/SKILL.md) ok "F2 qp → $R (원래 못 닿던 선재 갭도 같이 닫혔다)" ;;
51
+ "") ng "F2 qp 미해결 — 일반화가 fh-qp 에 안 닿는다" ;;
52
+ *) ng "F2 qp 가 엉뚱한 곳으로: $R" ;;
53
+ esac
54
+
55
+ echo
56
+ echo "== 🟥 컨트롤 — 아무 이름이나 닿으면 위 둘은 아무것도 증명 안 한다 =="
57
+ OUT=$(FH_DRY_RUN=1 timeout 60 bash "$HERE/scripts/fh-run.sh" --backend codex --skill zz_no_such_skill_probe 2>&1); RC=$?
58
+ if [ "$RC" -ne 0 ] && printf '%s' "$OUT" | /usr/bin/grep -q 'unable to resolve'; then
59
+ ok "F3 없는 이름 → rc=$RC · «unable to resolve» (판별력 있음)"
60
+ else
61
+ ng "F3 없는 이름이 통과했다 (rc=$RC) — 해석기가 아무거나 잡는다"
62
+ fi
63
+
64
+ echo
65
+ echo "== 우선순위가 안 뒤집혔나 (일반화의 대가) =="
66
+ # fh-meta 에 실재하는 이름은 여전히 fh-meta 로 가야 한다.
67
+ M=$(ls plugins/fh-meta/skills 2>/dev/null | head -1)
68
+ if [ -n "$M" ]; then
69
+ R=$(resolve "$M")
70
+ case "$R" in
71
+ plugins/fh-meta/*) ok "F4 fh-meta 스킬 «${M}» 은 여전히 fh-meta 로 (우선순위 보존)" ;;
72
+ *) ng "F4 «${M}» 이 $R 로 갔다 — 일반화가 해석 순서를 바꿨다" ;;
73
+ esac
74
+ else
75
+ ng "F4 fh-meta 스킬을 못 찾았다 — 계기 오류(레포가 헐었거나 경로가 틀렸다)"
76
+ fi
77
+ C=$(ls plugins/fh-commons/skills 2>/dev/null | head -1)
78
+ if [ -n "$C" ]; then
79
+ R=$(resolve "$C")
80
+ case "$R" in
81
+ plugins/fh-commons/*) ok "F5 fh-commons 스킬 «${C}» 은 여전히 fh-commons 로" ;;
82
+ *) ng "F5 «${C}» 이 $R 로 갔다" ;;
83
+ esac
84
+ else
85
+ ng "F5 fh-commons 스킬을 못 찾았다 — 계기 오류"
86
+ fi
87
+
88
+ echo
89
+ echo "== 명시 지정이 여전히 이긴다 =="
90
+ R=$(resolve "fh-preprep:preprep")
91
+ case "$R" in
92
+ plugins/fh-preprep/skills/preprep/SKILL.md) ok "F6 plugin:name 형태도 해석된다" ;;
93
+ *) ng "F6 명시 지정 실패: ${R:-(없음)}" ;;
94
+ esac
95
+
96
+ echo
97
+ echo "== 백엔드 바이너리가 없는 환경에서도 같은 답인가 (CI 형태) =="
98
+ # 🟥 이 레인이 있는 이유: 초판이 로컬에서만 통과하고 CI 에서 6/6 빨갰다. PATH 를 깎아
99
+ # codex·claude 를 **실제로 없앤** 상태에서 재는 것이 그 차이를 로컬에서도 재현한다.
100
+ # 🟥 `timeout` 이 `env` «앞»이어야 한다 — GNU coreutils 의 timeout 은 /usr/bin 에 없어서
101
+ # PATH 를 깎은 «뒤»에 찾으면 command-not-found 로 죽는다. 초판이 그렇게 써서 F7 이
102
+ # 로컬에서도 빨갰고, 그게 사실은 «이 레인이 계기 고장을 잡았다»는 증거다.
103
+ R7=$(timeout 60 env PATH=/usr/bin:/bin FH_DRY_RUN=1 bash "$HERE/scripts/fh-run.sh" \
104
+ --backend codex --skill preprep 2>&1 \
105
+ | /usr/bin/grep -oE 'plugins/[A-Za-z0-9_-]+/skills/[A-Za-z0-9_.-]+/SKILL\.md' | head -1)
106
+ case "${R7}" in
107
+ plugins/fh-preprep/skills/preprep/SKILL.md) ok "F7 백엔드 바이너리 0개인 환경에서도 해석된다 (CI 와 같은 답)" ;;
108
+ "") ng "F7 백엔드 없는 환경에서 해석 실패 — 이 레인은 로컬 전용이다(= CI 에서 빨개진다)" ;;
109
+ *) ng "F7 엉뚱한 곳으로: ${R7}" ;;
110
+ esac
111
+
112
+ echo
113
+ echo "════════════════════════════════════════"
114
+ printf ' PASS %s FAIL %s\n' "$PASS" "$FAIL"
115
+ echo "════════════════════════════════════════"
116
+ [ "$FAIL" -eq 0 ] || exit 1
@@ -253,6 +253,37 @@ else
253
253
  fi
254
254
  rm -rf "$D10"
255
255
 
256
+ # ── P 갈래 (2026-09-13) — 파이썬 바이트코드가 발행 집합에 실리지 않나 ──────────────────
257
+ #
258
+ # 실사고: `plugins/*/skills` 가 디렉터리 항목이라 그 아래 `__pycache__/*.pyc` 를 통째로 끌어갔고,
259
+ # .pyc 안에는 **빌드한 머신의 절대 경로와 계정명**이 박혀 있다. 로컬 `npm pack` 이 그걸 실었다.
260
+ # 🟢 발행된 3.4.0·3.1.4 는 깨끗했다 — CI 가 «깨끗한 체크아웃»에서 패킹하기 때문이고, 그건
261
+ # **우연히 안전했던 것**이지 막혀 있던 게 아니다(폴백 = 로컬 발행 경로는 그대로 샜다).
262
+ # 🟥 `files[]` 가 있으면 `.npmignore` 도 `files[]` 부정 패턴도 이 자리에 안 먹는다(둘 다 실측).
263
+ # 그래서 막는 자리는 **`prepack`** 뿐이다.
264
+ echo
265
+ echo "== P: 발행 집합에 .pyc 가 없나 =="
266
+ _P_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
267
+ if ! /usr/bin/grep -q '"prepack"' "$_P_ROOT/package.json"; then
268
+ bad "P1 package.json 에 prepack 이 없다 — .pyc 를 막는 유일한 자리가 비었다"
269
+ else
270
+ ok "P1 prepack 선언 실재"
271
+ _PD="$_P_ROOT/plugins/fh-preprep/skills/preprep/__pycache__"
272
+ mkdir -p "$_PD" && printf 'x' > "$_PD/_probe_lane.cpython-313.pyc"
273
+ _PT=$(mktemp -d)
274
+ if ( cd "$_P_ROOT" && npm pack --pack-destination "$_PT" >/dev/null 2>&1 ); then
275
+ _n=$(tar tzf "$_PT"/*.tgz 2>/dev/null | /usr/bin/grep -c '[.]pyc$')
276
+ _c=$(tar tzf "$_PT"/*.tgz 2>/dev/null | /usr/bin/grep -c 'skills/preprep/lane_')
277
+ if [ "${_n:-x}" = "0" ]; then ok "P2 심어둔 .pyc 가 패킹본에 없다 (실측 ${_n}개)"
278
+ else bad "P2 .pyc ${_n}개가 실렸다 — prepack 이 안 돌거나 범위를 못 덮는다"; fi
279
+ if [ "${_c:-0}" -ge 5 ]; then ok "P3 컨트롤: 실물 lane 모듈은 그대로 실린다 (${_c}개) — 과삭제 아님"
280
+ else bad "P3 컨트롤 죽음: lane 모듈이 ${_c}개뿐 — prepack 이 너무 많이 지운다"; fi
281
+ else
282
+ bad "P2 npm pack 실패 — 계기 오류(판정 아님)"
283
+ fi
284
+ rm -rf "$_PT"; rm -f "$_PD/_probe_lane.cpython-313.pyc"
285
+ fi
286
+
256
287
  echo "files-manifest-shipping lanes: $pass passed, $fail failed"
257
288
  if [ "$fail" -gt 0 ]; then
258
289
  exit 1
@@ -11,7 +11,7 @@
11
11
  # J4 처방 몇 자 / 몇 % / 어느 문장인지를 내나 — 숫자 없는 지적은 처방이 아니다
12
12
  set -uo pipefail
13
13
  HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
14
- SKILL="$HERE/plugins/fh-commons/skills/preprep"
14
+ SKILL="$HERE/plugins/fh-preprep/skills/preprep"
15
15
  PASS=0; FAIL=0
16
16
  ok(){ echo " ✅ $1"; PASS=$((PASS+1)); }
17
17
  ng(){ echo " ❌ $1"; FAIL=$((FAIL+1)); }
@@ -14,7 +14,7 @@
14
14
  # «굽힘» 쪽 실사용은 tracks-meta/dispatch/2026-09-05_archify-preprep/REPORT.md 가 기록한다.
15
15
  set -uo pipefail
16
16
  HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
17
- SKILL="$HERE/plugins/fh-commons/skills/preprep"
17
+ SKILL="$HERE/plugins/fh-preprep/skills/preprep"
18
18
  PASS=0; FAIL=0
19
19
  ok(){ echo " ✅ $1"; PASS=$((PASS+1)); }
20
20
  ng(){ echo " ❌ $1"; FAIL=$((FAIL+1)); }
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env bash
2
2
  # test_preprep_drift_anchor.sh — 이원화의 단일-소스 앵커.
3
3
  #
4
- # preprep 은 두 진입점을 갖는다: FH 안의 스킬(plugins/fh-commons/skills/preprep/)과,
4
+ # preprep 은 두 진입점을 갖는다: FH 안의 스킬(plugins/fh-preprep/skills/preprep/)과,
5
5
  # 거기서 뽑아 세우는 standalone 현장 하네스. 🟥 **코드 사본이 둘이면 갈린다** —
6
6
  # FH 자신의 규칙이 그것을 «single source of truth collapse · double maintenance burden»
7
7
  # 이라 부른다. 그래서 이원화는 «복사본 둘»이 아니라 «단일 소스 + 얇은 두 진입점»이어야 하고,
@@ -14,7 +14,7 @@
14
14
  # D3 진입점 문서가 단일 소스를 가리키나 (죽은 포인터 금지)
15
15
  set -uo pipefail
16
16
  HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
17
- SRC="$HERE/plugins/fh-commons/skills/preprep"
17
+ SRC="$HERE/plugins/fh-preprep/skills/preprep"
18
18
  # standalone 배포 위치는 환경변수로 받는다. 기본값을 박으면 다른 머신에서 거짓 SKIP 이 된다.
19
19
  # 🟥 2026-09-03 — 그런데 «아무도 그 변수를 안 걸어서» D2 가 여태 SKIP 이었고, 그 사이 컴패니언
20
20
  # 저장소의 fork(724줄)가 정본(789줄)과 갈라져 L9~L11 을 안 부르고 있었다 — 정본 주석이 이미
@@ -32,19 +32,31 @@ ng(){ echo " ❌ $1"; FAIL=$((FAIL+1)); }
32
32
  sk(){ echo " ⏭ $1 — SKIPPED (**통과 아님**)"; SKIP=$((SKIP+1)); }
33
33
 
34
34
  # D1 — 단일 소스
35
+ # 🟥 2026-09-10 정정: 파일 목록을 **박아 두지 않는다.** 박아 둔 목록은 조용히 낡는다 —
36
+ # 이 검사는 「10파일 · python 7파일」이라고 출력하면서 그 사이 늘어난 레인 셋
37
+ # (lane_slide_relations · lane_geometry · lane_slide_refs)을 **한 번도 안 봤다.**
38
+ # 개수는 참이었고 «무엇에 대한 개수인가»가 어긋난 형태다
39
+ # ([[feedback_count_is_true_but_referent_drifted]]).
40
+ # ⇒ 필수 문서만 이름으로 걸고, **python 은 디렉터리에서 뽑는다** — 레인을 새로 지으면
41
+ # 자동으로 이 앵커 안에 들어온다. 배선을 잊는 것으로 검사를 벗어날 수 없다.
35
42
  missing=""
36
- for f in preprep.py interslide_deps.py lane_progression.py lane_adjacent_dup.py lane_promise.py lane_diagram.py diagram_from_json.py SKILL.md README.md surfaces.example.yaml; do
43
+ for f in SKILL.md README.md surfaces.example.yaml preprep.py; do
37
44
  [ -f "$SRC/$f" ] || missing="$missing $f"
38
45
  done
46
+ PYN=0
47
+ for f in "$SRC"/*.py; do [ -f "$f" ] && PYN=$((PYN+1)); done
39
48
  if [ -n "$missing" ]; then ng "D1 단일 소스 결손:$missing"
49
+ elif [ "$PYN" -lt 5 ]; then
50
+ ng "D1 python 파일이 $PYN 개뿐 — 스킬이 헐었거나 SRC 가 틀린 곳을 가리킨다"
40
51
  elif ! command -v python3 >/dev/null 2>&1; then
41
52
  sk "D1 구문 검사 — python3 부재라 «돌 수 있나»를 못 쟀다(UNMEASURED)"
42
53
  else
43
54
  synerr=""
44
- for f in preprep.py interslide_deps.py lane_progression.py lane_adjacent_dup.py lane_promise.py lane_diagram.py diagram_from_json.py; do
45
- python3 -c "import ast,sys;ast.parse(open(sys.argv[1],encoding='utf-8').read())" "$SRC/$f" 2>/dev/null || synerr="$synerr $f"
55
+ for f in "$SRC"/*.py; do
56
+ python3 -c "import ast,sys;ast.parse(open(sys.argv[1],encoding='utf-8').read())" "$f" 2>/dev/null \
57
+ || synerr="$synerr $(basename "$f")"
46
58
  done
47
- [ -z "$synerr" ] && ok "D1 단일 소스 10파일 실재 + python 7파일 구문 통과" \
59
+ [ -z "$synerr" ] && ok "D1 필수 문서 3 + preprep.py 실재 · python ${PYN}파일 전부 구문 통과(디렉터리에서 뽑음)" \
48
60
  || ng "D1 구문 실패:$synerr"
49
61
  fi
50
62
 
@@ -54,13 +66,16 @@ if [ -z "$DIST" ]; then
54
66
  elif [ ! -d "$DIST" ]; then
55
67
  ng "D2 $DIST_SRC 이 가리키는 곳이 없다: $DIST (설정됐는데 부재 = 드리프트 아니라 배선 결함)"
56
68
  else
57
- drift=""
58
- for f in preprep.py interslide_deps.py lane_progression.py lane_adjacent_dup.py; do
59
- if [ ! -f "$DIST/$f" ]; then drift="$drift $f(부재)"
60
- elif ! cmp -s "$SRC/$f" "$DIST/$f"; then drift="$drift $f(갈림)"; fi
69
+ # 🟥 같은 정정 — 여기도 목록이 박혀 있었고, 게다가 **네 개를 돌면서 「5파일」이라고 출력**했다.
70
+ # 라벨이 자기 루프에 대해서도 거짓말한 셈이라, 세는 것과 말하는 것을 한 변수로 묶는다.
71
+ drift=""; n=0
72
+ for f in "$SRC"/*.py; do
73
+ b=$(basename "$f"); n=$((n+1))
74
+ if [ ! -f "$DIST/$b" ]; then drift="$drift $b(부재)"
75
+ elif ! cmp -s "$f" "$DIST/$b"; then drift="$drift $b(갈림)"; fi
61
76
  done
62
- [ -z "$drift" ] && ok "D2 standalone 코드 5파일이 단일 소스와 바이트 동일 ($DIST_SRC)" \
63
- || ng "D2 드리프트:$drift ⇒ 사본이 둘이 됐다. 단일 소스에서 다시 뽑아라"
77
+ [ -z "$drift" ] && ok "D2 standalone python ${n}파일이 단일 소스와 바이트 동일 ($DIST_SRC)" \
78
+ || ng "D2 드리프트($n 중):$drift ⇒ 사본이 둘이 됐다. 단일 소스에서 다시 뽑아라"
64
79
  fi
65
80
 
66
81
  # D3 — 진입점 포인터가 죽었나
@@ -8,9 +8,11 @@
8
8
  # explicit var still wins · nothing set → SKIP (never a silent PASS).
9
9
  set -u
10
10
  HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"; A="$HERE/scripts/test_preprep_drift_anchor.sh"
11
- SRC="$HERE/plugins/fh-commons/skills/preprep"; T=$(mktemp -d); pass=0; fail=0
11
+ SRC="$HERE/plugins/fh-preprep/skills/preprep"; T=$(mktemp -d); pass=0; fail=0
12
12
  chk(){ if [ "$1" = 0 ]; then echo " ✅ $2"; pass=$((pass+1)); else echo " ❌ $2"; fail=$((fail+1)); fi; }
13
- mk(){ mkdir -p "$1"; for f in preprep.py interslide_deps.py lane_progression.py lane_adjacent_dup.py lane_promise.py; do cp "$SRC/$f" "$1/$f"; done; }
13
+ # 🟥 2026-09-11: 앵커(D2)는 «디렉터리의 *.py 전부» 돈다(2026-09-10 세션 정정). 여기 목록이 5개로 박혀 있어
14
+ # 새 레인 파일(lane_attr_consistency 등)이 «부재 드리프트» 로 읽혀 L2/L4 가 CI 에서 빨개졌다 — 앵커와 같은 규칙으로.
15
+ mk(){ mkdir -p "$1"; for f in "$SRC"/*.py; do cp "$f" "$1/$(basename "$f")"; done; }
14
16
  echo "[preprep-drift-anchor] D2 known pairs"
15
17
  # L1 nothing set → D2 SKIP (skip != pass), rc 0 (D2 is not a FAIL)
16
18
  out=$(env -u PREPREP_STANDALONE_DIR FH_COMPANION_STORE="$T/nostore" bash "$A" 2>&1); printf '%s' "$out" | grep -q "D2 .*SKIPPED"; chk $? "L1 var unset + no companion preprep → D2 SKIPPED (not PASS)"
@@ -16,15 +16,15 @@
16
16
  # 종료코드: 0 pass · 1 레인 실패 · 2 대상 부재 · 10 setup 실패
17
17
  set -uo pipefail
18
18
  cd "$(dirname "$0")/.." || exit 10
19
- LANE=plugins/fh-commons/skills/preprep/lane_font.py
20
- SELFTEST=plugins/fh-commons/skills/preprep/test_lane_font.py
21
- PROBE=plugins/fh-commons/skills/preprep/fixtures/font_revert_probe.py
19
+ LANE=plugins/fh-preprep/skills/preprep/lane_font.py
20
+ SELFTEST=plugins/fh-preprep/skills/preprep/test_lane_font.py
21
+ PROBE=plugins/fh-preprep/skills/preprep/fixtures/font_revert_probe.py
22
22
  [ -f "$LANE" ] || { echo "ⓘ $LANE absent — subject missing (NOT a pass)"; exit 2; }
23
23
  [ -f "$SELFTEST" ] || { echo "ⓘ $SELFTEST absent — subject missing (NOT a pass)"; exit 2; }
24
24
  [ -f "$PROBE" ] || { echo "ⓘ $PROBE absent — subject missing (NOT a pass)"; exit 2; }
25
25
  command -v python3 >/dev/null 2>&1 || { echo "ⓘ python3 absent — setup broke"; exit 10; }
26
26
  # 🟥 스테일 바이트코드가 뮤턴트/복원을 가린다 — 이웃 레인이 실측한 사고다(2026-09-06).
27
- rm -rf plugins/fh-commons/skills/preprep/__pycache__
27
+ rm -rf plugins/fh-preprep/skills/preprep/__pycache__
28
28
  PASS=0; FAIL=0
29
29
  ok(){ echo " ✅ $1"; PASS=$((PASS+1)); }
30
30
  ng(){ echo " ❌ $1"; FAIL=$((FAIL+1)); }
@@ -42,7 +42,7 @@ fi
42
42
 
43
43
  echo "== 배선·퇴화 — self-test 밖에서 한 번 더 =="
44
44
  OUT=$(python3 -c "
45
- import sys; sys.path.insert(0,'plugins/fh-commons/skills/preprep')
45
+ import sys; sys.path.insert(0,'plugins/fh-preprep/skills/preprep')
46
46
  import lane_font as L
47
47
  print(L.scan({}, '.')[1][0])
48
48
  print(L.scan({'surfaces_by_id':{'built_deck':{'path':'/nope/none.pptx'}}}, '.')[1][0])
@@ -51,11 +51,11 @@ echo "$OUT" | grep -q 'NOT_CONFIGURED' && echo "$OUT" | grep -q 'UNMEASURED' \
51
51
  && ok "미선언 → NOT_CONFIGURED · 실물 없음 → UNMEASURED (둘 다 0 아님)" \
52
52
  || ng "퇴화 표기가 0 으로 접힌다: $OUT"
53
53
 
54
- grep -q "import lane_font" plugins/fh-commons/skills/preprep/preprep.py \
54
+ grep -q "import lane_font" plugins/fh-preprep/skills/preprep/preprep.py \
55
55
  && ok "배선: preprep.py 가 이 레인을 부른다" \
56
56
  || ng "배선 없음 — 정의만 있고 아무도 안 부른다(built-but-not-wired)"
57
57
 
58
- grep -q "lane_font" plugins/fh-commons/skills/preprep/SKILL.md \
58
+ grep -q "lane_font" plugins/fh-preprep/skills/preprep/SKILL.md \
59
59
  && ok "명세: SKILL.md 가 이 레인을 적는다(고아 구현 아님)" \
60
60
  || ng "SKILL.md 에 없다 — 명세 없는 구현"
61
61
 
@@ -8,7 +8,7 @@
8
8
  # 🟥 P3 이 없으면 선언을 지우는 것만으로 이 레인이 조용해진다(가장 싼 무마 경로).
9
9
  set -uo pipefail
10
10
  HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
11
- SKILL="$HERE/plugins/fh-commons/skills/preprep"
11
+ SKILL="$HERE/plugins/fh-preprep/skills/preprep"
12
12
  PASS=0; FAIL=0
13
13
  ok(){ echo " ✅ $1"; PASS=$((PASS+1)); }
14
14
  ng(){ echo " ❌ $1"; FAIL=$((FAIL+1)); }