# findings — 사실 인덱스 · 코드 축 (11-index-code)

> **잠정 형식 경고.** `findings/` 기록 형식은 `00-protocol` 마일스톤 7 산출물이며 **아직 미확정**이다.
> 이 파일은 그 형식이 확정되면 재정리 대상이다. doc-kit(flow/ia/policy) 3종 어디에도 findings 형식이
> 없고 `lint-docs.mjs` 는 인자로 받은 파일만 검사하므로(전역 스캔 없음) 현재 이 파일에 강제되는
> 형식 규약은 없다. 서술 밀도는 `.claude/meta/06-access-model-findings.md` 를 따랐다.

- **출처 마일스톤**: 0·1·2·3·4 — 스크래치 승격 / `codes/*.yaml` 6분할 + 서브레벨 전수 / `policy-anchors.yaml` 5축 분리 / `env-gates.yaml`·`constants.yaml` 값 축 / `api-contracts.jsonl`·`schema-constraints.jsonl` 계약·스키마 축
- **조사 기준점**: 정본은 `docs/_meta/extraction-manifest.yaml` 의 `repos[].head` 다 — server
  `c7597fc7` · site `b9a90a42` · admin `7df733a5`. 초기 조사(마일스톤 0·1)는 server `4d798f55` ·
  site `767d1e44` · admin `fb001e1d`(2026-08-05 origin/stg tip)에서 시작했고 `bcb50ec` 에서 위
  SHA 로 재핀했다. **옛 기준점에서 잰 수치는 재측정 대상이며 차이는 결함이 아니라 stg 진행량**
  이라는 것이 manifest 규약이다. 아래 표의 수치는 재핀 후 `--report` 값이다.

> **승격 입력의 위치 — 이 저장소 안이 아니다.** 스크래치 2개는 `.gitignore` 대상이며 **메인 worktree**
> 에만 있다. 이 worktree(`.worktrees/index-code/`) 기준 상대경로는 `../../.claude/meta/` 이고,
> 저장소 루트 기준 `.claude/meta/` 가 아니다. 아래 표와 부록 A 의 `admin-corp.temp.md:NNN` 표기는
> **전부 `../../.claude/meta/admin-corp.temp.md` 를 가리킨다.** 이 worktree 안에서
> `.claude/meta/admin-corp.temp.md` 를 열면 없거나 다른 파일이다 — 대조할 때 경로를 먼저 맞출 것.
>
> | 파일 | 절대 위치(이 worktree 기준) | sha256 | 행 수 |
> |---|---|---|---|
> | `admin-corp.temp.md` | `../../.claude/meta/admin-corp.temp.md` | `947ad14f6489dcef1de250fe68816eb3a1fbf6fc90c0d02bd80e146a4a535c2a` | 753 |
> | `admin.temp.md` | `../../.claude/meta/admin.temp.md` | `552cafa05a8089abba15a3ff06025fff499e5ec74a51b55ea884330dc54e2896` | 575 |
>
> 행 수는 `wc -l` 기준이다(마지막 줄 개행 포함 시 `split("\n").length` 는 754/576 이 나온다).
> **sha256 이 정본 대조 수단이다** — 행 번호가 안 맞으면 먼저 sha256 을 확인하라. 다르면 다른
> 스냅샷을 보고 있는 것이다(예: `.claude/meta/backup-2026-08-05/`).

## 1. 발견

| # | 발견 | 근거 (파일:행) | 영향 | 조치 |
|---|---|---|---|---|
| 1 | **E코드는 유일하지 않다.** 한 코드값이 최대 4개 상수 심볼로 갈리고 의미가 전부 다르다. `E0202` = 계약일 중복 / 조직 내 활성 계약 중복 / 이용기간 만료 / 처리 실패 | `insuniverse-server/apps/api/src/constants/admin.constant.ts:18,63` · `api.constant.ts:40` · `client.constant.ts:41` (E0202) · 동 `admin.constant.ts:75` · `api.constant.ts:70` · `alimtalk.service.ts:37` · `treat.service.ts:64` (E0311) | PRD Open Question "E코드 계층 규칙" 에 대한 **부분 답이자 전제 반박** — 계층 이전에 코드값↔의미 1:1 대응 자체가 없다. 코드값 조인은 4개 후보를 1개로 뭉갠다 | `codes/error.yaml` 을 심볼 단위로 전개(8코드 → 19항목). 조인 키를 `id: <namespace>:<symbol>` 로 신설하고 검증기가 유일성을 강제. `code_shared_with` 상호 대칭도 기계 검사 |
| 2 | **`error_codes` 정규식이 admin 에러 표면을 0% 덮는다.** `^E[0-9]{4}$` 에 맞는 값이 admin `errorCodes.ts` 29키 중 0건. 실제는 SQL 형 5 · bare numeric 형 22 · 문자열 형 2 | `insuniverse-admin/src/shared/constants/errorCodes.ts:2-45` 전문 · `docs/_meta/extraction-manifest.yaml:134` | manifest 정규식만 믿으면 admin 에는 에러 코드가 없다고 결론난다. 마일스톤 1 전수 조사가 그대로 admin 을 통째로 누락한다 | `codes/_unclassified.yaml` 로 수집(형태 3종 + 대표 5키). **`00-protocol` 에 규약 개정 요청** — 아래 §2 |
| 3 | **`§14 → domain_status.yaml` 승격 매핑이 실물과 어긋난다.** §14 15행에 도메인 상태 코드 리터럴이 0건이다. 전부 필드 단위 표기(색상) 규약이다 | `.claude/meta/admin-corp.temp.md:698-720` | PRD 승격 표대로 하면 코드가 아닌 것을 코드 인덱스에 넣게 된다 | 승격하지 않는다. `domain_status.yaml` 은 두 스크래치 본문 실측 6종으로 채웠다. §14 15행은 **부록 A 에 원문 보존** 후 `50-domain` 이관 (그쪽 PRD 수정은 이 PRD 규약상 금지 → backlog) |
| 4 | **스크래치 프런트 문구 ↔ 서버 문구가 다르다.** §13 `E0202` = "계약일이 중복되었습니다." vs 서버 = "중복되는 계약이 존재합니다." | `.claude/meta/admin-corp.temp.md:687` · `insuniverse-server/apps/api/src/constants/admin.constant.ts:18` | 두 문구는 서로 다른 층이다 — 서버가 내려주는 `message` 와 프런트가 화면에 쓰는 문구. 어느 쪽을 "그 코드의 메시지" 로 볼지 규약이 없다 | `error.yaml` 의 `message` 는 **서버 문구**로 통일하고(관측 가능한 소스가 저장소 안이므로) 차이를 항목 `note` 에 남겼다. 프런트 문구 축은 `50-domain` 몫 |
| 5 | **심볼 이름조차 유일하지 않다.** `UNAUTHORIZED` 가 두 파일에서 서로 다른 코드로 export 된다(E0601 / E0001). `CERTIFY_IN_PROGRESS` 는 두 서비스에 module-local 로 중복 선언 | `insuniverse-server/apps/api/src/constants/admin.constant.ts:58` (E0601) · `api.constant.ts:16` (E0001) · `alimtalk.service.ts:37` · `treat.service.ts:64` | 계획이 전제한 `id: <namespace>:<symbol>` **전역 유일**이 그대로는 성립하지 않는다. 발견 1의 완화책 자체가 깨진다 | `symbol` 을 **모듈 한정 점표기**로 정의(`admin.constant.UNAUTHORIZED`). `id == namespace + ":" + symbol` 규칙은 그대로 두고 유일성만 실제로 성립시킨다. 검증기 selftest 6번이 이를 강제. **→ 마일스톤 1 에서 저장소 한정으로 승격**: 파일 stem 만으로는 3저장소 전수에서 다시 깨진다(같은 stem 이 여러 디렉터리·여러 저장소에 실재). 규칙은 `<repo>:<저장소 루트 기준 모듈 경로>.<선언 체인>.<이름>` 이고 selftest 23·24·25 가 디렉터리 충돌 · 저장소 충돌 · `localId` 불변식을 강제한다 |
| 6 | **`admin.temp.md §11` 은 코드 총람이 아니라 근거 파일 인덱스다.** 29행 전부 "주제 → 파일 경로" 표이고 코드 리터럴 0건 | `.claude/meta/admin.temp.md:542-570` | PRD 마일스톤 0 의 "§11 편입" 항목에 편입할 코드가 없다 | 승격 대상 아님. 경로 29개는 전부 admin 저장소에서 재구성 가능해 §14 와 손실 등급이 다르다(§14 는 저장소에 없는 규약, §11 은 저장소를 가리키는 포인터). 본문 보존 여부 판단은 `10-index-file` / `00-protocol` 몫 → backlog |
| 7 | **`PRD002` "사용처 없음" 이 신 SHA 실측과 다르다.** 스크래치는 "타입 시그니처에만 존재" 라고 적는데 서버에 한정 참조 4건이 있다 | `.claude/meta/admin-corp.temp.md:38` vs `insuniverse-server/.../order.repository.ts:926` · `order.service.ts:306` · `product.repository.ts:94` · `subscribe.repository.ts:682` | 스크래치 관측이 admin 저장소 한정이었거나 그 뒤 stg 가 진행됐다. 어느 쪽이든 **스크래치 서술을 그대로 승격하면 거짓이 들어간다** | 값을 스크래치에 맞추지 않고 실측대로 적었다(`domain_status.yaml` 해당 항목 `note`). 승격은 "옮겨 적기" 가 아니라 "신 SHA 에서 재확인" 이라는 PRD 규약이 실제로 작동한 사례 |

발견 1·2·3·4 는 계획이 예상한 4건이다. 5·6·7 은 승격 수행 중 측정으로 새로 드러난 것이다.

### 마일스톤 1 (전수 생성)

수치의 정본은 `node docs/_meta/verify/build-code-index.mjs --repos-root ../.. --report` 출력이다.
아래 표의 숫자는 전부 그 출력 또는 저장소 grep 실측이며, 계획의 추정치가 아니다.

| # | 발견 | 근거 (파일:행) | 영향 | 조치 |
|---|---|---|---|---|
| 8 | **`vendor_codes` 정규식이 벤더 코드가 아닌 env 변수명을 통과시킨다.** 3저장소 합집합 distinct 56 중 **26 이 `CODEF_ID`·`CODEF_SECRET`·`COOCON_AES_KEY` 류 환경변수 이름**이고 벤더 응답 코드가 아니다. 응답 코드 형태(`CF-`)는 30 | `docs/_meta/extraction-manifest.yaml:136` · `insuniverse-server/apps/api/src/modules/treat/error/treat-error-vendor-map.ts:8` | 정규식대로 담으면 `vendor.yaml` 절반이 코드가 아닌 설정 키가 된다. manifest 는 이 PRD 에서 **읽기 전용**이라 여기서 고칠 수 없다 | `vendor.yaml` 에 `CF-` 형만 담고, 무엇을 왜 뺐는지 `gap` 에 적었다. 정규식 개정 요청은 아래 §2 |
| 9 | **`menu_codes` 정규식이 서버 에러 상수를 과매칭한다.** `^[A-Z][A-Z0-9]{2}[0-9]{3}$` 가 `AE9999`·`CE9999`·`FC0001`·`FC9999`·`IE9999`·`TE9999`·`VE9999` 7종을 통과시킨다. 전부 메뉴와 무관한 외부 연동 실패 코드다 | `insuniverse-server/apps/api/src/constants/api.constant.ts` (7개 상수) · `docs/_meta/extraction-manifest.yaml:173` | `_unclassified` 의 반전 검사가 "menu_codes 정규식에 맞으니 여기 있으면 안 된다" 로 이 7종을 쫓아내는데, 갈 곳이 없다. 정규식만으로 축 소속을 판정하면 막다른 골목이 생긴다 | **namespace 를 두 종류로 나눴다** — 소속이 코드값 모양으로 정해지는 *선언 축*(error·domain_status·vendor·_unclassified)과 출처로 정해지는 *추출기 축*(menu·screen). 추출기 축의 `pattern` 은 구성원의 모양 제약이지 소유권 주장이 아니다. 검증기 `EXTRACTOR_OWNED_NAMESPACES` 가 이를 강제 |
| 10 | **축 간 코드값 중복이 실재한다.** `MEM002`·`POP001`·`SUB001`·`SUB002`·`SUB004` 5종이 도메인 상태 코드이면서 동시에 메뉴 코드다 | `insuniverse-server/apps/api/src/modules/subscribe/subscribe/subscribe.entity.ts` (SubscribeType) · `insuniverse-admin/src/features/authority/constants/menu.ts` · stg live DB `tn_menu` | 마일스톤 0 의 namespace 배타성 검사는 이것을 위반으로 판정한다. 그러나 같은 글자를 두 축이 각자의 뜻으로 쓰는 것은 **실측된 사실**이지 결함이 아니다 | 배타성은 선언 축 사이에서만 적용하고, 축을 가로지르는 공유는 `cross_source: true` + `cross_source_files` 로 **드러낸다**(PRD Success Metric "소스 축 중복은 cross_source 로 드러남"). 12개 항목에 표기됐고 검증기가 양방향 대칭을 강제한다 |
| 11 | **서브레벨 `ANS003A`/`ANS003B` 는 실재하지만 선언이 아니다.** manifest 정규식 `[0-9]{3}[A-Z]?` 가 이미 덮으며 소스에도 있다. 다만 등장 형태가 site 표시 설정 테이블의 **맨 객체 키**(`ANS003A: { title: … }`)이고 서버 enum 에는 없다 | `insuniverse-site/front/src/features/analysisDetailView/constants/analysisDetail.const.ts:15,24` · `insuniverse-server/apps/api/src/modules/disease/disease.repository.ts:3147`(주석) | 정규식은 문제가 없다. "전수" 를 코드값 기준으로 세면 잡히지만 **심볼 기준으로는 항목이 되지 못한다** — 조인 키를 만들 선언이 없다 | 항목으로 만들지 않고 심볼 없는 등장으로 계수했다. 생성기가 계획의 4형태(따옴표 키 포함)만 선언으로 인정하며, 맨 키는 조회 키로 본다. `domain_status.yaml` 의 `gap` 에 명시 |
| 12 | **심볼 없는 등장이 선언보다 훨씬 많다.** 3저장소 합계 **1,870건**(server 606 · site 722 · admin 542)이 선언 구간에 흡수되지 않았다. site 는 선언이 **1건**뿐이다 — 코드를 만들지 않고 소비만 한다 | `--report` 출력 · `insuniverse-server/apps/api/src/fliters/codef-exception.filter.ts:65-74`(`codefCode === 'CF-13000'` 형) | 코드값 distinct 로 세면 `CF-` 30·domain_status 73 인데 심볼 단위 항목은 22·59 다. **두 수를 같은 것으로 보면 "누락" 으로 오해된다** | 두 수를 구분해 기록했다. 심볼 없는 등장은 버리지 않고 각 파일 `gap` 에 개수로 남겼다. 정밀화(import 해석 기반 소비처 추적)는 backlog |
| 13 | **주석 처리된 코드가 진짜 선언과 같은 심볼을 만든다.** `analysisDetail.const.ts:122,126` 에 살아 있는 선언과 동일한 키의 주석 사본이 있다 | `insuniverse-site/front/src/features/analysisDetailView/constants/analysisDetail.const.ts:15,24` vs `:122,126` | 주석을 안 걷어내면 id 전역 충돌이 나고 생성기가 0건 쓰기로 멈춘다. 조용히 dedupe 하면 어느 쪽이 진짜인지 알 수 없게 된다 | 생성기가 스캔 전에 주석을 공백으로 덮는다(**행 번호는 보존** — 앵커가 행 번호이므로). manifest `comment_rules.exclude_commented_out_code: true` 와 같은 태도다 |

발견 8·9·10 은 계획이 예상하지 못한 것이다. 계획은 벤더 과매칭(8)만 예상했고, 정규식이 **축 소속을
판정할 수 없다**는 더 일반적인 사실(9·10)은 전수 생성을 실제로 돌려서야 드러났다.

### 마일스톤 2 (`policy-anchors.yaml` 5축 분리)

수치의 정본은 `node docs/_meta/verify/build-policy-anchors.mjs --repos-root ../.. --report` 출력이다.

