@things-factory/headless-twin 10.1.45 → 10.1.46

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -108,6 +108,8 @@ export interface ContinuitySeed {
108
108
  since: string;
109
109
  }[];
110
110
  energy?: unknown;
111
+ /** 접힌 오더(ADR-0092) — 모양은 커널이 안다(호스트는 있나만 본다). */
112
+ foldedOrders?: unknown;
111
113
  }
112
114
  export interface ContinuityPlan {
113
115
  seed: ContinuitySeed;
@@ -116,6 +118,8 @@ export interface ContinuityPlan {
116
118
  attentionSinceCount: number;
117
119
  /** 에너지 연속성을 이어받았나 — 열린 구간이 있으면 그 구간의 요금 근거가 살아난다. */
118
120
  energy: boolean;
121
+ /** 접힌 오더를 이어받았나(ADR-0092). */
122
+ foldedOrders: boolean;
119
123
  }
120
124
  /** 미러가 이어받을 축만 골라낸다 — 관측 축(재고·위치·설비)은 일부러 넣지 않는다. */
121
125
  export declare function planLiveContinuity(raw: unknown): ContinuityPlan;
@@ -139,15 +139,19 @@ function planLiveContinuity(raw) {
139
139
  : [];
140
140
  /* 에너지는 커널이 자기 모양을 안다 — 호스트는 「있나」만 본다(골라 담으면 규칙이 두 곳으로 갈린다). */
141
141
  const energy = state?.energy && typeof state.energy === 'object' ? state.energy : undefined;
142
+ /* 접힌 오더(ADR-0092) — 원천을 다시 읽는 미러도 세는 수와 경계를 잃지 않게(없으면 옛 종결이 뜨거운 줄로 되살아난다). */
143
+ const foldedOrders = state?.foldedOrders && typeof state.foldedOrders === 'object' ? state.foldedOrders : undefined;
142
144
  return {
143
145
  seed: {
144
146
  ...(acked.length ? { acked } : {}),
145
147
  ...(attentionSince.length ? { attentionSince } : {}),
146
- ...(energy ? { energy } : {})
148
+ ...(energy ? { energy } : {}),
149
+ ...(foldedOrders ? { foldedOrders } : {})
147
150
  },
148
151
  ackedCount: acked.length,
149
152
  attentionSinceCount: attentionSince.length,
150
- energy: !!energy
153
+ energy: !!energy,
154
+ foldedOrders: !!foldedOrders
151
155
  };
152
156
  }
153
157
  //# sourceMappingURL=warm-start.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"warm-start.js","sourceRoot":"","sources":["../../server/engine/warm-start.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * 웜스타트 판정 — **순수**. \"기동하는 커널에 직전 상태를 주입할 것인가, 주입한다면 무엇을\" 만 정한다.\n * 실제 주입(hydrateObserved 호출)과 로그는 엔진이 한다.\n *\n * ── 왜 떼어냈나 ─────────────────────────────────────────────────────────────\n * 이 판정이 틀리면 증상이 정반대 두 방향으로 나온다: 주입해야 할 때 안 주입하면 **있는 재고가 0 으로**\n * 보이고(2026-07-31 hatiolab-wms: 저널에 입고 540·출고 94 인데 재고 화면이 비어 있었다), 심지\n * 말아야 할 벤치에 주입하면 **용량 측정이 오염된다.** 둘 다 알리지 않고 틀리는 종류라 규칙을 고정한다.\n *\n * ── 오더를 버리던 전제가 낡았다 (2026-08-05) ────────────────────────────────\n * 예전 주석은 \"스냅샷의 오더에는 progress 밖에 없어서 역산하면 없는 숫자를 지어내는 셈이므로\n * 넘기지 않는다\" 였다. 그때는 맞았다. 그러나 커널 `OrderState` 가 **원값을 되찾은 뒤**\n * (`requested`·`fulfilled`·`lines`) 이 판정만 그대로 남아, 재기동하면 재고는 맞는데 **진행 중\n * 주문이 통째로 사라진** 화면이 나왔다. 오류는 나지 않는다 — 그래서 오래 남아 있었다.\n *\n * 두 스냅샷 생산 경로가 모두 원값을 담는다: 시뮬은 커널 `snapshot()`, 라이브(미러)는\n * `StateProjector` 다. 사람·자산·작업도 같은 스냅샷에 있고 커널이 심을 수 있는데 넘기지 않고 있었다.\n * 그래서 씨앗을 **관측 스냅샷 전체**로 넓힌다.\n *\n * 다만 넓히면서 **들어온 것이 없는 누락을 만들지 않는다.** 원값이 없는 오더는 커널이 걸러 내므로(지어내지\n * 않는다), 그 수를 여기서 세어 호출부가 말할 수 있게 한다. 세지 않으면 \"주문이 없다\" 와 \"주문을\n * 못 심었다\" 가 화면에서 같아진다.\n */\n\n/**\n * 커널에 심을 관측 상태 — 구조가 아니라 \"무엇이 어디에 얼마나\".\n *\n * 커널 `hydrateObserved` 가 받는 스냅샷 모양을 그대로 따른다. 여기서 골라 담지 않는 이유: 무엇을\n * 심을 수 있는지는 커널이 알고, 그 규칙(고아 작업 제외·이행 완료 오더 제외 등)도 커널에 있다.\n * 호스트가 미리 골라내면 규칙이 두 곳으로 갈린다.\n */\nexport interface ObservedSeed {\n locations: unknown[]\n items: unknown[]\n equipment: unknown[]\n persons?: unknown[]\n assets?: unknown[]\n tasks?: unknown[]\n orders?: unknown[]\n /** 확인해 둔 주목 신호 id — 상태에서 파생되지 않아 반드시 함께 넘긴다. */\n acked?: string[]\n /**\n * 주목 신호가 **처음 성립한 시각** — 확인과 같은 성질이다(계산으로 되살릴 수 없다).\n *\n * 넘기지 않으면 재기동 뒤 세 시간째 지속된 조건이 「0초째」로 되살아난다. 방금 찬 자리와 세 시간째\n * 막힌 자리는 할 일이 다르므로, 그 값이 거짓이면 화면이 사람을 잘못된 순서로 움직인다.\n */\n attentionSince?: { id: string; since: string }[]\n}\n\n/** 심은 것의 내역 — 로그와 테스트가 같은 숫자를 본다. */\nexport interface WarmStartCounts {\n itemCount: number\n equipmentCount: number\n personCount: number\n assetCount: number\n taskCount: number\n /** 원값(requested)이 있어 커널이 실제로 심을 수 있는 오더. */\n orderCount: number\n /** 이어받은 확인 처리 수 — 잃으면 확인해 둔 신호가 다시 빨개진다. */\n ackedCount: number\n /** 이어받은 시작 시각 수 — 잃으면 지속된 조건이 모두 「방금」이 된다. */\n attentionSinceCount: number\n /**\n * 원값이 없어 심지 못하는 오더 — **0 이 아니면 반드시 말한다.**\n * progress 만 있는 오더는 남은 수량을 알 수 없어 재계획의 근거가 되지 못한다. 지어내지 않고 뺀다.\n */\n ordersWithoutDemand: number\n /** Finished orders carried folded (ADR-0092) — 0 when the twin had folded nothing. */\n foldedOrderCount: number\n}\n\nexport type WarmStartPlan =\n | ({ action: 'hydrate'; seed: ObservedSeed } & WarmStartCounts)\n /** 벤치 트윈 — 새 시작에서 용량을 재는 게 목적이라 현재 상태를 주입하면 측정이 오염된다. */\n | { action: 'skip'; reason: 'bench' }\n /** 심을 상태가 없다 — 처음 만든 트윈이거나 저널·체크포인트가 비었다. 정상이다. */\n | { action: 'skip'; reason: 'no-state' }\n /** 커널이 관측 주입을 지원하지 않는다 — 구조만으로 시작하므로 보유량은 0 으로 읽힌다(알려야 한다). */\n | { action: 'skip'; reason: 'unsupported' }\n\n/** 스냅샷이 들고 올 수 있는 관측 축 — 어느 것이든 있으면 주입할 가치가 있다. */\ninterface ObservedState {\n locations?: unknown[]\n items?: unknown[]\n equipment?: unknown[]\n persons?: unknown[]\n assets?: unknown[]\n tasks?: unknown[]\n orders?: unknown[]\n foldedOrders?: unknown\n acked?: string[]\n attentionSince?: { id: string; since: string }[]\n}\n\nfunction foldedTotal(f: { counts: Record<string, Record<string, number>> }): number {\n let n = 0\n for (const byStatus of Object.values(f.counts)) for (const c of Object.values(byStatus ?? {})) n += Number(c) || 0\n return n\n}\n\n/** 관측 상태가 실려 있는지 — 축 하나라도 있으면 상태다(봉투에는 없다). */\nconst looksLikeState = (x: any): boolean =>\n !!x && typeof x === 'object' && ['locations', 'items', 'equipment', 'orders', 'tasks', 'persons', 'assets', 'acked'].some(k => k in x)\n\n/**\n * 봉투를 벗긴다 — **체크포인트가 겹포장돼 복원이 알리지 않고 죽던 자리.**\n *\n * `TwinEngine.snapshot()` 은 라이브에서는 상태를, 시뮬에서는 `runtime.resync()` 봉투(`{revision,state}`)를\n * 돌려준다. 그 봉투를 다시 `{revision, state}` 로 감싸 캐시에 넣었으므로, 꺼낸 `state` 는 축이 하나도\n * 없는 껍데기였다 → `planWarmStart` 가 \"심을 것이 없다\" 로 판정하고 **알리지 않고** 넘어갔다. 저널에 수천 건이\n * 있고 캐시도 최신인데 트윈이 빈 채로 떴다(오류도 로그도 없다).\n *\n * 반환 모양을 통일하는 것이 뿌리 수정이지만, 그 값은 State 구독 페이로드로도 나가므로 클라이언트 계약을\n * 건드린다. 그래서 **복원 경로에서만** 벗긴다. 겹겹이 싸여 있어도 벗긴다(과거에 저장된 값이 남아 있다).\n */\nexport function unwrapState(x: any, depth = 4): any {\n if (!x || typeof x !== 'object') return x\n if (looksLikeState(x)) return x\n if (depth > 0 && x.state && typeof x.state === 'object') return unwrapState(x.state, depth - 1)\n return x\n}\n\nconst arr = (v: unknown[] | undefined): unknown[] => (Array.isArray(v) ? v : [])\n\n/** 원값이 있는 오더인가 — 커널 `hydrateObserved` 의 판정(`requested !== undefined`)과 같은 기준. */\nconst hasDemand = (o: unknown): boolean =>\n !!o && typeof o === 'object' && (o as { requested?: unknown }).requested !== undefined\n\n/**\n * 무엇을 할지 정한다.\n *\n * 오더·작업·사람·자산도 함께 주입한다. 골라내는 규칙은 커널이 들고 있고(이행 완료 오더, 고아 작업),\n * 여기서는 **심을 수 없는 오더의 수만 센다** — 호출부가 그 사실을 말할 수 있어야 하기 때문이다.\n */\nexport function planWarmStart(state: ObservedState | null | undefined, purpose: string | undefined, canHydrate: boolean): WarmStartPlan {\n /* 벤치 판정이 먼저다 — 상태가 있든 없든 벤치에는 주입하지 않는다는 사실이 바뀌지 않는다. */\n if (purpose === 'bench') return { action: 'skip', reason: 'bench' }\n if (!state) return { action: 'skip', reason: 'no-state' }\n /* 봉투가 들어오면 스스로 벗는다 — 호출부가 한 곳이라도 잊으면 그 경로가 알리지 않고 죽는다(실제로 그랬다). */\n state = unwrapState(state)\n\n const locations = arr(state.locations)\n const items = arr(state.items)\n const equipment = arr(state.equipment)\n const persons = arr(state.persons)\n const assets = arr(state.assets)\n const tasks = arr(state.tasks)\n const orders = arr(state.orders)\n /* 확인 처리는 문자열 id 목록이다 — 배열이 아니면 없는 것으로 본다(지어내지 않는다). */\n const acked = Array.isArray(state.acked) ? state.acked.filter((x): x is string => typeof x === 'string') : []\n /* 시작 시각은 {id, since} 쌍 목록이다 — 모양이 아니면 없는 것으로 본다(지어낸 시각을 심으면 화면이\n 「세 시간째」라고 거짓을 말한다. 모르면 그 신호는 이어붙는 순간이 새 시작이다). */\n const attentionSince = Array.isArray(state.attentionSince)\n ? state.attentionSince.filter((x: any): x is { id: string; since: string } => !!x && typeof x.id === 'string' && typeof x.since === 'string')\n : []\n\n /*\n * Folded orders (ADR-0092) — their counts are the only record of the finished orders taken out of the hot\n * list; nothing re-sends them. The seed used to leave them out, so a restart lost every folded count.\n * Carried as the kernel wrote them; a value without a `counts` object is not a folded side.\n */\n const f: any = (state as any).foldedOrders\n const foldedOrders = f && typeof f === 'object' && f.counts && typeof f.counts === 'object' ? f : undefined\n\n /* 어느 축도 없으면 심을 것이 없다 — 빈 주입으로 로그만 남기지 않는다. */\n if (!locations.length && !items.length && !equipment.length && !persons.length && !assets.length && !tasks.length && !orders.length && !foldedOrders && !acked.length && !attentionSince.length) {\n return { action: 'skip', reason: 'no-state' }\n }\n\n /* 지원 여부는 마지막에 본다 — 주입할 것이 있는데 주입하지 못하는 상황이라야 경고할 가치가 있다. */\n if (!canHydrate) return { action: 'skip', reason: 'unsupported' }\n\n const seedable = orders.filter(hasDemand)\n\n return {\n action: 'hydrate',\n seed: {\n locations,\n items,\n equipment,\n ...(persons.length ? { persons } : {}),\n ...(assets.length ? { assets } : {}),\n ...(tasks.length ? { tasks } : {}),\n /* 원값 없는 오더는 넘기지 않는다 — 커널도 걸러 내지만, 넘기지 않으면 세어 둔 수와 어긋날 일이 없다. */\n ...(seedable.length ? { orders: seedable } : {}),\n ...(foldedOrders ? { foldedOrders } : {}),\n /* 확인 처리는 조건이 사라진 id 가 섞여 있어도 해롭지 않다 — 그 신호가 없으면 표시할 대상이 없다. */\n ...(acked.length ? { acked } : {}),\n /* 조건이 사라진 id 가 섞여 있어도 해롭지 않다 — 커널이 그 신호가 없으면 함께 지운다. */\n ...(attentionSince.length ? { attentionSince } : {})\n },\n itemCount: items.length,\n equipmentCount: equipment.length,\n personCount: persons.length,\n assetCount: assets.length,\n taskCount: tasks.length,\n orderCount: seedable.length,\n foldedOrderCount: foldedOrders ? foldedTotal(foldedOrders) : 0,\n ordersWithoutDemand: orders.length - seedable.length,\n ackedCount: acked.length,\n attentionSinceCount: attentionSince.length\n }\n}\n\n/*\n * ── 미러(관측 구동) 재기동의 연속성 ─────────────────────────────────────────\n *\n * 미러의 진실은 원천이다. 그래서 재고·위치·설비 상태는 심지 않는다 — 다음 계측·다음 마스터 재읽기가\n * 곧 정정하므로, 심으면 **떠난 물건이 되살아나는** 위험만 남는다(그래서 `startLive` 는 오랫동안 아무것도\n * 심지 않았다).\n *\n * 그런데 원천이 **애초에 다시 말해 주지 않는 축**이 있다. 우리가 계산해 만든 것들이다:\n * · 확인(ack)·조건이 처음 성립한 시각 — 사람이 한 일과 우리가 기억한 시각\n * · 에너지의 열린 구간 누적·적산 기준점·관측 이후 최대 — SCADA 는 「이번 15분에 지금까지 얼마」를 모른다\n *\n * 잃으면 **오류 없이 값이 작아진다**(실측 2026-08-18: 재기동 뒤 그 구간의 전력량·피크가 작아지고, 세\n * 시간째 지속된 경보가 「0초째」가 됐다). 그래서 축을 갈라 되풀어 주지 않는 것만 이어받는다.\n *\n * 무엇을 이어받을지는 여기서 고르고, **어떻게 흡수할지는 커널이 정한다**(`hydrateContinuity`) — 도메인\n * 규칙이 호스트로 새면 트윈마다 다른 답이 된다.\n */\nexport interface ContinuitySeed {\n acked?: string[]\n attentionSince?: { id: string; since: string }[]\n energy?: unknown\n}\n\nexport interface ContinuityPlan {\n seed: ContinuitySeed\n /** 무엇을 이어받았나 — 로그와 시험이 같은 숫자를 본다(0 이면 이어받을 것이 없었다는 사실이다). */\n ackedCount: number\n attentionSinceCount: number\n /** 에너지 연속성을 이어받았나 — 열린 구간이 있으면 그 구간의 요금 근거가 살아난다. */\n energy: boolean\n}\n\n/** 미러가 이어받을 축만 골라낸다 — 관측 축(재고·위치·설비)은 일부러 넣지 않는다. */\nexport function planLiveContinuity(raw: unknown): ContinuityPlan {\n const state = raw ? (unwrapState(raw as any) as any) : undefined\n const acked = Array.isArray(state?.acked) ? state.acked.filter((x: any): x is string => typeof x === 'string') : []\n const attentionSince = Array.isArray(state?.attentionSince)\n ? state.attentionSince.filter((x: any): x is { id: string; since: string } => !!x && typeof x.id === 'string' && typeof x.since === 'string')\n : []\n /* 에너지는 커널이 자기 모양을 안다 — 호스트는 「있나」만 본다(골라 담으면 규칙이 두 곳으로 갈린다). */\n const energy = state?.energy && typeof state.energy === 'object' ? state.energy : undefined\n return {\n seed: {\n ...(acked.length ? { acked } : {}),\n ...(attentionSince.length ? { attentionSince } : {}),\n ...(energy ? { energy } : {})\n },\n ackedCount: acked.length,\n attentionSinceCount: attentionSince.length,\n energy: !!energy\n }\n}\n"]}
1
+ {"version":3,"file":"warm-start.js","sourceRoot":"","sources":["../../server/engine/warm-start.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * 웜스타트 판정 — **순수**. \"기동하는 커널에 직전 상태를 주입할 것인가, 주입한다면 무엇을\" 만 정한다.\n * 실제 주입(hydrateObserved 호출)과 로그는 엔진이 한다.\n *\n * ── 왜 떼어냈나 ─────────────────────────────────────────────────────────────\n * 이 판정이 틀리면 증상이 정반대 두 방향으로 나온다: 주입해야 할 때 안 주입하면 **있는 재고가 0 으로**\n * 보이고(2026-07-31 hatiolab-wms: 저널에 입고 540·출고 94 인데 재고 화면이 비어 있었다), 심지\n * 말아야 할 벤치에 주입하면 **용량 측정이 오염된다.** 둘 다 알리지 않고 틀리는 종류라 규칙을 고정한다.\n *\n * ── 오더를 버리던 전제가 낡았다 (2026-08-05) ────────────────────────────────\n * 예전 주석은 \"스냅샷의 오더에는 progress 밖에 없어서 역산하면 없는 숫자를 지어내는 셈이므로\n * 넘기지 않는다\" 였다. 그때는 맞았다. 그러나 커널 `OrderState` 가 **원값을 되찾은 뒤**\n * (`requested`·`fulfilled`·`lines`) 이 판정만 그대로 남아, 재기동하면 재고는 맞는데 **진행 중\n * 주문이 통째로 사라진** 화면이 나왔다. 오류는 나지 않는다 — 그래서 오래 남아 있었다.\n *\n * 두 스냅샷 생산 경로가 모두 원값을 담는다: 시뮬은 커널 `snapshot()`, 라이브(미러)는\n * `StateProjector` 다. 사람·자산·작업도 같은 스냅샷에 있고 커널이 심을 수 있는데 넘기지 않고 있었다.\n * 그래서 씨앗을 **관측 스냅샷 전체**로 넓힌다.\n *\n * 다만 넓히면서 **들어온 것이 없는 누락을 만들지 않는다.** 원값이 없는 오더는 커널이 걸러 내므로(지어내지\n * 않는다), 그 수를 여기서 세어 호출부가 말할 수 있게 한다. 세지 않으면 \"주문이 없다\" 와 \"주문을\n * 못 심었다\" 가 화면에서 같아진다.\n */\n\n/**\n * 커널에 심을 관측 상태 — 구조가 아니라 \"무엇이 어디에 얼마나\".\n *\n * 커널 `hydrateObserved` 가 받는 스냅샷 모양을 그대로 따른다. 여기서 골라 담지 않는 이유: 무엇을\n * 심을 수 있는지는 커널이 알고, 그 규칙(고아 작업 제외·이행 완료 오더 제외 등)도 커널에 있다.\n * 호스트가 미리 골라내면 규칙이 두 곳으로 갈린다.\n */\nexport interface ObservedSeed {\n locations: unknown[]\n items: unknown[]\n equipment: unknown[]\n persons?: unknown[]\n assets?: unknown[]\n tasks?: unknown[]\n orders?: unknown[]\n /** 확인해 둔 주목 신호 id — 상태에서 파생되지 않아 반드시 함께 넘긴다. */\n acked?: string[]\n /**\n * 주목 신호가 **처음 성립한 시각** — 확인과 같은 성질이다(계산으로 되살릴 수 없다).\n *\n * 넘기지 않으면 재기동 뒤 세 시간째 지속된 조건이 「0초째」로 되살아난다. 방금 찬 자리와 세 시간째\n * 막힌 자리는 할 일이 다르므로, 그 값이 거짓이면 화면이 사람을 잘못된 순서로 움직인다.\n */\n attentionSince?: { id: string; since: string }[]\n}\n\n/** 심은 것의 내역 — 로그와 테스트가 같은 숫자를 본다. */\nexport interface WarmStartCounts {\n itemCount: number\n equipmentCount: number\n personCount: number\n assetCount: number\n taskCount: number\n /** 원값(requested)이 있어 커널이 실제로 심을 수 있는 오더. */\n orderCount: number\n /** 이어받은 확인 처리 수 — 잃으면 확인해 둔 신호가 다시 빨개진다. */\n ackedCount: number\n /** 이어받은 시작 시각 수 — 잃으면 지속된 조건이 모두 「방금」이 된다. */\n attentionSinceCount: number\n /**\n * 원값이 없어 심지 못하는 오더 — **0 이 아니면 반드시 말한다.**\n * progress 만 있는 오더는 남은 수량을 알 수 없어 재계획의 근거가 되지 못한다. 지어내지 않고 뺀다.\n */\n ordersWithoutDemand: number\n /** Finished orders carried folded (ADR-0092) — 0 when the twin had folded nothing. */\n foldedOrderCount: number\n}\n\nexport type WarmStartPlan =\n | ({ action: 'hydrate'; seed: ObservedSeed } & WarmStartCounts)\n /** 벤치 트윈 — 새 시작에서 용량을 재는 게 목적이라 현재 상태를 주입하면 측정이 오염된다. */\n | { action: 'skip'; reason: 'bench' }\n /** 심을 상태가 없다 — 처음 만든 트윈이거나 저널·체크포인트가 비었다. 정상이다. */\n | { action: 'skip'; reason: 'no-state' }\n /** 커널이 관측 주입을 지원하지 않는다 — 구조만으로 시작하므로 보유량은 0 으로 읽힌다(알려야 한다). */\n | { action: 'skip'; reason: 'unsupported' }\n\n/** 스냅샷이 들고 올 수 있는 관측 축 — 어느 것이든 있으면 주입할 가치가 있다. */\ninterface ObservedState {\n locations?: unknown[]\n items?: unknown[]\n equipment?: unknown[]\n persons?: unknown[]\n assets?: unknown[]\n tasks?: unknown[]\n orders?: unknown[]\n foldedOrders?: unknown\n acked?: string[]\n attentionSince?: { id: string; since: string }[]\n}\n\nfunction foldedTotal(f: { counts: Record<string, Record<string, number>> }): number {\n let n = 0\n for (const byStatus of Object.values(f.counts)) for (const c of Object.values(byStatus ?? {})) n += Number(c) || 0\n return n\n}\n\n/** 관측 상태가 실려 있는지 — 축 하나라도 있으면 상태다(봉투에는 없다). */\nconst looksLikeState = (x: any): boolean =>\n !!x && typeof x === 'object' && ['locations', 'items', 'equipment', 'orders', 'tasks', 'persons', 'assets', 'acked'].some(k => k in x)\n\n/**\n * 봉투를 벗긴다 — **체크포인트가 겹포장돼 복원이 알리지 않고 죽던 자리.**\n *\n * `TwinEngine.snapshot()` 은 라이브에서는 상태를, 시뮬에서는 `runtime.resync()` 봉투(`{revision,state}`)를\n * 돌려준다. 그 봉투를 다시 `{revision, state}` 로 감싸 캐시에 넣었으므로, 꺼낸 `state` 는 축이 하나도\n * 없는 껍데기였다 → `planWarmStart` 가 \"심을 것이 없다\" 로 판정하고 **알리지 않고** 넘어갔다. 저널에 수천 건이\n * 있고 캐시도 최신인데 트윈이 빈 채로 떴다(오류도 로그도 없다).\n *\n * 반환 모양을 통일하는 것이 뿌리 수정이지만, 그 값은 State 구독 페이로드로도 나가므로 클라이언트 계약을\n * 건드린다. 그래서 **복원 경로에서만** 벗긴다. 겹겹이 싸여 있어도 벗긴다(과거에 저장된 값이 남아 있다).\n */\nexport function unwrapState(x: any, depth = 4): any {\n if (!x || typeof x !== 'object') return x\n if (looksLikeState(x)) return x\n if (depth > 0 && x.state && typeof x.state === 'object') return unwrapState(x.state, depth - 1)\n return x\n}\n\nconst arr = (v: unknown[] | undefined): unknown[] => (Array.isArray(v) ? v : [])\n\n/** 원값이 있는 오더인가 — 커널 `hydrateObserved` 의 판정(`requested !== undefined`)과 같은 기준. */\nconst hasDemand = (o: unknown): boolean =>\n !!o && typeof o === 'object' && (o as { requested?: unknown }).requested !== undefined\n\n/**\n * 무엇을 할지 정한다.\n *\n * 오더·작업·사람·자산도 함께 주입한다. 골라내는 규칙은 커널이 들고 있고(이행 완료 오더, 고아 작업),\n * 여기서는 **심을 수 없는 오더의 수만 센다** — 호출부가 그 사실을 말할 수 있어야 하기 때문이다.\n */\nexport function planWarmStart(state: ObservedState | null | undefined, purpose: string | undefined, canHydrate: boolean): WarmStartPlan {\n /* 벤치 판정이 먼저다 — 상태가 있든 없든 벤치에는 주입하지 않는다는 사실이 바뀌지 않는다. */\n if (purpose === 'bench') return { action: 'skip', reason: 'bench' }\n if (!state) return { action: 'skip', reason: 'no-state' }\n /* 봉투가 들어오면 스스로 벗는다 — 호출부가 한 곳이라도 잊으면 그 경로가 알리지 않고 죽는다(실제로 그랬다). */\n state = unwrapState(state)\n\n const locations = arr(state.locations)\n const items = arr(state.items)\n const equipment = arr(state.equipment)\n const persons = arr(state.persons)\n const assets = arr(state.assets)\n const tasks = arr(state.tasks)\n const orders = arr(state.orders)\n /* 확인 처리는 문자열 id 목록이다 — 배열이 아니면 없는 것으로 본다(지어내지 않는다). */\n const acked = Array.isArray(state.acked) ? state.acked.filter((x): x is string => typeof x === 'string') : []\n /* 시작 시각은 {id, since} 쌍 목록이다 — 모양이 아니면 없는 것으로 본다(지어낸 시각을 심으면 화면이\n 「세 시간째」라고 거짓을 말한다. 모르면 그 신호는 이어붙는 순간이 새 시작이다). */\n const attentionSince = Array.isArray(state.attentionSince)\n ? state.attentionSince.filter((x: any): x is { id: string; since: string } => !!x && typeof x.id === 'string' && typeof x.since === 'string')\n : []\n\n /*\n * Folded orders (ADR-0092) — their counts are the only record of the finished orders taken out of the hot\n * list; nothing re-sends them. The seed used to leave them out, so a restart lost every folded count.\n * Carried as the kernel wrote them; a value without a `counts` object is not a folded side.\n */\n const f: any = (state as any).foldedOrders\n const foldedOrders = f && typeof f === 'object' && f.counts && typeof f.counts === 'object' ? f : undefined\n\n /* 어느 축도 없으면 심을 것이 없다 — 빈 주입으로 로그만 남기지 않는다. */\n if (!locations.length && !items.length && !equipment.length && !persons.length && !assets.length && !tasks.length && !orders.length && !foldedOrders && !acked.length && !attentionSince.length) {\n return { action: 'skip', reason: 'no-state' }\n }\n\n /* 지원 여부는 마지막에 본다 — 주입할 것이 있는데 주입하지 못하는 상황이라야 경고할 가치가 있다. */\n if (!canHydrate) return { action: 'skip', reason: 'unsupported' }\n\n const seedable = orders.filter(hasDemand)\n\n return {\n action: 'hydrate',\n seed: {\n locations,\n items,\n equipment,\n ...(persons.length ? { persons } : {}),\n ...(assets.length ? { assets } : {}),\n ...(tasks.length ? { tasks } : {}),\n /* 원값 없는 오더는 넘기지 않는다 — 커널도 걸러 내지만, 넘기지 않으면 세어 둔 수와 어긋날 일이 없다. */\n ...(seedable.length ? { orders: seedable } : {}),\n ...(foldedOrders ? { foldedOrders } : {}),\n /* 확인 처리는 조건이 사라진 id 가 섞여 있어도 해롭지 않다 — 그 신호가 없으면 표시할 대상이 없다. */\n ...(acked.length ? { acked } : {}),\n /* 조건이 사라진 id 가 섞여 있어도 해롭지 않다 — 커널이 그 신호가 없으면 함께 지운다. */\n ...(attentionSince.length ? { attentionSince } : {})\n },\n itemCount: items.length,\n equipmentCount: equipment.length,\n personCount: persons.length,\n assetCount: assets.length,\n taskCount: tasks.length,\n orderCount: seedable.length,\n foldedOrderCount: foldedOrders ? foldedTotal(foldedOrders) : 0,\n ordersWithoutDemand: orders.length - seedable.length,\n ackedCount: acked.length,\n attentionSinceCount: attentionSince.length\n }\n}\n\n/*\n * ── 미러(관측 구동) 재기동의 연속성 ─────────────────────────────────────────\n *\n * 미러의 진실은 원천이다. 그래서 재고·위치·설비 상태는 심지 않는다 — 다음 계측·다음 마스터 재읽기가\n * 곧 정정하므로, 심으면 **떠난 물건이 되살아나는** 위험만 남는다(그래서 `startLive` 는 오랫동안 아무것도\n * 심지 않았다).\n *\n * 그런데 원천이 **애초에 다시 말해 주지 않는 축**이 있다. 우리가 계산해 만든 것들이다:\n * · 확인(ack)·조건이 처음 성립한 시각 — 사람이 한 일과 우리가 기억한 시각\n * · 에너지의 열린 구간 누적·적산 기준점·관측 이후 최대 — SCADA 는 「이번 15분에 지금까지 얼마」를 모른다\n *\n * 잃으면 **오류 없이 값이 작아진다**(실측 2026-08-18: 재기동 뒤 그 구간의 전력량·피크가 작아지고, 세\n * 시간째 지속된 경보가 「0초째」가 됐다). 그래서 축을 갈라 되풀어 주지 않는 것만 이어받는다.\n *\n * 무엇을 이어받을지는 여기서 고르고, **어떻게 흡수할지는 커널이 정한다**(`hydrateContinuity`) — 도메인\n * 규칙이 호스트로 새면 트윈마다 다른 답이 된다.\n */\nexport interface ContinuitySeed {\n acked?: string[]\n attentionSince?: { id: string; since: string }[]\n energy?: unknown\n /** 접힌 오더(ADR-0092) — 모양은 커널이 안다(호스트는 있나만 본다). */\n foldedOrders?: unknown\n}\n\nexport interface ContinuityPlan {\n seed: ContinuitySeed\n /** 무엇을 이어받았나 — 로그와 시험이 같은 숫자를 본다(0 이면 이어받을 것이 없었다는 사실이다). */\n ackedCount: number\n attentionSinceCount: number\n /** 에너지 연속성을 이어받았나 — 열린 구간이 있으면 그 구간의 요금 근거가 살아난다. */\n energy: boolean\n /** 접힌 오더를 이어받았나(ADR-0092). */\n foldedOrders: boolean\n}\n\n/** 미러가 이어받을 축만 골라낸다 — 관측 축(재고·위치·설비)은 일부러 넣지 않는다. */\nexport function planLiveContinuity(raw: unknown): ContinuityPlan {\n const state = raw ? (unwrapState(raw as any) as any) : undefined\n const acked = Array.isArray(state?.acked) ? state.acked.filter((x: any): x is string => typeof x === 'string') : []\n const attentionSince = Array.isArray(state?.attentionSince)\n ? state.attentionSince.filter((x: any): x is { id: string; since: string } => !!x && typeof x.id === 'string' && typeof x.since === 'string')\n : []\n /* 에너지는 커널이 자기 모양을 안다 — 호스트는 「있나」만 본다(골라 담으면 규칙이 두 곳으로 갈린다). */\n const energy = state?.energy && typeof state.energy === 'object' ? state.energy : undefined\n /* 접힌 오더(ADR-0092) — 원천을 다시 읽는 미러도 세는 수와 경계를 잃지 않게(없으면 옛 종결이 뜨거운 줄로 되살아난다). */\n const foldedOrders = state?.foldedOrders && typeof state.foldedOrders === 'object' ? state.foldedOrders : undefined\n return {\n seed: {\n ...(acked.length ? { acked } : {}),\n ...(attentionSince.length ? { attentionSince } : {}),\n ...(energy ? { energy } : {}),\n ...(foldedOrders ? { foldedOrders } : {})\n },\n ackedCount: acked.length,\n attentionSinceCount: attentionSince.length,\n energy: !!energy,\n foldedOrders: !!foldedOrders\n }\n}\n"]}
@@ -1,3 +1,4 @@
1
+ import { type OrderFoldPolicy } from '@operato/ops-contract';
1
2
  export interface EntityDelta {
2
3
  tag: string;
3
4
  data: any;
@@ -46,4 +47,6 @@ export interface EntityDeltaScope {
46
47
  /** 개별 payload 를 만들 물품 태그. 없으면 전부. */
47
48
  items?: ReadonlySet<string>;
48
49
  }
49
- export declare function buildEntityDeltas(state: any, instanceId: string, scope?: EntityDeltaScope): EntityDelta[];
50
+ export declare function buildEntityDeltas(state: any, instanceId: string, scope?: EntityDeltaScope,
51
+ /** 끝난 오더를 접는 경계(모델의 `orderFold`) — 「최근 T 완료」의 T 다. 없으면 커널의 기본. */
52
+ orderFold?: OrderFoldPolicy): EntityDelta[];
@@ -1,34 +1,9 @@
1
1
  "use strict";
2
- /*
3
- * 트윈 State 스냅샷 → 라이브 바인딩 delta 목록 (순수 매핑 — pubsub/도메인 무관 → 단위 테스트 가능).
4
- * publishEntityData 가 이 목록을 엔티티별 시그니처로 dedup 후 data(tag) 채널로 발행한다.
5
- *
6
- * ── 규율: 레퍼런스가 아는 것은 **전부** 내보낸다 ────────────────────────────
7
- * 트윈에서 사소한 속성은 없다. 소속 구역·소속 팔레트·수량·만료처럼 곁가지로 보이는 값이 실제로는
8
- * 구역 롤업·적재 표현·로트 추적의 전제다. 그래서 여기서 **필드를 손으로 골라 담지 않는다.**
9
- *
10
- * 예전에는 엔티티마다 필요할 것 같은 필드만 열거했고, 그 결과 계약에 있는 것이 오류 없이 빠졌다
11
- * (2026-08-01 감사: 노드 parentId 누락 · 작업 전량 누락 · 물품 전량 누락). 화이트리스트 방식은
12
- * **계약이 자라도 화면이 따라오지 못한다** — 커널이 속성을 늘릴 때마다 이 파일을 고쳐야 하고, 고치는
13
- * 것을 잊으면 아무 신호 없이 정보가 사라진다.
14
- *
15
- * 그래서 반대로 한다: **상태 객체가 가진 것을 그대로 통과**시키고, 통과시키지 **않을** 것만 이유와 함께
16
- * 열거한다(EXCLUDED). 새 속성은 자동으로 화면에 도달하고, 빼려면 이유를 적어야 한다.
17
- *
18
- * ── 표식은 도메인 낱말을 훔치지 않는다 ─────────────────────────────────────
19
- * 예전 payload 는 엔티티 종류 표식을 `kind` 로 실었다. 그런데 `kind` 는 **도메인의 낱말**이다 —
20
- * 무버 kind=cutter, 작업 kind=putaway, 오더 kind=workorder. 표식이 그 이름을 차지하면 도메인 속성을
21
- * 실을 자리가 없어진다(같은 병을 오늘 ox-timeline 의 `group` 에서도 고쳤다).
22
- * 그래서 표식은 `entity`(우리 이름)로 옮기고 `kind` 는 도메인에 돌려준다.
23
- *
24
- * payload 계약(씬 컴포넌트가 소비): `entity`(표식) + 그 엔티티의 모든 속성(도메인 `kind` 포함).
25
- * entity = location · equipment · person · asset · order · task · item,
26
- * 그리고 집약 태그(__orders__ · __tasks__ · __items__ · __persons__ · __assets__).
27
- */
28
2
  Object.defineProperty(exports, "__esModule", { value: true });
29
3
  exports.ITEMS_AGGREGATE_LIMIT = exports.energyTag = exports.assetsTag = exports.personsTag = exports.itemsTag = exports.tasksTag = exports.ordersTag = exports.ENERGY_TAG_PREFIX = exports.ASSETS_TAG_PREFIX = exports.PERSONS_TAG_PREFIX = exports.ITEMS_TAG_PREFIX = exports.TASKS_TAG_PREFIX = exports.ORDERS_TAG_PREFIX = void 0;
30
4
  exports.aggregateTag = aggregateTag;
31
5
  exports.buildEntityDeltas = buildEntityDeltas;
6
+ const ops_contract_1 = require("@operato/ops-contract");
32
7
  /** 집약 고정 태그 — 클라 provider 와 일치해야 한다(런타임 생성 엔티티는 개별 배치가 불가하므로 집약 뷰). */
33
8
  /*
34
9
  * 집약 채널의 접두사 — **실제 태그는 트윈 식별자를 붙여 만든다.**
@@ -176,7 +151,9 @@ function energyByLocation(state) {
176
151
  }
177
152
  return out;
178
153
  }
179
- function buildEntityDeltas(state, instanceId, scope) {
154
+ function buildEntityDeltas(state, instanceId, scope,
155
+ /** 끝난 오더를 접는 경계(모델의 `orderFold`) — 「최근 T 완료」의 T 다. 없으면 커널의 기본. */
156
+ orderFold) {
180
157
  const out = [];
181
158
  const locEnergy = energyByLocation(state);
182
159
  for (const n of state?.locations ?? []) {
@@ -220,7 +197,24 @@ function buildEntityDeltas(state, instanceId, scope) {
220
197
  * 런타임에 생기고 사라지는 엔티티(오더·작업·물품)는 보드에 개별 배치할 수 없다 → 한 컴포넌트가
221
198
  * 전체를 추적하는 집약 태그를 항상 방출한다. */
222
199
  const orders = state?.orders ?? [];
223
- out.push({ tag: (0, exports.ordersTag)(instanceId), data: { entity: 'orders', orders: orders.map((o) => aggregateItem(o, 'id')) } });
200
+ /*
201
+ * **「최근 T 안에 끝난」 수와 접힌 수를 함께 싣는다**(ADR-0092 결정 2 보탬 · 3). 칩은 뜨거운 줄로 세면 건수 경계(N)에서
202
+ * 멈추므로, 접힌 쪽의 시간 칸까지 더한 수를 계약의 한 함수(`finishedFlowOrdersWithin`, KPI 와 같은 판정)로 낸다. 「지금」은
203
+ * 그 구동의 지금(`nowTime` — 시뮬은 시계, 미러는 들은 시각의 최대)이다. 모르면 싣지 않는다 — 0 으로 메우지 않는다.
204
+ */
205
+ const nowMs = Date.parse(String(state?.nowTime ?? ''));
206
+ const policy = orderFold ?? ops_contract_1.ORDER_FOLD_DEFAULT;
207
+ const finished = Number.isFinite(nowMs) ? (0, ops_contract_1.finishedFlowOrdersWithin)(orders, state?.foldedOrders, nowMs, policy) : undefined;
208
+ const folded = (0, ops_contract_1.foldedOrderCount)(state?.foldedOrders);
209
+ out.push({
210
+ tag: (0, exports.ordersTag)(instanceId),
211
+ data: {
212
+ entity: 'orders',
213
+ orders: orders.map((o) => aggregateItem(o, 'id')),
214
+ ...(finished ? { finished: { ...finished, windowMs: policy.afterMs } } : {}),
215
+ ...(folded ? { folded } : {})
216
+ }
217
+ });
224
218
  const tasks = state?.tasks ?? [];
225
219
  out.push({ tag: (0, exports.tasksTag)(instanceId), data: { entity: 'tasks', tasks: tasks.map((t) => aggregateItem(t, 'id')) } });
226
220
  /* 사람 집약 — 인원은 보드에 개별 배치할 수도 있지만(설비처럼), 전체를 한눈에 보는 소비처도 있다.
@@ -1 +1 @@
1
- {"version":3,"file":"entity-delta.js","sourceRoot":"","sources":["../shared/entity-delta.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * 트윈 State 스냅샷 → 라이브 바인딩 delta 목록 (순수 매핑 — pubsub/도메인 무관 → 단위 테스트 가능).\n * publishEntityData 가 이 목록을 엔티티별 시그니처로 dedup 후 data(tag) 채널로 발행한다.\n *\n * ── 규율: 레퍼런스가 아는 것은 **전부** 내보낸다 ────────────────────────────\n * 트윈에서 사소한 속성은 없다. 소속 구역·소속 팔레트·수량·만료처럼 곁가지로 보이는 값이 실제로는\n * 구역 롤업·적재 표현·로트 추적의 전제다. 그래서 여기서 **필드를 손으로 골라 담지 않는다.**\n *\n * 예전에는 엔티티마다 필요할 것 같은 필드만 열거했고, 그 결과 계약에 있는 것이 오류 없이 빠졌다\n * (2026-08-01 감사: 노드 parentId 누락 · 작업 전량 누락 · 물품 전량 누락). 화이트리스트 방식은\n * **계약이 자라도 화면이 따라오지 못한다** — 커널이 속성을 늘릴 때마다 이 파일을 고쳐야 하고, 고치는\n * 것을 잊으면 아무 신호 없이 정보가 사라진다.\n *\n * 그래서 반대로 한다: **상태 객체가 가진 것을 그대로 통과**시키고, 통과시키지 **않을** 것만 이유와 함께\n * 열거한다(EXCLUDED). 새 속성은 자동으로 화면에 도달하고, 빼려면 이유를 적어야 한다.\n *\n * ── 표식은 도메인 낱말을 훔치지 않는다 ─────────────────────────────────────\n * 예전 payload 는 엔티티 종류 표식을 `kind` 로 실었다. 그런데 `kind` 는 **도메인의 낱말**이다 —\n * 무버 kind=cutter, 작업 kind=putaway, 오더 kind=workorder. 표식이 그 이름을 차지하면 도메인 속성을\n * 실을 자리가 없어진다(같은 병을 오늘 ox-timeline 의 `group` 에서도 고쳤다).\n * 그래서 표식은 `entity`(우리 이름)로 옮기고 `kind` 는 도메인에 돌려준다.\n *\n * payload 계약(씬 컴포넌트가 소비): `entity`(표식) + 그 엔티티의 모든 속성(도메인 `kind` 포함).\n * entity = location · equipment · person · asset · order · task · item,\n * 그리고 집약 태그(__orders__ · __tasks__ · __items__ · __persons__ · __assets__).\n */\n\nexport interface EntityDelta {\n tag: string\n data: any\n}\n\n/** 집약 고정 태그 — 클라 provider 와 일치해야 한다(런타임 생성 엔티티는 개별 배치가 불가하므로 집약 뷰). */\n/*\n * 집약 채널의 접두사 — **실제 태그는 트윈 식별자를 붙여 만든다.**\n *\n * 예전에는 이 상수가 곧 태그였다. 그런데 발행 키는 (도메인, 태그)이고, **한 공간에 트윈이 여럿**\n * 있으면(실측: 3곳 × 3개) 셋이 같은 태그로 발행해 서로 덮었다. 보드의 주문 목록판이 세 공장의 주문\n * 중 마지막에 도착한 것을 보여 준다 — 오류 없이.\n *\n * 구간 성과 태그가 이미 `__kpi__:<트윈>` 으로 트윈을 붙이고 있었다(`kpi-broadcast.ts`). 같은 규약으로 맞춘다.\n */\nexport const ORDERS_TAG_PREFIX = '__orders__'\nexport const TASKS_TAG_PREFIX = '__tasks__'\nexport const ITEMS_TAG_PREFIX = '__items__'\nexport const PERSONS_TAG_PREFIX = '__persons__'\nexport const ASSETS_TAG_PREFIX = '__assets__'\n/** 에너지 집약 — 수요 구간·피크·계약은 **트윈 하나의 사실**이라 개체 태그가 없다. */\nexport const ENERGY_TAG_PREFIX = '__energy__'\n\n/** 이 트윈의 집약 채널 태그. 트윈 식별자가 없으면 만들지 않는다 — 덮어쓰기의 원인이었다. */\nexport function aggregateTag(prefix: string, instanceId: string): string {\n if (!instanceId) throw new Error('aggregateTag: instanceId required — tags without instanceId overwrite other twin states across tenants')\n\n return `${prefix}:${instanceId}`\n}\n\nexport const ordersTag = (instanceId: string) => aggregateTag(ORDERS_TAG_PREFIX, instanceId)\nexport const tasksTag = (instanceId: string) => aggregateTag(TASKS_TAG_PREFIX, instanceId)\nexport const itemsTag = (instanceId: string) => aggregateTag(ITEMS_TAG_PREFIX, instanceId)\nexport const personsTag = (instanceId: string) => aggregateTag(PERSONS_TAG_PREFIX, instanceId)\nexport const assetsTag = (instanceId: string) => aggregateTag(ASSETS_TAG_PREFIX, instanceId)\nexport const energyTag = (instanceId: string) => aggregateTag(ENERGY_TAG_PREFIX, instanceId)\n\n/**\n * 통과시키지 않는 필드 — **빼는 데는 이유가 필요하다.**\n *\n * · `id`/`epc` — 태그로 이미 나가므로 개별 payload 에 중복해 싣지 않는다(집약 뷰에는 넣는다).\n * · `oee` — 원시 누적(idleMs 등)이 매 delta 증가해 \"무변화 생략\" 을 무력화한다. shapeOee 로 정형화해 얹는다.\n */\nconst EXCLUDED = new Set(['id', 'epc', 'oee'])\n\n/**\n * OEE 원시 비율/카운트 → 표시용 정수 퍼센트 + 카운트.\n * 원시 누적 ms(idleMs 등)는 매 delta 증가해 무변화-생략을 무력화하므로 제외(유휴 자원의 매초 재브로드캐스팅 억제).\n * oee 미측정이면 undefined.\n */\nfunction shapeOee(o: any): any {\n if (!o) return undefined\n return {\n overall: Math.round((o.overall ?? 0) * 100),\n availability: Math.round((o.availability ?? 0) * 100),\n performance: Math.round((o.performance ?? 0) * 100),\n quality: Math.round((o.quality ?? 0) * 100),\n good: o.goodCount ?? 0,\n scrap: o.nonconformingCount ?? 0\n }\n}\n\n/**\n * 엔티티 하나의 payload — `entity` 표식 + 나머지 전부(도메인 `kind` 포함).\n *\n * `undefined` 값은 싣지 않는다(키의 유무가 시그니처를 흔들지 않게). 값이 없는 것과 키가 없는 것은\n * 소비처에서 같으므로 안전하고, 무변화 생략이 잘 듣는다.\n */\nfunction payloadOf(entityKind: string, entity: any): any {\n /* 표식은 `entity` — 도메인의 `kind` 를 덮지 않는다. */\n const data: any = { entity: entityKind }\n for (const [k, v] of Object.entries(entity ?? {})) {\n if (EXCLUDED.has(k) || v === undefined) continue\n data[k] = v\n }\n return data\n}\n\n/**\n * 집약 뷰의 항목 — 개별 payload 와 같은 내용 + 식별자.\n * 집약은 \"전체를 한 컴포넌트가 추적\" 하는 용도라 식별자가 있어야 한다.\n */\nfunction aggregateItem(entity: any, idKey: 'id' | 'epc'): any {\n const out: any = { [idKey]: entity?.[idKey] }\n for (const [k, v] of Object.entries(entity ?? {})) {\n if (k === idKey || k === 'oee' || v === undefined) continue\n out[k] = v\n }\n const shaped = shapeOee(entity?.oee)\n if (shaped) out.oee = shaped\n return out\n}\n\n/**\n * 물품 집약의 상한 — 큰 현장은 물품이 수만 개라, 전량을 매 브로드캐스팅에 실으면 전달이 현장을 느리게 한다.\n *\n * **알리지 않고 자르지 않는다**: 넘치면 `total`(전체 수)과 `truncated`(잘린 수)를 함께 실어, 소비처가\n * \"이게 전부\" 라고 오해하지 않게 한다. 개별 물품은 태그별 delta 로 따로 나가고 그쪽은 시그니처 dedup\n * 이 걸려 **변한 것만** 재브로드캐스팅된다.\n */\nexport const ITEMS_AGGREGATE_LIMIT = 500\n\n/**\n * 자리별 에너지 — **그 자리의 계량기들이 잰 것**을 그 자리 태그에 실을 수 있게 모은다.\n *\n * ── 왜 자리에도 필요한가 (2026-08-14) ───────────────────────────────────────\n * 카탈로그는 자리에도 `metered` 를 준다(수전·분기·구역 계량은 재는 자리다). 그런데 값은 계량기\n * **설비** 태그로만 나갔다. 그래서 계통도의 분기 자리에 계량 컴포넌트를 놓으면 **영원히 「—」** 였다 —\n * 능력은 「잰다」고 말하는데 값이 오지 않는 자리.\n *\n * 「이 구간이 얼마를 먹나」는 자리의 사실이고, 사람은 계통도의 자리에 붙여 본다.\n *\n * ── 계층은 여기서 문제가 되지 않는다 ────────────────────────────────────────\n * 각 자리에는 **그 자리에 붙은 계량기만** 더한다(수전 자리에는 수전 계량기, 분기 자리에는 그 분기의\n * 것). 커널이 부하를 뿌리로만 세는 것과 다른 층의 계산이다 — 여기서는 자리마다 자기 것을 말한다.\n *\n * 근거를 함께 낸다(`meters`) — 그 자리의 수가 어느 계량기에서 왔는지 되짚을 수 있어야 한다.\n */\nfunction energyByLocation(state: any): Map<string, Record<string, unknown>> {\n const out = new Map<string, Record<string, unknown>>()\n const points = state?.energy?.points\n if (!Array.isArray(points) || !points.length) return out\n\n /* 계량기 → 자리. 자리를 모르는 계량기는 자리 payload 에 실을 데가 없다(설비 태그로만 나간다). */\n const locOf = new Map<string, string>()\n for (const e of state?.equipment ?? []) if (e?.id && e?.homeLocation) locOf.set(e.id, e.homeLocation)\n\n for (const pt of points) {\n const loc = pt?.id ? locOf.get(pt.id) : undefined\n if (!loc) continue\n const acc = out.get(loc) ?? { meters: [] as string[], samplesInWindow: 0 }\n ;(acc.meters as string[]).push(pt.id)\n acc.samplesInWindow = Number(acc.samplesInWindow ?? 0) + (Number(pt.samplesInWindow) || 0)\n /* 값은 **있는 것만** 더한다 — 하나도 없으면 키를 만들지 않는다(0 은 「안 썼다」가 된다). */\n if (Number.isFinite(Number(pt.kW))) acc.kW = Number(acc.kW ?? 0) + Number(pt.kW)\n if (Number.isFinite(Number(pt.kWh))) acc.kWh = Number(acc.kWh ?? 0) + Number(pt.kWh)\n /* 나이는 **가장 최근**을 쓴다 — 자리에 계량기가 여럿이면 그중 마지막 관측이 그 자리의 나이다. */\n if (pt.atMs !== undefined) {\n const iso = new Date(Number(pt.atMs)).toISOString()\n if (!acc.measuredAt || String(acc.measuredAt) < iso) acc.measuredAt = iso\n }\n out.set(loc, acc)\n }\n return out\n}\n\n/**\n * 물품 축만 **범위를 줄일 수 있다** (2026-08-21).\n *\n * ── 왜 이 인자가 생겼나 ─────────────────────────────────────────────────────\n * 브로드캐스팅 한 번이 만드는 payload 는 엔티티 수만큼이고 그 대부분이 물품이다(실측: 엔티티 3,611 중\n * 물품 2,400 · 만드는 데 1.8ms · 문자열로 바꾸는 데 1.0ms). 바뀐 것이 물품 하나여도 전부 만들어\n * 「같다」를 확인하고 버렸다.\n *\n * `items` 를 주면 **그 태그의 물품만** 개별 payload 를 만든다. 주지 않으면(`undefined`) 전부 만든다 —\n * 부르는 쪽이 무엇이 바뀌었는지 말할 수 없을 때의 답이다.\n *\n * **집약 뷰는 범위를 받지 않는다.** 집약은 목록 전체가 한 payload 라서, 한 물품이 바뀌면 그 payload 는\n * 통째로 새로 만들어야 한다(상한 500으로 이미 묶여 있다). 자리·설비·인원·자산·작업·오더도 범위를 받지\n * 않는다 — 수가 적고, 잘못 걸러서 잃는 것이 크다(자리 점유는 물품이 떠난 자리와 도착한 자리가 함께\n * 바뀌는데 사건은 한 지점만 말한다).\n */\nexport interface EntityDeltaScope {\n /** 개별 payload 를 만들 물품 태그. 없으면 전부. */\n items?: ReadonlySet<string>\n}\n\nexport function buildEntityDeltas(state: any, instanceId: string, scope?: EntityDeltaScope): EntityDelta[] {\n const out: EntityDelta[] = []\n const locEnergy = energyByLocation(state)\n\n for (const n of state?.locations ?? []) {\n /* 에너지는 자리 payload 에 **합친다** — 같은 태그로 두 번 보내면 뒤엣것이 앞을 덮는다. */\n const energy = locEnergy.get(n.id)\n out.push({ tag: n.id, data: energy ? { ...payloadOf('location', n), ...energy } : payloadOf('location', n) })\n }\n for (const m of state?.equipment ?? []) {\n /* Mobile(location/motion) + Operable(status/held) + Processable(oee) — 한 payload 로.\n * oee 만 정형화가 필요해 따로 얹는다(나머지는 통과). */\n const data = payloadOf('equipment', m)\n const oee = shapeOee(m?.oee)\n if (oee) data.oee = oee\n out.push({ tag: m.id, data })\n }\n /* 사람 — 설비와 별개 엔티티다. 같은 통과 규칙(가진 것을 전부 내보낸다). */\n for (const p of state?.persons ?? []) {\n out.push({ tag: p.id, data: payloadOf('person', p) })\n }\n /* 물리 자산(반복사용) — 물품도 설비도 아닌 네 번째 자원. */\n for (const a of state?.assets ?? []) {\n out.push({ tag: a.id, data: payloadOf('asset', a) })\n }\n for (const o of state?.orders ?? []) {\n out.push({ tag: o.id, data: payloadOf('order', o) })\n }\n for (const t of state?.tasks ?? []) {\n out.push({ tag: t.id, data: payloadOf('task', t) })\n }\n for (const it of state?.items ?? []) {\n /* 물품의 태그는 epc(개체 식별자) — 자리·설비와 달리 id 가 아니다. */\n if (!it?.epc) continue\n /* 범위가 주어졌으면 그 안의 물품만 만든다 — 나머지는 값이 그대로이므로 보낼 것이 없다. */\n if (scope?.items && !scope.items.has(it.epc)) continue\n out.push({ tag: it.epc, data: payloadOf('item', it) })\n }\n\n /* ── 집약 뷰 ──────────────────────────────────────────────────────────────\n * 런타임에 생기고 사라지는 엔티티(오더·작업·물품)는 보드에 개별 배치할 수 없다 → 한 컴포넌트가\n * 전체를 추적하는 집약 태그를 항상 방출한다. */\n const orders = state?.orders ?? []\n out.push({ tag: ordersTag(instanceId), data: { entity: 'orders', orders: orders.map((o: any) => aggregateItem(o, 'id')) } })\n\n const tasks = state?.tasks ?? []\n out.push({ tag: tasksTag(instanceId), data: { entity: 'tasks', tasks: tasks.map((t: any) => aggregateItem(t, 'id')) } })\n\n /* 사람 집약 — 인원은 보드에 개별 배치할 수도 있지만(설비처럼), 전체를 한눈에 보는 소비처도 있다.\n * 인원을 선언하지 않은 트윈에서는 빈 배열이 나간다(없음과 모름을 구별). */\n const persons = state?.persons ?? []\n out.push({ tag: personsTag(instanceId), data: { entity: 'persons', persons: persons.map((p: any) => aggregateItem(p, 'id')) } })\n\n const assets = state?.assets ?? []\n out.push({ tag: assetsTag(instanceId), data: { entity: 'assets', assets: assets.map((a: any) => aggregateItem(a, 'id')) } })\n\n const items = state?.items ?? []\n const shown = items.slice(0, ITEMS_AGGREGATE_LIMIT)\n out.push({\n tag: itemsTag(instanceId),\n data: {\n entity: 'items',\n items: shown.map((it: any) => aggregateItem(it, 'epc')),\n total: items.length,\n /* 잘렸으면 그 사실을 말한다 — \"이게 전부\" 로 읽히면 재고를 과소 판단한다. */\n ...(items.length > shown.length ? { truncated: items.length - shown.length } : {})\n }\n })\n\n /*\n * ── 에너지 (EMS) ─────────────────────────────────────────────────────────\n *\n * 계측은 `state.energy` 에 산다 — 자원 축(`equipment`·`locations`)이 아니다. 그래서 이 매핑이 없으면\n * 보드에 놓은 계량기가 **아무 값도 받지 못한다**(컴포넌트는 있는데 영원히 빈 모습).\n *\n * 두 갈래로 낸다:\n * · **개체별** — 계량 지점 하나하나의 마지막 관측(kW·kWh·역률·시각). 태그는 그 설비 id 다.\n * · **집약** — 수요 구간·피크·계약전력. 이것은 트윈 하나의 사실이라 개체에 실을 수 없다.\n *\n * 값이 없으면 **키를 넣지 않는다**(0 으로 만들지 않는다) — 「못 읽었다」와 「0 이었다」는 다른 사실이고,\n * 그 구별이 이 트윈의 값어치다.\n */\n const energy = state?.energy\n if (energy) {\n for (const pt of energy.points ?? []) {\n if (!pt?.id) continue\n out.push({\n tag: pt.id,\n data: {\n entity: 'meter',\n ...(pt.kW !== undefined ? { kW: pt.kW } : {}),\n ...(pt.kWh !== undefined ? { kWh: pt.kWh } : {}),\n ...(pt.powerFactor !== undefined ? { powerFactor: pt.powerFactor } : {}),\n /* 마지막 표본 시각 — 계측이 끊긴 것을 화면이 스스로 알 수 있게(우리가 판정하지 않는다). */\n ...(pt.atMs !== undefined ? { measuredAt: new Date(pt.atMs).toISOString() } : {}),\n samplesInWindow: pt.samplesInWindow ?? 0\n }\n })\n }\n out.push({\n tag: energyTag(instanceId),\n data: {\n entity: 'energy',\n ...(energy.contractKW !== undefined ? { contractKW: energy.contractKW } : {}),\n ...(energy.open ? { open: energy.open } : {}),\n ...(energy.peakSince ? { peakSince: energy.peakSince } : {}),\n /* 마감된 구간은 **최근 것만** 낸다 — 하루치를 매 브로드캐스팅에 실으면 채널이 그것으로 찬다.\n 전체 수는 함께 낸다(알리지 않고 자르지 않는다). */\n recent: (energy.closed ?? []).slice(-8),\n closedTotal: energy.closedTotal ?? 0\n }\n })\n }\n\n return out\n}\n"]}
1
+ {"version":3,"file":"entity-delta.js","sourceRoot":"","sources":["../shared/entity-delta.ts"],"names":[],"mappings":"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","sourcesContent":["import { ORDER_FOLD_DEFAULT, finishedFlowOrdersWithin, foldedOrderCount, type OrderFoldPolicy } from '@operato/ops-contract'\n\n/*\n * 트윈 State 스냅샷 → 라이브 바인딩 delta 목록 (순수 매핑 — pubsub/도메인 무관 → 단위 테스트 가능).\n * publishEntityData 가 이 목록을 엔티티별 시그니처로 dedup 후 data(tag) 채널로 발행한다.\n *\n * ── 규율: 레퍼런스가 아는 것은 **전부** 내보낸다 ────────────────────────────\n * 트윈에서 사소한 속성은 없다. 소속 구역·소속 팔레트·수량·만료처럼 곁가지로 보이는 값이 실제로는\n * 구역 롤업·적재 표현·로트 추적의 전제다. 그래서 여기서 **필드를 손으로 골라 담지 않는다.**\n *\n * 예전에는 엔티티마다 필요할 것 같은 필드만 열거했고, 그 결과 계약에 있는 것이 오류 없이 빠졌다\n * (2026-08-01 감사: 노드 parentId 누락 · 작업 전량 누락 · 물품 전량 누락). 화이트리스트 방식은\n * **계약이 자라도 화면이 따라오지 못한다** — 커널이 속성을 늘릴 때마다 이 파일을 고쳐야 하고, 고치는\n * 것을 잊으면 아무 신호 없이 정보가 사라진다.\n *\n * 그래서 반대로 한다: **상태 객체가 가진 것을 그대로 통과**시키고, 통과시키지 **않을** 것만 이유와 함께\n * 열거한다(EXCLUDED). 새 속성은 자동으로 화면에 도달하고, 빼려면 이유를 적어야 한다.\n *\n * ── 표식은 도메인 낱말을 훔치지 않는다 ─────────────────────────────────────\n * 예전 payload 는 엔티티 종류 표식을 `kind` 로 실었다. 그런데 `kind` 는 **도메인의 낱말**이다 —\n * 무버 kind=cutter, 작업 kind=putaway, 오더 kind=workorder. 표식이 그 이름을 차지하면 도메인 속성을\n * 실을 자리가 없어진다(같은 병을 오늘 ox-timeline 의 `group` 에서도 고쳤다).\n * 그래서 표식은 `entity`(우리 이름)로 옮기고 `kind` 는 도메인에 돌려준다.\n *\n * payload 계약(씬 컴포넌트가 소비): `entity`(표식) + 그 엔티티의 모든 속성(도메인 `kind` 포함).\n * entity = location · equipment · person · asset · order · task · item,\n * 그리고 집약 태그(__orders__ · __tasks__ · __items__ · __persons__ · __assets__).\n */\n\nexport interface EntityDelta {\n tag: string\n data: any\n}\n\n/** 집약 고정 태그 — 클라 provider 와 일치해야 한다(런타임 생성 엔티티는 개별 배치가 불가하므로 집약 뷰). */\n/*\n * 집약 채널의 접두사 — **실제 태그는 트윈 식별자를 붙여 만든다.**\n *\n * 예전에는 이 상수가 곧 태그였다. 그런데 발행 키는 (도메인, 태그)이고, **한 공간에 트윈이 여럿**\n * 있으면(실측: 3곳 × 3개) 셋이 같은 태그로 발행해 서로 덮었다. 보드의 주문 목록판이 세 공장의 주문\n * 중 마지막에 도착한 것을 보여 준다 — 오류 없이.\n *\n * 구간 성과 태그가 이미 `__kpi__:<트윈>` 으로 트윈을 붙이고 있었다(`kpi-broadcast.ts`). 같은 규약으로 맞춘다.\n */\nexport const ORDERS_TAG_PREFIX = '__orders__'\nexport const TASKS_TAG_PREFIX = '__tasks__'\nexport const ITEMS_TAG_PREFIX = '__items__'\nexport const PERSONS_TAG_PREFIX = '__persons__'\nexport const ASSETS_TAG_PREFIX = '__assets__'\n/** 에너지 집약 — 수요 구간·피크·계약은 **트윈 하나의 사실**이라 개체 태그가 없다. */\nexport const ENERGY_TAG_PREFIX = '__energy__'\n\n/** 이 트윈의 집약 채널 태그. 트윈 식별자가 없으면 만들지 않는다 — 덮어쓰기의 원인이었다. */\nexport function aggregateTag(prefix: string, instanceId: string): string {\n if (!instanceId) throw new Error('aggregateTag: instanceId required — tags without instanceId overwrite other twin states across tenants')\n\n return `${prefix}:${instanceId}`\n}\n\nexport const ordersTag = (instanceId: string) => aggregateTag(ORDERS_TAG_PREFIX, instanceId)\nexport const tasksTag = (instanceId: string) => aggregateTag(TASKS_TAG_PREFIX, instanceId)\nexport const itemsTag = (instanceId: string) => aggregateTag(ITEMS_TAG_PREFIX, instanceId)\nexport const personsTag = (instanceId: string) => aggregateTag(PERSONS_TAG_PREFIX, instanceId)\nexport const assetsTag = (instanceId: string) => aggregateTag(ASSETS_TAG_PREFIX, instanceId)\nexport const energyTag = (instanceId: string) => aggregateTag(ENERGY_TAG_PREFIX, instanceId)\n\n/**\n * 통과시키지 않는 필드 — **빼는 데는 이유가 필요하다.**\n *\n * · `id`/`epc` — 태그로 이미 나가므로 개별 payload 에 중복해 싣지 않는다(집약 뷰에는 넣는다).\n * · `oee` — 원시 누적(idleMs 등)이 매 delta 증가해 \"무변화 생략\" 을 무력화한다. shapeOee 로 정형화해 얹는다.\n */\nconst EXCLUDED = new Set(['id', 'epc', 'oee'])\n\n/**\n * OEE 원시 비율/카운트 → 표시용 정수 퍼센트 + 카운트.\n * 원시 누적 ms(idleMs 등)는 매 delta 증가해 무변화-생략을 무력화하므로 제외(유휴 자원의 매초 재브로드캐스팅 억제).\n * oee 미측정이면 undefined.\n */\nfunction shapeOee(o: any): any {\n if (!o) return undefined\n return {\n overall: Math.round((o.overall ?? 0) * 100),\n availability: Math.round((o.availability ?? 0) * 100),\n performance: Math.round((o.performance ?? 0) * 100),\n quality: Math.round((o.quality ?? 0) * 100),\n good: o.goodCount ?? 0,\n scrap: o.nonconformingCount ?? 0\n }\n}\n\n/**\n * 엔티티 하나의 payload — `entity` 표식 + 나머지 전부(도메인 `kind` 포함).\n *\n * `undefined` 값은 싣지 않는다(키의 유무가 시그니처를 흔들지 않게). 값이 없는 것과 키가 없는 것은\n * 소비처에서 같으므로 안전하고, 무변화 생략이 잘 듣는다.\n */\nfunction payloadOf(entityKind: string, entity: any): any {\n /* 표식은 `entity` — 도메인의 `kind` 를 덮지 않는다. */\n const data: any = { entity: entityKind }\n for (const [k, v] of Object.entries(entity ?? {})) {\n if (EXCLUDED.has(k) || v === undefined) continue\n data[k] = v\n }\n return data\n}\n\n/**\n * 집약 뷰의 항목 — 개별 payload 와 같은 내용 + 식별자.\n * 집약은 \"전체를 한 컴포넌트가 추적\" 하는 용도라 식별자가 있어야 한다.\n */\nfunction aggregateItem(entity: any, idKey: 'id' | 'epc'): any {\n const out: any = { [idKey]: entity?.[idKey] }\n for (const [k, v] of Object.entries(entity ?? {})) {\n if (k === idKey || k === 'oee' || v === undefined) continue\n out[k] = v\n }\n const shaped = shapeOee(entity?.oee)\n if (shaped) out.oee = shaped\n return out\n}\n\n/**\n * 물품 집약의 상한 — 큰 현장은 물품이 수만 개라, 전량을 매 브로드캐스팅에 실으면 전달이 현장을 느리게 한다.\n *\n * **알리지 않고 자르지 않는다**: 넘치면 `total`(전체 수)과 `truncated`(잘린 수)를 함께 실어, 소비처가\n * \"이게 전부\" 라고 오해하지 않게 한다. 개별 물품은 태그별 delta 로 따로 나가고 그쪽은 시그니처 dedup\n * 이 걸려 **변한 것만** 재브로드캐스팅된다.\n */\nexport const ITEMS_AGGREGATE_LIMIT = 500\n\n/**\n * 자리별 에너지 — **그 자리의 계량기들이 잰 것**을 그 자리 태그에 실을 수 있게 모은다.\n *\n * ── 왜 자리에도 필요한가 (2026-08-14) ───────────────────────────────────────\n * 카탈로그는 자리에도 `metered` 를 준다(수전·분기·구역 계량은 재는 자리다). 그런데 값은 계량기\n * **설비** 태그로만 나갔다. 그래서 계통도의 분기 자리에 계량 컴포넌트를 놓으면 **영원히 「—」** 였다 —\n * 능력은 「잰다」고 말하는데 값이 오지 않는 자리.\n *\n * 「이 구간이 얼마를 먹나」는 자리의 사실이고, 사람은 계통도의 자리에 붙여 본다.\n *\n * ── 계층은 여기서 문제가 되지 않는다 ────────────────────────────────────────\n * 각 자리에는 **그 자리에 붙은 계량기만** 더한다(수전 자리에는 수전 계량기, 분기 자리에는 그 분기의\n * 것). 커널이 부하를 뿌리로만 세는 것과 다른 층의 계산이다 — 여기서는 자리마다 자기 것을 말한다.\n *\n * 근거를 함께 낸다(`meters`) — 그 자리의 수가 어느 계량기에서 왔는지 되짚을 수 있어야 한다.\n */\nfunction energyByLocation(state: any): Map<string, Record<string, unknown>> {\n const out = new Map<string, Record<string, unknown>>()\n const points = state?.energy?.points\n if (!Array.isArray(points) || !points.length) return out\n\n /* 계량기 → 자리. 자리를 모르는 계량기는 자리 payload 에 실을 데가 없다(설비 태그로만 나간다). */\n const locOf = new Map<string, string>()\n for (const e of state?.equipment ?? []) if (e?.id && e?.homeLocation) locOf.set(e.id, e.homeLocation)\n\n for (const pt of points) {\n const loc = pt?.id ? locOf.get(pt.id) : undefined\n if (!loc) continue\n const acc = out.get(loc) ?? { meters: [] as string[], samplesInWindow: 0 }\n ;(acc.meters as string[]).push(pt.id)\n acc.samplesInWindow = Number(acc.samplesInWindow ?? 0) + (Number(pt.samplesInWindow) || 0)\n /* 값은 **있는 것만** 더한다 — 하나도 없으면 키를 만들지 않는다(0 은 「안 썼다」가 된다). */\n if (Number.isFinite(Number(pt.kW))) acc.kW = Number(acc.kW ?? 0) + Number(pt.kW)\n if (Number.isFinite(Number(pt.kWh))) acc.kWh = Number(acc.kWh ?? 0) + Number(pt.kWh)\n /* 나이는 **가장 최근**을 쓴다 — 자리에 계량기가 여럿이면 그중 마지막 관측이 그 자리의 나이다. */\n if (pt.atMs !== undefined) {\n const iso = new Date(Number(pt.atMs)).toISOString()\n if (!acc.measuredAt || String(acc.measuredAt) < iso) acc.measuredAt = iso\n }\n out.set(loc, acc)\n }\n return out\n}\n\n/**\n * 물품 축만 **범위를 줄일 수 있다** (2026-08-21).\n *\n * ── 왜 이 인자가 생겼나 ─────────────────────────────────────────────────────\n * 브로드캐스팅 한 번이 만드는 payload 는 엔티티 수만큼이고 그 대부분이 물품이다(실측: 엔티티 3,611 중\n * 물품 2,400 · 만드는 데 1.8ms · 문자열로 바꾸는 데 1.0ms). 바뀐 것이 물품 하나여도 전부 만들어\n * 「같다」를 확인하고 버렸다.\n *\n * `items` 를 주면 **그 태그의 물품만** 개별 payload 를 만든다. 주지 않으면(`undefined`) 전부 만든다 —\n * 부르는 쪽이 무엇이 바뀌었는지 말할 수 없을 때의 답이다.\n *\n * **집약 뷰는 범위를 받지 않는다.** 집약은 목록 전체가 한 payload 라서, 한 물품이 바뀌면 그 payload 는\n * 통째로 새로 만들어야 한다(상한 500으로 이미 묶여 있다). 자리·설비·인원·자산·작업·오더도 범위를 받지\n * 않는다 — 수가 적고, 잘못 걸러서 잃는 것이 크다(자리 점유는 물품이 떠난 자리와 도착한 자리가 함께\n * 바뀌는데 사건은 한 지점만 말한다).\n */\nexport interface EntityDeltaScope {\n /** 개별 payload 를 만들 물품 태그. 없으면 전부. */\n items?: ReadonlySet<string>\n}\n\nexport function buildEntityDeltas(\n state: any,\n instanceId: string,\n scope?: EntityDeltaScope,\n /** 끝난 오더를 접는 경계(모델의 `orderFold`) — 「최근 T 완료」의 T 다. 없으면 커널의 기본. */\n orderFold?: OrderFoldPolicy\n): EntityDelta[] {\n const out: EntityDelta[] = []\n const locEnergy = energyByLocation(state)\n\n for (const n of state?.locations ?? []) {\n /* 에너지는 자리 payload 에 **합친다** — 같은 태그로 두 번 보내면 뒤엣것이 앞을 덮는다. */\n const energy = locEnergy.get(n.id)\n out.push({ tag: n.id, data: energy ? { ...payloadOf('location', n), ...energy } : payloadOf('location', n) })\n }\n for (const m of state?.equipment ?? []) {\n /* Mobile(location/motion) + Operable(status/held) + Processable(oee) — 한 payload 로.\n * oee 만 정형화가 필요해 따로 얹는다(나머지는 통과). */\n const data = payloadOf('equipment', m)\n const oee = shapeOee(m?.oee)\n if (oee) data.oee = oee\n out.push({ tag: m.id, data })\n }\n /* 사람 — 설비와 별개 엔티티다. 같은 통과 규칙(가진 것을 전부 내보낸다). */\n for (const p of state?.persons ?? []) {\n out.push({ tag: p.id, data: payloadOf('person', p) })\n }\n /* 물리 자산(반복사용) — 물품도 설비도 아닌 네 번째 자원. */\n for (const a of state?.assets ?? []) {\n out.push({ tag: a.id, data: payloadOf('asset', a) })\n }\n for (const o of state?.orders ?? []) {\n out.push({ tag: o.id, data: payloadOf('order', o) })\n }\n for (const t of state?.tasks ?? []) {\n out.push({ tag: t.id, data: payloadOf('task', t) })\n }\n for (const it of state?.items ?? []) {\n /* 물품의 태그는 epc(개체 식별자) — 자리·설비와 달리 id 가 아니다. */\n if (!it?.epc) continue\n /* 범위가 주어졌으면 그 안의 물품만 만든다 — 나머지는 값이 그대로이므로 보낼 것이 없다. */\n if (scope?.items && !scope.items.has(it.epc)) continue\n out.push({ tag: it.epc, data: payloadOf('item', it) })\n }\n\n /* ── 집약 뷰 ──────────────────────────────────────────────────────────────\n * 런타임에 생기고 사라지는 엔티티(오더·작업·물품)는 보드에 개별 배치할 수 없다 → 한 컴포넌트가\n * 전체를 추적하는 집약 태그를 항상 방출한다. */\n const orders = state?.orders ?? []\n /*\n * **「최근 T 안에 끝난」 수와 접힌 수를 함께 싣는다**(ADR-0092 결정 2 보탬 · 3). 칩은 뜨거운 줄로 세면 건수 경계(N)에서\n * 멈추므로, 접힌 쪽의 시간 칸까지 더한 수를 계약의 한 함수(`finishedFlowOrdersWithin`, KPI 와 같은 판정)로 낸다. 「지금」은\n * 그 구동의 지금(`nowTime` — 시뮬은 시계, 미러는 들은 시각의 최대)이다. 모르면 싣지 않는다 — 0 으로 메우지 않는다.\n */\n const nowMs = Date.parse(String(state?.nowTime ?? ''))\n const policy = orderFold ?? ORDER_FOLD_DEFAULT\n const finished = Number.isFinite(nowMs) ? finishedFlowOrdersWithin(orders, state?.foldedOrders, nowMs, policy) : undefined\n const folded = foldedOrderCount(state?.foldedOrders)\n out.push({\n tag: ordersTag(instanceId),\n data: {\n entity: 'orders',\n orders: orders.map((o: any) => aggregateItem(o, 'id')),\n ...(finished ? { finished: { ...finished, windowMs: policy.afterMs } } : {}),\n ...(folded ? { folded } : {})\n }\n })\n\n const tasks = state?.tasks ?? []\n out.push({ tag: tasksTag(instanceId), data: { entity: 'tasks', tasks: tasks.map((t: any) => aggregateItem(t, 'id')) } })\n\n /* 사람 집약 — 인원은 보드에 개별 배치할 수도 있지만(설비처럼), 전체를 한눈에 보는 소비처도 있다.\n * 인원을 선언하지 않은 트윈에서는 빈 배열이 나간다(없음과 모름을 구별). */\n const persons = state?.persons ?? []\n out.push({ tag: personsTag(instanceId), data: { entity: 'persons', persons: persons.map((p: any) => aggregateItem(p, 'id')) } })\n\n const assets = state?.assets ?? []\n out.push({ tag: assetsTag(instanceId), data: { entity: 'assets', assets: assets.map((a: any) => aggregateItem(a, 'id')) } })\n\n const items = state?.items ?? []\n const shown = items.slice(0, ITEMS_AGGREGATE_LIMIT)\n out.push({\n tag: itemsTag(instanceId),\n data: {\n entity: 'items',\n items: shown.map((it: any) => aggregateItem(it, 'epc')),\n total: items.length,\n /* 잘렸으면 그 사실을 말한다 — \"이게 전부\" 로 읽히면 재고를 과소 판단한다. */\n ...(items.length > shown.length ? { truncated: items.length - shown.length } : {})\n }\n })\n\n /*\n * ── 에너지 (EMS) ─────────────────────────────────────────────────────────\n *\n * 계측은 `state.energy` 에 산다 — 자원 축(`equipment`·`locations`)이 아니다. 그래서 이 매핑이 없으면\n * 보드에 놓은 계량기가 **아무 값도 받지 못한다**(컴포넌트는 있는데 영원히 빈 모습).\n *\n * 두 갈래로 낸다:\n * · **개체별** — 계량 지점 하나하나의 마지막 관측(kW·kWh·역률·시각). 태그는 그 설비 id 다.\n * · **집약** — 수요 구간·피크·계약전력. 이것은 트윈 하나의 사실이라 개체에 실을 수 없다.\n *\n * 값이 없으면 **키를 넣지 않는다**(0 으로 만들지 않는다) — 「못 읽었다」와 「0 이었다」는 다른 사실이고,\n * 그 구별이 이 트윈의 값어치다.\n */\n const energy = state?.energy\n if (energy) {\n for (const pt of energy.points ?? []) {\n if (!pt?.id) continue\n out.push({\n tag: pt.id,\n data: {\n entity: 'meter',\n ...(pt.kW !== undefined ? { kW: pt.kW } : {}),\n ...(pt.kWh !== undefined ? { kWh: pt.kWh } : {}),\n ...(pt.powerFactor !== undefined ? { powerFactor: pt.powerFactor } : {}),\n /* 마지막 표본 시각 — 계측이 끊긴 것을 화면이 스스로 알 수 있게(우리가 판정하지 않는다). */\n ...(pt.atMs !== undefined ? { measuredAt: new Date(pt.atMs).toISOString() } : {}),\n samplesInWindow: pt.samplesInWindow ?? 0\n }\n })\n }\n out.push({\n tag: energyTag(instanceId),\n data: {\n entity: 'energy',\n ...(energy.contractKW !== undefined ? { contractKW: energy.contractKW } : {}),\n ...(energy.open ? { open: energy.open } : {}),\n ...(energy.peakSince ? { peakSince: energy.peakSince } : {}),\n /* 마감된 구간은 **최근 것만** 낸다 — 하루치를 매 브로드캐스팅에 실으면 채널이 그것으로 찬다.\n 전체 수는 함께 낸다(알리지 않고 자르지 않는다). */\n recent: (energy.closed ?? []).slice(-8),\n closedTotal: energy.closedTotal ?? 0\n }\n })\n }\n\n return out\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@things-factory/headless-twin",
3
- "version": "10.1.45",
3
+ "version": "10.1.46",
4
4
  "main": "dist-server/index.js",
5
5
  "things-factory": true,
6
6
  "author": "heartyoh <heartyoh@hatiolab.com>",
@@ -29,11 +29,11 @@
29
29
  "dependencies": {
30
30
  "@operato/ops-contract": "^0.9.38",
31
31
  "@operato/twin-kernel": "^0.11.27",
32
- "@things-factory/auth-base": "^10.1.44",
33
- "@things-factory/cache-service": "^10.1.44",
32
+ "@things-factory/auth-base": "^10.1.46",
33
+ "@things-factory/cache-service": "^10.1.46",
34
34
  "@things-factory/env": "^10.1.20",
35
- "@things-factory/ingest": "^10.1.44",
36
- "@things-factory/shell": "^10.1.44"
35
+ "@things-factory/ingest": "^10.1.46",
36
+ "@things-factory/shell": "^10.1.46"
37
37
  },
38
- "gitHead": "d72e1fd1285797fcd7839d97a18e31f5bf076ae3"
38
+ "gitHead": "d3467cde550453f3348fed0ed6c79a12a6ba0db4"
39
39
  }
@@ -0,0 +1,12 @@
1
+ /*
2
+ * **체크포인트를 쓰기 직전에 끝난 오더를 접는다** — ADR-0092 결정 1.
3
+ *
4
+ * 커널은 스스로 접지 않는다. 접는 때를 체크포인트에 맞춰야 접힌 쪽이 씨앗과 같은 때에 적히고, 재기동 동치(`재개점 + 꼬리 ==
5
+ * 0부터 접기`)가 선다. 그래서 체크포인트를 쓰는 호스트가 부른다. 두 구동이 가진 커널이 다르다 — 미러는 `projector`(관측 모드
6
+ * 커널), 시뮬은 `kernel` 이다. 둘 중 그 구동의 것을 부르고, 접는 문이 없는 옛 커널이면 아무것도 하지 않는다.
7
+ */
8
+ export function foldForCheckpoint(inst: { mode?: 'sim' | 'live'; projector?: any; kernel?: any } | undefined): number {
9
+ const k = inst?.mode === 'live' ? inst.projector : inst?.kernel
10
+ if (typeof k?.foldFinishedOrders !== 'function') return 0
11
+ return Number(k.foldFinishedOrders()) || 0
12
+ }
@@ -50,6 +50,8 @@ import { computeTwinKpi } from './kpi-query.js'
50
50
  import { createHash } from 'node:crypto'
51
51
 
52
52
  import { buildEntityDeltas } from '@things-factory/headless-twin/dist-shared/entity-delta.js'
53
+ import { orderFoldPolicyOf } from '@operato/ops-contract'
54
+ import { foldForCheckpoint } from './checkpoint-fold.js'
53
55
  import { fleetLoad, judgeCycle, loadSummary, newLoadMeter, recordFork, recordPhase, slowTickMessage, type LoadMeter, type LoadPhase } from './load-meter.js'
54
56
  import { loopLag } from './loop-lag.js'
55
57
  import { touchedItemKeys } from '@things-factory/headless-twin/dist-shared/touched-items.js'
@@ -466,6 +468,8 @@ export class TwinEngine {
466
468
  * 그것을 다시 `{revision, state}` 로 감싸 넣어 왔다 → 꺼낸 값에 축이 하나도 없어 웜스타트가
467
469
  * 알리지 않고 넘어갔다(저널에 수천 건이 있어도 트윈이 빈 채로 떴다).
468
470
  */
471
+ /* 끝난 오더를 접고 나서 상태를 뜬다 — 접힌 쪽이 이 체크포인트와 같은 때에 적힌다(ADR-0092 · `checkpoint-fold.ts`). */
472
+ foldForCheckpoint(inst)
469
473
  const state = unwrapState(this.snapshot(domainId, instanceId))
470
474
  if (!state) return
471
475
  /*
@@ -1599,7 +1603,7 @@ export class TwinEngine {
1599
1603
  const cached = this.recovered[runtimeKey(domainId, id)]?.state
1600
1604
  if (!cached) return
1601
1605
  const plan = planLiveContinuity(cached)
1602
- if (!plan.ackedCount && !plan.attentionSinceCount && !plan.energy) return
1606
+ if (!plan.ackedCount && !plan.attentionSinceCount && !plan.energy && !plan.foldedOrders) return
1603
1607
  if (typeof kernel.hydrateContinuity !== 'function') {
1604
1608
  twinWarn(
1605
1609
  `[twin-engine] kernel for "${id}" cannot carry continuity (no hydrateContinuity) — the open demand window, ` +
@@ -3714,7 +3718,9 @@ export class TwinEngine {
3714
3718
  * 그물이다** — 사건 없이 바뀌는 자리를 찾으면 그것을 고치는 것이 답이고 이 그물은 시간을 벌 뿐이다.
3715
3719
  */
3716
3720
  const full = inst.fullBroadcastDue === true || !inst.dirtyItems
3717
- const deltas = buildEntityDeltas(st, inst.id, full ? undefined : { items: inst.dirtyItems! })
3721
+ /* 「최근 T 완료」의 T 는 모델의 선언이다(ADR-0092) — 커널이 불러올 때 이미 거절했으므로 여기 오는 값은 옳다. */
3722
+ const fold = orderFoldPolicyOf((inst.model as any)?.orderFold)
3723
+ const deltas = buildEntityDeltas(st, inst.id, full ? undefined : { items: inst.dirtyItems! }, 'policy' in fold ? fold.policy : undefined)
3718
3724
  this.broadcastPasses++
3719
3725
  if (full) this.broadcastFullPasses++
3720
3726
  /* 이번 창의 범위는 여기서 닫는다 — 다음 창은 다시 모은다. */
@@ -224,6 +224,8 @@ export interface ContinuitySeed {
224
224
  acked?: string[]
225
225
  attentionSince?: { id: string; since: string }[]
226
226
  energy?: unknown
227
+ /** 접힌 오더(ADR-0092) — 모양은 커널이 안다(호스트는 있나만 본다). */
228
+ foldedOrders?: unknown
227
229
  }
228
230
 
229
231
  export interface ContinuityPlan {
@@ -233,6 +235,8 @@ export interface ContinuityPlan {
233
235
  attentionSinceCount: number
234
236
  /** 에너지 연속성을 이어받았나 — 열린 구간이 있으면 그 구간의 요금 근거가 살아난다. */
235
237
  energy: boolean
238
+ /** 접힌 오더를 이어받았나(ADR-0092). */
239
+ foldedOrders: boolean
236
240
  }
237
241
 
238
242
  /** 미러가 이어받을 축만 골라낸다 — 관측 축(재고·위치·설비)은 일부러 넣지 않는다. */
@@ -244,14 +248,18 @@ export function planLiveContinuity(raw: unknown): ContinuityPlan {
244
248
  : []
245
249
  /* 에너지는 커널이 자기 모양을 안다 — 호스트는 「있나」만 본다(골라 담으면 규칙이 두 곳으로 갈린다). */
246
250
  const energy = state?.energy && typeof state.energy === 'object' ? state.energy : undefined
251
+ /* 접힌 오더(ADR-0092) — 원천을 다시 읽는 미러도 세는 수와 경계를 잃지 않게(없으면 옛 종결이 뜨거운 줄로 되살아난다). */
252
+ const foldedOrders = state?.foldedOrders && typeof state.foldedOrders === 'object' ? state.foldedOrders : undefined
247
253
  return {
248
254
  seed: {
249
255
  ...(acked.length ? { acked } : {}),
250
256
  ...(attentionSince.length ? { attentionSince } : {}),
251
- ...(energy ? { energy } : {})
257
+ ...(energy ? { energy } : {}),
258
+ ...(foldedOrders ? { foldedOrders } : {})
252
259
  },
253
260
  ackedCount: acked.length,
254
261
  attentionSinceCount: attentionSince.length,
255
- energy: !!energy
262
+ energy: !!energy,
263
+ foldedOrders: !!foldedOrders
256
264
  }
257
265
  }
@@ -1,3 +1,5 @@
1
+ import { ORDER_FOLD_DEFAULT, finishedFlowOrdersWithin, foldedOrderCount, type OrderFoldPolicy } from '@operato/ops-contract'
2
+
1
3
  /*
2
4
  * 트윈 State 스냅샷 → 라이브 바인딩 delta 목록 (순수 매핑 — pubsub/도메인 무관 → 단위 테스트 가능).
3
5
  * publishEntityData 가 이 목록을 엔티티별 시그니처로 dedup 후 data(tag) 채널로 발행한다.
@@ -192,7 +194,13 @@ export interface EntityDeltaScope {
192
194
  items?: ReadonlySet<string>
193
195
  }
194
196
 
195
- export function buildEntityDeltas(state: any, instanceId: string, scope?: EntityDeltaScope): EntityDelta[] {
197
+ export function buildEntityDeltas(
198
+ state: any,
199
+ instanceId: string,
200
+ scope?: EntityDeltaScope,
201
+ /** 끝난 오더를 접는 경계(모델의 `orderFold`) — 「최근 T 완료」의 T 다. 없으면 커널의 기본. */
202
+ orderFold?: OrderFoldPolicy
203
+ ): EntityDelta[] {
196
204
  const out: EntityDelta[] = []
197
205
  const locEnergy = energyByLocation(state)
198
206
 
@@ -235,7 +243,24 @@ export function buildEntityDeltas(state: any, instanceId: string, scope?: Entity
235
243
  * 런타임에 생기고 사라지는 엔티티(오더·작업·물품)는 보드에 개별 배치할 수 없다 → 한 컴포넌트가
236
244
  * 전체를 추적하는 집약 태그를 항상 방출한다. */
237
245
  const orders = state?.orders ?? []
238
- out.push({ tag: ordersTag(instanceId), data: { entity: 'orders', orders: orders.map((o: any) => aggregateItem(o, 'id')) } })
246
+ /*
247
+ * **「최근 T 안에 끝난」 수와 접힌 수를 함께 싣는다**(ADR-0092 결정 2 보탬 · 3). 칩은 뜨거운 줄로 세면 건수 경계(N)에서
248
+ * 멈추므로, 접힌 쪽의 시간 칸까지 더한 수를 계약의 한 함수(`finishedFlowOrdersWithin`, KPI 와 같은 판정)로 낸다. 「지금」은
249
+ * 그 구동의 지금(`nowTime` — 시뮬은 시계, 미러는 들은 시각의 최대)이다. 모르면 싣지 않는다 — 0 으로 메우지 않는다.
250
+ */
251
+ const nowMs = Date.parse(String(state?.nowTime ?? ''))
252
+ const policy = orderFold ?? ORDER_FOLD_DEFAULT
253
+ const finished = Number.isFinite(nowMs) ? finishedFlowOrdersWithin(orders, state?.foldedOrders, nowMs, policy) : undefined
254
+ const folded = foldedOrderCount(state?.foldedOrders)
255
+ out.push({
256
+ tag: ordersTag(instanceId),
257
+ data: {
258
+ entity: 'orders',
259
+ orders: orders.map((o: any) => aggregateItem(o, 'id')),
260
+ ...(finished ? { finished: { ...finished, windowMs: policy.afterMs } } : {}),
261
+ ...(folded ? { folded } : {})
262
+ }
263
+ })
239
264
 
240
265
  const tasks = state?.tasks ?? []
241
266
  out.push({ tag: tasksTag(instanceId), data: { entity: 'tasks', tasks: tasks.map((t: any) => aggregateItem(t, 'id')) } })
@@ -68,11 +68,15 @@ const sortByTag = (arr: any[]) => [...arr].sort((a, b) => (a.tag < b.tag ? -1 :
68
68
  * 지금 남는 차이는 하나뿐이다:
69
69
  * · `oee` — 시뮬은 자기가 계측하고, 라이브는 **호스트**가 이벤트를 적분해 채운다(withLiveOee).
70
70
  * 커널 층에서만 비대칭이고 호스트가 닫는다(커널 적합성 하네스도 같은 예외를 선언한다).
71
+ * · `__orders__.finished` — 「최근 T 완료」 창의 오른쪽 끝은 그 구동의 「지금」이다(ADR-0092). 시뮬은 시계,
72
+ * 호스트의 미러는 관측 모드 커널이 마지막으로 들은 시각을 싣는다. 이 시험의 `StateProjector` 는 저널 재생용이라
73
+ * 「지금」이 없고, 그래서 창을 싣지 않는다 — 0 으로 메우지 않는 것이 설계다. 창 수의 정합은 order-fold-host 시험이 본다.
71
74
  * (`location.status` 는 예외였다가 해소됐다 — 저장값이 아니라 포화도 파생이라 두 구동이 같은 값을 낸다.)
72
75
  */
73
76
  const coreOnly = (d: any) => {
74
77
  const c = { ...d }
75
78
  if (c.entity === 'equipment') delete c.oee
79
+ if (c.entity === 'orders') delete c.finished
76
80
  return c
77
81
  }
78
82