quad/.claude/session/2026-08-16-03-doc-check-section-convention.md
qwreey 40a5daf694
tooling: 절 인용 규약 신설 + doc-check 절 참조를 ERROR 게이트로 승격
사용자 제기 — doc-check.py가 정규식으로 결정론적 판정을 하는데 표기가 흔들리면
문제가 커지니, 정규식을 늘리기보다 "예상 가능 범위"를 컨벤션으로 좁히는 게 싸지
않냐. 실측해보니 날짜 표기는 이미 100% 균일해서 고칠 게 없었고(강제 장치 없이),
드리프트는 절 인용 쪽이었다 — WARN 86건 중 78건(91%)이 절 참조 불일치.

핵심은 그 78건이 코퍼스가 지저분한 게 아니라 **검사기가 못 읽는 것**이었다는
점이다. 이 코퍼스는 `**볼드**` 줄을 하위 절로 쓰는데 headings()가 `#`만 봤다.

## 동작 변화 (문서 정정으로만 보이지만 게이트가 바뀐다)

- 절 참조 불일치가 **WARN → ERROR**. 이제 절 인용 오류가 커밋을 막는다.
- 절 인식이 `#` 헤딩 + `**볼드**` 절로 확장. 단 볼드는 **빈 줄 다음이나
  리스트 항목 머리**만 인정 — 문단이 줄바꿈되며 우연히 줄머리에 걸린 강조를
  절로 오인하던 걸 커밋 전 감사가 잡아 조였다.
- 인용 길이 상한 60→160자. 60자를 넘으면 매칭 자체가 안 걸려 검사에서
  **조용히** 빠져나갔음(위양성보다 나쁜 구멍).
- 비교를 공백 무시로(줄바꿈 인용 대응), 선두 장식·상태/날짜 태그 정규화,
  `initreq/` 대상 인용은 절 검사 면제(읽기 전용 외부 원본).

## 규약

`conventions.md`에 "문서 표기 규약" 절 신설 — 절 인용 규약(의역 금지, 헤딩은
부분문자열/볼드는 앞부분일치, 태그로 닫히는 볼드 캐비엇, blockquote 함정),
세션은 산문 서수 말고 파일 ID로 지칭. 날짜 마커 라벨 어휘 닫기는 사용자 판단
으로 기각(기계 검사 대상이 아니라 읽는 쪽 판단 재료).

## 결과

절 참조 불일치 78 → 0. 36건은 검사기 수정으로 사라졌고(애초에 위양성), 42건은
인용을 실제 절 제목으로 손으로 고쳤다. 마지막 15건은 서브에이전트 3개에 병렬
위임해 추적 — **설계 서술이 유실된 건은 0건**, 대부분 코드 주석·본문 산문·
주제명처럼 애초에 절이 아닌 걸 절로 인용해온 것이었다.

부수로 드러나 같이 고친 것: onchange-plan이 9차 분할 때 일부러 안 옮긴 절을
잘못된 파일로 가리키던 것, brand-plan이 이미 이행된 정정을 "정정 대상"이라
부르던 것, ROADMAP의 blockquote가 인용 줄바꿈 때문에 깨져 있던 것,
pre-implementation-audit의 해소된 항목이 "아직 안 고침" 절에 남아 있던 것
(사용자 결정으로 "이미 고침"으로 이동).

커밋 전 감사 4라운드(에이전트 8개)를 돌렸고, 발견 추이는 2→2→1→0이다.
매 라운드 발견이 "직전 라운드 수정이 만든 새 결함"이었던 게 특징 — 규약을
세우는 커밋이 그 규약의 첫 위반자가 된다는 걸 실측으로 확인했다. 상세는
.claude/session/2026-08-16-03-doc-check-section-convention.md.