| # | 발견 | 근거 (파일:행) | 영향 | 조치 |
|---|---|---|---|---|
| 14 | **PRD 의 "4축" 과 spec §2 의 5개념이 다르고, PRD Risk 완화책의 역할 enum 이 실물과 어긋난다.** PRD 는 "역할·능력·메타키·에러상수 4축" 이라 적고 완화책에 "역할 필드는 enum 2종(`CORP_SUPER`/`CORP_ORG`)만 허용" 이라 적는데, `@Roles()` 가 실제로 받는 값은 `admin`·`sales`·`corp` 다 | PRD `:55` · `.claude/meta/05-final-spec.md` §2 · `insuniverse-server/apps/api/src/modules/auth/admin/roles.decorator.ts:4` (`Roles = (...roles: string[])`) · `auth.service.ts:132,192` (`agcRole`) | PRD 대로 스키마를 짜면 **`@Roles` 매트릭스의 역할값이 역할 필드에 못 들어간다.** 4축은 역할·기업역할을 한 칸으로 센 것이고, 둘을 합치면 정확히 spec §2 가 경고한 개념 혼동이 스키마 층에서 재생산된다 | PRD 헤더가 `05-final-spec.md` 를 정본으로 못박았으므로 **5축으로 갔다**(역할 / 기업역할 / 능력 / 메타키 / 에러상수). PRD Success Metric(`CORP_FORBIDDEN`·`CORP_ANALYSIS_VIEW_KEY` 가 역할 목록에 없음)은 5축에서 더 강하게 성립한다 — `roles` 축이 `{admin, sales, corp}` 로 **닫혀** 있어 검증기가 어휘 초과로 떨어뜨린다(selftest D2). PRD 본문 Risk 문구 수정은 이 마일스톤의 2행 규약 밖 → backlog |
| 15 | **가드는 붙었는데 역할 제한이 0인 엔드포인트가 6개 있다.** `RolesGuard` 를 클래스 레벨에 달았지만 `@Roles` 가 0건이고, `RolesGuard` 는 메타데이터가 없으면 **통과시킨다** | `insuniverse-server/apps/api/src/modules/treat/monitor/treat-monitor.controller.ts:9` (`@UseGuards(AdminJwtAuthGuard, RolesGuard)`) · `roles.guard.ts:24` (`if (!requiredRoles) return true`) · 핸들러 6 (`overview`·`monthly`·`enterprises`·`daily`·`dailyExcel`·`vendorErrors`) | 매트릭스 모집단을 `@Roles` 보유 컨트롤러로 잡으면 이 6개가 통째로 빠진다 — 매트릭스가 드러내야 할 표면을 **구조적으로** 숨긴다. 누락이 아니라 모델이 틀린 것이라 나중에 못 고친다 | 모집단을 **`RolesGuard` 가 유효 적용되는 핸들러 전수**로 정의했다(356행). 이 6개는 `roles_scope: none` + `rolesguard_passthrough: true` 행으로 존재하고, 검증기가 커버리지를 양방향으로 강제하며 selftest 픽스처가 6핸들러를 **이름으로** 박아 모집단이 다시 좁아지면 먼저 깨진다. 이 6개를 열어둘지 닫을지는 문서 축이 아니라 제품 판단이라 여기서 정하지 않는다 |
| 16 | **가드는 12종이다. spec §2·PRD 의 "13종" 과 다르다.** `*.guard.ts` 12 = `implements CanActivate` 12 = `@UseGuards` distinct 12, 3중 일치 | `.claude/meta/05-final-spec.md:187` · PRD `:22` vs `policy-anchors.yaml` `guards` 12항목 (전부 앵커 검증됨) | 구 SHA 기준 수치가 신 SHA 기준 acceptance 로 그대로 쓰이면 "1종 누락" 으로 오판된다 | 값을 지어내지 않았다. 12 로 적고 13 과의 차이를 `gap` 에 **양쪽 SHA 와 함께** 병기했다. 정정 요청은 아래 §2 |
| 17 | **`@Roles` 137 은 `dist/` 혼입과 무관하다.** PRD `:22` 는 "136 → 재측정 137, 원인 = `dist/` 혼입" 이라 적는데, 체크아웃 트리에 `dist/` 디렉터리가 **0건**이고 `exclude_glob` 적용/미적용 결과가 둘 다 137 이다 | PRD `:22` · 저장소 스캔 (`find -type d -name dist` = 0) · `--report` 의 `@Roles=137` | PRD 가 기록한 원인 설명이 현 HEAD 에서 재현되지 않는다. 그 설명을 그대로 승계하면 다음 재측정에서 또 "과다 계수 재발" 로 오판된다 | 관측만 기록한다. 구 SHA 136 과의 차이는 stg 진행량일 가능성이 높지만 **구 SHA 를 다시 체크아웃해 재측정하지 않는 한 단정하지 않는다.** 인자 조합 5종(`('admin')` 63 · `('admin','sales','corp')` 31 · `('admin','corp')` 23 · `('admin','sales')` 12 · `('corp')` 8)도 함께 남겨 다음 재측정이 개수만이 아니라 분포로 대조되게 했다 |
| 18 | **`build-code-index.mjs` 의 `stripComments` 가 정규식 리터럴을 모른다.** 정규식 안의 따옴표를 **문자열 시작**으로 읽고 그 뒤를 통째로 삼킨다 | `insuniverse-server/libs/youtube/src/youtube.service.ts:59` (`/(?:youtube\.com\/…([^"&?\/\s]{11})/`) · `docs/_meta/verify/build-code-index.mjs:299-314` | 마일스톤 1 에서는 증상이 안 났다 — 그쪽은 선언 구간을 최상위 선언 위치로만 자르므로 삼킨 구간이 항목을 만들지 않았을 뿐이다. **중괄호 깊이로 클래스 구간을 자르는 순간 터진다**: 마일스톤 2 생성기가 `class YoutubeService 의 본문 중괄호가 닫히지 않았다` 로 멈췄다 | 마일스톤 2 는 정규식 리터럴을 함께 덮는 `blankCommentsAndRegex` 를 자기 파일에 두고 `stripComments` 를 쓰지 않는다. `build-code-index.mjs` 쪽은 **고치지 않았다** — 마일스톤 1 산출물이고 현재 증상이 없으며, 고치면 재생성 결과가 바뀌어 그 마일스톤의 재현성 지표를 이 마일스톤에서 흔들게 된다 → backlog |
| 19 | **`*_KEY` 라는 상수 이름으로 메타키 축을 잡으면 admin 의 브라우저 저장소 키가 섞인다.** 첫 구현이 `export const X_KEY = '문자열'` 을 전부 받아 `LOGIN_PORTAL_KEY='last_login_portal'` · `CORP_MY_PAGE_VERIFIED_KEY='corpMyPageVerified'` 2건이 축에 들어왔다 | `insuniverse-admin/src/features/login/constants.ts:7` · `insuniverse-admin/src/features/authority/ui/CorpMyPageEntry.tsx:11` | "리플렉션 메타데이터 키" 라는 축의 뜻이 무너진다. 이름 규약(`_KEY` 접미)은 소속의 근거가 아니다 — 발견 9(정규식이 축 소속을 판정할 수 없다)의 같은 부류다 | 축 소속을 **`SetMetadata(<심볼>, …)` 로 실제 선언됐는가**로 판정하도록 바꿨다. 선언(`export const`)과 사용(`SetMetadata`)을 따로 모아 저장소 전수에서 교차시킨다. 결과 3종(`roles` · `corpAnalysisViewLevel` · `allowOrgWrite`)이고 생성기·검증기 두 스캐너가 독립적으로 같은 3종을 낸다 |
| 20 | **`verify/` 소유 규약을 구현 착수 전에 개정했다.** PRD `배타 소유 경로` 절은 이 PRD 가 `docs/_meta/verify/` 에 **파일 1개만** 쓴다고 못박았는데, 마일스톤 1 이 이미 `build-code-index.mjs` 를 추가했고 마일스톤 2 가 2개를 더 만든다 | PRD 개정 전 `:170` · 커밋 `d053c20` (Task 0b, 구현 커밋보다 앞선다) | findings 에 개정 요청만 적는 것은 규약을 바꾸지 않는다. 어긴 채 `complete` 를 찍으면 acceptance 가 정본과 모순되고, 나중에 이 디렉터리를 보는 쪽이 **의도된 구조와 규약 위반을 구별할 수 없다**. 마일스톤 1 의 선례는 허가가 아니다 | 자기 PRD 이므로 본문 개정이 허용된다(마일스톤 1 Task 0 이 Scope 예시 2줄을 고친 선례). 문장을 **소유 파일 4개 열거**로 바꿨다. 디렉터리는 여전히 `00-protocol` 소유이고 `extract-*.mjs` 2종도 그쪽 소유로 남는다. 판정기와 생성기를 한 파일로 합치지 않은 이유는 개정문에 함께 적었다 |

발견 18·19 는 계획이 예상하지 못한 것이다. 둘 다 **구현을 실제로 돌려서야** 드러났고, 공통점은
"이름·모양으로 소속을 판정하면 틀린다" 는 발견 9 와 같은 축이다.

### 마일스톤 3 (`env-gates.yaml`·`constants.yaml` 값 축)

수치의 정본은 `node docs/_meta/verify/build-policy-values.mjs --repos-root ../.. --report` 출력이다.

| # | 발견 | 근거 (파일:행) | 영향 | 조치 |
|---|---|---|---|---|
| 21 | **spec §1 version 3 의 게이트 15종은 커밋되지 않았다.** 이 브랜치의 커밋된 `05-final-spec.md` §1 `known_gates` 는 **11종**이고, 15종(+ `RUN_MODE`·`SCHEDULER_TYPE`·`REACT_APP_SMARTRO_MODE`·`REACT_APP_TOSS_CLIENT_KEY`)은 **main 체크아웃의 미커밋 작업본**에만 있다 | 이 worktree `.claude/meta/05-final-spec.md:150-161` (11종) vs `../../.claude/meta/05-final-spec.md` 작업본 (15종) · `git -C ../.. status --porcelain .claude/meta/05-final-spec.md` → `M` · `git worktree list` (`index-runtime` 브랜치가 그 4종의 요청자) | 계획과 Plan-Codex R1 F1 흡수는 spec v3 를 **정본으로 취급**해 15종을 싣기로 했었다. 미커밋 파일을 소스로 쓰면 다른 클론에서 11종이 나와 **재현성 지표가 처음부터 거짓**이 된다 — `.env` 를 배제하는 것과 정확히 같은 이유다 | **11종으로 갔다.** 검증기의 게이트 목록 규칙은 "정확 일치" 가 아니라 **"manifest 하한 + 정본 출처 강제"** 라, spec+manifest 가 커밋되면 그 4종이 `source: spec_v3` + `pending_manifest_registration: true` 로 **파일을 안 고쳐도** 통과한다. 넷 다 코드에는 실재한다(각 4·4·1·1 파일). 커밋 요청은 아래 §2 |
| 22 | **3저장소 전부에 `.env` 가 실재하고 manifest 의 exclude 2목록 어디에도 없다.** spec §6-1 은 "`insuniverse-server`에 `.env`가 없다" 를 전제로 `conditional-unknown` 을 확정했는데 그 전제가 낡았다 | `insuniverse-server/.env` · `insuniverse-site/front/.env` · `insuniverse-admin/.env` (셋 다 `git ls-files --error-unmatch` 실패 = 미추적) · `insuniverse-server/.gitignore:37,51,52` · `extraction-manifest.yaml:48-77` | 값 축 스캐너가 이걸 읽으면 (a) 로컬 개발 시크릿이 **커밋되는 인덱스**로 새고 (b) 다른 머신엔 없는 파일이 소스가 되어 재현성이 무너진다 | **결론(`conditional-unknown`)은 유지하되 이유가 바뀐다** — 없어서가 아니라, 있는 것이 운영값이 아니고 재현 가능하지도 않아서다. 방어 3중: 생성기 하드 제외 · 검증기가 **구조화 앵커 필드**의 `.env` 경로를 error · 앵커 파일의 git 추적 여부를 `execFileSync` **배열 인자**로 확인. 산문(`gap`)의 `.env` **언급**은 결손 기록이므로 통과다(selftest D1·D6·D9). manifest 개정 요청은 아래 §2 |
| 23 | **시드 5패턴을 닫힌 목록으로 읽으면 금액 상한이 통째로 빠진다.** spec §2:188 원문은 "`MAX_MONEY_*`·`*_TTL_MS`·`RATE_LIMIT_*`·`BATCH_SIZE`·`*_TIMEOUT` **등**" 으로 끝난다 | `.claude/meta/05-final-spec.md:188` · `--report` 의 `constants_written=62` / `constants_matched_seed=21` · 시드 밖 실물 `MAX_CHARGE_AMOUNT`·`MIN_CHARGE_AMOUNT`·`MAX_GRANT_AMOUNT`·`MAX_CORP_MONEY_AMOUNT`·`MIN_AGE`·`DAILY_MAX_SPAN_DAYS` 등 | 5패턴만 쓰면 항목이 21 로 줄고, spec §4:261 이 `01-amounts-and-quotas.md` 로 요구한 금액 상한이 사라진다 — 소비 PRD 가 코드를 다시 뒤지게 되며 이 PRD 의 존재 이유가 무너진다 | 5패턴을 **시드**로 쓰고 수치 초기자를 가진 SCREAMING_SNAKE 불변 선언을 함께 수집한 뒤 `matched_seed` 로 출처를 드러냈다. **넓게 뽑고 좁게 쓰는 것은 되지만 반대는 안 된다.** 축 소속 규칙(`isPolicyValueDecl`)은 검증기에 한 벌만 두고 생성기가 import 한다 |
| 24 | **`AUTH_TIMEOUT` 은 시간 상수가 아니라 enum 멤버다. `VENDOR_TIMEOUTS` 는 실물에 0건이다.** spec §4:262 가 둘을 `02-ttl-and-timeouts.md` 에 배치했다 | `.claude/meta/05-final-spec.md:262` · `insuniverse-server/apps/api/src/modules/treat/error/treat-error-category.ts:12` (`AUTH_TIMEOUT = 'AUTH_TIMEOUT'`) · 3저장소 `VENDOR_TIMEOUTS` grep 0건 | 시드 `*_TIMEOUT` 이 이 18개소를 잡는다. **spec 자체의 오분류**이고, 이름 모양으로 소속을 정하면 재발하는 발견 9·19 와 같은 부류다 | `kind` 를 **이름이 아니라 값**으로 판정한다 — 시드에 맞는 이름이어도 값이 비수치면 `kind: non_numeric` · `value_number: null` 로 싣고, 검증기가 "값이 비수치인데 `kind: number`" 를 error 로 막는다(selftest D10). **다만 `AUTH_TIMEOUT` 자체는 이 축에 없다**: 이 축은 불변 **선언** 4형태만 담는데 enum 멤버는 선언 키워드가 없어 스캐너에 잡히지 않는다. 이름이 시드에 맞는 것과 축에 속하는 것은 다른 문제다 — 실측으로 `policy-anchors.yaml` · `codes/*.yaml` 어디에도 없다(전부 0건). `VENDOR_TIMEOUTS` 부재와 함께 `gap` 에 기록했고, 소속 결정은 규약 개정 사안이라 아래 §2 |
| 25 | **발견 18 의 `stripComments` 결함을 실측했다 — 6파일·양방향.** 그리고 마일스톤 2 의 행 단위 `blankComments` 는 같은 결함이 **한 행에 갇힌다** | `docs/_meta/verify/build-code-index.mjs:299-314` vs `verify-policy-anchors.mjs:137` · 3저장소 `src_roots` `.ts`/`.tsx` **1729 파일** 대조 | `stripComments`: 6 파일에서 갈리고 `fix-strips-more` 4(주석 미제거 → 거짓 양성) · `fix-restores-code` 2(실코드 제거 → 거짓 음성). `blankComments`: 갈리는 **행 3**, 그 중 스캔 대상 가시성을 바꾸는 **material 0**. 문자 스트림은 짝이 어긋나면 파일 끝까지 오염되고 행 단위는 한 행에서 끝난다 | 마일스톤 3 은 `blankCommentsSafe` 를 자기 파일에 두고 정규식 리터럴 분기를 더했다(같은 행에서 안 닫히면 정규식이 아니라고 판정해 되돌린다). **기존 두 파일은 안 고쳤다** — 고치면 마일스톤 1·2 산출물의 재생성 결과가 이 마일스톤에서 조용히 바뀐다. 이로써 같은 목적의 스트리퍼가 **세 벌**이 됐다 → backlog(통합 대상 지정) |
| 26 | **초기자가 다음 행에서 시작하는 선언을 행 단위 스캐너가 못 본다. 3자 대조가 잡았다** | `insuniverse-server/apps/api/src/modules/treat/treat.service.ts:70-75` (`const CERTIFY_CLAIM_TTL_MS =` 다음 행에 `Number(...)`) 3건 | 생성기만 있었으면 3항목이 **조용히** 빠졌다. 마일스톤 2 의 `*_observed` 양방향 대조만 썼어도 못 잡는다 — 생성기가 축 목록과 observed 를 둘 다 쓰므로 사각이 생기면 양쪽이 같이 줄어 통과한다 | 검증기가 **생성기를 import 하지 않고** 자기 코드로 소스를 다시 훑어 **3자 대조**한다(항목 목록 == `*_observed` == 독립 재스캔). 구현도 일부러 다르다 — 생성기는 행 단위 상태 기계, 검증기는 오프셋 구간(`deadSpans`) + 전역 정규식. 생성기에 계속 행 규칙을 더하고 selftest 픽스처(D9b)로 못박았다 |
| 27 | **게이트 2종은 env 단독이 아니라 DB 우선이고, 게이트 2종의 미설정 방향을 계획이 부정확하게 적었다** | `treat-provider.service.ts:105,110,139,146` + `drizzle/0012_treat_setting.sql:4` ("기존 env(ENABLE_TREAT_CAR)를 대체") · `car-view-admin.guard.ts:27,29-37,40` · `analyze.service.ts:1268,1294-1298` | (a) `ENABLE_TREAT_CAR`·`INHOUSE_CAR_CANARY` 를 env 단독으로 적으면 "env 만 보면 된다" 가 성립하는 것처럼 읽힌다 — 실제로는 `tn_treat_setting` 이 15초 캐시로 우선한다. (b) `CAR_VIEW_ADMIN_IDS` 는 미설정 시 **fail-closed**(admin 거부)인데 계획은 "빈 화이트리스트" 로만 적어 방향이 없었고, 같은 가드 `:27` 에서 `corp` 역할이 allowlist 를 **우회**한다. (c) `DISEASE_PDF_HIDE_NAME_AGENCY_ID` 는 "NaN 이 어떤 agcId 와도 불일치" 가 아니라 `!Number.isInteger(x) \|\| x <= 0` 가드에서 **단락**되어 스코프 조회 자체를 안 한다 | `resolution_order` 필드로 관측만 적었다(`["db:tn_treat_setting", "env"]`) — 게이트의 **정책 의미**는 spec §1:42-153 이 `12-index-runtime` 소유로 못박았으므로 해석하지 않는다. `unset_behavior` 에 방향(fail-open / fail-closed)과 우회 경로, 정정된 기전을 근거 행과 함께 적었다. (c) 는 security-reviewer 가 지목하고 실물로 재확인했다 |

| 28 | **검사 도구 자신의 침묵 2건 — "검사가 성립하지 않은 것" 을 "검사가 실패한 것" 으로 고쳐 읽고 있었다.** santa-loop 라운드 1 의 외부 리뷰어(gpt-5.4)가 자기 샌드박스에서 실제로 밟았다 | `verify-policy-values.mjs` `checkDotenvAndTracked` 의 `catch { ok = false }` · `build-policy-values.mjs` `readExisting` 의 `catch { return [] }` | (a) `git ls-files` 가 어떤 이유로든 실패하면 전부 "앵커가 git 추적 대상이 아니다" 가 된다. 리뷰어 환경에서 dubious-ownership(exit 128) 하나가 **가짜 오류 95건**으로 번져 진짜 원인을 덮었다. (b) 기존 인덱스 파싱이 실패하면 큐레이션(`unset_behavior`·`resolution_order`·`note`)이 조용히 사라지고, `existingEntries` 가 비므로 "기존 id 가 재생성 결과에 없다" 안전장치까지 **같이** 무력화된다 | (a) exit 1(매치 없음)만 미추적으로 판정하고 나머지는 원인을 실은 채 throw. (b) 파싱 실패·`entries` 비배열은 중단 사유로 승격. 픽스처 2종(검증기 D14 · 생성기 D11)으로 못박았다. **같은 라운드에서 기각한 지적 2건**: 독립 재스캔이 다음 행 초기자를 놓친다(`=\s*` 가 개행을 넘으므로 실측 반증 — 픽스처 D15 로 못박았다) · 첫 생성의 `unset_behavior: unknown` 을 검증기가 막는 것이 결함이다(큐레이션 계약이고 M1·M2 의 `PRESERVE` 와 같다. 재생성 결과가 바이트 동일함을 확인했다) |

발견 21·26 은 계획이 예상하지 못한 것이다. 21 은 **정본이라 믿은 파일이 커밋돼 있지 않았다**는
것이고, 26 은 **검사를 3자로 짜지 않았으면 조용히 통과했을** 누락이다. 둘 다 "무엇이 정본인가 ·
검사가 누구에 대해 성립하는가" 라는 같은 질문의 두 얼굴이다.

### 마일스톤 4 (`api-contracts.jsonl`·`schema-constraints.jsonl` 계약·스키마 축)

수치의 정본은 두 생성기의 `--report` 출력이다
(`build-api-contracts.mjs --repos-root ../.. --report` · `build-schema-constraints.mjs --repos-root ../.. --report`).

