@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.
- package/.claude-plugin/marketplace.json +9 -3
- package/README.md +2 -2
- package/docs/codex-compat.md +1 -1
- package/knowledge/shared/learnings/subagent_invocations_log.yaml +89 -4
- package/package.json +8 -1
- package/plugins/fh-commons/.claude-plugin/plugin.json +2 -2
- package/plugins/fh-meta/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-meta/CHANGELOG.md +47 -0
- package/plugins/fh-preprep/.claude-plugin/plugin.json +22 -0
- package/plugins/fh-preprep/README.md +24 -0
- package/plugins/{fh-commons → fh-preprep}/skills/preprep/SKILL.md +75 -3
- package/plugins/{fh-commons → fh-preprep}/skills/preprep/fixtures/mk_slide_fixtures.py +152 -1
- package/plugins/fh-preprep/skills/preprep/lane_attr_consistency.py +370 -0
- package/plugins/fh-preprep/skills/preprep/lane_geometry.py +339 -0
- package/plugins/fh-preprep/skills/preprep/lane_screen_parity.py +232 -0
- package/plugins/fh-preprep/skills/preprep/oox.py +305 -0
- package/plugins/{fh-commons → fh-preprep}/skills/preprep/preprep.py +52 -4
- package/plugins/fh-preprep/skills/preprep/safe_install.py +369 -0
- package/plugins/{fh-commons → fh-preprep}/skills/preprep/surfaces.example.yaml +29 -0
- package/plugins/fh-preprep/skills/preprep/test_preprep_lanes_rp.py +1417 -0
- package/plugins/fh-preprep/skills/preprep/usage_ledger.py +231 -0
- package/plugins/fh-qp/.claude-plugin/plugin.json +1 -1
- package/scripts/fh-run.sh +13 -0
- package/scripts/field_canon_preload.sh +1 -1
- package/scripts/selfcheck.sh +12 -8
- package/scripts/test_fh_run_resolver_lanes.sh +116 -0
- package/scripts/test_files_manifest_shipping_lanes.sh +31 -0
- package/scripts/test_preprep_adjacent_dup_lanes.sh +1 -1
- package/scripts/test_preprep_diagram_lanes.sh +1 -1
- package/scripts/test_preprep_drift_anchor.sh +27 -12
- package/scripts/test_preprep_drift_anchor_lanes.sh +4 -2
- package/scripts/test_preprep_font_lanes.sh +7 -7
- package/scripts/test_preprep_progression_lanes.sh +1 -1
- package/scripts/test_preprep_promise_lanes.sh +1 -1
- package/scripts/test_preprep_retired_lanes.sh +45 -2
- package/scripts/test_preprep_slide_refs_lanes.sh +5 -5
- package/scripts/test_usage_ledger_lanes.sh +284 -0
- package/scripts/usage_report.py +211 -0
- package/plugins/fh-commons/skills/preprep/lane_geometry.py +0 -181
- package/plugins/fh-commons/skills/preprep/test_preprep_lanes_rp.py +0 -184
- /package/plugins/{fh-commons → fh-preprep}/skills/preprep/README.md +0 -0
- /package/plugins/{fh-commons → fh-preprep}/skills/preprep/canon_terms.example.yaml +0 -0
- /package/plugins/{fh-commons → fh-preprep}/skills/preprep/diagram_from_json.py +0 -0
- /package/plugins/{fh-commons → fh-preprep}/skills/preprep/fixtures/adjdup_known_negative.md +0 -0
- /package/plugins/{fh-commons → fh-preprep}/skills/preprep/fixtures/adjdup_known_positive.md +0 -0
- /package/plugins/{fh-commons → fh-preprep}/skills/preprep/fixtures/fixture_R3_negative.pptx +0 -0
- /package/plugins/{fh-commons → fh-preprep}/skills/preprep/fixtures/fixture_R3_positive.pptx +0 -0
- /package/plugins/{fh-commons → fh-preprep}/skills/preprep/fixtures/font_revert_probe.py +0 -0
- /package/plugins/{fh-commons → fh-preprep}/skills/preprep/fixtures/progression_known_negative.md +0 -0
- /package/plugins/{fh-commons → fh-preprep}/skills/preprep/fixtures/progression_known_positive.md +0 -0
- /package/plugins/{fh-commons → fh-preprep}/skills/preprep/fixtures/promise_known_negative.md +0 -0
- /package/plugins/{fh-commons → fh-preprep}/skills/preprep/fixtures/promise_known_positive.md +0 -0
- /package/plugins/{fh-commons → fh-preprep}/skills/preprep/interslide_deps.py +0 -0
- /package/plugins/{fh-commons → fh-preprep}/skills/preprep/jargon_terms.example.yaml +0 -0
- /package/plugins/{fh-commons → fh-preprep}/skills/preprep/lane_adjacent_dup.py +0 -0
- /package/plugins/{fh-commons → fh-preprep}/skills/preprep/lane_diagram.py +0 -0
- /package/plugins/{fh-commons → fh-preprep}/skills/preprep/lane_font.py +0 -0
- /package/plugins/{fh-commons → fh-preprep}/skills/preprep/lane_progression.py +0 -0
- /package/plugins/{fh-commons → fh-preprep}/skills/preprep/lane_promise.py +0 -0
- /package/plugins/{fh-commons → fh-preprep}/skills/preprep/lane_slide_refs.py +0 -0
- /package/plugins/{fh-commons → fh-preprep}/skills/preprep/lane_slide_relations.py +0 -0
- /package/plugins/{fh-commons → fh-preprep}/skills/preprep/ooxml/c1_baseline.txt +0 -0
- /package/plugins/{fh-commons → fh-preprep}/skills/preprep/ooxml/gate.py +0 -0
- /package/plugins/{fh-commons → fh-preprep}/skills/preprep/preprep_wire.py +0 -0
- /package/plugins/{fh-commons → fh-preprep}/skills/preprep/presentation_checklist.md +0 -0
- /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)
|
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-
|
|
152
|
+
skill_canon_emit preprep "plugins/fh-preprep/skills/preprep" \
|
|
153
153
|
"발표" "장표" "슬라이드" "덱" "대본" "리허설" "presentation" "keynote"
|
|
154
154
|
|
|
155
155
|
# 매핑된 프로젝트 = tracks/ 하위 디렉토리(언더스코어 접두는 메타라 제외)
|
package/scripts/selfcheck.sh
CHANGED
|
@@ -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-
|
|
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-
|
|
711
|
-
"plugins/fh-
|
|
712
|
-
"plugins/fh-
|
|
713
|
-
"plugins/fh-
|
|
714
|
-
"plugins/fh-
|
|
715
|
-
"plugins/fh-
|
|
716
|
-
"plugins/fh-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
|
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
|
|
45
|
-
python3 -c "import ast,sys;ast.parse(open(sys.argv[1],encoding='utf-8').read())" "$
|
|
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
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
|
63
|
-
|| ng "D2
|
|
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-
|
|
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
|
-
|
|
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-
|
|
20
|
-
SELFTEST=plugins/fh-
|
|
21
|
-
PROBE=plugins/fh-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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)); }
|