부수: __pycache__를 .gitignore에 추가하고 추적 해제(32e9db0에 실수로 딸려
들어가 있었음). quad-doc-auditor에 작업 트리를 바꾸는 git 명령 금지 규약 추가
— 감사자가 git stash를 걸어 메인 세션 스테이지가 반복적으로 풀렸다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011zk7XHkSfiBfdPLZUQHdZf
2026-08-16 10:47:29 +09:00

17 KiB

2026-08-16 세 번째 세션 — 표기 컨벤션으로 doc-check 정규식 줄이기

전 세션(session/2026-08-16-02-audit-tooling-verification-and-first-real-run.md) 인수인계를 받고 시작. 설계는 안 건드림, 문서 툴링만.

사용자 문제 제기

현행 md 툴링 py 상 날짜 적는거 형식 같은것도, 강제화 하게 하는게 맞다 생각이 드는듯. doc-check.py 자체가 regex 를 여럿 사용해 정적으로, 결정론적이게 분석하는데, 형식이 많이 왔다 갔다 하면 문제가 커져. 여러 regex 를 추가해가며 하는것도 좋지만, '예상 가능 범위' 를 어느정도 컨벤션으로 정하는게 좋다 생각하는데(가장 비용이 쌈), 어떻게 봄?

실측 — 날짜는 이미 문제가 아니었다

전수 조사:

20NN-NN-NN 형태     1864건 / 1864건 (100%)
다른 구분자(. 또는 /)     0건
비-ASCII 공백 혼입        0건
대괄호 마커              383건

강제 장치가 없는데 100% 균일. 사용자 직감("싼 컨벤션이 통한다")을 반증하는 게 아니라 오히려 뒷받침하는 데이터라, 이걸 근거로 삼되 드리프트가 실제로 있는 곳으로 방향을 돌렸다.

doc-check.py 당시 출력: 라이브 문서 88개, ERROR 0, WARN 86건 — 그중 절 참조 불일치가 78건(91%). 날짜 없는 주장은 3건뿐.

재검증 시 주의: 이 78은 실제 작업 트리 기준이다. git archive HEAD 같은 걸로 격리 체크아웃해 돌리면 72가 나오는데, 그건 아카이브에 gitignore된 .claude/initreq/가 안 들어가서 raw-userinput.md를 가리키는 인용들이 "절 참조 불일치"가 아니라 "깨진 파일 참조"로 분류되기 때문이다 (커밋 전 4라운드 감사가 실제로 이 차이에 걸렸다). 숫자를 대조할 땐 트리를 갈아엎지 말고 git show HEAD:<경로>로 파일 단위 비교를 쓸 것.

프레이밍 교정 — 컨벤션은 정규식의 대안이 아니다

사용자가 "regex를 늘릴까 / 컨벤션을 정할까"의 양자택일로 물었지만, 실제 관계는 컨벤션이 정규식을 열거형에서 앵커형으로 바꿔 작게 만드는 것이다. DATED가 "±3줄 안에 아무 날짜나 있으면 통과"라는 느슨한 판정을 하는 이유가 정확히 앵커할 표준 마커가 없어서다.

동시에 컨벤션이 못 고치는 영역도 명확히 했다 — 검사는 두 종류다.

  • (A) 이스케이프 해치형: 도구가 위험 패턴을 잡고 사람이 표준 마커로 잠재움(REF 절 인용, DATED, OURS). 컨벤션이 곧바로 이득.
  • (B) 탐지형: 도구가 위험한 문장을 먼저 찾아야 함(TEMPORAL의 6개 한국어 표현, '아직 반영 안 됨' 리터럴). 컨벤션이 원리적으로 못 고침 — "시한부 주장에 날짜를 붙여라"를 정해도 정규식은 날짜를 안 붙인 문장을 못 찾는다. 이 잔여가 quad-doc-auditor의 몫.

우선순위 제안과 사용자 결정

  1. 절 인용 규약 + ERROR 승격 (WARN의 91%)
  2. 세션 지칭 표기 (grep을 깨뜨림)
  3. 날짜 마커 라벨 어휘 닫기