| # | 발견 | 근거 (파일:행) | 영향 | 조치 |
|---|---|---|---|---|
| 29 | **DTO 의 과반이 자기 파일에 속성을 하나도 안 갖는다 — 데코레이터를 파일 단위로 세면 그 계약이 "0건" 이 된다.** `CreateAdminDto` 의 본문은 문자 그대로 `{}` 다 | `insuniverse-server/apps/api/src/modules/admin/admin.dto.ts:51-54` · `--report` 의 `mapped type 합성 573` · `own_property_count: 0 인 클래스 334` | 누락이 아니라 **거짓**이다. `03-contracts/01-input-validation.md` 가 "이 엔드포인트에는 검증이 없다" 를 게시하게 된다. 소비자는 `PartialType` 이 만든 선택성도 `OmitType` 이 뺀 필드도 알 수 없어 NestJS 타입 해석기를 각자 재구현한다 — 이 PRD 의 존재 이유와 정면 충돌 | 합성 간선을 1급 행(`composed_from`)으로 싣고 **확정될 때만** 4연산 집합 대소로 `effective_properties` 를 푼다. `extends_expr` 이 mapped type 인데 `composed_from` 이 비면 검증기 error. 실증: `CreateAdminDto` 는 own 0 → **유효 속성 6**(`admName`·`admLoginId`·`admPassword`·`admEmail`·`admPhone`·`athId`)이고 각 항목이 `source_class`(`admin.entity.Admin` / `authority.entity.Authority`)와 `ops_applied`(`["intersection","omit"]`)로 되짚힌다. 현 HEAD 해석률 573/573 |
| 30 | **상태 컬럼을 이름으로 판정하면 컬럼 1,579 중 172 가 근거 없이 상태 컬럼이 된다.** 이름 패턴(`*status*`·`*state*`·`*_type`·`*_code`·`*_flag`·`*_yn`) 적중 200 중 증거 보유는 53 뿐이다 | `--report` 의 `name_pattern_hit 200 · 그 중 증거 0 = 172` · `insuniverse-server/drizzle/schema.ts` 의 `dsn_major_code`·`dfc_minor_code`(질병 분류 코드) | 발견 9(`menu_codes` 정규식이 서버 에러 상수 과매칭) · 19(`*_KEY` 로 메타키 축을 잡으면 브라우저 저장소 키가 섞임) · 24(`*_TIMEOUT` 이 enum 멤버를 잡음)와 **정확히 같은 부류**다. `04-state-transitions.md` 가 존재하지 않는 상태 기계를 게시한다 | `status_evidence` 채널을 2종(`default_literal`·`code_colocation`)으로 **닫고** `name_pattern_hit` 을 채널에서 배제했다(검증기가 채널로 싣는 것을 error). 다만 이분법으로 두지 않는다 — `status_classification` 3분류(`confirmed` 53 · `name_only_unreviewed` 172 · `not_a_candidate` 1,354, 합 == 1,579)로 **"검토해서 아님" 과 "아직 안 봄" 을 가른다.** 이름은 여전히 증거가 아니고, `name_only_unreviewed` 행에 `namespace`/`value` 를 붙이는 것도 error 다 |
| 31 | **코드값의 namespace 를 정규식으로 판정할 수 없다 — `menu_codes` 가 코드값 형태 전부를 과매칭한다.** `^(?:[A-Z][A-Z0-9]{2}[0-9]{3}\|…)$` 는 `ACT001`·`PCY001` 뿐 아니라 `POP001`·`POP002`·`BRD001` 까지 전부 받는다 | `extraction-manifest.yaml:172` · `docs/_meta/index/codes/domain_status.yaml:957`(POP001) 과 `codes/menu_db.yaml:91`(POP001) 양쪽 등재 · `--report` 의 코드형 default 5개소 | 정규식으로 고르면 **객체 순회 순서가 소속을 정한다.** 첫 구현이 실제로 그랬고 `ACT001`·`PCY001` 이 `menu_codes` 로 찍혔다. 발견 9·10 이 예고한 것이 실행 단계에서 그대로 재현됐다 | **열거로 판정한다** — `codes/*.yaml` 에 등재된 값만 그 namespace 다. 추출기 축(`menu_codes`·`screen_codes`)은 후보에서 제외한다: `verify-code-index.mjs:61-75` 가 그 축의 소속은 **모양이 아니라 출처**로 정해진다고 이미 못박았고, 컬럼 기본값은 값이지 출처가 아니다. 그 구분을 복제하지 않으려고 `EXTRACTOR_OWNED_NAMESPACES` 를 export 했다. 결과: `POP001`·`POP002` → `domain_status_codes`, **`ACT001`(1개소)·`PCY001`(2개소) → `unclassified` + `value` 필수**로 실렸다(버리지 않는다). manifest 개정 요청은 아래 §2 |
| 32 | **응답 계약을 "전역 규약" 으로 서술하면 틀린다.** 응답 봉투 인터셉터를 가진 컨트롤러는 **118 중 3**, `@ApiOkResponse` 를 가진 엔드포인트는 **652 중 226(35%)** 다 | `--report` 의 `응답 봉투 보유 컨트롤러 3/118` · `@ApiOkResponse=226` · `insuniverse-server/apps/api/src/swagger.ts:13` (Swagger 문서 1개, admin/client 분리 없음) | 나머지 115 컨트롤러의 실제 응답 형태는 핸들러 반환형이라 **정적 추출 대상이 아니다.** "전역 봉투" 로 적으면 소비 문서가 존재하지 않는 규약을 게시한다 | `response_envelope` 를 엔드포인트 행에 두어 **비어 있음이 다수라는 사실이 파일에 드러나게** 했다. 안 잰 것(나머지의 반환형)은 `gap` 에 안 쟀다고 적었다 |
| 33 | **DB 스키마 소스가 `src_roots` 밖이다.** `manifest.repos[].src_roots` 는 server 가 `[apps, libs]` 인데 `drizzle/schema.ts` 는 둘 다 아니다 | `extraction-manifest.yaml:27` · `insuniverse-server/drizzle/schema.ts`(3,113행) · `drizzle/` 는 `exclude_pathspec`·`exclude_glob` 어디에도 없음 | 마일스톤 1~3 의 생성기는 전부 `src_roots` walk 였다. 이 축만 규칙이 다른데 그것을 적어두지 않으면 다음 축이 같은 질문을 다시 하거나, walk 를 임의로 넓혀 exclude 규약이 무너진다 | **고정 경로 상수 1개**(`SCHEMA_SOURCE`)로 못박고 walk 로 번지지 않게 했다. `relations.ts` 는 담지 않았다 — `relations(` 92 선언은 drizzle ORM 의 조인 편의이지 DB 제약이 아니고, 섞으면 "DB 가 보장하는 것" 과 "ORM 이 편의로 아는 것" 이 한 파일에서 구별되지 않는다. 실제 FK 는 테이블 레벨 `foreignKey` 4 + 컬럼 레벨 `.references` 97 이며 그것만 담았다. manifest 개정 요청은 아래 §2 |
| 34 | **라우트 데코레이터 657 중 5 는 주석 처리된 코드다.** 주석을 덮고 세면 **652** 이다 | `agency-fc-admin.controller.ts:463` (`// @Post('resign/cancel')`) · `order-detail-admin.controller.ts:187` · `refund-admin.controller.ts:7,20,34` | 계획의 실측표가 적은 656(= GET 374 + POST 246 + DELETE 14 + PUT 12 + PATCH 10)은 **raw grep 값**이다. 그것을 acceptance 목표로 쓰면 주석 5건을 실제 엔드포인트로 만들어야 통과한다 — 발견 16(가드 13↔12)이 밟을 뻔한 함정과 같다 | 3자 대조(항목 652 == `meta.endpoint_observed` 652 == 검증기 독립 재스캔 652)를 전부 **주석 제거 후** 값으로 맞췄다. POST 는 246 이 아니라 **241** 이다. 수치 정본은 `--report` 이고 구 SHA 참고치와 raw 값은 `gap` 에 원인 단정 없이 병기했다 |
| 35 | **`policy-anchors.yaml` 의 `http` 1행에 슬래시가 겹쳐 있고, 마일스톤 2 의 생성기와 검증기가 그것을 각자 재현한다** | `docs/_meta/index/policy-anchors.yaml` 의 `POST /admin/agency-fc//subscribe/upload` · 소스는 `agency-fc-admin.controller.ts:54` `@Controller('admin/agency-fc')` + `:394` `@Post('/subscribe/upload')` (실제 라우트는 `/admin/agency-fc/subscribe/upload`) · 조립 규칙이 `build-policy-anchors.mjs` 인라인과 `verify-policy-anchors.mjs` 독립 재스캔 **두 벌** | 선행 슬래시를 가진 경로는 `@Post('/subscribe/upload')` 1건 · `@Controller('/loss')` 1건이다. 마일스톤 4 가 정규화하면 `policy-anchors.yaml` 이 재생성되어 마일스톤 2 의 재현성 지표가 흔들리고, 생성기만 고치면 검증기의 독립 재스캔(3자 대조 · 발견 26)이 터진다 | **이 마일스톤에서 고치지 않았다.** 조립 규칙을 `httpString` 으로 export 해 두 생성기가 **한 벌만** 쓰게 하고(복제 금지), 의미는 마일스톤 2 그대로 두었다 — 그래야 교집합 356행의 교차 검증이 성립한다. 실측으로 정규화 시 바뀌는 행이 **정확히 1행**임을 확인했다(재생성 diff). 결함 자체는 backlog 로 올린다 |
| 36 | **drizzle 테이블의 둘째 인자에 두 형태가 있고, 테이블 레벨 제약 5행이 컬럼과 같은 들여쓰기로 적혀 있다** | `schema.ts:3094`·`:3110` 의 `(table) => ({ … })` 간결 화살표 본문 2테이블 · `:3095-3097`·`:3111-3112` 의 1탭 제약 5행 (다른 곳은 전부 `=> { return { … } }` + 2탭) | (a) 블록 본문만 파싱하면 그 2테이블의 제약 5건(`unique` 1 · `index` 4)이 **조용히 사라진다** — 첫 구현이 실제로 그랬다(unique 39/40 · index 142/146). (b) 컬럼과 제약을 들여쓰기로 가르면 그 5행이 컬럼이 되어 컬럼 수가 1,579 → **1,584** 로 부푼다 | 컬럼/제약 구분을 **들여쓰기가 아니라 구간과 생성자 이름**으로 한다: 생성기는 첫 인자(컬럼 맵)와 둘째 인자를 구간으로 자르고 두 화살표 형태를 모두 받으며, 검증기의 독립 재스캔은 생성자 이름(`primaryKey`·`unique`·`index`·`uniqueIndex`·`foreignKey`)으로 뺀다. 두 기전이 4항목(테이블 155 · 컬럼 1,579 · `notNull` 907 · `default` 301)에서 일치하는 것을 selftest 가 못박는다 |
| 37 | **`main.ts` 의 `useGlobalPipes` 는 주석 처리돼 있다 — 그 한 줄만 보면 "전역 검증이 꺼져 있다" 로 오독된다** | `insuniverse-server/apps/api/src/main.ts:21`(주석) · `app.module.ts:188`(`APP_PIPE` 로 `ClassValidatorPipe` 등록) · `validation.pipe.ts:16-18`(`whitelist: true` · `forbidNonWhitelisted: true` · `transform: true`) | 전역성은 `APP_PIPE` 가 준다. 두 줄을 같이 안 보면 반대 결론이 나오고, `whitelist: true` 는 **데코레이터가 없는 속성을 버리므로** 그 사실 자체가 계약이다 | 3플래그와 앵커를 `meta.global_validation` 에 **한 번만** 실었다(엔드포인트마다 반복하면 갈라진다). 검증기가 앵커 2개 미만을 error 로 막는다. 데코레이터가 하나도 없는 속성 81건도 `property` 행으로 실었다 — `whitelist: true` 가 그것을 버리는 것이 계약 사실이기 때문이다 |

| 38 | **블록 스칼라 호출은 앵커 불변식을 동시에 만족할 수 없다 — `run: >` 스텝은 어느 행도 앵커가 되지 못한다.** 키 행에는 `npx`/`jest` 가 없고 본문 행에는 `run` 이 없다 | `docs/_meta/verify/verify-tests.mjs:151-155`(`ANCHOR_TOKENS.invocation = ["run","uses","test"]`) · 동 `:258`(`COMMAND_HEAD_TOKENS`) · 교정 전 `:518-526` 이 앵커가 지목한 **한 행**만 읽었다 | 워크플로가 `run: >` · `run: \|` 로 명령을 적는 것은 흔한 형태다. 그 스텝은 실제로 테스트를 돌리는데도 문서로 표현할 방법이 없었다 — 5b 가 그것을 만나면 정직한 호출을 버리거나 앵커를 지어내야 한다 | 검증기에 **블록 헤더 파서**(`readRunBlock`)를 두어 앵커 행의 지시자를 읽고 그보다 깊은 연속 행을 본문 span 으로 자른다. 실행 토큰 검사를 그 span 에서 돌린다. **접기는 하지 않는다** — 접기 규칙은 `tests-hop.mjs` 의 `foldBody` 단독 소유로 남는다(같은 규칙 두 벌이 이 축의 뿌리다). `checkAnchor` 는 무변경이다(검증기 5종 공유). 인라인은 span 이 그 행 하나라 동작 불변이고, 변이 시험이 그것을 못박는다(span 을 한 행으로 되돌리면 `F-a1` **하나만** 붉어진다) |
| 39 | **스칼라 스타일은 wire 6키에서 복원되지 않는다 — 같은 `raw` 가 `literal` 이면 미해석, `folded` 면 확정인데 `command` 는 두 경우 글자 그대로 같다** | `docs/_meta/verify/tests-hop.mjs:236-241`(`foldBody` 의 style 분기) · 동 `:758-768`(`record` 6키에 style 없음) · 실행 재현: `{style:"literal", raw:"npx\njest --ci"}` → `unresolved` / `{style:"folded", …}` → `resolved` | 열거자가 `\|` 를 `>` 로 오독하면 거짓 `run_in_ci` 가 나가고 **검증기가 반증할 수단이 없다.** 앞선 네 라운드는 `scalar_style`·`hop_chain`·`selector` 를 차례로 wire 에 실었는데, 실을 때마다 그 필드도 생산자 것이라 다음 라운드에 다시 뚫렸다 | **필드를 더하지 않고 독립 재열거로 닫는다.** `verify-policy-values.mjs:300` `rescanRepos` 의 3자 대조(발견 26)와 `verify-tests.mjs` `rescanCandidates` 의 형제로 `rescanInvocations` 를 두었다 — 워크플로·`package.json`·Dockerfile 을 검증기가 **스스로 열거**하고 판정은 `resolveInvocationContext` 를 import 해서 받는다(열거는 독립, 판정은 정본 1벌, 생성기는 import 하지 않는다). 대조는 레코드 집합의 **6키 전부 값 일치**이고, 스타일 오독은 `reachability`·`selection_scope` 에서, 명령 바꿔치기는 `command` 에서, dispatcher 체인 위조는 `config_path` 에서 갈린다 — **세 위조가 같은 대조 하나에서** 잡힌다(`F-r2`·`F-r3`·`F-r4`). wire 는 6키 그대로다 |
| 40 | **잔여가 호출에 결속되지 않아 판정이 저장소째로 뭉개졌다 — 검사는 저장소 단위 전칭인데 같은 줄의 진단 문면은 "그 호출에 걸린 잔여" 라고 적고 있었다** | `docs/_meta/verify/verify-tests.mjs:466-468`(교정 전: 잔여가 **하나라도** 있으면 `run_in_ci` 거부) · `docs/_meta/verify/tests-hop.mjs:706`(교정 전 `unresolved_reasons: string[]` — 네 사유 중 둘에만 kind 접두) | 컨테이너를 쓰는 저장소는 미판독 ONBUILD 잔여가 **항상 최소 1개**라 양성을 영원히 못 낸다. 사유가 문자열이라 출처도 누락도 검증기가 확인할 수 없어, `record` 만 대조하면 생산자가 잔여를 **누락**해도 6키가 같아 통과한다 | 반환을 `{record, typed_residues}` 로 가르고 잔여를 `bind` 로 **discriminated** 했다(`residue_bind` 어휘 신설 · `unsupported_construct_kind` 8→10). 판정식은 `blocks(r, inv)` **하나**이고, `governs` 는 파일 공존이 아니라 **제어흐름 경로**다(호출 step 안 · 같은 job 의 호출 앞 step · workflow 최상위 · `needs:` 전이 선행). container 예외는 **채널 조건부**라 Dockerfile `RUN` 유래 호출은 막고 workflow 유래는 면제한다. `bind` 는 믿지 않고 검증한다(orphan `invocation_id` · 저장소 밖 `source_anchor` · kind→허용 bind 표). 잔여 집합도 재열거와 정규화 튜플로 정확 대조한다 — 안 하면 누락이 통과한다 |

발견 38~40 은 5b 계획의 게이트가 **실코드 대조·실행으로 재현**한 5a 계약의 결함이다. 셋 다 오늘
실측으로는 절대 안 드러난다 — 3저장소 CI 의 테스트 호출이 0건이라 세 경로 모두 관측으로 도달할
수 없고, 기존 픽스처 `A61` 은 인라인 `npm test` 만 통과시켜 이 경로를 검증하지 않았다. 그래서 이
교정의 수용 조건은 **전부 픽스처**이고 산출물 수치가 아니다(`F-a1`·`F-r1~F-r5`·`F-c1~F-c6` 12종).

발견 29·31·34·35·36 은 계획이 예상하지 못한 것이다. 31 은 **발견 9·10 이 예고한 과매칭이 실행
단계에서 그대로 재현된** 것이고, 34·35 는 **계획의 실측치와 이미 출하된 산출물이 각각 주석과
슬래시 하나만큼 틀려 있었다**는 것이며, 36 은 **소스가 한 가지 모양일 거라는 가정**이 두 곳에서
깨진 것이다. 넷 다 "세는 규칙이 무엇을 세는지" 를 안 물으면 조용히 통과한다.

### 마일스톤 5b (`tests.jsonl` — skip·CI 축)

수치의 정본은 `build-tests.mjs --repos-root ../.. --report --json` 출력이다. 아래 표의 값은 전부
manifest `repos[].head` 3종(server `c7597fc7` · site `b9a90a42` · admin `7df733a5`)에서 측정했다 —
생성기가 **HEAD blob 에서** 읽으므로 작업트리 상태와 무관하다.

