/* * **원본의 표면 중 얼마를 분류했나** — 순수(import 없음). * * ── 왜 이 축이 생겼나 (2026-08-20) ────────────────────────────────────────── * 탐침 열한 개는 「이 단계가 되나」를 묻는다. `live-ingest` 는 **레코드 한 건이 통과하면 초록**이다. * 그래서 재고만 읽는 커넥터에도 「라이브 통과」가 나왔고, 그 사이 원본의 `production_input`(무엇으로 * 무엇을 만들었나 · 실제 투입량 · 선언된 필요량)과 오더 네 종류와 팔레트가 읽히지 않고 있었다. * * 그것들은 대화에서 하나씩 「아차」로 드러났다. 그 방식은 반복된다 — 빠진 것을 기억으로 찾기 때문이다. * 그래서 축을 하나 더 둔다: **원본이 내놓는 표면을 세고, 그 각각을 커넥터가 무엇으로 처리하는지 적는다.** * * ── 규율 ──────────────────────────────────────────────────────────────────── * 표면 목록은 **원본에서 얻는다**(GraphQL 인트로스펙션 등). 커넥터가 손으로 적은 목록으로 세면 같은 * 눈먼 자리를 그대로 물려받는다 — 내가 잊은 표는 목록에도 없다. * * 「읽지 않는다」는 결론은 **정당할 수 있다.** 파생 표(집계·추적)나 우리 어휘에 자리가 없는 것을 읽지 * 않는 것은 결함이 아니다. 결함은 **분류하지 않은 것**이다 — 그것은 생각한 적이 없다는 뜻이다. * * 그래서 판정은 「전부 읽었나」가 아니라 **「전부 판단했나」** 다. */ /** * 표면 하나를 커넥터가 무엇으로 처리하는가 — **각 낱말에 적용 가능한 기준과 조치할 사람이 있다.** * 기준이 없으면 분류가 느낌이 되고, 조치자가 없으면 표를 읽어도 누가 움직일지 알 수 없다. * * · `read` — **지금 커넥터가 질의한다.** 기준: 커넥터 코드에 그 질의 이름이 있다(확인 가능). * 조치자: 없음(완료분). * * · `todo` — **실려야 하는 사실인데 아직 안 읽는다.** 기준 셋을 다 만족: ① 원본에 읽을 길이 * 있다 ② 우리 어휘에 담을 자리가 있다 ③ 아직 안 읽는다. 조치자: **커넥터**. * * · `blocked` — **실려야 하고 담을 자리도 있는데 원본이 읽을 길을 주지 않는다.** 기준: 목록 질의가 * 없거나, 있어도 필수 인자 때문에 전역으로 읽을 수 없다. 조치자: **원본 쪽**. * `todo` 와 한 칸에 두지 않는 이유가 이것이다 — 움직여야 하는 사람이 다르다. * * · `no-home` — **원본에 길은 있는데 그 사실이 상태로 남을 자리가 없다.** 기준은 어휘가 아니라 * **상태에 반영하는 것**이다 — 이벤트 어휘에 통과할 자리가 있어도 상태에 반영되지 * 않으면 사실은 남지 * 않는다(오히려 더 나쁘다: 검증을 통과하므로 실은 줄 알고 잃는다). * 조치자: **커널·아키텍처** — 반영할 것인지 판단해야 한다. * 냉장 온도가 그 예다. EPCIS `SensorElement`·`sensorReport` 는 이미 있고 검증기도 * 본다. **소비하는 곳이 0 이다.** 그러므로 「어휘에 자리가 없다」가 아니라 「상태에 * 반영하는 곳이 없다」가 정확하고, 할 일도 자리 만들기가 아니라 그 반영이다. * **「중요하지 않다」가 아니다.** 오히려 중요한데 못 담는다는 뜻이고, 그래서 판단이 * 필요하다. 냉장 온도가 그 예다 — 식품에서 일급 사실인데 계량 어휘가 에너지 전용이다. * * · `derived` — **원본이 다른 표에서 계산해 내놓는 값.** 기준: 그 질의의 결과가 우리가 읽는(또는 * 읽을) 사실로부터 계산된다. 읽으면 **같은 수를 두 곳에서 만들게 되고**, 두 수가 * 갈리면 어느 것을 믿을지 알 수 없다. 조치자: 없음 — 읽지 않는 것이 정답. * * · `duplicate` — **같은 사실을 다른 문으로 내놓는 것.** 기준: 목록 질의로 이미 읽는(또는 읽을) 그 * 사실의 단건 드릴다운이다. 계산된 값이 아니므로 `derived` 와 다르고, 관심 밖이 * 아니므로 `out-of-scope` 와도 다르다. 조치자: 없음. * * · `out-of-scope` — **공장의 상태나 활동이 아니다.** 기준: 그 사실이 자리·물건·설비·사람·작업· * 오더 어디에도 해당하지 않는다(사용자 계정·세무·거래처 마스터). 조치자: 없음. * * 「읽지 않는다」는 결론은 정당할 수 있다(`derived`·`duplicate`·`out-of-scope`). 결함은 **분류하지 * 않은 것**이다 — 그것은 생각한 적이 없다는 뜻이다. 그래서 판정은 「전부 읽었나」가 아니라 * **「전부 판단했나」** 다. */ export type Disposition = 'read' | 'derived' | 'duplicate' | 'no-home' | 'out-of-scope' | 'blocked' | 'todo' /** 표면 하나에 대한 판단 — **이유가 필수다.** 이유 없는 분류는 분류한 척이다. */ export interface SurfaceRuling { disposition: Disposition /** 왜 그렇게 정했나. 사람이 읽는 한 문장. */ why: string } export interface CoverageInput { /** 원본이 실제로 내놓는 표면 — **원본에서 얻은 것.** 커넥터가 적은 목록이 아니다. */ sourceSurface: readonly string[] /** 커넥터의 판단표. */ rulings: Readonly> } export interface CoverageReport { /** 원본 표면 수. */ total: number /** 분류된 수. */ classified: number /** **분류하지 않은 표면** — 생각한 적 없는 것. 이 목록이 비어야 한다. */ unclassified: string[] /** 판단표에는 있는데 원본에 없는 것 — 원본이 바뀌었거나 이름을 잘못 적었다. */ stale: string[] byDisposition: Record /** 실어야 하는데 아직 아닌 것. 초록을 막지는 않지만 **세어 보여야** 한다. */ todo: string[] /** 원본이 길을 주지 않아 막힌 것 — 이 목록이 곧 **원본 쪽 선결 과제**다. */ blocked: string[] } const DISPOSITIONS: Disposition[] = ['read', 'derived', 'duplicate', 'no-home', 'out-of-scope', 'blocked', 'todo'] export function assessCoverage(input: CoverageInput): CoverageReport { const surface = [...new Set(input.sourceSurface)].sort() const byDisposition = DISPOSITIONS.reduce( (acc, d) => ((acc[d] = []), acc), {} as Record ) const unclassified: string[] = [] for (const name of surface) { const r = input.rulings[name] /* 이유가 없으면 분류로 세지 않는다 — 낱말만 채운 것을 판단으로 세면 이 축이 무의미해진다. */ if (!r || !DISPOSITIONS.includes(r.disposition) || !r.why?.trim()) { unclassified.push(name) continue } byDisposition[r.disposition].push(name) } const known = new Set(surface) const stale = Object.keys(input.rulings) .filter(k => !known.has(k)) .sort() return { total: surface.length, classified: surface.length - unclassified.length, unclassified, stale, byDisposition, todo: byDisposition.todo, blocked: byDisposition.blocked } } /** * 한 줄 요약 — 화면과 로그가 같은 문장을 쓰도록. * * **「전부 읽었다」고 말하지 않는다.** 말하는 것은 「전부 판단했다」다. 그리고 분류하지 않은 것이 남아 * 있으면 그 수를 앞세운다 — 그것이 이 축의 유일한 목적이다. */ export function coverageLine(r: CoverageReport): string { if (r.unclassified.length) { const head = r.unclassified.slice(0, 5).join(', ') const more = r.unclassified.length > 5 ? ` +${r.unclassified.length - 5}` : '' return `${r.unclassified.length}/${r.total} surface(s) not yet judged — ${head}${more}` } const read = r.byDisposition.read.length const parts = [`${read} read`] if (r.todo.length) parts.push(`${r.todo.length} still to carry`) if (r.blocked.length) parts.push(`${r.blocked.length} blocked at the source`) return `all ${r.total} surface(s) judged; ${parts.join(', ')}` }