tools(docs): 코퍼스 기계 점검 doc-check.py 신설 + 중대 변경 핸드오버 체크리스트
사용자 질문: "이런 깊은 감사를 재발 방지하는 방법이 있나. 중대 변경에 대해
그 사항을 아는 세션이 stale 없게 핸드오버해야 하는데 그런 게 없어서 쌓임."
진단: 7·8차 감사에서 찾은 55건이 거의 전부 **변경한 세션이 그 자리에서
안 한 일**이었고, 그중 상당수는 grep으로 잡히는 기계적 종류였음. 그래서
규율 문서만 쓰지 않고 **실제 검사기**를 만듦.
.claude/tools/doc-check.py — 라이브 문서 61개 대상:
[ERROR] 깨진 파일 참조(우리 문서 명명 관례에 맞는 것만; 아직 없는 소스
트리 .luau와 외부 문서명은 오탐이라 WARN으로 분리)
[WARN] 절 참조 불일치 — `foo.md` "절 제목"이 그 파일에 없음
[ERROR] README 색인 누락
[WARN] 날짜 없는 시한부 주장("아직 안 돌려봄"/"열린 질문 없음" 등)
[WARN] 미반영 ⚠️ 배너 단 파일 vs 반영 목록 일치
현재 ERROR 0 / WARN 58(대부분 절 제목 의역 인용, 판단 대상).
**검사기가 즉시 실효를 증명함** — 같은 세션에 bind-system-plan.md를 쪼개며
"이중 바인딩 금지" 절 참조 4곳을 ref-plan.md로 잘못 옮긴 걸(실제로는
Observer/Effect 절이라 이동 대상이 아니었음) 이 스크립트가 잡아내 되돌림.
CLAUDE.md "작업 방식"에 중대 변경 핸드오버 체크리스트 6단계 명문화:
doc-check 실행 → 부정당한 *본문 문장* 전수 grep(배너만 달고 본문 방치가
최다 실패) → 뒤집힌 원문은 archive로 → 개수/상태는 소스 하나만 → 시한부
주장엔 날짜 → 인덱스 레이어 3개 동시 갱신.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y6hzeUi5QdLPEk69B6cXFa
This commit is contained in:
parent
0eedbad0b9
commit
0fe92fc22a
8 changed files with 278 additions and 6 deletions
|
|
@ -17,6 +17,7 @@
|
|||
| `feedback/` | 실사용 피드백을 정리한 긴 로그 — 지금은 비어있음(구현 시작 전) |
|
||||
| `luau-test/` | **[2026-08-09 신설]** `base/` 확정 사항 중 "추론만으로 확정하고 실제 Luau로 부딪혀본 적 없는 것"(M0 스파이크 대상)을 `luau`/`luau-analyze`/`luau-lsp`/Roblox Studio로 사용자가 직접 돌려볼 독립 실행 스크립트 모음. **[2026-08-13 여섯 번째 세션, 첫 실측]** `luau`/`luau-analyze` 바이너리가 생겨 처음으로 실제 실행 — **런타임 12개 전원 통과**, 타입 쪽에서 `:Compute(fn)` lazy 핸들 계약이 Luau 추론과 충돌하는 게 드러남(`question.md` 0-Y). **`STATUS.md`가 상태의 소스**(pass / 사람 결정 필요 / 스파이크 깨짐 / 미실행 분류 — 사람이 먼저 볼 것만 위에), `luau-test/README.md`는 각 파일의 검증 의도·배경, 실행 결과 상세는 `audit/luau-test-first-run-2026-08-13.md` |
|
||||
| `audit/` | **[2026-08-13 신설]** `luau-test/` 등 스파이크를 실제로 돌려본 뒤 "무엇이 확인됐고 무엇이 아직 안 됐는지"를 기록하는 곳 — 스크립트/계획 자체가 아니라 **실측 결과**만 다룸. base/luau-test와 달리 부분 확인(일부만 통과)도 있는 그대로 기록, 완전히 해소되면 관련 `base/`/`luau-test/README.md` 캐비엇을 지우고 이 문서는 근거로 남김. **현재 2개**: `luau-test-first-run-2026-08-13.md`(첫 실측 라운드 전체 — 런타임 12개 통과, `question.md` 0-Y의 1차 근거), `gcconn-trick-verification.md`(사용자가 Studio에서 직접 돌린 gcconn 트릭 부분 확인 — `luau-test/10`의 A 섹션 앞부분만, A-1/A-2/B/C는 미확인) |
|
||||
| `tools/` | **[2026-08-13 여덟 번째 세션 신설]** 코퍼스 기계 점검 — `doc-check.py`가 깨진 파일/절 참조, README 색인 누락, 날짜 없는 시한부 주장("아직 안 돌려봄" 등), 미반영 ⚠️ 배너를 한 번에 훑음. **중대 변경 후 커밋 전에 돌릴 것**(`python3 .claude/tools/doc-check.py`) — 수동 감사에서 나온 발견의 대부분이 이 종류였고, 실제로 문서를 쪼개다 잘못 옮긴 참조를 이게 잡아냄. ERROR는 고치고 WARN은 판단 대상 |
|
||||
| `session/` | **[2026-08-11 신설]** 세션별 상세 로그 원문(시행착오·정정 전 서술 포함, `quadnomicon` 개발로그 소재용) — 루트 `CLAUDE.md`가 3196줄까지 불어나 성능 저하를 유발해서 분리함. 파일명 `YYYY-MM-DD-NN-slug.md`, CLAUDE.md의 "세션 히스토리" 절에서 각 항목이 여기로 링크. 항상 읽을 필요 없음 — 결정의 논의 과정이 궁금할 때만 |
|
||||
| `initreq/` | 프로젝트 착수 시 클론해둔 참고 레포(quad v1, fusion, vide, rbvm, tbox, code-docker) + PA님 실 코드(`artworks/`, 4차 라운드 교차검증 근거) + 원본 요청(`req.md`, `raw-userinput.md`) + `quad2-try`(이전에 시도했다 폐기한 v2 재작성 시도 — 리서치 완료, 결론은 `base/bind-system-plan.md`) — 읽기 전용 리서치 소스, 여기 내용을 옮기지 말고 항상 원본 그대로 유지 |
|
||||
|
||||
|
|
|
|||
|
|
@ -307,7 +307,7 @@ context-rejected.md`. 아래는 그중 **아직 실제로 열려있는 것만**
|
|||
`base/slot-plan.md` "`Slot:List(...)`" 절.
|
||||
- **[해소됨, 2026-08-09 세션]** `Bound` — **`canBound(handle): boolean`
|
||||
탑레벨 함수로 확정**, `canExecute`와 같은 결(raw 필드를 직접 노출하는
|
||||
대신 predicate 함수로 감쌈). `base/ref-plan.md` "이중 바인딩
|
||||
대신 predicate 함수로 감쌈). `base/bind-system-plan.md` "이중 바인딩
|
||||
금지" 절 참고.
|
||||
- **`Brand`(3순위, 사소함, 2026-08-07 여덟 번째 세션 추가)**: 런타임
|
||||
nominal 타입 판별 통합 메커니즘(`Brand.set`/`Brand.get`, `isState`를
|
||||
|
|
|
|||
|
|
@ -154,7 +154,7 @@ quad의 반응형 그래프/cleanup 인체공학만 재사용하는 경우)로
|
|||
경로를 하나만 가져야 한다는 게 맞는 방향이라 판단이 뒤집힘 — 상세
|
||||
규칙과 `canBound(handle)` 기반 즉시-에러 메커니즘(구 가칭 `Bound`
|
||||
플래그, 2026-08-09 세션에서 이름 확정)은
|
||||
`base/ref-plan.md`의 "이중 바인딩 금지" 절 참고. **[정정,
|
||||
`base/bind-system-plan.md`의 "이중 바인딩 금지" 절 참고. **[정정,
|
||||
2026-08-09 여섯 번째 세션] leaf 부착 후 조기 해제는 `:Unsubscribe()`가
|
||||
아니라 `unbindLifetime(inst, value)`** — leaf 부착 자체가 내부적으로
|
||||
`bindLifetime(inst, value)` 호출이라, 그 해제도 짝인 `unbindLifetime`
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ ROADMAP 항목 근거인지, 어떻게 실행하는지, 실행 후 뭘 확인해
|
|||
| `07-relate-weak-table-gc.luau` | `Relate`의 lazy 서브테이블 생성 + weak-key GC가 실제로 동작하는지 | `relate-plan.md` "M2 착수 시 실측 확인" **[2026-08-13 보강]** 4번 섹션 신설 — `_countEntries()`(테스트 전용) + weak-value canary로 **"inst가 죽으면 중첩 StrongMap 안의 payload까지 연쇄 GC되는가"를 직접 검증**(원래는 sanity check만 하고 헤더의 핵심 주장은 미검증이었음). 파일이 스스로 적어둔 "weak table 엔트리를 셀 표준 API가 없다"는 전제도 틀렸음 — outer가 `__mode="k"`라 GC 후 `pairs`에서 사라짐 |
|
||||
| `08-type-source-satisfies-state.luau` (타입체크 전용) | `Source<T>`가 `State<T>`를 구조적으로 만족하는 제네릭 타입이 솔버에서 안전한지 | `store-semantics.md` "검증 필요", ROADMAP M0-2 |
|
||||
| `09-type-modifier-overridden-subtype.luau` (타입체크 전용) | `FrameModifier <: GuiObjectModifier`처럼 서브타입 관계인 Modifier를 `Overridden`으로 섞을 때 타입이 통과하는지 | `modifier-plan.md` 9-2번, ROADMAP M7 |
|
||||
| `10-roblox-studio-checks.server.luau` (Studio 전용) | (A) `bindLifetime`/`unbindLifetime`/`canExecute`/`canBound`의 gcconn 트릭 + 이중 바인딩 게이트(Destroy 시 Connected 전환 포함), (B) Attribute의 Instance 참조 타입 지원, (C) CollectionService 태그/GetTagged 왕복 — **[2026-08-13]** A 섹션 앞부분(신호 미발화, Destroy 시 Connected 즉시 전환)은 사용자 자작 스크립트로 부분 확인됨, `audit/gcconn-trick-verification.md` 참고. A-1/A-2(`canBound` 게이트)/B/C는 이 공식 파일로 아직 확인 안 됨 | `lifecycle-pattern.md`, `ref-plan.md` "이중 바인딩 금지", CLAUDE.md 2026-08-06 세션, `debug-tooling-plan.md` |
|
||||
| `10-roblox-studio-checks.server.luau` (Studio 전용) | (A) `bindLifetime`/`unbindLifetime`/`canExecute`/`canBound`의 gcconn 트릭 + 이중 바인딩 게이트(Destroy 시 Connected 전환 포함), (B) Attribute의 Instance 참조 타입 지원, (C) CollectionService 태그/GetTagged 왕복 — **[2026-08-13]** A 섹션 앞부분(신호 미발화, Destroy 시 Connected 즉시 전환)은 사용자 자작 스크립트로 부분 확인됨, `audit/gcconn-trick-verification.md` 참고. A-1/A-2(`canBound` 게이트)/B/C는 이 공식 파일로 아직 확인 안 됨 | `lifecycle-pattern.md`, `bind-system-plan.md` "이중 바인딩 금지", CLAUDE.md 2026-08-06 세션, `debug-tooling-plan.md` |
|
||||
| `11-modifier-illegal-value-error.luau` | Modifier 필드에 Ref/PreRef/Observer/Effect/Slot/Modifier가 들어오면 즉시 error, State/Source가 확정하는 값이 Modifier면 즉시 error(2026-08-09 세션에 "UB"에서 전환된 규칙) | `modifier-plan.md` "핸들러 계층 값 즉시 error" 절 + 7번 절 |
|
||||
| `12-type-attribute-generic-key-narrowing.luau` (타입체크 전용) | `[AttributeKey<<T>> "name"] = value`(구 `Attribute<<T>>`)처럼 제네릭 DI 키를 쓸 때 `value`의 타입이 실제로 `T`로 좁혀지는지 — base 문서 자신이 "미검증"이라 명시한 항목 | `attribute-plan.md` "[실측 필요, M0/M10]" (2026-08-09 열한 번째 세션 신설) |
|
||||
| `13-type-ref-preref-subtype.luau` | (A, 타입) `PreRef<T>`가 `Ref<T>`를 구조적으로 만족하는지, (B, 런타임) `isRef`/`isPreRef` 합성이 재정정대로 동작하는지(`isRef(preRefInstance)`가 이제 `true`) + Leaf 핸들러가 `isRef(v) and not isPreRef(v)`로 명시적으로 좁혀야 하는 이유 | `brand-plan.md`의 `Brand` 절(2026-08-09 열한 번째 세션 재정정) |
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
정해지면 `bind-system-plan.md`/`tag-plan.md`/`slot-plan.md`/
|
||||
`attribute-plan.md`를 한 번에 옮기면 됨.
|
||||
|
||||
> **문서 이력**: 최초엔 `dispatch-hint-to-oldvalue-plan.md`라는 이름으로
|
||||
> **문서 이력**: 최초엔 dispatch-hint-to-oldvalue-plan(현재 없는 옛 파일명)으로
|
||||
> "힌트 대신 `oldValue`를 넘기자"는 보완안을 담고 있었으나, 사용자가
|
||||
> **"이전 값인 oldValue는 처음부터 클로저라 이미 본인이 알지 않아요?"**
|
||||
> 라고 지적해 그 보완안이 통째로 불필요함이 드러남(아래 3-2) — 파일명도
|
||||
|
|
|
|||
239
.claude/tools/doc-check.py
Normal file
239
.claude/tools/doc-check.py
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
.claude/ 코퍼스 기계 점검 — 깊은 수동 감사가 반복되는 걸 막기 위한 도구.
|
||||
|
||||
배경: 2026-08-13 일곱/여덟 번째 세션에 6+6라운드 수동 감사를 돌려 총 39+16건을
|
||||
찾았는데, **그중 대부분이 grep으로 잡히는 종류**였음(옮겨간 절을 가리키는 참조,
|
||||
색인 누락, 날짜 없는 "아직 안 함" 주장). 사람/에이전트의 성실성에 기대는 대신
|
||||
기계가 매번 같은 걸 확인하게 함.
|
||||
|
||||
실행: python3 .claude/tools/doc-check.py
|
||||
python3 .claude/tools/doc-check.py --quiet # 실패한 검사만
|
||||
종료코드: 오류(ERROR)가 하나라도 있으면 1, 경고만 있으면 0.
|
||||
|
||||
검사 항목
|
||||
1. [ERROR] 깨진 파일 참조 — 라이브 문서가 가리키는 .md/.luau가 실제로 없음
|
||||
2. [ERROR] 깨진 절 참조 — `foo.md` "절 제목" 이 그 파일에 없음
|
||||
(문서를 쪼개거나 헤딩을 고칠 때 가장 잘 깨지는 것 — 여덟 번째 세션에
|
||||
bind-system-plan.md를 분할하며 20곳이 여기 걸렸음)
|
||||
3. [ERROR] 색인 누락 — base/research/archive/reference 파일이 README에 없음
|
||||
4. [WARN] 날짜 없는 시한부 주장 — "아직 안 돌려봄", "열린 질문 없음" 등
|
||||
시간이 지나면 거짓이 되는데 언제 기준인지 안 적힌 문장
|
||||
5. [WARN] 미반영 배너를 단 파일 vs 반영 목록 일치 여부
|
||||
|
||||
`session/`(원문 보존), `initreq/`(읽기 전용 클론)는 검사 대상에서 제외.
|
||||
"""
|
||||
import os, re, sys, collections
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
CLAUDE = os.path.join(ROOT, '.claude')
|
||||
SKIP_DIRS = ('session', 'initreq', 'worktrees', 'tools')
|
||||
|
||||
errors, warns = [], []
|
||||
|
||||
|
||||
def live_docs():
|
||||
out = []
|
||||
for base in (CLAUDE, ROOT):
|
||||
for dp, dn, fn in os.walk(base):
|
||||
rel = os.path.relpath(dp, ROOT)
|
||||
if any(p in rel.split(os.sep) for p in SKIP_DIRS):
|
||||
dn[:] = []
|
||||
continue
|
||||
if base is ROOT and rel != '.' and not rel.startswith('.claude'):
|
||||
continue
|
||||
for f in fn:
|
||||
if f.endswith('.md'):
|
||||
out.append(os.path.join(dp, f))
|
||||
if base is ROOT:
|
||||
break
|
||||
# 루트 md 직접 추가
|
||||
for f in ('CLAUDE.md', 'ROADMAP.md', 'HUMAN_TODO.md', 'SAFETY.md'):
|
||||
p = os.path.join(ROOT, f)
|
||||
if os.path.exists(p):
|
||||
out.append(p)
|
||||
return sorted(set(out))
|
||||
|
||||
|
||||
def rel(p):
|
||||
return os.path.relpath(p, ROOT)
|
||||
|
||||
|
||||
# ---------- 1 & 2: 참조 무결성 ----------
|
||||
# `(경로/)파일.md` 또는 `파일.luau` (+ 바로 뒤에 "절 제목"이 오면 절 검사도)
|
||||
REF = re.compile(r'`\.?/?((?:[\w.-]+/)*[\w.@-]+\.(?:md|luau))`(\s*(?:의\s*)?"([^"\n]{2,60})")?')
|
||||
|
||||
def resolve(target, src):
|
||||
"""참조 문자열을 실제 경로로 해석 — 상대/부분 경로를 관대하게 매칭."""
|
||||
cands = [
|
||||
os.path.join(ROOT, target),
|
||||
os.path.join(CLAUDE, target),
|
||||
os.path.join(os.path.dirname(src), target),
|
||||
]
|
||||
for c in cands:
|
||||
if os.path.exists(c):
|
||||
return c
|
||||
# 파일명만으로 찾히면 인정(initreq 포함 — 읽기 전용 클론이지만 실재함)
|
||||
name = os.path.basename(target)
|
||||
for dp, dn, fn in os.walk(CLAUDE):
|
||||
if 'worktrees' in os.path.relpath(dp, ROOT).split(os.sep):
|
||||
dn[:] = []
|
||||
continue
|
||||
if name in fn:
|
||||
return os.path.join(dp, name)
|
||||
return None
|
||||
|
||||
|
||||
# 이 레포의 문서 명명 관례 — 여기 걸리면 "우리 문서"이므로 못 찾으면 ERROR.
|
||||
# 안 걸리면 외부 문서명(Compose 가이드라인 등)일 수 있어 WARN으로 낮춤.
|
||||
OURS = re.compile(r'(-plan|-reversed|-rejected|-findings|-map|-audit|-verification'
|
||||
r'|^README|^STATUS|^CLAUDE|^ROADMAP|^HUMAN_TODO|^SAFETY'
|
||||
r'|^question|^architecture|^agent-mistake)\.md$')
|
||||
|
||||
|
||||
def headings(path):
|
||||
if not path.endswith('.md') or not os.path.exists(path):
|
||||
return None
|
||||
hs = []
|
||||
for line in open(path, encoding='utf-8'):
|
||||
if line.startswith('#'):
|
||||
# 백틱/강조 표기는 인용할 때 자주 빠지므로 정규화해서 비교
|
||||
hs.append(re.sub(r'[`*]', '', line.lstrip('#')).strip())
|
||||
return hs
|
||||
|
||||
|
||||
def interesting(target):
|
||||
"""검사 가치가 있는 참조인가.
|
||||
|
||||
제외: (a) `initreq/`(읽기 전용 클론, gitignore됨), (b) 아직 존재하지 않는
|
||||
**미래 소스 트리**의 `.luau`(구현 시작 전이라 당연히 없음 — `architecture.md`가
|
||||
설계로서 적어둔 것), (c) `YYYY-MM-DD-NN-slug.md` 같은 템플릿 자리표시자.
|
||||
실제로 지금 존재해야 하는 건 `.md` 문서와 `luau-test/`의 스파이크뿐.
|
||||
"""
|
||||
if 'YYYY' in target:
|
||||
return False
|
||||
if target.endswith('.luau'):
|
||||
return 'luau-test' in target or re.match(r'^\d\d-', os.path.basename(target))
|
||||
return True
|
||||
|
||||
|
||||
def check_refs(docs):
|
||||
hcache = {}
|
||||
for d in docs:
|
||||
# archive는 히스토리라 절 참조까지 강제하지 않음(파일 존재만)
|
||||
is_archive = '/archive/' in rel(d)
|
||||
for ln, line in enumerate(open(d, encoding='utf-8'), 1):
|
||||
for m in REF.finditer(line):
|
||||
target, _, section = m.group(1), m.group(2), m.group(3)
|
||||
if not interesting(target):
|
||||
continue
|
||||
p = resolve(target, d)
|
||||
if p is None:
|
||||
name = os.path.basename(target)
|
||||
msg = f"{rel(d)}:{ln} 깨진 파일 참조 → `{target}`"
|
||||
(errors if OURS.search(name) else warns).append(
|
||||
msg if OURS.search(name) else msg + " (외부 문서명일 수 있음)")
|
||||
continue
|
||||
if section and not is_archive and p.endswith('.md'):
|
||||
if p not in hcache:
|
||||
hcache[p] = headings(p) or []
|
||||
core = re.sub(r'[`*]', '', section).strip()
|
||||
if not any(core in h for h in hcache[p]):
|
||||
# 절 제목은 의역해서 인용하는 관례가 있어 WARN — 다만
|
||||
# 문서를 쪼개거나 헤딩을 고칠 때 여기가 제일 먼저 걸린다.
|
||||
warns.append(
|
||||
f"{rel(d)}:{ln} 절 참조 불일치 → {os.path.basename(p)}에 "
|
||||
f'"{section}" 절 없음')
|
||||
|
||||
|
||||
# ---------- 3: 색인 누락 ----------
|
||||
def check_index():
|
||||
readme = os.path.join(CLAUDE, 'README.md')
|
||||
if not os.path.exists(readme):
|
||||
errors.append("`.claude/README.md`가 없음")
|
||||
return
|
||||
txt = open(readme, encoding='utf-8').read()
|
||||
for sub in ('base', 'research', 'archive', 'reference'):
|
||||
d = os.path.join(CLAUDE, sub)
|
||||
if not os.path.isdir(d):
|
||||
continue
|
||||
for f in sorted(os.listdir(d)):
|
||||
if f.endswith('.md') and f'`{f}`' not in txt:
|
||||
errors.append(f"README 색인 누락 → {sub}/{f}")
|
||||
|
||||
|
||||
# ---------- 4: 날짜 없는 시한부 주장 ----------
|
||||
TEMPORAL = [
|
||||
(r'아직 (?:사용자가 )?안 돌려\S*', '실행 여부 주장'),
|
||||
(r'결과 미확인', '실행 여부 주장'),
|
||||
(r'열린 질문(?:은)? 없(?:음|다)', '완결 주장'),
|
||||
(r'더 이상 (?:막힌|열려있는|열려 있는)\S* 없\S*', '완결 주장'),
|
||||
(r'남은 (?:유일한|건)[^\n]{0,20}뿐', '완결 주장'),
|
||||
(r'전부 확정됨', '완결 주장'),
|
||||
]
|
||||
DATED = re.compile(r'20\d\d-\d\d-\d\d|\[\s*(?:해소|정정|갱신|확정|역전)')
|
||||
|
||||
def check_temporal(docs):
|
||||
for d in docs:
|
||||
if '/archive/' in rel(d):
|
||||
continue
|
||||
lines = open(d, encoding='utf-8').read().split('\n')
|
||||
for i, line in enumerate(lines, 1):
|
||||
for pat, kind in TEMPORAL:
|
||||
if re.search(pat, line):
|
||||
ctx = '\n'.join(lines[max(0, i - 3):i + 2])
|
||||
if not DATED.search(ctx):
|
||||
warns.append(
|
||||
f"{rel(d)}:{i} 날짜 없는 {kind} — "
|
||||
f"\"{re.search(pat, line).group(0)}\" "
|
||||
f"(언제 기준인지 적을 것)")
|
||||
|
||||
|
||||
# ---------- 5: 미반영 배너 ----------
|
||||
def check_banner(docs):
|
||||
banner = [d for d in docs if '아직 반영 안 됨' in open(d, encoding='utf-8').read()]
|
||||
if not banner:
|
||||
return
|
||||
names = sorted(os.path.basename(b) for b in banner)
|
||||
plan = os.path.join(CLAUDE, 'research', 'dispatch-redispatch-diff-plan.md')
|
||||
if os.path.exists(plan):
|
||||
txt = open(plan, encoding='utf-8').read()
|
||||
missing = [n for n in names if n not in txt]
|
||||
if missing:
|
||||
warns.append(
|
||||
"미반영 ⚠️ 배너를 달고 있는데 반영 목록"
|
||||
"(dispatch-redispatch-diff-plan.md 6절)에 없는 파일: "
|
||||
+ ', '.join(missing))
|
||||
warns.append(f"미반영 배너를 단 파일 {len(names)}개 — 해소 시 배너도 같이 걷을 것: "
|
||||
+ ', '.join(names))
|
||||
|
||||
|
||||
def main():
|
||||
quiet = '--quiet' in sys.argv
|
||||
docs = live_docs()
|
||||
check_refs(docs)
|
||||
check_index()
|
||||
check_temporal(docs)
|
||||
check_banner(docs)
|
||||
|
||||
print(f"검사 대상 라이브 문서 {len(docs)}개 "
|
||||
f"(session/·initreq/ 제외)\n")
|
||||
if errors:
|
||||
print(f"■ ERROR {len(errors)}건 — 고쳐야 함")
|
||||
for e in errors:
|
||||
print(" " + e)
|
||||
print()
|
||||
elif not quiet:
|
||||
print("■ ERROR 0건 ✓\n")
|
||||
if warns:
|
||||
print(f"■ WARN {len(warns)}건 — 확인 권장(자동 판정 불가)")
|
||||
for w in warns:
|
||||
print(" " + w)
|
||||
print()
|
||||
elif not quiet:
|
||||
print("■ WARN 0건 ✓\n")
|
||||
return 1 if errors else 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
34
CLAUDE.md
34
CLAUDE.md
|
|
@ -102,8 +102,40 @@ modifier/Ref의 컴포넌트 경계 통과 방식) 논의도 2026-08-04 세션
|
|||
조사하게 되는 재작업을 막기 위함. `.claude/base/`로 승격, `.claude/qa-request/`로
|
||||
이동, 또는 문서 자체를 갱신. code-docker/webmanager의 `.claude/` 관리 방식이
|
||||
좋은 예시(`.claude/initreq/code-docker/webmanager/.claude/README.md` 참고).
|
||||
- **⭐ 중대 변경 핸드오버 체크리스트 — 확정된 결정을 뒤집거나 문서를
|
||||
쪼갤 때 반드시 이 순서를 밟을 것.** 2026-08-13 일곱/여덟 번째 세션에
|
||||
6+6라운드 수동 감사로 55건을 찾았는데, **거의 전부가 "변경한 세션이
|
||||
그 자리에서 안 한 일"이 나중에 stale로 쌓인 것**이었음. 감사로 뒤늦게
|
||||
줍지 말고 바꾸는 그 순간에 닫을 것:
|
||||
1. **`python3 .claude/tools/doc-check.py`를 돌릴 것**(아래 항목 참고) —
|
||||
ERROR 0을 유지한 채로 커밋. 이게 규율 대부분을 기계가 대신함.
|
||||
2. **바꾼 주장을 부정당하는 *본문 문장*을 grep으로 전수 찾을 것.**
|
||||
헤더에 정정 배너만 달고 본문 bullet을 안 고치는 게 가장 잦은 실패 —
|
||||
실제로 `CLAUDE.md`가 "스파이크를 아직 안 돌려봄"이라고 서술한 채
|
||||
한 라운드를 통과했음. **배너를 달았으면 그 배너가 부정하는 문장을
|
||||
같은 커밋에서 고쳤는지 확인.**
|
||||
3. **뒤집힌 원문은 `archive/`로 옮기고 포인터만 남길 것** — 본문에
|
||||
"히스토리로만 보존"이라며 두면 구현자가 앞에서부터 읽다가 그
|
||||
"확정"을 그대로 믿음(`slot-plan.md`에서 실제로 발생).
|
||||
4. **개수·목록·상태는 소스를 하나만 둘 것.** "20개 중 19개", "4개
|
||||
문서", "남은 건 X뿐" 류는 두 곳 이상에 적는 순간 반드시 갈라짐 —
|
||||
한 곳(예: `luau-test/STATUS.md`, 폴더 구조)을 소스로 하고 나머지는
|
||||
가리키기만.
|
||||
5. **시한부 주장엔 날짜를 붙일 것.** "아직 안 돌려봄"이 아니라
|
||||
"[2026-08-09 기준] 아직 안 돌려봄" — 날짜가 있으면 다음 세션이
|
||||
의심할 수 있지만, 없으면 영원히 현재형으로 읽힘.
|
||||
6. **인덱스 레이어 3개를 같이 갱신**: `.claude/README.md`(색인),
|
||||
`question.md`(사용자가 답할 것만), 루트 `ROADMAP.md`/`HUMAN_TODO.md`.
|
||||
- **기계 점검 — `python3 .claude/tools/doc-check.py`.** 깨진 파일/절
|
||||
참조, README 색인 누락, 날짜 없는 시한부 주장, 미반영 배너를 한 번에
|
||||
훑음. **커밋 전에 돌리는 게 기본** — 수동 감사에서 나온 발견의 대부분이
|
||||
이걸로 잡히는 종류였고, 실제로 여덟 번째 세션에 문서를 쪼개면서 잘못
|
||||
옮긴 참조를 이 스크립트가 잡아냈음. ERROR는 고치고, WARN은 판단이
|
||||
필요한 것(절 제목을 의역해 인용한 관례 등)이라 늘 0일 필요는 없음.
|
||||
- **문서가 쌓이면서 모순/중복/stale 마커가 생기기 쉬움 — 주기적으로 감사할
|
||||
것.** 2026-08-04 세션에 실제로 전체 `.claude/` 코퍼스에서 이런 문제가
|
||||
것.** 다만 위 체크리스트+`doc-check.py`가 자리잡으면 이 감사는
|
||||
"기계가 못 보는 것"(설계 자체의 자기모순, 의사코드 손 트레이싱)에만
|
||||
집중하면 됨. 2026-08-04 세션에 실제로 전체 `.claude/` 코퍼스에서 이런 문제가
|
||||
다수 발견되어 정리함(아래 "세션 히스토리" 참고) — 여러 라운드에 걸쳐
|
||||
같은 문서를 계속 고치다 보면 "정정됨" 표시가 원래 문장에 안 반영되고
|
||||
방치되는 패턴이 반복되니, 큰 방향 전환이 있을 때마다 관련 문서 전체를
|
||||
|
|
|
|||
|
|
@ -232,7 +232,7 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
|
|||
- [ ] Observer/Effect 이중 바인딩 금지 — `canBound(handle)` predicate로
|
||||
`:Subscribe()`(전역)와 `bindLifetime`(inst-scoped, leaf 부착도
|
||||
내부적으로 이걸 호출)이 동시에 걸리면 즉시 `error`(`base/
|
||||
ref-plan.md` "이중 바인딩 금지" 절, 2026-08-07 일곱 번째
|
||||
bind-system-plan.md` "이중 바인딩 금지" 절, 2026-08-07 일곱 번째
|
||||
세션 신설, 이름은 2026-08-09 세션에 `canBound`로 확정, 같은 날
|
||||
여섯 번째 세션에서 "leaf 부착=bindLifetime 호출"로 정정 — 진짜
|
||||
독립 경로는 둘뿐). `canBound`의 내부 플래그는 `canExecute`가 보는
|
||||
|
|
|
|||
Loading…
Reference in a new issue