| # | 발견 | 근거 (파일:행) | 영향 | 조치 |
|---|---|---|---|---|
| 41 | **워크플로 `run:` 스텝의 과반이 블록 스칼라라, 키-값만 읽는 스캐너는 실행 명령의 절반 이상을 못 본다** | 3저장소 11 워크플로 실측 `run:` 스텝 **54** 중 블록 스칼라 30 · 인라인 24 · `docs/_meta/verify/verify-tests.mjs:1436-1469`(`readRunBlock`) | 키-값 스캐너는 `run: \|` 행에서 값이 `\|` 하나라고 읽는다. 그 30 스텝의 명령이 통째로 사라지면 "CI 가 테스트를 안 돌린다" 가 **관측이 아니라 스캐너 결함**에서 나온다 | 생성기가 지시자를 읽고 본문 span 을 잘라 **통째로** `{style, raw}` 로 넘긴다. 행으로 쪼개지 않는다 — 쪼개면 `if false; then` 다음 행의 `npm test` 가 단독 명령으로 승격돼 거짓 확정이 난다. 접기는 `foldBody`(5a) 단독 소유다 |
| 42 | **셸 층 도달성은 오늘 실측에서 잔여 55건으로만 닫힌다 — 다행 본문·셸 메타문자가 있으면 그 명령이 무엇이든 미해석이다** | `--report` 의 `unsupported_constructs_by_kind.shell_reachability_unresolved: 55` · `docs/_meta/verify/tests-hop.mjs:296-304`(조건 1·2) | 실제로 도달하는 다행 배포 명령도 미확정으로 떨어진다. **의도된 방향이다** — 과소 확정은 허용, 과대 확정은 금지 | 잔여를 `bind:"invocation"` 으로 그 호출에 걸어 행을 `undetermined` 로 떨어뜨린다. `gap` 4 에 네 층(YAML·셸·컨테이너·액션 순서)을 전부 적었다 |
| 43 | **"allowlist 밖 후보 0" 은 완전성 receipt 가 아니다 — 부정 검사를 화이트리스트로 오칭하면 하한이 상한인 척한다** | 선언 후보 총수 regex 388(server 280 · site 108) vs 계획이 적은 AST 재계수 396 · `outside_allowlist_files` server 1 · site 9 · admin 245 | 총수는 **방법 의존**이라 대조 근거가 될 수 없다. 그것을 acceptance 로 쓰면 세는 규칙이 바뀔 때마다 정직한 산출물이 FAIL 한다 | 총수는 **보고만** 하고 대조하지 않는다. 방법 독립으로 일치하는 **경로 집합**(`test_files`·`outside_allowlist_files`·`skip_evidence`)만 3자 대조한다 |
| 44 | **접근형 catch-all 은 선언 채널만으로 `none` 을 확정하지 못한다 — 근거는 두 채널 집합이 **모두** 빈 것이다** | 3저장소 58 테스트 파일 전수에서 `declared` **0** · `residue` **0**(생성기·검증기 재스캔 양쪽) · `docs/_meta/verify/tests-hop.mjs:380-458`(`classifySkipToken`) | "skip 은 어떤 형태든 리터럴을 남긴다" 는 **거짓**이다. 계산된 접근·동적 호출·옵션 객체 spread 는 리터럴을 안 남긴다 | 선언 채널과 미해석 채널을 **별개 배열**로 싣고, `skip_status: none` 의 근거를 "둘 다 빈 것" 으로 `gap` 12 에 명시했다. 잔여가 하나라도 있으면 확정값을 못 낸다(검증기 상호 강제) |
| 45 | **액션 순서 층이 잔여 48건으로 남는다 — `uses:` 액션이 무엇을 설치·설정하는지 정적으로 읽지 않는다** | `--report` 의 `preceding_action_unresolved: 48` · `docs/_meta/verify/verify-tests.mjs:1832-1840` | 호출이 없어도 존재하는 잔여라 `bind:"source"` 다. 차단 여부는 `governs` 가 제어흐름으로 정한다 — 파일 공존으로 걸면 무관한 job 의 잔여가 증명된 호출을 강등한다 | 열거자가 job 범위 안의 `uses:` 행 전수를 `source` 잔여로 낸다. 제3자 액션 참조 자체는 `unresolved_action_refs`(server 7 · site 5 · admin 5)로 따로 싣고 `gap` 2 가 그 미해석을 적는다 |
| 46 | **`평가 단위` 와 `항목` 은 정본이 정의하지 않은 채 소비 문서가 연언으로 읽어 왔다** | `.claude/meta/05-final-spec.md:168`(`항목` 제약) · 동 `:172`(S등급 규칙 문면) · 어휘 `evaluation_unit.values = [null]` | 평가 단위에 값을 적으면 정본에 없는 규약을 발행하는 것이고, coverage 를 언급하면 연언 verdict 가 부활한다 | `evaluation_unit: null` + `evaluation_unit_status: "canon_undefined"` 로 **정의되지 않았다는 사실 자체**를 싣는다. 저장 단위는 `storage_granularity: test_file` / `no_stable_case_id` 로 분리했고 둘을 한 값으로 합치는 것을 스키마가 막는다 |
| 47 | **마일스톤 5 통합 계획이 21라운드를 돈 원인은 판정 규칙을 검증기와 생성기가 문면으로 두 벌 적은 것이었다** | `.claude/plans/insuniverse-docs-11-index-code-m5.plan.md`(21라운드) · 5a/5b 분할 이후 라운드 수 8(5a) · 8(5a-fix) · 7(5b) | 문면 동기화 장치를 더할 때마다 그 장치가 다음 라운드의 지적을 낳았다. 규칙을 문면에 적는 한 라운드는 수렴하지 않는다 | 규칙을 **코드 1벌**로 내렸다(`tests-vocab.mjs` 어휘 · `tests-hop.mjs` 판정). 5b 생성기는 그것을 import 하고 **다시 적지 않는다**. 문서 내부 드리프트가 인터페이스 드리프트로 바뀌었고, 그 인터페이스는 `CONTRACT_VERSION` 등호 preflight 가 기계로 지킨다 |
| 48 | **`uses: ./…` 로 불리는 재사용 워크플로의 `run:` 은 caller 조건 없이 열거된다 — 그대로 두면 `if: false` 아래의 테스트 호출이 양성으로 올라간다** | `docs/_meta/verify/verify-tests.mjs:1794-1820`(재열거기가 모든 워크플로 파일의 `run:` 을 직접 연다) · 동 `:1832-1840`(`uses:` 행에는 caller 좌표의 `source` 잔여만) · 실측 로컬 재사용 참조 8개소(전부 `notification.yml`) | 전개하면 caller 조건이 붙어 재열거 결과와 갈리고, 전개 안 하면 레코드가 모자란다 — **어느 쪽이든 `checkRescanParity` 가 거부한다.** 중단으로 막으면 자체 트리거·복수 caller 의 정당한 테스트 스텝까지 산출물을 통째로 0건으로 만든다 | **열거는 재열거기와 동형**으로 두고(재귀 없음), 앵커 파일이 로컬 재사용 대상 집합 `T` 에 속하는 러너 호출의 `select` 만 `"unresolved"` 로 **한 방향 클램프**한다. `checkRescanParity` 는 `selection_by_invocation` 을 보지 않고(`:448-472`), `--file` 은 `"unresolved"` 를 재계산하지 않으므로(`:926-939`) parity 가 살아 있다. 대칭 네 갈래를 `--selftest` 가 고정한다(B-17a~d) |
| 49 | **워크플로 구조 파서가 중첩 블록 스칼라 본문을 마스킹하지 않아, step 안 마지막 `run:` 매치가 실 run 앵커를 덮는다** | `docs/_meta/verify/verify-tests.mjs:1701-1708`(`run:` 분기가 스칼라 span 을 모른다) · 재현: `- run: echo harmless` + `env.SCRIPT: \|` 본문 `run: npm test` → `run_line` 이 본문 행으로 이동 · 생성기 `--selftest` `B-3` | 무해한 스텝이 `npm test` 호출로 둔갑해 **거짓 `run_in_ci`** 를 낼 수 있다. 생산자만 고치면 재열거 결과와 갈려 parity 가 깨지므로, 5b 는 고칠 수도 없다 | **재현한다.** 생성기가 같은 성질을 그대로 갖고, `--selftest` `B-3` 이 그 동형을 고정한다 — 5a 가 마스킹을 넣으면 이 픽스처가 큰 소리로 깨져 두 쪽을 같은 편집에서 고치게 만든다. 오늘 실측 54 run step 전수에서 발화하지 않는다(전부 실제 배포 명령). 개정 요청 (e) · 이월 9 |
| 50 | **로컬 재사용 대상 집합 `T` 를 리터럴 `uses: ./…` 로만 모으면 유효한 YAML 인용형이 clamp 를 우회한다** | `uses: "./.github/workflows/x.yml"` 은 리터럴 매치가 안 된다 · `docs/_meta/verify/verify-tests.mjs:1603-1614`(`scalarValue` 가 인용부호와 후행 주석을 벗긴다) · 생성기 `--selftest` `B-4` | 우회하면 `if: false` caller 아래 callee 의 테스트 호출이 `select:"true"` 로 남는다 — 0-f 클램프의 존재 이유가 정확히 그것이다 | `T` 는 **검증기가 보지 않는 생산자 전용 집합**이라 여기서 고쳐도 parity 를 깨지 않는다. 값 판독을 `scalarValue` 동형(인용·주석 처리)으로 바꾸고 `B-4` 로 고정했다. 오늘 실측 8개소는 전부 비인용형이라 값은 안 바뀐다 |
| 51 | **내용만 manifest 리비전에 고정하면 목록은 가변 인덱스에 남는다 — 청결 검사는 스냅샷이 아니다** | 검사와 열거 사이의 staged 삭제가 manifest HEAD 의 파일을 목록에서 지운다 · `git ls-tree` 는 `:(glob)` magic 미지원(실행 확인: `fatal: pathspec magic not supported by this command: 'glob'`) | `source_sha` 는 manifest SHA 를 주장하는데 스캔한 파일 집합은 다른 리비전의 것이 된다. 재현성 지표(2회 `diff`)는 같은 잘못된 집합을 두 번 내므로 이것을 못 잡는다 | `git read-tree <manifest head>` 로 **버려지는 임시 인덱스**를 만들고 그 위에서 `git ls-files -- <pathspec>` 를 돌린다 — 같은 명령·같은 pathspec magic 이고 읽는 인덱스만 불변 스냅샷이다. pathspec 매칭을 재구현하지 않는다 |
| 52 | **`outside_allowlist_files` 의 제외 목록이 두 벌이다 — manifest `exclude_glob` 11 과 재열거기 walk 의 디렉터리 9 가 다르다** | manifest `exclude_glob`(`_statics/`·`pdfjs-dist/`·`planning/`·`migrations/`·`seeds/`·`artillery/` 포함) vs `docs/_meta/verify/verify-tests.mjs:1009`(`node_modules`·`.git`·`dist`·`build`·`coverage`·`.next`·`out`·`.turbo`·`.cache`) · 실측 admin 245 파일이 전부 `public/_statics/adminLTE/**` | 이 필드는 3자 대조 대상이라 두 목록이 다르면 **복합 게이트가 필연적으로 실패한다.** manifest 목록을 쓰면 admin 245 vs 0 으로 갈린다 | 0-f·R7 F0 이 세운 규율(**열거 형태는 재열거기가 정본**)을 이 축에도 적용해 walk 목록에 맞췄다. 그 결과 벤더 자산이 이 필드에 실리는 것은 사실이므로 `gap` 16 에 적고 규약 정정을 개정 요청 (f) 로 올린다 |
| 53 | **`resolvable_scan_result` 는 "테스트 호출을 찾았다" 가 아니라 "run 스텝을 열거했다" 를 뜻한다** | `docs/_meta/verify/verify-tests.mjs:688-691`(`want = invs.length ? "test_invocation_found" : …`) · `invocations[]` 는 parity 때문에 **모든** `run:` 스텝을 담아야 한다(`:462-464`) | 계획 문면은 오늘 값을 `no_test_invocation_found` 로 예상했는데, 검증기 계약상 그 값은 워크플로 `run:` 스텝이 0건일 때만 나온다. 필드 이름을 그대로 읽으면 "테스트 호출 55건" 으로 오독된다 | 계약을 따르고(값 `test_invocation_found`) 오독 가능성을 `gap` 17 에 명시했다. 테스트 호출은 3저장소 전부 **0건**이며 그 사실은 `ci_status`·`in_ci`·`selection_by_invocation` 이 진다 |
| 54 | **재열거기가 만든 Dockerfile 호출 레코드를 같은 파일의 앵커 검사가 거부한다 — `ANCHOR_TOKENS.invocation` 은 소문자 `run` 인데 Dockerfile 지시어는 대문자 `RUN` 이다** | `docs/_meta/verify/verify-tests.mjs:151-155`(`invocation: ["run","uses","test"]`) · 동 `:166-173`(같은 파일의 `RESIDUE_ANCHOR_TOKENS.container_reachability_unresolved` 는 **대문자** `["FROM","ONBUILD","RUN","COPY"]`) · `verify-code-index.mjs:522`(`boundedIncludes` 는 대소문자를 가린다 — 실행 확인: `("RUN npm install","run")` → `false`) · 재현: `--rescan-invocations` 가 `insuniverse-admin/public/_statics/adminLTE/Dockerfile:8` 을 내고 `--file` 이 같은 앵커를 거부 | **`container_build` 채널을 가진 저장소의 문서는 원리적으로 통과할 수 없다.** 재열거기가 그 호출을 만들고 `checkRescanParity` 가 문서에 그것을 요구하는데, 문서에 싣는 순간 앵커 검사가 거부한다 — 생산자가 무엇을 해도 빠져나갈 수 없다. 같은 파일이 잔여 쪽에는 대문자를 이미 넣어 뒀으므로 **한쪽만 반영된 누락**이다 | **마일스톤 5b 가 고칠 수 없다** — `verify-tests.mjs` 편집은 마일스톤 5a 소유이고(UI6) 계획 Acceptance 가 FAIL 로 건다. 실행해서 재현한 상태 그대로 이월 9 로 올린다. 처방은 `ANCHOR_TOKENS.invocation` 에 `RUN` 을 더하거나 앵커 토큰 대조를 채널별로 가르는 것이고, 픽스처(`F-c*`)도 같은 편집에서 실저장소 형태를 덮어야 한다. **→ 닫힘 (2026-08-14, 마일스톤 5a-fix-2).** 처방은 계획했던 "토큰을 더한다 / 실재 검사를 채널 조건부로 둔다" 가 **아니었다** — 컨테이너 호출의 저장소 좌표를 `null` 로 만들고 이미지 경로를 typed 잔여 `container_workdir_unresolved` 로 옮기니 발견 55 는 `isNonEmptyString(null)` 이 거짓이라 검사가 **애초에 안 돌고**, 54 는 앵커 대조를 정본 파서에 물어 닫혔다. 검사를 더한 것이 아니라 뺐다 — `docs/_meta/verify/verify-tests.mjs:163`(`ANCHOR_TOKENS.container_invocation`) · 동 `:197`(`containerAnchorCheck` 가 정본 파서로 대조) · 픽스처 `F-c7`·`F-c8③` |
| 55 | **Dockerfile `WORKDIR` 은 컨테이너 경로인데 검증기가 저장소 디렉터리 실재를 요구한다** | `docs/_meta/verify/verify-tests.mjs:1727`(`WORKDIR` 값에서 선행 `/` 만 벗겨 `working_directory` 로 넣는다) · 동 `:677-682`(`working_directory` 가 실재하는 디렉터리일 것을 요구) · 재현: `WORKDIR /code` → `working_directory: "code"` → `working_directory 가 실재하지 않는다 — code` | 발견 54 와 **독립된 두 번째 차단**이다. 54 를 고쳐도 이것이 남는다. 컨테이너 이미지 안의 경로가 저장소 안에 같은 이름으로 있을 이유가 없으므로, 임의의 Dockerfile 이 이 검사를 통과하는 것은 우연이다 | 같은 이유로 5b 가 고칠 수 없다 — 이월 10. 처방은 `working_directory` 실재 검사를 **채널 조건부**로 두는 것이다(`workflow_run_step` 은 저장소 좌표, `container_build` 는 이미지 좌표). `blocks` 의 container 예외가 이미 채널 조건부이므로(`:571`) 같은 형태의 분기가 계약 안에 있다. **→ 닫힘 (2026-08-14, 마일스톤 5a-fix-2).** 처방은 계획했던 "토큰을 더한다 / 실재 검사를 채널 조건부로 둔다" 가 **아니었다** — 컨테이너 호출의 저장소 좌표를 `null` 로 만들고 이미지 경로를 typed 잔여 `container_workdir_unresolved` 로 옮기니 발견 55 는 `isNonEmptyString(null)` 이 거짓이라 검사가 **애초에 안 돌고**, 54 는 앵커 대조를 정본 파서에 물어 닫혔다. 검사를 더한 것이 아니라 뺐다 — `docs/_meta/verify/tests-hop.mjs:758`(컨테이너 모드가 좌표 넷과 잔여 1건을 원자적으로 낸다) · 픽스처 `F-c3` |
| 56 | **`.github/workflows/Dockerfile.yml` 은 워크플로 열거와 Dockerfile 이름 술어에 *둘 다* 걸린다 — 채널을 앵커 *경로*에서 도출하면 그 틈으로 유령 좌표가 통과한다** | `docs/_meta/verify/verify-tests.mjs:1736`(`WORKFLOW_EXT_RE` 는 `.github/workflows/**.ya?ml` 전수) · `docs/_meta/verify/tests-hop.mjs:990`(`isDockerfilePath` 는 basename `dockerfile.<suffix>`) · 재현: 그 경로의 `- run: npm test` + `working-directory: ghost` 스텝 | 재열거는 그 호출을 `workflow_run_step` 이라 하는데 경로 판정은 `container_build` 라 한다. 컨테이너 완화가 경로에서 나오면 **실재하지 않는 `working_directory` 를 가진 워크플로 호출이 면제를 받는다** | 채널 출처를 **재열거 `rescan.channel[<앵커>]` 하나**로 못박았다(`docs/_meta/verify/verify-tests.mjs:177` `isContainerCoord`). `rescan` 부재·키 부재는 전부 저장소 좌표(엄격한 쪽)로 닫는다. 픽스처 `F-c8①`·`F-c8⑧` 이 세 상태를 고정하고, 반사실(채널 출처를 경로로 되돌림)이 `F-c8⑧` 을 깨는 것을 실행으로 확인했다 |
| 57 | **`evidence.anchors[]` 가 재열거 호출 id 집합에 묶여 있지 않았다 — 무관한 앵커 하나로 "양성은 앵커 없이 나올 수 없다" 요건을 채울 수 있다** | `docs/_meta/verify/verify-tests.mjs:448-472`(`checkRescanParity` 는 `anchors[]` 를 대조하지 않는다) · 동 `run_in_ci` 분기는 배열이 비지 않기만 요구한다 | 컨테이너 앵커에 `RUN` 을 허용하면 출처가 넓어진다 — 무관한 `RUN npm install` 앵커 하나가 요건을 채운다. 좁히는 쪽(컨테이너 앵커 불허)은 5b 를 다시 막으므로 완화는 짝이 필요하다 | **집합 동일성**으로 닫았다 — `evidence.anchors[]` 집합 == `invocations[].id` 집합(`docs/_meta/verify/verify-tests.mjs:557`). 클램프에 영향받지 않고 문서 안에서만으로 검사되며 무관한 앵커·누락 앵커를 **둘 다** 막는다(`F-c8⑦` 이 양방향 고정). **남는 것**: 집합 동일성은 "모든 호출에 앵커가 있다" 만 증명하고 **"어느 호출이 그 양성을 냈나" 는 보존하지 않는다** — wire 가 클램프 이전 `select` 를 안 싣기 때문이고 그것은 계약 변경이라 backlog 다 |
| 58 | **`boundedIncludes` 는 대소문자를 가리는데 Docker 지시어는 대소문자 무관이다 — 토큰 대조로는 컨테이너 앵커를 검사할 수 없다** | `docs/_meta/verify/verify-code-index.mjs:522-526`(정규식에 `i` 플래그 없음) · 실행 확인: `boundedIncludes("Run npx jest", "RUN")` → `false` · 정본이 `Run`·`run`·`RUN` 을 모두 `RUN` 으로 읽는다(`docs/_meta/verify/tests-hop.mjs:888`) | 계획이 정한 `ANCHOR_TOKENS.container_invocation = ["RUN"]` 을 문면대로 쓰면 `Run npx jest` 가 **생산자는 정당하게 내는데 검증기가 거부하는** 레코드가 된다 — 발견 54 와 정확히 같은 형태다. 소문자까지 열거하는 안은 `rUn` 을 못 덮어 같은 결함이 남는다 | 컨테이너 앵커만 **정본 파서 대조**로 바꿨다(`docs/_meta/verify/verify-tests.mjs:197` `containerAnchorCheck`) — "이 행이 `RUN` 논리 지시어가 시작한 행인가". 토큰 대조보다 **강하다**: 연속행 안에 `RUN` 이라는 글자가 있는 행은 토큰 대조를 통과하지만 이 검사는 거부한다. `checkAnchor`·`boundedIncludes`(검증기 5종 공유 자산)는 건드리지 않았다. `F-c8③` 이 고정하고, 반사실(토큰 대조 복귀)이 그것을 깨는 것을 실행으로 확인했다 |
| 59 | **`readRunBlock` 은 YAML `run:` 리더라 Dockerfile 앵커에서 `null` 을 내고, 명령 토큰 검사의 span 이 물리 한 행으로 접힌다** | `docs/_meta/verify/verify-tests.mjs:1557`(`RUN_KEY_RE` 는 소문자 `run\s*:` 를 요구) · 재현: `insuniverse-server/apps/api/dockerfile:7` 의 `RUN apk add … && \` 는 실행 토큰 `npm` 이 **8행**에 있어 접힌 span 에서 안 보인다 | 정직한 컨테이너 호출이 `command 의 실행 토큰이 앵커 블록 본문에 없다` 로 거부된다. 구 파서에서는 그 파일이 대문자 정규식에 안 걸려 **애초에 스캔되지 않아** 발화하지 않았다 — 소문자 파일명을 편입하는 순간 드러난 잠복 결함이다 | 컨테이너 좌표일 때 span 을 **정본 파서의 논리 지시어 span**(`line`~`end_line`)으로 잡는다(`docs/_meta/verify/verify-tests.mjs:728-735`). 워크플로 경로는 `readRunBlock` 그대로다 |
| 60 | **실저장소 Dockerfile 이 CRLF 다 — `\n` 으로만 쪼개는 파서는 후행 escape 판정이 `\r` 에서 끊긴다** | 실측: `insuniverse-server/apps/api/dockerfile` 이 CRLF(`insuniverse-admin/…/adminLTE/Dockerfile` 은 LF) · 구 파서 `body.endsWith("\\")` 는 `… && \\\r` 에서 거짓 | 연속이 끊기면 그 지시어의 `end_line` 과 `args` 가 틀리고, **정본을 공유하는 생성기와 검증기가 같은 틀린 사실에 합의**해 파리티가 그것을 승인한다. 손으로 센 oracle 밖에서는 안 잡힌다 | 정본 파서가 `\r\n`·단독 `\r` 을 LF 로 정규화하고(`docs/_meta/verify/tests-hop.mjs:888` 이하 `dockerLines`) 후행 escape 를 **홀짝**으로 센다(`\\\\` 는 escape 된 escape 라 연속이 아니다). `# escape=` 대체 문자와 heredoc 본문 귀속도 같은 함수가 진다 — 셋 다 실저장소 적중은 CRLF 하나뿐이지만 공유 파서라 함께 닫았다 |
| 61 | **계획 Validation 스니펫이 생성기 CLI 와 안 맞았다 — 리다이렉션이 요약 한 줄만 캡처하고 `mv` 가 정상 산출물을 덮었다** | `docs/_meta/verify/build-tests.mjs:1717-1718`(`writeDoc(result)` 가 파일을 **직접** 쓰고 stdout 에는 `tests.jsonl 생성: … (행 58 …)` 한 줄만 낸다) · 실제 발생: `build-tests.mjs … > "$TMP" && mv "$TMP" tests.jsonl` 이 58행 문서를 1행으로 만들었다(`git checkout` 으로 복구) | 계획이 그 패턴을 고른 이유("`>` 는 preflight 보다 먼저 파일을 truncate 한다")는 맞지만, 이 CLI 는 **이미** 모든 안전 장치 뒤에 자기가 쓴다. 스니펫이 그 사실과 어긋났고 검토 7라운드가 그것을 못 봤다 — **문면 명령은 실행되기 전까지 검증되지 않는다** | 실제 호출은 `build-tests.mjs --repos-root ../../`(리다이렉션 없음), 재현성은 `--out <임시경로>` + `diff` 다. 안전 장치 2종(id 충돌·기존 id 소실)은 그대로 산다 |
| 62 | **`buildReport` 의 `unsupported_constructs_by_kind` 는 손으로 적은 kind 목록이라 어휘 추가와 조용히 갈린다** | `docs/_meta/verify/build-tests.mjs:1449-1452`(리터럴 10종) vs `docs/_meta/verify/check-tests-crosscheck.mjs:536`(`zeroDist("unsupported_construct_kind")` 는 어휘에서 파생) · 재현: 어휘에 `container_workdir_unresolved` 를 더하자 복합 게이트가 `계수 leaf 가 정수가 아니다 — null` · `null vs 4` 로 거부 | 파생시키면 구조 검사가 항진명제가 되므로 손으로 적는 것이 규율이다(어휘 `size` 와 같은 이유). 갈림을 **기계가 잡는가**가 관건이었고 — 잡았다 | 규율을 유지하고 목록을 손으로 올렸다. 3자 대조가 이 드리프트의 계기라는 사실을 여기 기록한다 — 다음 어휘 추가도 같은 자리에서 걸린다 |