사용자 결정: 1+2 채택, 3 기각. 3에 대한 논거가 날카로웠음 —

  1. 의 경우 사실상 에이전트가 순서 섞였을 때 최신의, 옳은 요소 선택에 도움이 되는 정보에 가깝지 이게 warn 을 만들지는 못할듯.

즉 라벨은 기계 검사 대상이 아니라 읽는 쪽(에이전트)의 판단 재료다. 어휘를 닫아도 검사가 하나도 안 생기므로 비용만 든다. 3에 딸려 있던 DATED 앵커 좁히기만은 유일하게 WARN을 만드는 항목이라 성격이 다르지만, 지금 그 위음성이 아프다는 증거가 없어 같이 보류.

핵심 발견 — 78건은 대부분 "코퍼스가 지저분한 것"이 아니라 "검사기가 못 읽는 것"이었다

dispatch-core-plan.md# 헤딩이 11개뿐인데 다른 문서들이 인용하는 "우선순위 동률/매치 실패 처리", "핸들러 내부 상태 저장", "props 순회 순서"는 전부 실재한다 — **볼드**로 시작하는 줄로. headings()# 줄만 수집하니 이게 통째로 안 보였다.

그런데 볼드를 통째로 절 제목으로 인정하면 검사가 장식이 된다:

파일 # 헤딩 볼드(전체) 볼드(줄머리)
dispatch-core-plan.md 11 264 130
slot-plan.md 40 331 162
architecture.md 9 60 23

세 규칙을 실측 대조:

A 느슨 볼드 + 부분문자열      해소 30
B 줄머리 볼드 + 부분문자열     해소 32
C 줄머리 볼드 + 앞부분일치     해소 26  → 선두 장식(⚠️) 정규화 추가 후 30

C가 A와 해소 건수는 같으면서 매칭 표면만 좁다. C 채택. # 헤딩은 부분문자열, 볼드 절은 앞부분일치라는 비대칭이 핵심 — 헤딩은 명시적 절 마커라 오인 위험이 없고, 볼드는 강조와 구분이 안 되니 더 엄격해야 한다.

부수로 1. **…**(번호 리스트 볼드)도 절로 인정하도록 넓힘 — conventions.md의 핸드오버 체크리스트가 정확히 그 형태고 다른 문서가 그걸 절로 인용한다.

initreq/ 면제

raw-userinput.md.claude/initreq/에 있는 읽기 전용 원본 요청이고, 스스로 "진짜 생각의 흐름 대로 작성되어있으며 정리가 없음"이라 밝힌 문서다. 여길 가리키는 인용은 절 제목이 아니라 원문 문장을 따오는 것이라 헤딩 대조가 애초에 무의미 → 폴더 범주로 면제. (is_history()를 넓히는 건 오답이었음 — 그건 인용하는 쪽 문서를 면제하는 스위치인데 여기선 인용 대상이 문제였다.)

결과

절 참조 WARN  78 → 42 (검사기 수정) → 23 (인용 19건 교정)
              → 15 (인용 8건 추가 교정) → 0 (추적 후 15건 교정)
그리고 WARN → ERROR 승격

교정한 인용은 대부분 "실제 절이 명확히 존재하는데 의역돼 있던 것". 대표적으로 architecture.md복사(clone) 구현 지양을 "복사 구현 지양"으로, 이벤트 핸들러는 self(Instance)를 받지 않는다를 "self를 받지 않는다"로 줄여 쓴 것들. operator-sugar-plan.mdtween-plan.mdresearch/base/로 승격된 걸 안 따라와서 경로도 같이 고침.

큰따옴표가 든 절 제목은 인용 문법("...")을 깨므로 앞부분만 인용하는 규칙이 여기서 나왔다(pluggable 디스패치 엔진 자체도 "인터페이스"로 base가 소유).

