/* * 유입 경로가 **모두 같은 장부**에 적는다 — 소스 규율 검사. * * ── 왜 기계가 지켜야 하나 ─────────────────────────────────────────────────── * 피드를 여는 경로가 둘이다(커넥터 `openLiveFeed` · 시나리오 `twin-ingest` 태스크). 한쪽만 장부에 * 적으면 그 경로로 먹이는 트윈이 **「한 번도 받지 않은 것」**으로 보인다. 그 판정은 `unknown` 이고, * `unknown` 은 화면에서 「정상」과 구별되지 않는다 — 즉 **오류 없이 틀린 답**이 된다. * * 세 번째 경로가 생길 때 사람이 이 사실을 기억할 것이라고 믿지 않는다. 인제스트를 부르는 자리를 * 소스에서 찾아 장부 기록이 함께 있는지 본다. */ import { test } from 'node:test' import assert from 'node:assert/strict' import { readFileSync, readdirSync, statSync } from 'node:fs' import { join } from 'node:path' import { guardRoots } from './guard-roots.ts' /** 훑는 곳 — 이 저장소에서 인제스트를 부를 수 있는 층 둘. */ const ROOTS = guardRoots(import.meta.url, ['../server']) /** 인제스트를 여는 함수 이름. 이 이름이 나오는 자리가 곧 유입 경로다. */ const INGEST_CALL = 'ingestCanonicalRecords(' /** 장부에 적는 창구. */ const LEDGER_CALL = 'recordIngestResult(' /** * 예외 — 인제스트를 **부르지 않고 다시 내보내기만** 하는 자리. * * 늘리려면 이유를 함께 적는다. 「부르는데 안 적는다」는 예외가 될 수 없다 — 그게 이 검사가 막는 것이다. */ const REEXPORT_ONLY = ['board/tasks/twin-ingest-core.ts', 'engine/index.ts', 'engine/canonical-ingest.ts'] function sources(dir: string): string[] { let entries: string[] try { entries = readdirSync(dir) } catch { return [] } return entries.flatMap(f => { const p = join(dir, f) return statSync(p).isDirectory() ? sources(p) : p.endsWith('.ts') && !p.endsWith('.d.ts') ? [p] : [] }) } function ingestCallSites(): { file: string; src: string }[] { const out: { file: string; src: string }[] = [] for (const root of ROOTS) { for (const f of sources(root)) { const src = readFileSync(f, 'utf-8') if (!src.includes(INGEST_CALL)) continue const rel = f.slice(root.length + 1) if (REEXPORT_ONLY.some(x => rel.endsWith(x))) continue out.push({ file: rel, src }) } } return out } test('인제스트를 부르는 곳은 모두 장부에 적는다 — 한 경로만 빠지면 그 트윈은 「모름」으로 보인다', () => { const missing = ingestCallSites() .filter(s => !s.src.includes(LEDGER_CALL)) .map(s => s.file) assert.deepEqual( missing, [], `장부에 적지 않는 유입 경로 ${missing.length}곳:\n ${missing.join('\n ')}\n\n` + `그 경로로 먹이는 트윈은 twinIngestHealth 에서 「한 번도 받지 않음」(unknown)으로 보이고, ` + `화면에서 정상과 구별되지 않는다. TwinEngine.recordIngestResult(도메인, 인스턴스, 제시수, 거부) 를 부른다.` ) }) test('검사가 실제로 경로를 보고 있다 — 대상이 사라지면 초록이 거짓이 된다', () => { const sites = ingestCallSites().map(s => s.file) assert.ok( sites.length >= 2, `유입 경로를 ${sites.length}곳 찾았다 — 둘(커넥터·시나리오)보다 적으면 호출 형태가 바뀐 것이다. ` + `이 검사의 패턴을 함께 옮긴다. 찾은 것: ${sites.join(', ') || '없음'}` ) }) test('유입 경로는 커널이 실제로 받은 수를 견준다 — 버려진 것을 통과로 세지 않게', () => { /* * 겪은 일: 트윈이 멈춘 뒤에도 피드는 레코드를 나르고 커널은 알리지 않고 버렸는데, 장부는 그것을 「통과」로 * 셌다. 화면은 멈춘 트윈 옆에 「150 통과 · 100%」라고 적었다 — 사실이 사라지는 동안 안심시킨 것이다. * * 고친 방법은 `ingestLive` 가 **넣은 수를 돌려주는** 것이다. 새 경로가 그 반환을 안 쓰고 0 을 넘기면 * 같은 침묵이 돌아온다. 그래서 경로마다 미전달을 세는지 소스에서 본다. */ const missing = ingestCallSites() .filter(s => !s.src.includes('undelivered')) .map(s => s.file) assert.deepEqual( missing, [], `커널이 받은 수를 견주지 않는 유입 경로 ${missing.length}곳:\n ${missing.join('\n ')}\n\n` + `TwinEngine.ingestLive(...) 또는 ingestLiveResult(...) 의 반환(넣은 수)을 받아 accepted.length 와의 ` + `차이를 undelivered 라는 이름으로 recordIngestResult(..., nowMs, undelivered) 에 넘긴다. ` + `그러지 않으면 트윈이 멈춘 사이 버려진 레코드가 「통과」로 집계되어 화면이 100% 라고 말한다.\n\n` + `이 검사는 **그 이름이 있는지**만 본다(소스를 글자로 본다). 이름을 적고 0 을 넘기면 통과한다 — ` + `그 경우는 사람이 보는 수밖에 없다.` ) }) test('제시 수를 거부 수로 대신하지 않는다 — 통과율의 분모가 뒤바뀌지 않게', () => { /* * 실수하기 쉬운 자리다: `recordIngestResult(..., rejected.length, rejected)` 처럼 적으면 분모가 * 거부 수가 되어 **통과율이 언제나 0** 이 된다. 오류 없이 「전부 거부」로 읽힌다. */ const bad = ingestCallSites() .filter(s => /recordIngestResult\([^)]*rejected\.length\s*,/.test(s.src)) .map(s => s.file) assert.deepEqual(bad, [], `제시 수 자리에 거부 수를 넘긴 곳:\n ${bad.join('\n ')}`) })