| 63 | **heredoc 열림 판정에 앵커가 없어 평범한 `RUN` 본문의 `<<` 가 뒤따르는 실 지시어를 통째로 삼킨다** | `docs/_meta/verify/tests-hop.mjs` 구 정규식 `/<<(-?)\s*(["']?)([A-Za-z_][A-Za-z0-9_]*)\2/g` — 낱말 경계도, 지시어 제한도, 미종료 처리도 없다 · 실행 재현 4종: `RUN grep -o '<<TOKEN' f` 뒤의 `RUN npm test` 가 호출 목록에서 소실(2건→1건) · `ENV LITERAL=<<EOF` 는 호출 **0건**(ENV 는 heredoc 을 받지도 않는다) · `RUN echo $(( 1 << shift ))` 의 `shift` 가 종료 단어로 잡힘 · 미종료 heredoc 이 파일 끝까지 삼킴 | **escalation 이 물은 바로 그 형태다.** 손실이 세 검사를 전부 통과한다 — `checkRescanParity` 는 생산자·검증기가 **같은 함수**를 부르니 1건 대 1건으로 일치하고, D-4b sweep 은 파일 **이름 멤버십**만 재며(`verify-tests.mjs` §D-4b), `containerAnchorCheck` 는 같은 파서에 묻는데 사라진 행을 주장하는 레코드가 없어 아무도 안 묻는다. 3저장소 실측 적중 **0건**(`<<` 출현 0)이나 방아쇠는 `sed` 자리표시자 같은 평범한 명령이다 | 열림 판정을 셋으로 좁히고(**heredoc 을 받는 지시어**만 · `<<` 가 **낱말 시작** · 종료 단어가 `<<` 에 **인접**) 종료 단어를 **못 찾으면 아무것도 안 먹는다**. 픽스처 `F-c9` 는 오라클을 정본 파서에서 파생시키지 않고 **손으로 적은 앵커**와 대조한다 — 구 파서에서 5건 FAIL, 신 파서에서 전건 PASS 로 반사실 확인 |
| 64 | **파서 지시자 구역이 빈 줄·평범한 주석에서 안 닫혀 뒤늦은 `# escape=` 가 먹는다** | `docs/_meta/verify/tests-hop.mjs` 구 루프가 `if (t === "") continue;` 로 빈 줄을 건너뛰고 지시자 아닌 주석도 통과시켰다 · Docker 는 첫 비-지시자 행에서 구역을 닫는다 · 재현: 평범한 주석 뒤 `# escape=\`` → `RUN echo a \`` 와 `RUN npm test` 가 **한 지시어로 접혀** 뒤 호출이 사라짐 | 발견 63 과 같은 손실 형태이고 같은 이유로 세 검사를 통과한다. 이쪽은 Docker 가 빌드를 거부하는 입력이라 실해는 더 낮지만, 연속행 판정이 통째로 갈리는 자리다 | 구역을 **첫 비-지시자 행**에서 닫는다(빈 줄 · 평범한 주석 · 지시어 전부). 픽스처 `F-c10` 이 손으로 적은 앵커로 고정하고, 맨 위의 정당한 `# escape=` 가 여전히 유효한 것도 같은 픽스처가 지킨다 |

발견 63·64 는 `/mccp:santa-loop` 라운드 1 의 소산이다. **Reviewer A(Opus)와 Reviewer B(codex
gpt-5.6-sol)가 컨텍스트 격리 상태에서 같은 두 자리를 독립으로 지목**했고, A 는 구조적 서술에
그쳐 심각도 계약상 blocking 이 아니었으나 반증 시도가 구체 입력을 냈다. 손으로 적은 오라클이
없으면 이 부류는 원리적으로 안 잡힌다 — 파서가 정본이라 생성기·검증기·앵커검사 셋이 모두
같은 답을 쓰고, 오라클을 그 함수에서 뽑는 픽스처는 파서와 함께 틀린다.

| 65 | **63 의 좁히기가 반대 방향 오류를 열었다 — `ONBUILD RUN <<EOF` 와 미종료 heredoc 이 본문 행을 유령 호출로 올린다** | 라운드 1 이 heredoc 수용을 `RUN`·`COPY`·`ADD` 로 제한했는데 `ONBUILD` 는 **다른 지시어를 인자로 받는** 지시어라 바깥만 보면 안쪽 `RUN` 을 못 본다 · 같은 라운드가 미종료 heredoc 을 "안 삼킨다" 로 바꿨다 · `b333e04` 실행 대조: `ONBUILD` 는 `[6]`→`[4,6]`, 미종료는 `[2]`→`[2,3]` (둘 다 유령 증가) | **결손보다 무거운 방향이다.** 이 축은 `in_ci` 어휘가 `true`·`null` 2값이라 **양성만 확정**한다 — 없는 호출을 만드는 것은 그 규칙을 정면으로 깬다. 63 이 닫은 것이 "호출을 잃는" 결함이었는데 그 수정이 "호출을 만드는" 결함을 열었다 | heredoc 수용을 **유효 지시어**로 판정한다(`heredocHostDirective` — `ONBUILD` 면 본문 첫 토큰). 미종료는 **끝까지 삼켜** 미해석으로 둔다(라운드 1 이전 동작). 픽스처 `F-c9` 에 두 케이스 추가 — 라운드 1 코드에서 `got [4,6] want [6]` · `got [2,5] want [2]` 로 FAIL, 현재 PASS |
| 66 | **파서 지시자 구역이 미지 이름에서 안 닫혀, 64 의 수정이 같은 손실을 남겨 뒀다** | 라운드 1 루프가 `키=값` 모양이면 이름을 안 보고 구역을 이었다 · Docker 가 아는 이름은 `syntax`·`escape`·`check` 셋뿐이고 모르는 이름은 평범한 주석이다 · 재현: `# foo=bar` + `# escape=\`` → `RUN@[4]`, 옳은 답은 `[4,5]` (`b333e04` 도 `[4]`) | 64 는 "빈 줄·평범한 주석이 구역을 닫는다" 만 고쳤고 **미지 지시자 이름**을 안 봤다. 그래서 64 가 닫으려던 손실이 한 겹 안쪽에 그대로 남았다 — 문면상 닫힌 것으로 읽혔다는 점이 이 발견의 실질이다 | `DOCKER_PARSER_DIRECTIVES` 세 이름만 구역을 잇는다. 픽스처 `F-c10` 에 미지 키 케이스 + `syntax` 뒤 escape 가 여전히 유효한 회귀 방지 케이스 |

발견 65·66 은 `/mccp:santa-loop` 라운드 2 의 소산이고 **전부 라운드 1 패치를 겨눈다**. 원 산출물을
겨눈 지적은 0건이었고, 그것이 루프를 라운드 2 에서 종료한 근거다(patch-chasing). 라운드 1 이
"호출을 잃는" 결함을 닫으면서 "호출을 만드는" 결함 둘을 열었다는 사실 자체가 기록 대상이다 —
흡수는 매번 새 표면을 연다.

**이월 3건** (전부 `b333e04` 대비 악화 없음, 자기 마일스톤 필요):
1. `RUN <<123` 처럼 **숫자·임의 단어 구분자**를 못 읽어 본문이 유령 호출이 된다. 옳은 처방은
   BuildKit 호환 토큰화(단어 문법 · 인용 · fd 접두 · `ONBUILD` 복합)이고, 라운드가 흡수할 크기가
   아니다. `b333e04` 에서도 같았다.
2. 종료 단어 대조가 `trimEnd()` 라 `EOF ` 를 받는다. 1번과 같은 작업에 딸린다 — 단독으로 고치면
   `<<-` 의 선행 탭 규칙과 갈린다.
3. 미종료 heredoc 은 삼키는 것보다 **파일 전체를 미해석 잔여로 표시**하는 것이 옳다. 어휘 신설
   (`size` 손계수 · 검증기 · 생성기 · 픽스처)이라 마일스톤 크기다.

미해소로 남긴 것 하나 더: 백슬래시 뒤 **후행 공백**을 이 파서는 연속으로 읽고 Docker 는 안 읽는다.
Docker 가 거부하는 입력이라 실해가 없고, 고치면 `\ ` 로 끝나는 정직한 행의 판정이 바뀌므로
반례 없이 건드리지 않는다.

발견 54·55 는 **마일스톤 5b 의 산출을 실제로 차단한 둘**이다. 3자 대조(생성기 report · 독립 재스캔 ·
JSONL)는 전부 통과했고, 실패한 것은 `verify-tests.mjs --file` 의 하위 스키마 검사뿐이며 그 셋 다
admin 벤더 Dockerfile 한 파일에서 나온다. 둘 다 **재열거기 자신의 출력이 자기 검증기에 거부되는**
형태라 어느 생산자도 빠져나갈 수 없다 — 5a 계약의 결함이지 이 생성기의 결함이 아니다.

발견 41~55 는 전부 마일스톤 5b 소산이다. 이 중 **49·50·51 은 Implement-Codex 게이트가 실코드
대조로 재현**했고(각각 HIGH·HIGH·MEDIUM), 49 는 5a 소유라 재현으로만 닫혔다 — 그 사실이 이월 9 다.
52·53 은 계획 문면과 배달된 5a 계약이 갈린 자리이고, 둘 다 **계약을 정본으로 삼고 문면을 정정**했다.

발견 56~62 는 마일스톤 5a-fix-2 소산이다. 56·57 은 게이트(Plan-Codex 7라운드)가 드러냈고,
58·59·60·62 는 **구현 중 실행이 드러냈다** — 셋 다 계획 문면대로 짰으면 그대로 배달됐을 것이고,
특히 58·59 는 발견 54 와 같은 형태(생산자가 정당하게 내는 값을 검증기가 거부)를 새로 만들 뻔했다.
61 은 계획의 Validation 명령 자체가 틀렸던 자리다. 공통 교훈 하나: **문면은 실행되기 전까지
검증되지 않는다** — 7라운드 리뷰도, 손으로 센 oracle 도 그것을 대신하지 못했다.

이번 마일스톤이 파서를 정본 1벌로 옮기면서 **자기일치의 위치가 바뀌었다**. 구 판은 같은 파서
두 벌의 정확 일치를 파리티라 불렀고(`build-tests.mjs:496` 이 자기를 "동형" 이라고 선언했다),
신 판은 한 벌을 공유한다 — 그 한 벌이 틀리면 생성기·검증기·파리티·`container_observation` 이
**함께** 틀린다. 그래서 두 계기를 따로 뒀다: 파서 밖 **손으로 센 oracle**
(`insuniverse-server/apps/api/dockerfile` 의 논리 `RUN` 은 행 7·18·24 정확히 3건이고
`ARG RUN_MODE`(35행)·`ENV RUN_MODE`(37행)는 호출을 만들지 않는다 — 실측 일치)와, 멤버십 술어를
**안 쓰는** 이름-only 완전성 sweep(D-4b)이다. 후자의 우주는 검증기 `walkFiles` + `exclude_glob`
이라 제외 디렉터리 밑의 tracked Dockerfile 은 못 본다 — 세 저장소 실측 적중 0건이고 backlog 에
적었다.
54·55 는 위에 적은 대로 산출 자체를 막았고 5a 소유다(이월 9·10).

**로컬 리뷰(2026-08-14)가 5b 산출물 자체에서 잡은 것**은 별도다 — `.claude/reviews/local-m5b-r3-review.md`.
차단과 무관한 회귀 방어 결함이었고 같은 사이클에서 닫았다: (i) 복합 게이트의 위조 음성 픽스처와
0-f 강등 픽스처가 검사 대상이 아니라 **그 로직의 사본**을 시험하고 있어 검사를 지워도 초록이었다
— `compareSources`·`clampSelect` 로 정본을 뽑아 생산 경로와 픽스처가 같은 함수를 부르게 했고
mutation 으로 무는 것을 확인했다. (ii) `runVerifier` 가 `json` 모드에서 자식의 **비영 종료를
삼켰다** — `--file` 만 `allowNonZero` 로 예외를 명시하고 나머지는 전파한다. (iii) `git ls-files` 가
기본 `core.quotePath=true` 라 비ASCII 경로(site 4 · admin 31 실재)를 C-인용으로 내 열거에서
빠뜨릴 수 있었다 — `core.quotePath=false` 를 못박고 `B-27` 로 고정했다.

### 마일스톤 6a (정책 후보 통합)

수치의 정본은 `build-policy-candidates.mjs --index-dir docs/_meta/index --report` 출력이다. 입력은
전부 커밋된 `docs/_meta/index/**` 13종이고 이 마일스톤은 소스 저장소를 스캔하지 않는다 — 아래 값은
manifest `repos[].head` 3종(server `c7597fc7` · site `b9a90a42` · admin `7df733a5`)에서 뽑힌 상류
산출물 위의 순수 함수다.