마지막 15건 — 서브에이전트 3개에 병렬 위임

"그 절이 어디로 갔는가"를 따져야 해서 읽을 양이 많았고, 메인 컨텍스트를 지키려고 대상 파일별로 3등분해 위임했다(읽기 전용, 발견만 리포트). 총평: 설계 서술이 유실된 건은 0건. 대부분 "애초에 절이 아닌 것을 절로 인용해온 것"이었다 — 코드 주석 한 줄(클로저 생존이 곧 gchold 생존), 본문 산문(데이터를 테이블에 직접 두고), 앞으로 쓸 문서의 주제명 (왜 quad를 쓰는가), 그냥 의역(bind는 유일 슬롯).

부수로 드러난 것 — onchange-plan.md가 9차 분할 때 일부러 안 옮긴 절을 event-plan.md로 잘못 가리키고 있었고(실제 위치는 bind-system-plan.md), brand-plan.md는 2026-08-07에 이미 이행된 정정을 아직 "정정 대상"이라 부르고 있었다.

검사기 보완 — 조용한 통과 구멍 둘

에이전트들이 검사기 자체의 결함을 짚었다.

  1. 인용이 60자를 넘으면 REF가 매칭 자체를 안 해 검사에서 조용히 빠져나갔다. 위양성보다 나쁜 종류라 상한을 160자로 넓힘. 그러자 3건이 새로 드러났는데 전부 줄바꿈 때문에 원문에 없는 공백이 인용에 끼어든 케이스여서, 비교를 공백 무시로 바꿔 순증 작업량 0으로 닫음.
  2. 선두 상태/날짜 태그(**[2026-08-16 신설]** 제목…)가 볼드 절 제목의 앞부분을 차지해 어떤 제목형 인용도 매칭 불가였다. 태그는 시간이 지나면 바뀌는 메타 표시라 제목의 일부가 아니므로 양쪽에서 벗겨내고 비교. 이건 ERROR 승격 직후 게이트가 내가 방금 쓴 인용 두 개를 잡아서 발견됐다.

인용을 짧게 자르다 문장을 깬 것들 — 프로즈 패스에서 회수

정규식이 통과해도 의미는 깨질 수 있다. 커밋 전 diff를 "인용 매칭"이 아니라 "문장 읽기" 모드로 다시 훑어 회수한 것들:

  • ROADMAP.md의 blockquote를 통째로 깨뜨렸었다 — 인용이 > 줄바꿈에 걸쳐 있어 마커가 인용문 안에 들어가 있었고, 그걸 지우면서 인용 블록이 무너졌다. 규약에 함정으로 명시.
  • 절 제목을 명사구 자리에 그대로 끼워 문법이 깨진 문장 5건 ("pluggable 디스패치 엔진 자체도"는 원칙에…). "…" 절이 세운 원칙 형태로 교정하고 이것도 규약에 넣음.
  • 축약하다 주장이 빠진 인용 1건(README.md의 "빈 자리 아님" 결론).

1라운드 감사 — 규칙 자체의 정밀도 구멍

두 번째 감사자가 더 중요한 걸 짚었다 — attribute-plan.mddispatch-core-plan.md가 인용한 architecture.md**pluggable 디스패치 엔진 자체도 …**절이 아니다. 한 문단이 줄바꿈되면서 강조 스팬이 우연히 줄머리에 걸린 자리이고, 진짜 절은 두 줄 위의 **패키지 경계**다. 같은 원문을 인용하는 slot-plan.md:209는 이미 올바르게 "패키지 경계"를 쓰고 있어서, 같은 diff 안에 옳은 인용과 우연한 인용이 공존하고 있었다.

