/* * 어휘 가드 — 규칙은 커널이 한 벌 들고 있고(`@operato/twin-kernel` `retiredVocabularyIn`), * 이 테스트는 **자기 패키지 소스를 훑는다.** 판정이 패키지마다 갈릴 수 없다. * * 계기(2026-08-01): 개명을 손으로 쓸다 같은 실수를 세 번 반복했다. 단어 경계로 찾으면 파생 식별자가 * 빠지고(`moverCount`·`locationType`), 빠진 것은 다음 파도에서야 드러난다. 그 사이 코드에는 두 어휘가 * 섞여 있고 다음 사람은 어느 쪽을 믿을지 모른다. * * 실패하면 위반 목록이 그대로 나온다 — 고치거나, 정당하면 그 줄에 `vocabulary-guard: allow <이유>` 를 * 남긴다(예외가 코드에 이유와 함께 남는다). */ import { test } from 'node:test' import assert from 'node:assert/strict' import { readdirSync, readFileSync, statSync } from 'node:fs' import { join } from 'node:path' import { retiredVocabularyIn } from '@operato/ops-contract' const ROOTS = ['server', 'client'].map(d => new URL(`../${d}`, import.meta.url).pathname) function walk(dir: string, out: string[] = []): string[] { let entries: string[] try { entries = readdirSync(dir) } catch { return out } for (const e of entries) { const p = join(dir, e) if (statSync(p).isDirectory()) walk(p, out) else if (p.endsWith('.ts') && !p.endsWith('.d.ts')) out.push(p) } return out } test('소스에 은퇴한 어휘(location·mover)가 남아 있지 않다', () => { const bad: string[] = [] for (const root of ROOTS) { for (const f of walk(root)) { readFileSync(f, 'utf-8').split('\n').forEach((line, i) => { const hits = retiredVocabularyIn(line) if (hits.length) bad.push(`${f.split('/').slice(-2).join('/')}:${i + 1} ${[...new Set(hits)].join(', ')}`) }) } } assert.deepEqual(bad, [], `은퇴 어휘 ${bad.length}곳:\n${bad.slice(0, 60).join('\n')}${bad.length > 60 ? `\n… ${bad.length - 60} more` : ''}`) })