/* * 시뮬 명세 자기보고 — **"이 예측으로 무엇을 말할 수 있나" 를 예측과 함께 내보낸다.** * * 배경(operato-twin design/plans/simulation-spec.md): 공정 소요·수율이 커널 소스의 상수였다. 이제 * 마스터가 명세를 실어 오면 커널이 그것으로 실행하고, `specCoverage()` 가 무엇을 선언값으로 무엇을 * 기본값으로 썼는지 보고한다. 그 보고를 **예측 응답에 붙이지 않으면** 받는 쪽은 상수로 계산한 숫자와 * 현장 명세로 계산한 숫자를 구별할 수 없다 — 똑같이 "p50 168" 로 보인다. * * 언어 중립: 여기서는 **코드와 수치만** 낸다(`qualification` + 카운트). 사람이 읽는 문장은 표현 * 계층(클라 i18next / AI 답변)이 만든다 — attention 과 같은 규율. */ /** 예측 자격 — 선언된 명세가 얼마나 덮는지에 따라 이 예측으로 정당하게 말할 수 있는 범위가 달라진다. */ export type ForecastQualification = /** 모든 소요가 커널 기본 상수 → **상대 비교만**("자원 2대 더 넣으면 나아지나"). 절대 시각 주장 불가. */ | 'relative' /** 일부만 선언·실측 → 그 구간은 계산하지, 나머지는 상수. 혼합. */ | 'partial' /** 소요 전부 선언 → 절대 예측 시도 가능. 단 **정확도는 이력이 확인한다**(선언 != 검증). */ | 'absolute-capable' /** * 소요 전부가 **이 현장의 이력에서 온 값**(추정기) → 가장 강한 근거. 선언은 사람이 적은 값이고 * 이것은 실제로 일어난 값이다. 그래도 "정확하다" 가 아니라 "관측에 계산하지됐다" 는 뜻이다. */ | 'calibrated' export interface SpecCoverageReport { operations: { kind: string duration: 'measured' | 'declared' | 'default' variability?: string parameters: string[] /** * 그 모수를 **무엇으로** 읽었나(`id → measured|declared`) — 없는 id 는 코드 상수를 썼다는 뜻이다. * * 소요시간이 근거를 밝히듯 모수도 밝힌다(2026-08-19): 수율이 이력에서 온 값인지 사람이 적은 값인지 * 상수인지에 따라 「불량이 늘었다」는 화면 문장의 무게가 다르다. */ parameterBasis?: Record }[] /** 이력·계산에서 온 소요(추정기) — 선언값보다 강한 근거. */ measuredDurations: number declaredDurations: number defaultDurations: number /** 변동(분포)을 가진 작업 종류 수. **0 이면 모든 소요가 고정값**이라 대기·병목이 과소평가된다. */ varyingDurations: number qualification: ForecastQualification } /** * 명세 자격 판정(순수) — 커널 보고를 받아 분류한다. 커널을 모르므로 테스트가 쉽다. * 관측된 작업이 하나도 없으면(`operations` 빈 배열) 판정할 근거가 없으므로 **undefined**(모른다). */ export function classifySpecCoverage(cov: { operations?: { kind: string duration: 'measured' | 'declared' | 'default' variability?: string parameters?: string[] parameterBasis?: Record }[] measuredDurations?: number declaredDurations?: number defaultDurations?: number }): SpecCoverageReport | undefined { const ops = cov?.operations if (!ops?.length) return undefined const operations = ops.map(o => ({ kind: o.kind, duration: o.duration, ...(o.variability ? { variability: o.variability } : {}), parameters: o.parameters ?? [], /* 근거는 커널이 말한 것만 나른다 — 없으면 필드를 만들지 않는다(상수를 썼다는 뜻이 빈칸이다). */ ...(o.parameterBasis && Object.keys(o.parameterBasis).length ? { parameterBasis: o.parameterBasis } : {}) })) const measuredDurations = cov.measuredDurations ?? operations.filter(o => o.duration === 'measured').length const declaredDurations = cov.declaredDurations ?? operations.filter(o => o.duration === 'declared').length const defaultDurations = cov.defaultDurations ?? operations.filter(o => o.duration === 'default').length const varyingDurations = operations.filter(o => o.variability && o.variability !== 'constant').length /* 사다리: 전부 이력 → calibrated · 상수 없음 → absolute-capable · 계산하지가 하나도 없음 → relative · 나머지 → partial. */ const grounded = measuredDurations + declaredDurations const qualification: ForecastQualification = grounded === 0 ? 'relative' : defaultDurations > 0 ? 'partial' : declaredDurations === 0 ? 'calibrated' : 'absolute-capable' return { operations, measuredDurations, declaredDurations, defaultDurations, varyingDurations, qualification } } /** * 커널에서 읽는다 — **아직 이 API 가 없는 커널(발행 이전)이면 undefined.** * 그때 `undefined` 는 "명세가 없다" 가 아니라 "모른다" 다. 소비처가 그 둘을 섞지 않도록 주석을 남긴다: * 없는 것을 "전부 기본값" 이라고 단정하지 않는다(단정하려면 커널이 말해 줘야 한다). */ export function readSpecCoverage(kernel: any): SpecCoverageReport | undefined { if (typeof kernel?.specCoverage !== 'function') return undefined try { return classifySpecCoverage(kernel.specCoverage()) } catch { return undefined } }