| # | 발견 | 근거 (파일:행) | 영향 | 조치 |
|---|---|---|---|---|
| 67 | **정본이 "정책 후보 단위" 를 정의하지 않는다.** spec §5 2단계는 "위 전부를 정책 후보 단위로 통합" 이라고만 적고 무엇이 후보 1개인지는 어디에도 없다. 5b 의 `evaluation_unit` `canon_undefined` 와 같은 구조다 | `.claude/meta/05-final-spec.md:346-347` (2단계 문면 전문) · 정의 부재는 같은 파일 전문 grep 0건 | 단위를 안 정하면 8,143 입력 항목을 8,143 후보로 세든 1,991 로 세든 둘 다 문면을 만족한다. 소비 PRD 가 다시 걸러야 하고 지표(`status` 부여율)의 분모가 정해지지 않는다 | 이 마일스톤이 **집행 지점 규칙**으로 세웠다(사용자 결정 D2-A) — 후보 = 정책이 실제로 집행되는 지점 1개(access·contract·value·data). 규칙은 문면이 아니라 `docs/_meta/verify/verify-policy-candidates.mjs` 의 `classify` 에 두고, 규약 승격 요청은 아래 §2 |
| 68 | **spec §3 status 8종에 미확정 값이 없다.** 코드 축 단독으로 도달 가능한 값은 `conditional-unknown` 1종(env 게이트 11행)뿐이라 나머지 5,188행이 어휘 밖으로 떨어진다 | `.claude/meta/05-final-spec.md:205-212` (8종 전수) · `docs/_meta/index/env-gates.yaml` 11항목 전부 `status: conditional-unknown` | 8종 중 하나를 억지로 고르면 그것이 곧 지어낸 값이다(UI2 위반). 필드를 비우면 "부여율 100%" 지표를 못 맞춘다 | `status: null` + `status_undetermined_reasons[]` 닫힌 어휘로 적었다. 1급 `undetermined` 신설 또는 `null` 허용의 명문화 요청은 §2 |
| 69 | **폐기 판정 5중 필터의 소관 축이 spec 에 없다.** 필터 1·2 는 코드 축에서 판정되지만 3·4 는 git 축, 5 는 릴리스 축이며 그 배분이 정본 어디에도 적혀 있지 않다 | `.claude/meta/05-final-spec.md:214-219` (5중 필터 전문 — 소관 열 없음) | 축별 PRD 가 각자 "왜 우리 축에서 status 가 안 나오나" 를 매번 재발견한다. 실측: 코드 축 단독 확정률 **11/5,199 (0.21%)** | 필터별 소관을 각 후보 행의 `status_basis.filters_owned_by_other_axes` 에 기계 판독 가능하게 실었다 — `10-index-git` 이 나오면 6b 가 **재생성만으로** 채운다. 표 명시 요청은 §2 |
| 70 | **Success Metric "status 부여율 100%" 가 필드 존재와 값 확정을 가르지 않는다.** 판정 기준이 "`status` 없는 행 0" 이라 `null` 을 넣어도 100% 이고, 값 확정 기준으로 읽으면 0.21% 다 | `.claude/prds/insuniverse-docs-11-index-code.prd.md:221` (지표 행 전문) | 같은 지표가 두 자릿수 다른 두 값으로 읽힌다. 어느 쪽으로 읽든 한쪽 해석에서는 미충족인 채 `complete` 가 찍힌다 | 마일스톤 6 을 6a/6b 로 쪼개 미완 작업이 표에 남게 했다(Plan-Codex R4 F1, 2026-08-14). 6a 는 필드를 닫힌 어휘로 세우고 도달 가능한 값만 채운다. 지표 문면 확정 + **6b 소관 명시** 요청은 §2 |
| 71 | **`api-contracts.jsonl` 이 DTO 의 입출력 방향과 endpoint 결합을 담지 않는다.** `param_bindings[].type_ref` 이 **비한정 이름**(distinct 347종)이라 동명이인을 가르지 못한다 — 속성 2,463개(validator 보유분) 중 확정 결합이 **0건**이고 이름 일치는 499건에 그친다 | `docs/_meta/verify/build-api-contracts.mjs:662` (`type_ref` 이 정규식 캡처 문자열) · `docs/_meta/verify/verify-api-contracts.mjs:406` (재스캔도 같은 형태) · `docs/_meta/index/api-contracts.jsonl` property 3,208행 | 결합과 방향이 없으면 어떤 속성이 **입력** DTO 의 것인지 증명할 수 없다. `meta.global_validation.forbidNonWhitelisted: true` 때문에 입력에서는 선언 자체가 집행이므로, 입력이 아님을 증명하지 못하는 한 `not_a_candidate` 로 내릴 수 있는 속성이 **오늘 하나도 없다** | 속성 3,208 전수를 `candidates-unqualified.code.jsonl` 로 격리하고 `relation_unresolved: ["endpoint_dto_link","io_direction"]` 을 행마다 달았다. 부모 `class_kind`(전수 dto 2,243 / entity 965)와 이름 일치 여부는 **신호이지 확정이 아님**을 명시해 행에 그대로 실었다. 마일스톤 4 축에 `type_ref` 한정 해석 또는 방향 필드 추가 요청은 §2 |
| 72 | **부모 PRD 의 소비 계약이 glob 이라 새 산출물이 자동 편입된다.** `policy-candidates.*.jsonl` 을 축별 3파일로 `cat` 병합한다고 못박혀 있어, 축별 PRD 가 이름만 그렇게 지으면 **네 번째 파일**이 확정 소비 경로에 그대로 들어온다 | `.claude/prds/insuniverse-policy-docs.prd.md:145` (`cat` 이 병합 — 중앙 병합 파일 없음) | 잠정 산출물을 `policy-candidates.provisional.jsonl` 류로 이름 지었다면 파일 분리라는 방벽이 **이름 때문에** 무력화된다. 소비자는 잠정 3,208행을 확정으로 읽는다 | 이름을 glob 밖 `candidates-unqualified.code.jsonl` 로 뺐다(Plan-Codex R5 F0 — 실파일 대조로 검증). 소비 입력을 **확정 3파일 allowlist 로 명문화**할 것을 §2 로 요청한다. 형제 PRD 불간섭 규약상 이 계획이 직접 고치지 않는다 |
| 73 | **기준 SHA 드리프트가 필터 3·4·5 를 막는다 — 축의 부재가 아니다.** 코드 축 기준선(manifest `repos[].head`)과 git 축 기준선(`frozen_heads`)이 3/3 저장소에서 다르고, 그 사이 구간을 설명하는 소유 산출물이 저장소에 없다 | `docs/_meta/extraction-manifest.yaml` `repos[].head` = `c7597fc7`(server) · `b9a90a42`(site) · `7df733a5`(admin) ↔ `docs/_meta/index/releases.yaml` `frozen_heads` = `4d798f55` · `767d1e44` · `fb001e1d` (`dead-candidates.yaml` `frozen_heads` 도 동일) | 코드 축 head 3종은 `commits.<repo>.jsonl` 에 **0/0/0** 건이다(인덱스 커밋 수 server 2,587 · site 2,446 · admin 1,157). 즉 git 축은 신 기준선을 **한 커밋도 모른다** | 6a 의 `release_axis_absent`(축이 없다 — 거짓)를 **제거**하고 `baseline_drift_unverified` 로 갈았다. `meta.baseline_drift` 가 두 정본의 SHA 삼중을 담고, 판정기가 (1) 선언 부재 (2) 선언↔입력 불일치 (3) 드리프트 상태의 확정을 각각 error 로 막는다. 근본 해결 회부는 §2 (h)·(i) |
| 74 | **경로 실재는 심볼 실재가 아니다.** 후보 `defined_in` 경로를 git 인덱스 경로 모집단(server 1,193 · site 1,516 · admin 715)에 대조하면 미실재 0건이지만, 그것은 그 경로가 **과거 어느 시점에** 존재했다는 뜻일 뿐이다 | `docs/_meta/index/commits.<repo>.jsonl` 의 `files[].path` ↔ 후보 `defined_in` | 6b 계획 R1 이 이 대조를 근거로 "드리프트가 막는 행 0건" 이라고 오측했다. 실제로 드리프트가 막는 것은 **필터 3·4·5 전부**다 | 후보 단위 적용 가능성을 `meta.baseline_drift.granularity: candidate_unverifiable` 로 명시했다. 판정을 내리지 않고 관측만 `status_basis.filters.*.historical_observation` 에 남긴다 |
| 75 | **파일 마지막 접촉으로 도달성을 복제하면 안 된다 — 설계 노트.** `insuniverse-server/drizzle/schema.ts` 는 인덱스 2,587커밋 중 **302커밋**에 등장하고 마지막 접촉 `4d798f55` 가 `releases.yaml` `reachability.unreached`(64개)에 들어 있는데, 그 302 중 unreached 접촉은 **5**뿐이다 | `docs/_meta/index/commits.server.jsonl` (302 접촉) · `docs/_meta/index/releases.yaml` `reachability.unreached[insuniverse-server]` | 원안("경로의 마지막 접촉 커밋의 `reached_prod` 를 승계")대로면 이 한 파일에 몰린 data 후보 **1,265행 전부**가 `staged-only` 로 확정된다. 접촉 이력을 갈라 보면 도달·미도달 혼재 1,996 · 전부 미도달 33 · 전부 도달 3,170 · 인덱스 밖 0 | **6b 는 필터 5 판정 규칙을 구현하지 않는다.** SHA 가 다르면 UNKNOWN(`baseline_drift_unverified`), 같으면 **error**(전제 만료) 두 갈래뿐이다. 기준선이 맞는 날의 올바른 형태는 "접촉 이력 **전체**를 읽고 혼재는 UNKNOWN" 이며, 이 문단이 그 설계 노트다 |
| 76 | **`reference_count_absent` 는 결손이 아니라 계측기 부적용이었다.** `active_reference_count` 는 심볼의 지역 식별자를 단어 경계로 세는 규칙이라 NestJS 핸들러(라우팅으로 불린다) · SQL 식별자(심볼이 아니다) · DTO 속성(endpoint 결합 미해결)에는 성립하지 않는다 | `docs/_meta/index/constants.yaml` 만 계수를 갖는다(63/63) · `docs/_meta/verify/verify-policy-values.mjs` 의 `active_reference_count_rule` | **축 단위로 찍으면 안 된다**: `contract` 축 3,504행 중 296 만 핸들러이고 3,208 은 DTO 속성이라 집행 여부를 모르는 3,208행이 참조 필터를 그냥 통과한다. 실측 source kind 원소: dto_property 3,208 · handler 652 · schema_constraints 1,265 · policy_anchors 356 · policy_constants 63 · env_gates 13 | 어휘에서 **제거**하고 `status_basis.reference_applicability`(`reference_applicability/v1`)로 옮겼다. `per_source` 가 `sources[]` 와 다중집합으로 일치하고 kind 별 `applicable` + `basis` 를 싣는다. 어휘에 두면서 status 를 non-null 로 올리는 것은 `docs/_meta/verify/verify-policy-candidates.mjs` 의 `status 가 값인데 사유가 비어 있지 않다` 불변식과 충돌한다 — 불변식을 약화하지 않고 필드를 옮겼다 |
| 77 | **`active_reference_count` 는 상한이다.** `constants.yaml` 63행 중 **15행**이 `identifier_ambiguous: true` 이고, 그 계수는 정의 행을 포함한 동명이인 합산이다 | `docs/_meta/index/constants.yaml` 의 `identifier_ambiguous: true` 15건 | 상한으로 임계식(`sum(active_reference_count) - 1 > 0`)을 판정하면 **미사용 상수가 active 로 올라간다** | `applicable` 과 `reference_verdict` 를 분리했다. 모호 행은 `reference_verdict: null` + `reference_count_ambiguous` 사유다. 실산출: 적용 63행 중 판정 **48**(비모호) · 모호 **15**. 판정기가 "모호 사유를 달고 verdict 를 확정한 행" 을 매 실행 error 로 막는다 |
| 78 | **`build-policy-values.mjs` 가 하한 검사의 방향을 반대로 읽었다.** 6a 문면은 "검증기가 정확 일치가 아니라 하한 + 출처 강제라 그때 이 파일을 안 고쳐도 4종 추가가 막히지 않는다" 고 적었다 | `docs/_meta/verify/verify-policy-values.mjs:471` (manifest `known_gates` 전 항목이 산출물에 있어야 통과) · 6a `env-gates.yaml` 헤더 주석 | 하한은 "정본이 늘면 산출물이 따라 늘어야 통과" 다. main 병합이 `known_gates` 를 11 → 15 로 올리자 곧바로 `verify-policy-values` 가 exit 1 이 됐다 | 생성기 헤더·`gap` 문면 양쪽을 정정하고 리터럴 `11종` 을 manifest 에서 재도출하게 바꿨다. 재생성으로 11 → **13** 이 됐다 (남는 2종은 발견 79) |
| 79 | **`RUN_MODE` · `SCHEDULER_TYPE` 은 결손이 아니라 코드 축 스캔 우주 밖이다.** 우주는 manifest `repos[].src_roots` 아래 `.ts/.tsx` 이고, 이 둘은 컨테이너·CI 선언 파일에서만 선언·독출된다 | `insuniverse-server/apps/api/dockerfile:35-41`(`ARG`/`ENV` + `CMD` 의 `$RUN_MODE` 분기 · `case "$SCHEDULER_TYPE"`) · `insuniverse-server/.github/workflows/api.yml:126,226`(build-arg · task-def 주입) | 6a `env-gates.yaml` gap 이 "넷 다 코드에 실재(각각 4·4·1·1 파일)" 라고 적은 것은 이 **우주 밖 파일까지** 센 것이다. 그래서 `verify-policy-values` 하한이 이 2종을 계속 막고 종료코드가 1 로 남는다 | 6b 는 이 축을 상류 게이트에서 **분리하고 진행**한다(사용자 결정 2026-08-18). 닫으려면 생성기와 판정기의 **독립 재스캔 우주를 함께** 넓혀야 하고(3자 대조가 한쪽만 넓히면 갈린다) 우주 정의는 manifest 소유다 — 회부 §2 (g). 컨테이너 논리 명령 파서는 마일스톤 5a-fix-2 산출을 재사용할 것 |
| 80 | **소유 주장을 이 브랜치의 PRD 만 보고 단정했다.** 6b 계획 R1 은 병합 충돌 2건을 두고 "어느 형제 PRD 도 이 둘을 배타 소유로 선언하지 않는다" 고 적었는데 거짓이었다 | `main` 의 `.claude/prds/archived/insuniverse-docs-00-protocol.prd.md` 263 행 배타 소유 목록에 `.gitattributes` 가 있고 280 행이 `.gitmodules` 와 같은 **루트 인프라로 그 PRD 가 소유**한다고 명시한다 | archived PRD 도 소유 원장이다. 활성 PRD 만 훑으면 소유자 있는 파일을 소유자 없는 것으로 오판한다 | `.gitattributes` 를 **저술하지 않고** main 쪽을 그대로 채택했다(`git diff --quiet main -- .gitattributes` 통과). 이 브랜치가 `97da72b` 에서 추가했던 `docs/_meta/index/** text eol=lf` 는 회부한다 — §2 (g2) |
| 81 | **main 병합이 테스트 축의 행 번호 앵커 2개를 끊었다 — 계획 범위 밖.** `.claude/meta/05-final-spec.md` 에 113행이 들어가며 앵커 대상이 밀렸다 | `docs/_meta/verify/build-tests.mjs:139,141` · `docs/_meta/verify/verify-tests.mjs:152,153` 이 `meta/05-final-spec.md:168`(요구 토큰 `항목`) · `:172`(`skip`) 을 핀으로 건다. 병합 후 실제 위치는 **242 · 246** 이다. 반사실: 병합 전 커밋 `0e3c9dc` 의 168·172 는 요구 토큰을 담고 있었다 | `verify-tests.mjs --selftest` 가 FAIL 34건으로 exit 1, `build-tests.mjs --report` 도 exit 1 이라 5b 정식 게이트(`check-tests-crosscheck.mjs`)를 돌릴 수 없다. 산출물 `docs/_meta/index/tests.jsonl` 의 `meta.s_grade_rule.{rule_anchor,not_source_wide_anchor}` 도 같은 핀을 담는다 | 재핀 대상 3파일이 전부 마일스톤 5b 소유이고 6b `Files to Change` 밖이라 **backlog·fix-task 등재 후 분리**했다(사용자 결정 2026-08-18). 6b 의 상류 판정기 게이트는 테스트 축을 뺀 6종이다. **근본 원인은 행 번호 핀 자체다** — 남의 PRD 파일이 움직이면 언제든 또 끊긴다. 내용/헤딩 기반 앵커로 교체 요청은 §2 (g3) |

> 67~70 은 넷 다 **정본이 안 정한 것을 산출물이 정해야 하는** 부류다. 5b 의 `evaluation_unit` 과
> 같은 구조이며, 이번에는 규칙을 문면이 아니라 판정기 코드에 두고 규약 승격을 요청하는 쪽으로
> 처리했다 — 문면에 두면 생성기와 두 벌이 되고 그것이 발견 52 계열이다.
> 71·72 는 성격이 다르다. 둘 다 **다른 파일의 실물을 읽어야만** 보이는 것이었고(마일스톤 4 산출물의
> `type_ref` 형태 · 부모 PRD 145 행의 `cat` 규약), 읽지 않았다면 각각 3,208행을 잘못 분류하거나
> 격리를 이름 하나로 무너뜨린 채 출하했다.

> 73~77 은 6b 가 **값이 아니라 참인 사유**를 낸 자리다. 6a 의 두 어휘(`release_axis_absent` ·
> `reference_count_absent`)는 각각 **거짓**과 **오분류**였고, 그 자리를 기준선 사실(73·74)과
> 계측기 적용 가능성(76·77)으로 갈랐다. 75 는 오늘 쓰이지 않는 설계 노트다 — 기준선이 맞는 날
> 같은 함정을 다시 밟지 않기 위해 남긴다.
> 78~81 은 **병합이 드러낸 것**이다. 넷 다 6a 시점에는 관측할 수 없었고(정본이 아직
> 커밋되지 않았거나 형제 축 산출물이 이 브랜치에 없었다), 병합이 입력을 바꾸자 즉시 드러났다.
> 79·81 은 이 마일스톤의 소유 경계 밖이라 회부·분리했다 — 조용히 통과시키지 않고 계수와
> 반사실을 함께 적었다.

## 2. `00-protocol` 에 대한 규약 개정 요청

발견 2 에 따른 요청이다. `extraction-manifest.yaml` `code_namespaces` 가 아래를 반영하지 못한다.