즉 "줄머리 볼드"라는 규칙만으로는 절과 랩된 강조를 못 가른다. 진짜 절 제목은 빈 줄 다음에 오거나 리스트 항목 머리라는 성질이 있어서 그걸 조건으로 추가했다. 실측 결과 정확히 그 가짜 인용 2건만 깨지고 나머지 46건은 그대로 통과했고, 볼드 후보 풀은 dispatch-core-plan.md 기준 131→104로 좁아졌다. 위양성 0으로 정밀도만 올린 셈.

교훈: 이 규칙은 "사람이 절이라고 인식하는 것"을 근사하는 휴리스틱이라, 근사가 어긋나는 자리는 실제 인용을 감사해서만 드러난다. 검사기가 통과시켰다고 그게 절인 건 아니다.

1라운드 감사 — 이 세션 자신의 실수

pre-implementation-audit.md의 "아직 안 고침" 절에 있는 State<Modifier> 비일관성 항목이 해소된 것으로 보여 **[해소됨 — 2026-08-16 확인]** 마커를 달았는데, 바로 다음 문단에 2026-08-09 세션이 단 **[완전 해소, 2026-08-09 세션]**이 이미 있었다. 중복일 뿐 아니라 해소 시점을 일주일 뒤로 잘못 읽히게 만들어 되돌렸다. 서브에이전트(추적 담당)가 "후임 절이 옛 서술을 뒤집었다"는 근거를 정확히 줬지만 그 항목이 이미 해소 표시를 달고 있다는 사실은 안 봤고, 메인 세션도 그 문단 아래를 안 읽었다 — 근거가 맞아도 "이미 처리돼 있지 않은가"를 따로 확인해야 한다는 교훈.

이 항목이 해소 표시를 달고도 "아직 안 고침" 헤더 아래 남아 있는 헤더-내용 불일치는 이 세션 이전부터 있던 별개 부채라 임의로 재배치하지 않고 사용자에게 물었고, "이미 고침" 절로 옮기라는 결정을 받아 반영했다. 그 헤더 아래엔 이제 진짜 미해소인 Destroying 훅 건만 남는다. 감사 세 라운드가 전부 이 자리를 지목했는데, 세 번 다 "고칠지 말지가 판단 문제"라 서브에이전트는 결론을 못 냈다 — 메인 세션만 사용자에게 물을 수 있다는 게 이 절차의 핵심인 이유다.

2라운드 감사 — "규칙을 세운 세션이 그 규칙을 어긴다"

1라운드 발견을 반영한 뒤 2라운드를 돌렸더니 또 실질 발견 2건이 나왔다. 둘 다 이번 세션이 새로 생산한 산문의 문제였다.

  1. base/brand-plan.md에 이번에 새로 쓴 문장이 "세션은 파일 ID로 지칭할 것" 규약을 어겼다 — 같은 커밋에서 만든 규약을 같은 커밋의 새 문장이 위반. 2026-08-07 여덟 번째 세션 → 세션 파일 ID로 교정했고, 이번에 건드린 다른 두 줄(README.mdtools/ 행, lifecycle-pattern.md)도 규약의 "만지는 김에 같이 고친다" 조항대로 처리했다.
  2. research/framework-comparison-findings.md의 수정이 "고침"이 아니라 "검사망 회피"였다"왜 quad를 쓰는가" 인용을 문법만 비껴가게 고쳐서 검사 대상에서 통째로 빠졌는데, 감사자가 REF 정규식으로 group(3)None이 되는 걸 직접 확인해 잡았다. 같은 diff의 다른 두 회피 (CLAUDE.md의 "세션 히스토리", 옛 Modifier는 핸들러 계층을 모름)는 과거형으로 참인 사실을 서술하니 정당하지만, 이건 대상 문서에 근거가 없는 미래형 주장이었다는 게 판정 근거. 실제 절(초심자 (getting-started) core loop/quadnomicon)을 가리키도록 고쳤다.

