/* * 이 트윈이 **어떤 공정을 돌리고, 그 시간이 어디서 오나.** * * ── 왜 이 목록이 필요한가 (2026-08-18) ───────────────────────────────────── * 「측정이 없을 때의 표준 시간」을 현장이 선언하려면 먼저 **무엇에 선언할지** 보여야 한다. 그런데 공정은 * 자리·설비와 달리 모델에 행이 없을 수 있다 — 실측: 운영 중인 트윈 35개 중 창고·야드는 공정 명세 행이 * 0개였고(MES만 4개), 그 트윈들의 시간은 전부 커널 상수에서 왔다. * * 그래서 목록을 **지어내지 않고 네 곳에서 모은다**: * ① 원천 명세 행(`model.operations`) — 원 시스템이 말한 공정. * ② 커널의 자기보고(`specCoverage()`) — **실제로 돌린** 공정과 그 시간의 출처. * ③ 이력에서 배운 종류(`measuredOperationKinds`) — 재기동에도 남는 근거. * ④ 우리가 이미 선언한 것(`model.localDurations`·`model.localParams`) — 선언해 둔 공정이 목록에서 * 사라지지 않게(사라지면 철회할 자리가 없다). * * ③이 필요한 이유는 실측으로 드러났다: ②는 **작업이 새로 생길 때** 채워지므로, 이력이 풍부한 창고 * 트윈도 재기동 직후에는 목록이 비었다 — 값을 채울 곳을 보여야 하는 화면이 정작 그때 아무 말도 못 한다. * * 그래도 셋 다 비는 트윈은 있다(한 번도 돌지 않고 명세 행도 없는 새 트윈). 그때 「공정이 없다」고 말하지 * 않는다(모르는 것이다) — 부르는 쪽이 그 사실을 그대로 화면에 옮긴다. */ import { readSpecCoverage } from './spec-coverage.ts' /** 공정 한 종류 — 이름과 「지금 무엇으로 도나」. */ export interface OperationBasisRow { /** 작업 종류(`FlowTask.kind` = `OperationDef.key`) — 선언이 겨누는 id 다. */ kind: string /** 사람이 읽는 이름 — 원천 명세 행이 있을 때만(없으면 종류 이름이 곧 이름이다). */ label?: string /** * 지금 이 종류의 시간이 어디서 오나 — 커널의 자기보고, 또는 **이력에서 배운 사실**. 없으면 모른다. * · `measured` — 이 현장의 이력에서 배운 값(가장 강한 근거). **선언보다 이긴다.** * · `declared` — 선언된 값(현장이 정한 것 또는 원천 명세 행). * · `default` — 커널 소스의 상수. 이 목록의 존재 이유다. */ basis?: 'measured' | 'declared' | 'default' /** 원천 명세 행이 말한 시간(ISO 8601) — 있으면. 우리 선언이 이것을 이긴다. */ referenceDuration?: string /** 우리가 선언해 둔 시간(ISO 8601) — 있으면. */ localDuration?: string /** * 우리가 선언해 둔 공정 모수 — **커널이 읽는 표기 그대로**(수율 0..1, 셋업 ISO 8601). * * 사람 단위(퍼센트·분)로 되돌리지 않는다: 화면은 선언 겹에서 사용자가 넣은 값을 그대로 들고 있고, * 여기 있는 것은 **실제로 커널에 실린 값**이다 — 둘을 견주면 환산이 잘못된 것도 드러난다. */ localParams?: Record /** 원천 명세 행이 말한 모수 — 있으면(우리 선언이 이것을 이긴다). */ referenceParams?: Record /** * 모수를 **무엇으로 읽었나** — 커널의 자기보고(`id → measured|declared`) (2026-08-19). * * 수율이 이력에서 온 값이면 선언값은 예비로만 남는다(소요시간과 같은 규율) — 화면이 그 사실을 말해야 * 사람이 헛일을 하지 않는다. 커널이 아직 그 공정을 돌리지 않았으면 이 칸이 빈다(모른다). */ paramBasis?: Record } /** * 공정 목록을 모은다 — **넷의 합집합**, 순서는 원천 행 → 돌린 것 → 이력에서 배운 것 → 선언한 것. * * `kernel` 이 없으면(정지 트윈) ②가 빠지고, `measuredKinds` 를 주지 않으면 ③이 빠진다: 그 사실은 * `basis` 가 비는 것으로 드러난다(빈칸이 곧 「모른다」다). */ export function operationBasis(model: any, kernel?: any, measuredKinds?: readonly string[]): OperationBasisRow[] { const rows = new Map() const at = (kind: string): OperationBasisRow => { const cur = rows.get(kind) if (cur) return cur const row: OperationBasisRow = { kind } rows.set(kind, row) return row } for (const o of (model?.operations ?? []) as any[]) { if (!o?.key) continue const row = at(String(o.key)) if (o.label) row.label = String(o.label) if (o.duration) row.referenceDuration = String(o.duration) /* 원천이 말한 모수도 함께 든다 — 우리 선언과 견줄 수 있어야 「무엇을 고쳤나」가 보인다. */ for (const p of (o.parameters ?? []) as any[]) { if (!p?.id || p.value === undefined) continue row.referenceParams = { ...(row.referenceParams ?? {}), [String(p.id)]: String(p.value) } } } const coverage = kernel ? readSpecCoverage(kernel) : undefined for (const o of coverage?.operations ?? []) { if (!o?.kind) continue const row = at(String(o.kind)) row.basis = o.duration if (o.parameterBasis && Object.keys(o.parameterBasis).length) row.paramBasis = { ...o.parameterBasis } } /* * 이력에서 배운 종류 — 커널이 아직 이 종류를 돌리지 않았어도 **추정기가 답할 수 있다**. 그래서 근거를 * `measured` 로 채운다: 짐작이 아니라 설치된 추정기가 그 종류에 실측값을 줄 것이라는 사실이다. * 커널이 이미 보고한 종류는 **덮지 않는다** — 실제로 돌린 것이 더 강한 사실이다. */ for (const kind of measuredKinds ?? []) { if (!kind) continue const row = at(String(kind)) if (!row.basis) row.basis = 'measured' } const declared = (model?.localDurations ?? {}) as Record for (const [kind, iso] of Object.entries(declared)) { if (!kind) continue at(kind).localDuration = String(iso) } const params = (model?.localParams ?? {}) as Record> for (const [kind, entries] of Object.entries(params)) { if (!kind) continue const row = at(kind) for (const [id, value] of Object.entries(entries ?? {})) row.localParams = { ...(row.localParams ?? {}), [id]: String(value) } } return [...rows.values()] } /** * 선언을 받아도 되는 공정 이름들 — **하나도 확인할 수 없으면 `undefined`**(모른다). * * 빈 배열로 답하지 않는 이유: 빈 배열은 「어떤 이름도 안 된다」로 읽히고, 그러면 아직 돌지 않은 트윈에는 * 아무것도 선언할 수 없다. 모르는 것과 없는 것은 다르다 — 모를 때는 받아 주고 그 사실을 말한다. */ export function knownOperationKinds(model: any, kernel?: any, measuredKinds?: readonly string[]): string[] | undefined { const kinds = operationBasis(model, kernel, measuredKinds).map(r => r.kind) return kinds.length ? kinds : undefined }