| 요청 | 현재 | 관측 | 제안 |
|---|---|---|---|
| admin 에러 표면 포괄 | `error_codes: '^E[0-9]{4}$'` 하나 | admin `ERROR_CODES` 29키가 3형태로 갈림 | `error_codes` 를 형태별로 쪼개거나, `sql_error_codes: '^SQL[0-9]{4}$'` · `legacy_numeric_codes: '^[0-9]{4}$'` · `flag_codes: '^[A-Z][A-Z0-9_]+$'` 를 신설. **정규식은 확정 제안이 아니다** — bare numeric 4자리는 다른 축(정산·주문)과 충돌 위험이 있어 정규식 단독으로는 namespace 판정이 안 된다 |
| 코드 항목의 식별자 | 규약 없음 | 코드값도 심볼 이름도 유일하지 않음(발견 1·5) | `id: <namespace>:<symbol>` 을 규약으로 승격하고 `symbol` 은 모듈 한정 점표기로 정의 |
| `fields_per_code` 중첩 규약 | 6필드 이름만 있음 | `active_reference_count` 는 저장소별 3키가 필요 | 중첩 필드 요구를 manifest 에 명시. 현재는 검증기 `NESTED_REQUIRED` 가 유일한 규약 소재지다 |
| **벤더 응답 코드 vs env 변수명** (발견 8) | `vendor_codes: '^(CF\|CODEF\|COOCON\|HYPHEN\|OCTOVER)[-_A-Z0-9]+$'` | 합집합 distinct 56 중 26 이 env 변수명(`CODEF_ID`·`CODEF_SECRET`·`COOCON_AES_KEY`…), 응답 코드 형태(`CF-`)는 30 | 벤더 **응답 코드**만 남기도록 좁히거나(`^CF-[0-9]+$` 계열) `vendor_env_names` 를 별도 namespace 로 분리. **정규식은 확정 제안이 아니다** — 다른 벤더가 `CF-` 가 아닌 형태를 쓸 수 있고 현 HEAD 관측만으로는 판정되지 않는다 |
| **메뉴 코드 vs 서버 에러 상수** (발견 9) | `menu_codes: '^(?:[A-Z][A-Z0-9]{2}[0-9]{3}\|(?:MNU\|CRP)_[A-Z0-9]+(?:_[A-Z0-9]+)*)$'` | `AE9999`·`CE9999`·`FC0001`·`FC9999`·`IE9999`·`TE9999`·`VE9999` 7종이 통과하지만 메뉴와 무관 | 좁히는 것만으로는 못 푼다 — 메뉴 코드 소속은 **출처**(`menu.ts` · `tn_menu`)로 정해지지 모양으로 정해지지 않는다. `code_namespaces` 항목에 `membership: pattern \| source` 를 명시해 정규식이 소유권 주장인지 모양 제약인지 구분할 것을 제안한다 |
| **축 간 코드값 중복의 지위** (발견 10) | 규약 없음 (배타성만 암묵) | `MEM002`·`POP001`·`SUB00x` 5종이 도메인 상태이면서 메뉴 코드 | 배타성은 **선언 축 사이에서만** 성립함을 명시하고, 축을 가로지르는 공유는 `cross_source` 로 표기하는 것을 규약으로 승격. 현재는 검증기 `EXTRACTOR_OWNED_NAMESPACES` 가 유일한 규약 소재지다 |
| **Guard 수 정정** (발견 16) | `05-final-spec.md:187` · PRD `:22` 가 **"Guard 13종"** | 신 SHA(server `4d798f55`) 기준 실측 **12종**. `*.guard.ts` 12 = `implements CanActivate` 12 = `@UseGuards` distinct 12, 3중 일치 | spec §2 와 PRD 의 13 을 12 로 정정하고, 수치에 **측정 SHA 를 함께** 적을 것. 구 SHA(server `a3b3694d`) 기준 13 이 맞았는지는 그 SHA 를 다시 체크아웃하지 않는 한 판정되지 않으므로 "정정" 이 아니라 "SHA 병기" 가 근본 요청이다 |
| **코드 인덱스가 아닌 축의 namespace 등록 규약** (마일스톤 2) | `code_namespaces` 는 **코드값 정규식** 축만 담는다. `policy_anchors` 처럼 정규식이 없는 축을 등록할 자리가 없다 | `policy-anchors.yaml` 의 `namespace: policy_anchors` 가 manifest 어디에도 없고, `verify-code-index.mjs` 게이트는 `codes/` 디렉터리의 모든 `.yaml` 에 코드 인덱스 스키마를 강제하므로(`:789-794`) 이 파일은 `index/` 직하에 둘 수밖에 없었다 | manifest 에 `axis_namespaces`(정규식 없음 · 판정기 경로를 값으로) 같은 블록을 신설하거나, `code_namespaces` 항목에 `kind: code \| axis` 를 명시할 것. 지금은 "어떤 판정기가 어떤 파일을 소유하는가" 가 각 판정기 소스 주석에만 있고 manifest 에 없다 — **마일스톤 3 이 같은 문제를 실제로 다시 만났다**: `env_gates`·`policy_constants` 두 축이 또 manifest 밖이고, `verify-policy-values.mjs` 의 `FILE_NAMESPACE` 상수가 유일한 소재지다 |
| **`.env` 를 exclude 2목록에 추가** (마일스톤 3 발견 22) | `exclude_pathspec` 14 · `exclude_glob` 11 어디에도 `.env` 가 없다 | 3저장소 체크아웃 전부에 미추적 `.env` 가 실재한다. 값 축 스캐너가 읽으면 로컬 시크릿이 **커밋되는 인덱스**로 새고 재현성도 무너진다 | 두 목록에 `.env` · `.env.*` 를 추가할 것. 지금은 `build-policy-values.mjs` 가 자기 안에서 하드 제외하고 `verify-policy-values.mjs` 가 앵커 층에서 막고 있는데, **규약이 아니라 구현에만 있는 방어**라 다음 스캐너가 같은 실수를 반복한다 |
| **`env_gate_rules.known_gates` 를 v3 로 커밋** (마일스톤 3 발견 21) | manifest v2 · 커밋된 spec §1 둘 다 **11종**. main 체크아웃 작업본 spec 에만 15종 | spec v3 의 4종(`RUN_MODE`·`SCHEDULER_TYPE`·`REACT_APP_SMARTRO_MODE`·`REACT_APP_TOSS_CLIENT_KEY`)이 전부 코드에 실재하는데 커밋된 정본에 없어 인덱스에 못 들어간다. 결제·스케줄러 제어면이 불완전한 채로 소비 PRD 에 넘어간다 | spec §1 을 version 3 으로 **커밋**하고 manifest `env_gate_rules.known_gates` 를 같은 15종으로 올릴 것. **그때 `env-gates.yaml` 을 손댈 필요는 없다** — `verify-policy-values.mjs` 의 게이트 규칙이 정확 일치가 아니라 하한 + 출처 강제라 재생성만 하면 4종이 `source: spec_v3` · `pending_manifest_registration: true` 로 들어오고, manifest 까지 오르면 그 표시가 자동으로 꺼진다 |
| **enum 멤버의 축 소속 규약** (마일스톤 3 발견 24 · santa 2차 R2) | 규약 없음. `code_namespaces` 는 코드값 정규식 축, 값 축 2종은 불변 **선언** 4형태만 담는다 | `TreatErrorCategory` 20여 멤버가 **어느 인덱스에도 없다** — `constants.yaml` · `policy-anchors.yaml` · `codes/*.yaml` 전부 `AUTH_TIMEOUT` 0건. 벤더 에러 택소노미의 단일 출처인데 인덱스에서는 보이지 않는다 | enum 멤버를 담을 축을 정할 것 — 에러 카테고리는 `codes/error.yaml`(코드값)과도 `policy_anchors` 의 에러 상수 축과도 모양이 달라 셋 중 어디로 갈지가 규약 판단이다. 값 축을 넓히는 선택지는 권하지 않는다: enum 멤버를 담기 시작하면 3저장소의 모든 enum 이 들어와 "정책 수치" 라는 축 정의가 무너진다 |
| **spec §2:188 의 5패턴이 예시임을 명시 / §4:262 정정** (마일스톤 3 발견 23·24) | §2:188 "…등" · §4:262 가 `AUTH_TIMEOUT`·`VENDOR_TIMEOUTS` 를 `02-ttl-and-timeouts.md` 로 배치 | 5패턴이 닫힌 목록으로 읽히면 금액 상한이 축에서 빠진다. `AUTH_TIMEOUT` 은 `TreatErrorCategory` enum 멤버이고 `VENDOR_TIMEOUTS` 는 3저장소에 0건이라, spec 이 이름 모양으로 소속을 정한 사례다 | §2:188 에 "예시이며 닫힌 목록이 아니다" 를 명시하고, §4:262 에서 `AUTH_TIMEOUT` 을 빼거나 "이름만 시간 상수 형태인 enum 멤버" 로 주석할 것. `VENDOR_TIMEOUTS` 는 실물이 없으므로 삭제하거나 근거를 밝힐 것 |
| **`code_namespaces` 에 `ACT`·`PCY` 접두어를 받을 자리가 없고, `menu_codes` 정규식이 코드값 형태 전부를 과매칭한다** (마일스톤 4 발견 31) | `domain_status_codes` 접두어 목록 17종에 `ACT`·`PCY` 가 없다. `menu_codes: '^(?:[A-Z][A-Z0-9]{2}[0-9]{3}\|…)$'` 는 `ACT001`·`PCY001` 은 물론 `POP001`·`BRD001` 까지 받는다 | DB 컬럼 기본값에 `ACT001`(`schema.ts:813`) · `PCY001`(`:1818,1819`) 이 실재하는데 어느 정규식도 그것을 **소유 주장**하지 못하고, 반대로 `menu_codes` 는 전부 소유 주장한다. 정규식만으로 고르면 순회 순서가 소속을 정한다 | 둘을 함께 고칠 것: (a) `domain_status_codes` 접두어에 `ACT`·`PCY` 를 추가하거나 새 namespace 를 신설. (b) 발견 9 의 요청(`membership: pattern \| source`)을 **`enumerated` 까지 포함해** 승격할 것 — 마일스톤 4 는 열거(`codes/*.yaml` 등재)로 판정했고, 그 규칙이 지금은 `build-schema-constraints.mjs` 의 `readCodeIndex` 에만 산다. 그때까지 이 3개소는 `namespace: unclassified` + `value` 로 실려 있다(버리지 않았다) |
| **`05-final-spec.md` §2:174 의 Swagger·class-validator 수치에 측정 규칙이 없다** (마일스톤 4) | spec §2:174 가 Swagger 3,213 · class-validator 3,047 을 적었으나 **어떤 데코레이터 목록으로 셌는지** 기록이 없다. 신 SHA 재측정은 3,736 / 3,973(raw grep) 이고 주석 제거 후 파일 단위 집계는 또 다르다 | 재측정 대조가 원리적으로 불가능하다. 값이 다를 때 (a) 코드가 변했는지 (b) 세는 규칙이 달랐는지 (c) 주석 포함 여부가 달랐는지를 가를 수 없다 — 발견 16(가드 13↔12)과 같은 구조다 | 수치에 **측정 규칙 + 측정 SHA + 주석 처리 여부**를 함께 적을 것을 규약으로 승격. 마일스톤 4 는 `--report` 를 수치 정본으로 두고 raw 값과 주석 제거 후 값을 `gap` 에 병기했다(발견 34) |
| **`manifest.repos[].src_roots` 가 DB 스키마 소스를 덮지 않는다** (마일스톤 4 발견 33) | server `src_roots: [apps, libs]` · 스키마 정본은 `insuniverse-server/drizzle/schema.ts`(3,113행) · `drizzle/` 는 exclude 2목록 어디에도 없다 | `src_roots` 를 소스 경계로 쓰는 모든 스캐너에서 DB 스키마가 **보이지 않는다.** 마일스톤 4 는 고정 경로 상수로 예외를 뒀지만, 규약이 아니라 구현에만 있는 예외는 다음 축이 또 만든다(`.env` 하드 제외가 밟은 것과 같은 길 — 위 항목) | `repos[]` 에 `extra_sources`(walk 대상이 아닌 고정 파일 목록) 같은 자리를 신설하거나, `src_roots` 에 `drizzle/schema.ts` 를 명시할 것. 후자를 택하면 walk 가 그 디렉터리 전체로 번지므로 **파일 단위 지정이 가능한 형태**여야 한다 |

### 마일스톤 5b 요청 (테스트 축)

| 요청 | 현재 | 관측 | 제안 |
|---|---|---|---|
| **(a) spec §2:172 의 테스트 수치에 측정 SHA 병기** | `05-final-spec.md:172` 가 server 37 · site 16 · admin 0 을 적었다. 측정 SHA 기록 없음 | manifest 동결 SHA 기준 실측 **server 39 · site 19 · admin 0**. 구 SHA 를 다시 체크아웃해 대조하지 않았으므로 차이 원인은 단정할 수 없다 | 발견 16·34 와 같은 요청이다 — 수치에 **측정 SHA + 세는 규칙**을 함께 적을 것. "정정" 이 아니라 "SHA 병기" 가 근본 요청이다 |
| **(a2) `항목` 의 정의를 명문화하고 `:172` 각 행이 `:168` 제약을 참조하게** | `:168` 이 `not_source_wide` 제약을 적고 `:172` 가 S등급 규칙을 적는데, **`항목`(평가 단위)이 무엇인지는 어디에도 없다** | 어휘가 `evaluation_unit.values = [null]` · `evaluation_unit_status = canon_undefined` 로 **정의되지 않았다는 사실 자체**를 싣게 됐다(발견 46) | `항목` 을 파일인지 케이스인지 명문화하고, `:172` 의 각 행이 `:168` 을 참조하게 할 것. 정의가 서면 `evaluation_unit` 어휘가 `[null]` 을 벗는다 |
| **(b) manifest `test_rules.record` 2필드 확장** | `record: [skip_status, in_ci]` 둘뿐 | 산출물이 실제로 싣는 축은 그 둘 말고도 `runner`·`selected_by_runner`·`selection_basis`·`invocable_by_script`·`enumerated_*`·`external_dependency_signals`·`selection_by_invocation` 이다 | `record` 를 실제 축 목록으로 확장하거나, "최소 요구" 임을 명시할 것. 지금은 manifest 가 산출물보다 좁아 규약이 하한인지 정확 목록인지 판정되지 않는다 |
| **(b2) `extra_sources` 에 `.github/workflows` 등록** | 발견 33 이 요청한 `extra_sources` 가 아직 없다. `.github/workflows/*.yml` 은 `src_roots` 밖이고 `test_rules.patterns` 와도 무관하다 | 마일스톤 5b 가 발견 33 과 **같은 처방**(고정 경로 상수 `CI_SOURCES`)을 또 썼다. 규약이 아니라 구현에만 있는 예외가 이번이 두 번째다 | 발견 33 의 요청에 `.github/workflows/*.yml`·`*.yaml` **두 확장자 전수**를 함께 등록할 것. `.yml` 만 닫으면 `ci.yaml` 안의 유일한 테스트 호출이 증거도 잔여도 없이 사라진다 |
| **(c) 제3자 액션 참조를 manifest 에 등록** | 규약 없음 | 실측 `unresolved_action_refs` server 7 · site 5 · admin 5. 전부 미해석이라 저장소 음성 결론이 `undetermined` 로 닫힌다 | 신뢰 목록을 manifest 에 등록해 "내려받지 않고도 환경 효과가 없다고 아는" 액션을 가를 것. **등록되면 그 미해석 원인만 사라지고 저장소 음성 자체는 여전히 확정되지 않는다** |
| **(d) `DOCKERFILE_RE` 를 대소문자 무시로** | `verify-tests.mjs:1596` `/(^\|\/)Dockerfile(\.[\w.-]+)?$/` | server `apps/api/dockerfile` 은 **소문자라 안 걸린다** — 워크플로가 동적으로 참조하는 실제 빌드 파일인데 생성기·검증기 **양쪽 사각지대**다. 반대로 admin `public/_statics/adminLTE/Dockerfile` 은 어느 워크플로도 참조하지 않는데 걸린다 | 정규식을 대소문자 무시로 바꾸고 픽스처를 같은 편집에서 덮을 것. 동적 `file:` 추적은 **요청하지 않는다** — 열거 형태의 정본은 재열거기이고, 동적 추적은 양방향으로 parity 를 깬다(발견 41 계열) |
| **(e) 워크플로 구조 파서가 중첩 스칼라 본문을 마스킹할 것** | `verify-tests.mjs:1701-1708` 이 스칼라 span 을 모른 채 `run:` 을 매치한다 | step 안 `env` 의 블록 스칼라 본문에 `run:` 이 있으면 그 행이 실 run 앵커를 덮는다(발견 49). 생산자만 고치면 parity 가 깨지므로 **양쪽을 같은 편집에서** 고쳐야 한다 | 스칼라 span 을 마스킹하고 `run` 을 step 매핑의 직접 키일 때만 받을 것. 무해한 스텝 + 중첩 `run: npm test` end-to-end 음성 픽스처를 함께 둘 것 |
| **(f) `outside_allowlist` 열거의 제외 목록을 한 벌로** | manifest `exclude_glob` 11 vs `verify-tests.mjs:1009` 의 walk prune 9 — **두 벌이고 내용이 다르다** | 이 필드는 3자 대조 대상이라 두 목록이 다르면 복합 게이트가 필연적으로 실패한다. 5b 는 재열거기 목록에 맞췄고 그 결과 admin 벤더 자산 245 파일이 이 필드에 실린다(발견 52 · `gap` 16) | 두 목록을 한 벌로 합치거나, walk prune 목록이 `exclude_glob` 을 읽게 할 것. 어느 쪽이든 **생성기와 재열거기가 같은 목록을 보는 것**이 요구사항이고, 지금은 그 사실이 어느 규약에도 없다 |

### 마일스톤 6a 요청 (후보 축)

| 요청 | 현재 | 관측 | 제안 |
|---|---|---|---|
| **(a) spec §5 2단계에 "정책 후보 단위" 의 정의를 명문화** | `.claude/meta/05-final-spec.md:346-347` 이 "위 전부를 정책 후보 단위로 통합" 이라고만 적고 단위를 정하지 않는다 (발견 67) | 같은 입력 8,143 항목이 단위 규칙에 따라 후보 8,143 / 5,199 / 1,991 중 무엇으로도 셀 수 있다. 이 마일스톤은 **집행 지점 1개**로 세웠다 | 집행 지점 규칙을 규약으로 승격하고 축(access·contract·value·data)을 정본 어휘로 등록할 것. 5b 의 `evaluation_unit` `canon_undefined` 요청과 같은 부류다 |
| **(b) spec §3 status 에 미확정 값을 1급으로 두거나 `null` 을 명문 허용** | `:205-212` 8종 전부가 **판정된** 값이고 "아직 판정 못 함" 자리가 없다 (발견 68) | 코드 축 단독 도달 가능 값은 `conditional-unknown` 1종뿐 — 실측 11/5,199 (0.21%). 나머지 5,188 은 8종 어디에도 정직하게 못 들어간다 | `undetermined` 를 1급으로 신설하거나, `null` + **닫힌 사유 어휘**를 정본이 허용한다고 적을 것. 이 마일스톤은 후자를 택하고 사유를 `release_axis_absent` · `reference_count_absent` 2종으로 닫았다 |
| **(c) 폐기 판정 5중 필터에 소관 축 열을 추가** | `:214-219` 이 필터 5개를 순서대로만 적는다 (발견 69) | 1·2 코드 축 / 3·4 git 축 / 5 릴리스 축(+ 참조 계수 절반). 배분이 문면에 없어 축별 PRD 가 매번 재발견한다 | 필터별 소관을 표 열로 명시할 것. 이 마일스톤은 그 배분을 `status_basis.filters_owned_by_other_axes` 로 산출물에 실어 두어 `10-index-git` 이 나오면 재생성만으로 채워지게 했다 |
| **(d) Success Metric "status 부여율" 을 값 확정 기준으로 확정하고 6b 소관을 지표에 적을 것** | `.claude/prds/insuniverse-docs-11-index-code.prd.md:221` 판정 기준이 "`status` 없는 행 0" 이다 (발견 70) | 필드 존재 기준 100% vs 값 확정 기준 0.21% — 두 자릿수 갈린다 | 판정 기준을 **값 확정**으로 확정하고, 코드 축 단독으로 도달 불가한 잔여가 **마일스톤 6b(선행 `10-index-git`)** 소관임을 지표 행에 병기할 것 |
| **(e) 마일스톤 4 축에 `type_ref` 한정 해석 또는 입출력 방향 필드를 추가** | `build-api-contracts.mjs:662` 이 `type_ref` 을 소스 표기 문자열 그대로 싣는다 — 모듈 한정이 아니다 (발견 71) | distinct 347종. validator 보유 2,463 속성 중 endpoint 확정 결합 **0건**, 이름 일치 499건 | `type_ref` 을 `id: <namespace>:<symbol>` 로 해석해 싣거나, 최소한 `param_bindings[].direction`(입력/응답)을 추가할 것. **이 계획은 상류를 고치지 않는다** — 계약 변경 규칙상 별도 correction 마일스톤이다 |
| **(f) 소비 입력을 확정 3파일 allowlist 로 명문화** | 부모 PRD `insuniverse-policy-docs.prd.md:145` 가 `policy-candidates.*.jsonl` 을 `cat` 으로 병합한다고 적는다 (발견 72) | glob 이라 축별 PRD 가 같은 stem 을 쓰면 잠정 산출물이 확정 소비 경로에 자동 편입된다. 이 마일스톤은 이름을 glob 밖으로 빼서 피했다 | 병합 입력을 **파일명 3개 열거**로 바꿀 것. 형제 PRD 불간섭 규약상 이 계획이 직접 고치지 않으므로 `00-protocol` 경유 요청으로 올린다 |