부수로, conventions.md에 박아둔 세션 서수 분포 숫자(971/68/8)가 재현되지 않았다 — 내 집계는 session/까지 포함한 전체 grep이었고 감사자는 doc-check.py가 훑는 88개 라이브 문서 기준이었다. 집계 범위를 안 적은 숫자를 매 세션 로드되는 파일에 박은 것 자체가 이번에 세운 "개수는 소스 하나만" 원칙 위반이라 정성적 서술로 바꿨다. 같은 이유로 날짜 균일 수치도 conventions.md에서 빼고 이 파일을 가리키게 했다.

교훈: 규약을 세우는 커밋이 그 규약의 첫 위반자가 된다. 규약을 쓴 세션은 자기 diff를 그 규약으로 한 번 되짚어야 하고, 그걸 자기 self-audit 으로는 잘 못 한다(이번에도 둘 다 서브에이전트가 잡았다).

3라운드 감사 — 수정이 만든 새 결함, 그리고 사각지대 하나

2라운드 수정을 반영하고 3라운드를 돌렸더니 내가 2라운드에 만든 마크다운 버그가 나왔다. "cleanup" 용어 인용을 규약대로 고치려고 볼드 스팬을 쪼개다 짝 없는 **를 남겼다lifecycle-pattern.md** 개수가 144(짝수)→145(홀수)로 바뀐 걸 감사자가 교차 확인해 잡았다. 렌더러에 따라 그 뒤 리스트의 볼드 페어링까지 밀려 깨질 수 있는 실제 결함이었다. 복구한 뒤 변경 파일 전체에 같은 검사를 기계적으로 돌려, 내 변경이 짝을 뒤집은 파일은 그것 하나뿐임을 확인했다(다른 홀수 파일들은 HEAD에서도 이미 홀수 — 코드블록 안의 ** 때문).

구조적 사각지대 하나가 같이 드러났다: - **[해소됨, …]** 실제 제목 — … 처럼 볼드가 상태 태그에서 닫히고 진짜 제목이 볼드 밖 평문이면 (archive/question-resolved.md가 이 형태) 검사기는 [해소됨, …]만 절 제목으로 본다. 절 참조가 ERROR로 승격된 뒤라, 이걸 모르고 규약대로 큰따옴표 인용을 하면 실재하는 서술인데도 커밋이 막힌다. 규약에 캐비엇으로 명시하고 "새로 쓸 땐 태그와 제목을 한 볼드 안에" 지침을 넣었다. 게다가 이 세션이 신설한 "세션은 파일 ID로 지칭" 관례가 **[태그, 세션ID]** 형태의 리드를 늘리는 방향이라 재생산될 구조였다는 지적까지 붙었다.

보류한 것 하나 — # 헤딩 검사가 부분문자열이라 "확정" 같은 짧은 인용은 같은 파일의 무관한 헤딩에 걸려 통과한다(slot-plan.md엔 "확정"이 든 헤딩이 6개). 다만 감사가 실제 오매칭 사례를 하나도 못 찾았고, 사용자와 같이 conventions.md의 "드문 오용이나 가상의 미래 요구까지 방어/최적화하려고 구조를 복잡하게 만들지 않는다" 원칙에 따라 지금은 손대지 않기로 했다.

감사자가 git stash를 쓴다 — 실동에서 드러난 위험

.pyc 추적 해제(git rm --cached)가 두 번 되돌아갔다. 원인은 감사자들이 HEAD와 대조하려고 git stash를 건 것 — 커밋 안 된 작업 트리에서 그걸 하면 메인 세션의 스테이지가 풀리고, pop이 실패하면 세션 작업을 통째로 날릴 수 있다. 이번엔 유실이 없음을 확인했고, .claude/agents/quad-doc-auditor.md에 작업 트리를 바꾸는 git 명령 금지와 대안(git show HEAD:<경로> 등)을 규약으로 넣었다. "읽기 전용"을 도구 목록으로 못 거는 이상(tools: 필드가 그대로 반영되지 않는 기존 미해결 항목) 이런 규약이 유일한 방어선이다.