| **(g) `env_gate_rules.known_gates` 의 관측 우주를 정의하거나 게이트마다 소관 축을 적을 것** (`00-protocol`) | manifest `env_gate_rules.known_gates` 15종이 축 구분 없이 한 목록이다. 코드 축 스캔 우주는 `repos[].src_roots` 아래 `.ts/.tsx` 다 | `RUN_MODE` · `SCHEDULER_TYPE` 은 그 우주 밖(컨테이너·CI 선언 파일)이라 `verify-policy-values.mjs:471` 하한이 **영구히** 막는다 — 발견 79 | 게이트마다 관측 우주(또는 소관 축)를 적거나, 코드 축이 우주를 넓히도록 규약을 열 것. 후자면 생성기와 판정기의 **독립 재스캔을 함께** 넓혀야 하고 컨테이너 논리 명령 파서는 5a-fix-2 산출을 재사용해야 한다 |
| **(g2) `.gitattributes` 에 `docs/_meta/index/** text eol=lf` 를 추가할 것** (`00-protocol`) | main 의 `.gitattributes` 규칙 4종이 `docs/_meta/index/` **직속 자식**만 덮는다 | `codes/*.yaml` 7종이 규칙 밖이다. 이 마일스톤이 쓰는 파일(`env-gates.yaml` · 후보 2종)은 전부 직속 자식이라 **6b 범위에서는 결손이 없다** | 소유자가 `00-protocol`(archived PRD 263·280 행)이므로 6b 는 저술하지 않고 회부만 한다 — 발견 80 |
| **(g3) 문서 앵커를 행 번호가 아니라 내용·헤딩 기반으로 바꿀 것** (`00-protocol` · 마일스톤 5b) | 테스트 축이 `meta/05-final-spec.md:168` · `:172` 를 **행 번호**로 핀한다 | main 병합이 spec 에 113행을 넣자 두 앵커가 즉시 끊겼다(실제 위치 242·246). 남의 PRD 가 소유한 파일이 움직이면 언제든 재발한다 — 발견 81 | 앵커를 헤딩 경로 + 토큰으로 바꾸거나, spec 이 앵커 대상 행에 안정 식별자를 달 것. 재핀 3파일은 5b 소유이므로 6b 는 backlog·fix-task 등재 후 분리했다 |
| **(h) manifest `repos[].head` ↔ 서브모듈 포인터 ↔ git 축 `frozen_heads` 3자 정합** (`00-protocol`) | 병합 후 실측: manifest head 와 서브모듈 포인터는 **같다**(`c7597fc7` · `b9a90a42` · `7df733a5`). git 축 `frozen_heads` 만 구 SHA(`4d798f55` · `767d1e44` · `fb001e1d`)다 | **이것이 6b 미확정의 근본 원인이다.** 필터 3·4·5 전부가 여기서 막힌다 — 발견 73 | 세 값을 한 커밋에서 함께 올리는 규약을 세우거나, 축마다 자기 기준선을 선언하고 소비자가 드리프트를 기계로 읽게 할 것. 6b 는 후자를 `meta.baseline_drift` 로 이미 낸다 |
| **(i) 두 기준선 사이 변경 집합을 `10-index-git` 소유 산출물로 제공할 것** (`10-index-git`) | 구 `frozen_heads` ↔ 신 manifest head 사이 구간을 설명하는 산출물이 저장소에 없다 | 코드 축이 그 구간을 직접 계산하려면 서브모듈 git 이력을 읽어야 하고, 그것은 UI3 가 배제한 축의 일이다(D4). 그래서 6b 는 필터 3·4·5 를 전부 미확정으로 닫았다 | 두 기준선 사이 **변경 파일 집합**을 핀·digest 된 소유 산출물로 낼 것. 그것이 오면 코드 축은 후보 단위 적용 여부를 판정할 수 있고, `status_basis.filters.*.historical_observation` 에 남긴 구 관측치가 **재조사 없이** 승격된다 |

## 3. 부록 A — `admin-corp.temp.md §14` 표기 규약 원문 보존

**보존 이유**: §14 는 `domain_status.yaml` 로 승격되지 않는다(발견 3). 그러나 이 15행은 **`.gitignore`
대상 파일에만 존재한다.** 승격하지 않는 것과 잃어버리는 것은 다르다. 누락이 개수로 잡히도록 원문과
digest 를 함께 남긴다.

- **소비처**: `50-domain` (기업 도메인 표기 규약). 그 PRD 의 scope/acceptance 갱신은 이 마일스톤 범위 밖 → backlog
- **원본**: `.claude/meta/admin-corp.temp.md:698-720` · 파일 sha256 `947ad14f6489dcef1de250fe68816eb3a1fbf6fc90c0d02bd80e146a4a535c2a`
- **row_count**: 15
- **set_sha256**: `b56564a39cc6036789237e16daf8b62041a5f79dd201a57c3e296155a6159abc` — 정렬된 `항목|색상규칙` 목록 기준
**재현 규칙 (정확히 이대로 해야 위 digest 가 나온다)**

1. 구간은 **행 전체가 정확히** `<!-- APPENDIX-A:BEGIN -->` / `<!-- APPENDIX-A:END -->` 인 두 줄 사이다.
   부분 일치로 찾으면 안 된다 — **이 문단 자체가 그 표식 문자열을 포함**하므로 부분 일치 검색은
   구간을 잘못 잡는다(실제로 두 명의 리뷰어가 여기서 어긋난 값을 냈다).
2. 본문 행 = `| ` 로 시작하는 줄 중 헤더(`| 항목 …`)와 구분선(`|---…`)을 뺀 것. → **15행**
   헤더 제외를 빠뜨리면 **16행 / `91ac9212b003bf8deba66d5dfcc26af27409121ecdae0254641f2bce78755c16`**
   이 나온다. 그 값이 나왔다면 필터에서 `| 항목` 제외가 빠진 것이다 — 문서가 틀린 게 아니다.
   (외부 리뷰어 2명이 실제로 이 값을 내고 문서를 오류로 판정했다. 아래 명령을 **그대로** 쓸 것.)
3. 각 행을 `|` 로 쪼개 앞뒤 공백을 제거하고 `첫째칸|둘째칸` 으로 잇는다.
4. 그 목록을 **정렬**한 뒤 `join("\n") + "\n"` 을 UTF-8 로 sha256.

```bash
# 개수
awk '$0=="<!-- APPENDIX-A:BEGIN -->"{f=1;next} $0=="<!-- APPENDIX-A:END -->"{f=0} f && /^\| /' \
  docs/_meta/findings/11-index-code.md | grep -vc '^| *항목\|^|---'
# 개수 + digest (원본 대조용 — 같은 규칙을 원본 698-720 구간에 적용하면 같은 값이 나온다)
node -e '
const fs=require("fs"),c=require("crypto");
const t=fs.readFileSync("docs/_meta/findings/11-index-code.md","utf8").split(/\r?\n/);
const a=t.indexOf("<!-- APPENDIX-A:BEGIN -->"), b=t.indexOf("<!-- APPENDIX-A:END -->");
const rows=t.slice(a+1,b).filter(l=>l.startsWith("| ")&&!l.startsWith("| 항목")&&!/^\|\s*-/.test(l));
const p=rows.map(l=>{const x=l.replace(/^\s*\|/,"").replace(/\|\s*$/,"").split("|").map(s=>s.trim());return x[0]+"|"+x[1];});
console.log(p.length, c.createHash("sha256").update([...p].sort().join("\n")+"\n","utf8").digest("hex"));'
# 기대: 15 b56564a39cc6036789237e16daf8b62041a5f79dd201a57c3e296155a6159abc
```

<!-- APPENDIX-A:BEGIN -->

| 항목 | 색상 규칙 |
|---|---|
| 접근차단상태 (`agcDeliquent`) | 허용(0)=파랑 / 차단(1)=빨강 |
| 계약종료여부 (`isContractExpired`) | 계약중(0)=파랑 / 계약종료(1)=빨강 |
| 과금유형 (`actBillingType`) | 후불=파랑 / 선불=녹색 / 무상=빨강 |
| 약관 사용여부 (`agtEnabled`) | 사용=blue / 미사용=red |
| 정산상태 | 정산대기=검정 / 정산확정=파랑 / 정산이월=주황 / 정산제외=빨강 · **취소 건은 공란** |
| 정산 — 입금완료여부 | 완료=파랑 / 미완료=빨강 |
| 정산 — 세금계산서 | 발행완료=파랑 / 미발행=빨강 |
| 주문상태 | 주문완료=파랑 / 주문취소=빨강 / 주문실패=녹색 |
| 기업구독 상태 | 구독=파랑 / 해지=빨강 / 변경=녹색 |
| 기업회원 — 회원구분 | 일반 회원=파랑 / **조직관리자=녹색** |
| 기업회원 — 구독상태 | 구독=파랑 / 해지=빨강 |
| 기업회원 — 사용여부 | 사용=파랑 / 중지=빨강 |
| 기업머니 내역 | 충전·지급=파랑 / 차감·지급회수=빨강 |
| 충전요청 상태 | 승인완료=파랑 / 반려=빨강 / 요청취소=주황 / 승인대기=미지정 |
| 소속조직 | 전체경로("대구 > 개발팀") → 조직명 → **"미배정"** |

<!-- APPENDIX-A:END -->

**표기 변경 이력(2026-07)** (원본 `admin-corp.temp.md:718`): 후불제→후불, 선불제→선불 (서버 전송 value 는 유지)

## 4. 이월

마일스톤 5b 가 **열었지만 닫지 못한** 항목이다. 6번은 사용자 결정 D1 로 닫힌 기록으로 남긴다.
11·12 는 마일스톤 6a 가 연 것이다.

| # | 이월 항목 | 왜 5b 가 못 닫나 | 소관 |
|---|---|---|---|
| 1 | **케이스 단위 행** | 안정된 케이스 id 를 만들 수단이 없다 — 정규식 계수가 스스로 근사치이고, 그 근사에서 뽑은 선언 체인은 문자열 하나만 바뀌어도 id 가 흔들린다. 틀리게 푼 결과는 안 푼 것보다 나쁘다 | 별도 축 |
| 2 | **테스트 ↔ 정책/생산 심볼 연결** | import 해석 + assertion 분석이 필요한 **새 축**이고 PRD Scope 밖이다 | 별도 PRD |
| 3 | **CLI 선택자 → 파일 집합 해석기** | 오늘 러너 호출이 0건이라 입력이 없다. `selection_scope` 로 범위를 명시해 fail-closed 로 뒀다 | 호출이 생긴 뒤 |
| 4 | **manifest 액션 참조 등록** | 규약 개정이라 이 마일스톤이 못 내린다 — §2 (c) | `00-protocol` |
| 5 | **`uses: ./…` 호출 그래프 의미 · call-site-qualified id · 검증기 측 fail-closed 강제** | 앵커 형식이 `checkAnchor` 공유 계약이라 생성기와 재열거기를 **같은 편집에서** 고쳐야 한다. **5b 가 이미 한 것**: 미확정을 중단이 아니라 과소 확정으로 내려앉히는 `select` 강등(발견 48). **남는 것**: caller 그래프를 실제로 모델링해 `T` 소속 호출도 확정할 수 있게 하는 것과, 그 판정을 재열거기에 넣어 **검증기가 다른 생산자의 caller `if: false` 위조 문서를 거부**하게 하는 것. 무조건·`false`·복수 caller·자체 트리거 병존 네 픽스처를 함께 둘 것 | 별도 correction |
| 6 | ~~3자 대조 검사 코드가 계획 본문에 산다~~ | **닫혔다** — 사용자 결정 **D1**(2026-08-13)로 `check-tests-crosscheck.mjs` 를 PRD 소유 목록에 추가했다. 이월이 아니라 이 마일스톤 산출물이다 | 닫힘 |
| 7 | **`verify-tests.mjs --file` 을 직접 부르는 제3의 소비자는 3자 대조를 건너뛴다** | 5b 의 게이트 경로는 복합 진입점이 닫았다. 남는 것은 `--file` **자체**가 완전성 검사 없이 성공한다는 것이고, 막으려면 `--file` 이 실패해야 한다 | 마일스톤 5a |
| 8 | **`DOCKERFILE_RE` 가 대소문자를 가려 실제 빌드 파일이 양쪽 사각지대에 있다** | server `apps/api/dockerfile` 이 소문자다. 정규식을 고치는 것은 `verify-tests.mjs` 편집이고 픽스처도 함께 바꿔야 한다 — §2 (d) | 마일스톤 5a . **→ 닫힘 (5a-fix-2).** 멤버십이 정본 술어 2단(`isDockerfilePath` 이름 후보 + `looksLikeDockerfile` 내용 확인)으로 갔고 생성기 git pathspec 도 `:(glob,icase)` 로 넓혔다. `insuniverse-server/apps/api/dockerfile` 이 이제 양쪽에 보이고 컨테이너 호출 3건을 낸다 |
| 9 | **`ANCHOR_TOKENS.invocation` 이 소문자라 Dockerfile `RUN` 호출 앵커가 통째로 거부된다** (발견 54 · **5b 산출을 차단함**) | 재열거기가 그 호출을 만들고 parity 가 문서에 그것을 요구하는데, 싣는 순간 앵커 검사가 거부한다 — **어느 생산자도 빠져나갈 수 없다.** 같은 파일이 잔여 쪽에는 대문자 `RUN` 을 이미 넣어 뒀으므로 한쪽만 반영된 누락이다 | 마일스톤 5a (**차단 해소 선행**) . **→ 닫힘 (5a-fix-2, 발견 54 와 함께).** 앵커 대조를 채널로 가르고 컨테이너 쪽은 정본 파서에 물었다 — 발견 58 참조 |
| 10 | **Dockerfile `WORKDIR` 이 컨테이너 경로인데 저장소 디렉터리 실재를 요구한다** (발견 55 · **5b 산출을 차단함**) | 9 를 고쳐도 남는 **독립된 둘째 차단**이다. 처방은 실재 검사를 채널 조건부로 두는 것이고, `blocks` 의 container 예외가 이미 같은 형태의 분기를 갖고 있다(`:571`) | 마일스톤 5a (**차단 해소 선행**) . **→ 닫힘 (5a-fix-2, 발견 55 와 함께).** 실재 검사를 채널 조건부로 두는 대신 좌표를 `null` 로 만들었다 — 조건부 면제가 **필요 없어졌다** |
| 11 | **후보 `status` 의 값 확정** (발견 68·69·70) | spec §3 폐기 판정 5중 필터 중 3(삭제 이력)·4(revert)는 git 축, 5(도달성)의 prod 배포 절반은 릴리스 축이다 | **마일스톤 6b 가 부분 처리하고 닫았다 (2026-08-18).** git·릴리스 축 산출물 6종이 도착해 코드 축이 읽는다. 그런데 두 기준선이 3/3 다르고(발견 73) 그 사이를 설명하는 소유 산출물이 없어 **필터 3·4·5 전부가 후보 단위로 판정 불가**다. 6b 가 낸 것은 값이 아니라 **참인 사유와 축 귀속**이다 — 실산출 status 확정 **13 / 1,993**(확정 후보 파일 기준 0.65%) · 미확정 5,188 이 전부 닫힌 사유 + `status_basis` 축 근거를 갖는다. 남는 것은 §2 (h)·(i) 회부이며 값 확정은 그 두 축이 답한 뒤다 |
| 12 | **endpoint → DTO 결합과 입출력 방향** (발견 71 — 이월 2 와 묶는다) | `param_bindings[].type_ref` 이 비한정 이름이라 동명이인을 못 가른다(distinct 347종). 결합·방향 없이는 속성 3,208 중 `not_a_candidate` 로 내릴 수 있는 것이 0건이고, 그래서 전수가 `candidates-unqualified.code.jsonl` 에 잠정으로 남는다. 상류(마일스톤 4 축) 수정이라 계약 변경 규칙상 이 마일스톤 밖이다 | 이월 2(테스트↔정책 심볼 연결)와 같은 부류의 **심볼 해석 축**이다 — 둘 다 import 해석이 선행이다. 별도 correction 마일스톤 또는 별도 PRD. 요청은 §2 (e) |
| 13 | **테스트 축 행 번호 앵커가 main 병합으로 끊겼다** (발견 81) | `meta/05-final-spec.md:168`·`:172` 핀이 실제 242·246 으로 밀렸다. 재핀 대상은 `build-tests.mjs:139,141` · `verify-tests.mjs:152,153` · `tests.jsonl` 의 `meta.s_grade_rule.*` 3파일이고 전부 5b 소유라 6b `Files to Change` 밖이다 | **마일스톤 5b** (별도 correction). 6b 는 상류 판정기 게이트를 테스트 축을 뺀 6종으로 좁히고 진행했다. 근본 처방은 §2 (g3) — 행 번호 핀을 내용·헤딩 기반으로 |
| 14 | **`env-gates.yaml` 하한 2종 미달 — 스캔 우주 밖** (발견 79) | `RUN_MODE` · `SCHEDULER_TYPE` 이 `src_roots` × `.ts/.tsx` 우주 밖이라 `verify-policy-values` 가 exit 1 로 남는다. 닫으려면 생성기와 판정기의 독립 재스캔 우주를 **함께** 넓혀야 하고 우주 정의는 manifest 소유다 | **`00-protocol`**(우주 정의 — §2 (g)) · **`12-index-runtime`**(스케줄러 게이트의 정책 의미). 컨테이너 논리 명령 파서는 5a-fix-2 산출을 재사용할 것 |
| 15 | **계획 Validation 의 `verify-tests.mjs` 무인자 호출은 원리적으로 통과 불가** | 그 파일은 모드 플래그가 없으면 usage 를 내고 exit 1 이다. 테스트 축 정식 게이트는 `build-tests.mjs --repos-root <루트> --report --json`(쓰기 없음) → `check-tests-crosscheck.mjs --repos-root <루트> --report <report.json> --jsonl docs/_meta/index/tests.jsonl` 다 | 하네스 문면을 마일스톤 착수 **전에** 검증하지 않은 자리다. 다음 계획서가 상류 판정기 루프를 쓸 때 이 형태로 적을 것 |
