@things-factory/headless-twin 10.0.11 → 10.0.12
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.
- package/dist-server/engine/canonical-ingest.d.ts +25 -2
- package/dist-server/engine/canonical-ingest.js +48 -10
- package/dist-server/engine/canonical-ingest.js.map +1 -1
- package/dist-server/engine/declared-stimulus.d.ts +43 -0
- package/dist-server/engine/declared-stimulus.js +57 -0
- package/dist-server/engine/declared-stimulus.js.map +1 -0
- package/dist-server/engine/index.d.ts +2 -0
- package/dist-server/engine/index.js +2 -0
- package/dist-server/engine/index.js.map +1 -1
- package/dist-server/engine/kpi-fold.d.ts +12 -0
- package/dist-server/engine/kpi-fold.js +21 -1
- package/dist-server/engine/kpi-fold.js.map +1 -1
- package/dist-server/engine/kpi-query.d.ts +3 -3
- package/dist-server/engine/kpi-query.js +4 -4
- package/dist-server/engine/kpi-query.js.map +1 -1
- package/dist-server/engine/live-feed-registry.d.ts +1 -1
- package/dist-server/engine/live-feed-registry.js +1 -1
- package/dist-server/engine/live-feed-registry.js.map +1 -1
- package/dist-server/engine/local-declarations.d.ts +3 -6
- package/dist-server/engine/local-declarations.js +97 -16
- package/dist-server/engine/local-declarations.js.map +1 -1
- package/dist-server/engine/measured-yield.d.ts +42 -0
- package/dist-server/engine/measured-yield.js +75 -0
- package/dist-server/engine/measured-yield.js.map +1 -0
- package/dist-server/engine/operation-basis.d.ts +16 -0
- package/dist-server/engine/operation-basis.js +20 -2
- package/dist-server/engine/operation-basis.js.map +1 -1
- package/dist-server/engine/property-effects.js +17 -0
- package/dist-server/engine/property-effects.js.map +1 -1
- package/dist-server/engine/restart-policy.d.ts +13 -0
- package/dist-server/engine/restart-policy.js +52 -0
- package/dist-server/engine/restart-policy.js.map +1 -0
- package/dist-server/engine/spec-coverage.d.ts +8 -0
- package/dist-server/engine/spec-coverage.js +3 -1
- package/dist-server/engine/spec-coverage.js.map +1 -1
- package/dist-server/engine/twin-engine.d.ts +48 -15
- package/dist-server/engine/twin-engine.js +179 -39
- package/dist-server/engine/twin-engine.js.map +1 -1
- package/dist-server/index.js +1 -1
- package/dist-server/index.js.map +1 -1
- package/dist-server/service/index.d.ts +1 -1
- package/dist-server/service/reference/control-routing.d.ts +14 -0
- package/dist-server/service/reference/control-routing.js +64 -0
- package/dist-server/service/reference/control-routing.js.map +1 -0
- package/dist-server/service/reference/index.d.ts +1 -0
- package/dist-server/service/reference/index.js +2 -0
- package/dist-server/service/reference/index.js.map +1 -1
- package/dist-server/service/reference/reference-adapter.d.ts +67 -0
- package/dist-server/service/reference/reference-adapter.js +18 -0
- package/dist-server/service/reference/reference-adapter.js.map +1 -1
- package/dist-server/service/reference/reference-live.js +3 -3
- package/dist-server/service/reference/reference-live.js.map +1 -1
- package/dist-server/service/reference/reference-resolver.d.ts +3 -3
- package/dist-server/service/reference/reference-resolver.js +35 -18
- package/dist-server/service/reference/reference-resolver.js.map +1 -1
- package/dist-server/service/twin-audit/command-audit.d.ts +34 -0
- package/dist-server/service/twin-audit/command-audit.js +15 -1
- package/dist-server/service/twin-audit/command-audit.js.map +1 -1
- package/dist-server/service/twin-audit/twin-audit-event.d.ts +3 -0
- package/dist-server/service/twin-audit/twin-audit-event.js +10 -0
- package/dist-server/service/twin-audit/twin-audit-event.js.map +1 -1
- package/dist-server/service/twin-control/twin-control-mutation.d.ts +15 -2
- package/dist-server/service/twin-control/twin-control-mutation.js +85 -34
- package/dist-server/service/twin-control/twin-control-mutation.js.map +1 -1
- package/dist-server/service/twin-instance/twin-instance.d.ts +1 -1
- package/dist-server/service/twin-instance/twin-instance.js +2 -2
- package/dist-server/service/twin-instance/twin-instance.js.map +1 -1
- package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js +2 -1
- package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js.map +1 -1
- package/dist-server/service/twin-model/twin-model-query.js +1 -1
- package/dist-server/service/twin-model/twin-model-query.js.map +1 -1
- package/package.json +3 -3
- package/server/engine/canonical-ingest.ts +78 -14
- package/server/engine/declared-stimulus.ts +66 -0
- package/server/engine/index.ts +2 -0
- package/server/engine/kpi-fold.ts +29 -1
- package/server/engine/kpi-query.ts +8 -8
- package/server/engine/live-feed-registry.ts +2 -2
- package/server/engine/local-declarations.ts +97 -19
- package/server/engine/measured-yield.ts +89 -0
- package/server/engine/operation-basis.ts +33 -2
- package/server/engine/property-effects.ts +17 -0
- package/server/engine/restart-policy.ts +55 -0
- package/server/engine/spec-coverage.ts +23 -3
- package/server/engine/twin-engine.ts +201 -42
- package/server/index.ts +1 -1
- package/server/service/reference/control-routing.ts +62 -0
- package/server/service/reference/index.ts +1 -0
- package/server/service/reference/reference-adapter.ts +78 -0
- package/server/service/reference/reference-live.ts +3 -3
- package/server/service/reference/reference-resolver.ts +46 -7
- package/server/service/twin-audit/command-audit.ts +34 -1
- package/server/service/twin-audit/twin-audit-event.ts +20 -0
- package/server/service/twin-control/twin-control-mutation.ts +75 -28
- package/server/service/twin-instance/twin-instance.ts +16 -10
- package/server/service/twin-lifecycle/twin-lifecycle-mutation.ts +2 -1
- package/server/service/twin-model/twin-model-query.ts +1 -1
- package/test/adopt-structure-live.test.ts +7 -7
- package/test/boot-resume.test.ts +3 -3
- package/test/control-capability.test.ts +103 -0
- package/test/declared-stimulus.test.ts +88 -0
- package/test/ingest-running-guard.test.ts +5 -5
- package/test/instance-cache-lifecycle.test.ts +1 -1
- package/test/kpi-baseline-db.test.ts +1 -1
- package/test/kpi-query-bench.test.ts +1 -1
- package/test/lineage-survives-restart.test.ts +3 -3
- package/test/live-feed-registry.test.ts +6 -6
- package/test/local-declarations.test.ts +108 -1
- package/test/measured-yield.test.ts +90 -0
- package/test/operation-basis.test.ts +28 -1
- package/test/operational-vocabulary.test.ts +108 -0
- package/test/operations-capability-db.test.ts +4 -4
- package/test/project-structure-db.test.ts +1 -1
- package/test/projection-reaches-screen.test.ts +1 -1
- package/test/property-effects.test.ts +28 -0
- package/test/restart-policy.test.ts +111 -0
- package/test/resync-origin-site.test.ts +7 -1
- package/test/source-outcome-audit.test.ts +104 -0
- package/test/structure-revision-db.test.ts +27 -27
- package/test/tenant-registry-db.test.ts +2 -2
- package/test/twin-model-item-db.test.ts +3 -3
- package/test/twin-model-tree-db.test.ts +7 -7
- package/test/twin-origin-resync.test.ts +4 -4
- package/test/yield-loop.test.ts +144 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"twin-model-query.js","sourceRoot":"","sources":["../../../server/service/twin-model/twin-model-query.ts"],"names":[],"mappings":"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","sourcesContent":["import { Arg, Ctx, Query, Resolver } from 'type-graphql'\n\nimport { ScalarObject, getRepository } from '@things-factory/shell'\n\nimport { TwinEngine } from '../../engine/index.js'\nimport { readBoardEquipment } from '@operato/twin-kernel'\nimport { TwinInstance } from '../twin-instance/twin-instance.js'\nimport { TwinSpace } from '../twin-space/twin-space.js'\nimport { TwinReference } from '../reference/twin-reference.js'\nimport { declarablePropertySpecs } from '../../engine/local-declarations.js'\nimport { operationBasis } from '../../engine/operation-basis.js'\nimport { TwinLocation } from './twin-location.js'\nimport { TwinEquipment } from './twin-equipment.js'\nimport { TwinOperation } from './twin-operation.js'\nimport { standardsForKind, type StandardId } from './standard-coverage.js'\nimport { isa95Coverage } from './isa95-coverage.js'\nimport { iec61850Coverage } from './iec61850-coverage.js'\nimport { epcisCoverage } from './epcis-coverage.js'\n\n/**\n * 이 공정에 선언된 시간을 **겹에서** 든다 — 좁은 겹이 이긴다(트윈 먼저, 없으면 현장).\n *\n * 모델에는 ISO 8601 로 구워지지만 화면에는 **사용자가 넣은 수와 단위**를 보인다: 「PT240S」를 보이면\n * 자기가 넣은 값인지 알아볼 수 없다. 선언이 없으면 빈 배열이다(빈 값을 지어내지 않는다).\n */\nfunction declaredDurationProperty(kind: string, instanceLayer?: any[], spaceLayer?: any[]): any[] {\n const find = (layer?: any[]) => {\n const d = (layer ?? []).find(x => x?.target === 'operation' && String(x?.id ?? '') === kind)\n return (d?.properties ?? []).find((p: any) => p?.id === 'operation.duration')\n }\n const p = find(instanceLayer) ?? find(spaceLayer)\n if (!p) return []\n return [{ id: 'operation.duration', value: String(p.value ?? ''), ...(p.uom ? { uom: String(p.uom) } : {}), source: 'local' }]\n}\n\n/** 표준 id → 그 표의 함수. 정책은 `standardsForKind` 가, 표는 각 파일이 가진다. */\nconst COVERAGE: Record<StandardId, (axes: string[]) => any> = { isa95: isa95Coverage, epcis: epcisCoverage, iec61850: iec61850Coverage }\nimport { unwrapState } from '../../engine/warm-start.js'\n/* 공유 모듈은 빌드 산출물로 참조한다 — 서버·클라이언트가 같은 규칙을 쓰는 자리다(같은 관행: entity-delta). */\nimport { axisValueOf } from '@things-factory/headless-twin/dist-shared/axis-read.js'\n\n/* 커널 런타임 로드 — CJS 번들(엔진과 같은 방식). 축·관계 선언의 SSOT 다. */\nconst { TWIN_AXES, TWIN_RELATIONS, axisInfo, axisSource, axisAppliesTo } = require('@operato/twin-kernel')\n\n/**\n * 축 → 그 축이 이미 **행으로 옮겨졌나**. 옮겨진 축은 DB 가 세고, 아직인 축은 문서를 세어야 한다.\n *\n * 이 표가 곧 `ADR-0032` 이주의 진척이다 — 여기 없는 축은 아직 `TwinModelDef` 문서 안에 산다.\n */\nconst ROW_BACKED: Record<string, any> = {\n locations: TwinLocation,\n equipment: TwinEquipment,\n operations: TwinOperation\n}\n\n/** `productionSpec.definition.recipes` 같은 경로를 문서에서 따라간다. */\nconst at = (o: any, path: string) => path.split('.').reduce((a: any, k: string) => (a == null ? undefined : a[k]), o)\n\nfunction countInDocument(model: any, path: string): number {\n const v = at(model, path)\n if (Array.isArray(v)) return v.length\n return v ? 1 : 0\n}\n\n/*\n * 트윈 모델 조회 — **이 트윈은 표준 모델의 어디를 채우고 있나**(`plans/twin-inspection.md` §2 ②).\n *\n * ── 답의 크기가 개념 수에 비례한다 ─────────────────────────────────────────\n * 엔티티를 **절대 싣지 않는다.** 실으면 계약이 곧 무너지고, 규모가 오는 날 이 질의가 그대로\n * 병목이 된다. 항목은 별도 질의(`twinModelEntities`)가 페이징으로 낸다.\n *\n * ── 개념 목록을 이 코드가 갖지 않는다 ───────────────────────────────────────\n * 축·관계는 **커널 카탈로그가 선언**한다(`TWIN_AXES`·`TWIN_RELATIONS`). 그래서 야드의 `dock-door`\n * 나 에너지의 계량기가 늘어도 여기 손댈 것이 없다 — 도메인마다 화면·질의가 하나씩 늘어나던\n * 구조를 끊는 것이 이 설계의 요점이다(`ADR-0031`).\n *\n * ── 어떻게 셌는지 밝힌다 ───────────────────────────────────────────────────\n * 지금은 축 셋만 행이고 나머지는 문서 안에 있다. 그 사실을 `basis` 로 **축마다** 말한다.\n * 밝히지 않으면 \"이 트윈은 3개 축뿐\" 으로 읽히고, 이주가 어디까지 왔는지도 아무도 모른다.\n */\n/** `inputs[].material` 처럼 배열을 지나는 선언 경로를 따라간다 — 배열 표기가 관계 선언의 문법이다. */\nfunction valuesAt(obj: any, field: string): any[] {\n let cur: any[] = [obj]\n for (const seg of field.split('.')) {\n const key = seg.endsWith('[]') ? seg.slice(0, -2) : seg\n const next: any[] = []\n for (const c of cur) {\n if (c == null) continue\n const v = c[key]\n if (v == null) continue\n if (Array.isArray(v)) next.push(...v)\n else next.push(v)\n }\n cur = next\n }\n return cur.filter(v => v != null)\n}\n\nconst idOf = (o: any): string | null => (typeof o === 'string' ? o : o?.id ?? o?.key ?? o?.gtin ?? null)\n\n@Resolver()\nexport class TwinModelQuery {\n /**\n * 이 선이 **얼마나 이어져 있나** — 그리고 **얼마나 끊겨 있나.**\n *\n * 선이 \"선언되었고 양끝이 존재한다\" 만 말하면 지도는 그림일 뿐이다. 관계에서 가장 중요한 정보는\n * 대개 **부재**다 — 기준 자리가 비어 있는 설비 열두 대, 어느 라우트에도 안 실린 공정. 그것이\n * 조감도에서 보여야 사람이 끊긴 데를 찾아다니지 않는다.\n *\n * 셋을 센다:\n * · `linked` — 값이 있고 그 값이 상대편에 **실제로 있다**\n * · `dangling` — 값은 있는데 **가리키는 것이 없다**(끊어진 참조 — 가장 나쁜 부류)\n * · `missing` — 값 자체가 없다(선택 관계면 정상, 필수 관계면 결손)\n *\n * 문서 어휘로 푼다 — 관계 선언의 필드 이름이 원본 문서의 어휘이기 때문이다(행은 자기 컬럼\n * 이름을 쓴다). 그 사실을 `fillBasis` 로 함께 낸다.\n */\n private edgeFill(model: any, r: any): any {\n const fromInfo = axisInfo(r.from)\n const toInfo = axisInfo(r.target.axis)\n if (!fromInfo || !toInfo) return {}\n\n /*\n * **문서로 셀 수 없는 관계는 세지 않는다.** 선 채움(이어짐·끊어짐·빈 것)은 두 축을 문서에서\n * 살펴 센다. 그런데 일정·실적은 문서에 없다(관측 축) — 그 관계를 문서로 세면 언제나 0 이 나오고,\n * 화면은 그것을 \"전부 끊어졌다\" 로 읽는다. 셀 수 없으면 **수를 내지 않는 편이 정직하다.**\n */\n if (axisSource(r.from) !== 'document' || axisSource(r.target.axis) !== 'document') return {}\n\n const listOf = (info: any) => {\n const raw = at(model, info.path)\n return Array.isArray(raw) ? raw : raw ? [raw] : []\n }\n const targets = new Set(listOf(toInfo).map(idOf).filter(Boolean) as string[])\n let linked = 0\n let dangling = 0\n let missing = 0\n for (const item of listOf(fromInfo)) {\n const vals = valuesAt(item, r.field).map(v => (typeof v === 'object' ? idOf(v) : v)).filter(v => v != null).map(String)\n if (!vals.length) { missing++; continue }\n /* 한 항목이 여럿을 가리킬 수 있다(`steps[]`). 하나라도 끊겼으면 끊긴 쪽으로 센다 —\n \"대체로 이어져 있다\" 는 진단이 아니다. */\n if (vals.every(v => targets.has(v))) linked++\n else dangling++\n }\n return { linked, dangling, missing, fillBasis: 'document' }\n }\n\n @Query(returns => ScalarObject, {\n nullable: true,\n description:\n 'What this twin reflects and where it fills the standard model: per-concept counts, how each was counted, and the declared relations between concepts. Never carries entities — use twinModelEntities for items.'\n })\n async twinModelSummary(@Arg('instanceId') instanceId: string, @Ctx() context: ResolverContext): Promise<any | null> {\n const domainId = context.state.domain.id\n const inst = await getRepository(TwinInstance).findOne({ where: { domain: { id: domainId }, instanceId } })\n if (!inst) return null\n\n /* 어느 원본을 반영하나 — 없을 수 있다(템플릿으로 세운 트윈). 그 사실도 사실이다. */\n const ref = await getRepository(TwinReference)\n .findOne({ where: { domain: { id: domainId }, source: instanceId } })\n .catch(() => null)\n\n const model = inst.model ?? {}\n /*\n * **한 요청에 저널은 한 번만 접는다.**\n *\n * 관측 축(`orders`·`tasks`)은 상태에서 센다. 그 둘이 각자 `recover()` 를 부르고 있었고, 커널이\n * 멈춰 있으면 그것은 **저널 전체를 다시 접는 일**이다(23,731건짜리 트윈에서 4.6초, 두 번이면 곱절).\n * 트윈을 전부 멈춰도 이 화면이 5~7초 걸린 이유가 이것이다 — 부하가 아니라 같은 일을 두 번 한 것이다.\n *\n * 한 번만 접어 나눠 쓴다. 값이 같아지는 것은 덤이 아니라 **옳음**이다: 두 번 접으면 그 사이에\n * 들어온 이벤트 때문에 오더와 작업이 서로 다른 시점의 사실이 될 수 있다.\n */\n let statePromise: Promise<{ snap: any; folded: boolean; observing: boolean }> | null = null\n const stateOnce = () =>\n (statePromise ??= (async () => {\n const observing = TwinEngine.isRunning(domainId, instanceId)\n let snap = observing ? unwrapState(TwinEngine.snapshot(domainId, instanceId)) : null\n let folded = false\n if (!snap) {\n try {\n snap = unwrapState(await TwinEngine.recover(domainId, instanceId))\n folded = !!snap\n } catch {\n /* 다시 계산할 것이 없다 — 0 으로 메우지 않는다(카드는 \"모름\" 을 낸다). */\n }\n }\n return { snap, folded, observing }\n })())\n const concepts = await Promise.all(\n TWIN_AXES.map(async (a: any) => {\n /*\n * **이 종류에 없는 개념인가** — 「아직 없음」과 「해당 없음」은 다른 말이다.\n *\n * 에너지 트윈에는 옮겨 다니는 물건도, 배정할 사람도, 공정도 없다(커널이 축마다 선언한다).\n * 그것을 「로트 0」으로 그리면 사용자는 채우러 갈 곳을 찾고, 적합성 표는 채우지 못한 것으로\n * 센다 — 둘 다 거짓이다. 수는 `null`(모름이 아니라 셀 대상이 아님)이고 이유를 코드로 낸다.\n *\n * 세지도 않는다: 없는 개념을 행에서 세는 것은 질의만 늘리고 답을 바꾸지 않는다.\n */\n if (!axisAppliesTo(a.axis, inst.kind)) {\n return {\n axis: a.axis,\n label: a.label,\n kind: a.kind,\n container: false,\n standardClass: a.standardClass,\n typeRole: a.typeRole ?? null,\n count: null,\n notApplicable: true,\n emptyBecause: 'not-applicable',\n basis: 'declaration'\n }\n }\n const entity = ROW_BACKED[a.axis]\n /* 문서 축만 문서로 센다 — 경로 없는 축을 세면 모델 루트를 훑는다(상태 축은 아래에서 센다). */\n const inDocument = axisSource(a.axis) === 'document' ? countInDocument(model, a.path) : 0\n const inRows = entity\n ? await getRepository(entity).count({ where: { domain: { id: domainId } as any, instance: { id: inst.id } as any } })\n : undefined\n /*\n * **둘을 함께 세고 어긋나면 말한다.**\n *\n * 행으로 옮긴 축이라도 그 트윈이 투영을 거치지 않았으면 행은 0이다(인제스트 없이\n * warm-start 된 트윈이 그렇다). 행만 보고 답하면 문서에 자리 6개가 있는데 화면은\n * **0 이라고 말한다** — 조용한 거짓이고, 이 프로젝트가 내내 잡아 온 바로 그 부류다.\n *\n * 그래서 어느 하나로 메우지 않는다. 행 수를 답하되 문서 수를 함께 내고, 다르면\n * `stale` 로 표시한다 — 그 차이 자체가 \"이 트윈은 투영 이후 인제스트되지 않았다\" 는 정보다.\n */\n const stale = entity !== undefined && inRows !== inDocument\n /*\n * **그릇인가** — 문서에 객체 하나로 있는 축(`productionSpec`).\n *\n * `kind` 로는 갈리지 않는다: 커널은 공정·레시피·라우트도 `spec` 이라 부른다(같은 선언 부류라는\n * 뜻이다). 그릇인지 아닌지는 **모양**이 말한다 — 목록이 되는 축은 배열이고, 한 벌뿐인 선언은\n * 객체 하나다. 개념 지도는 개체를 세는 면이므로 그릇은 자기 면에서 통째로 읽는다.\n */\n const raw = axisSource(a.axis) === 'document' ? at(model, a.path) : undefined\n /*\n * **관측 축은 상태가 센다**(일정·실적 — ISA-95 Part 4). 커널이 멈춰 있으면 `null` 이다:\n * 0 으로 내면 \"작업이 없는 공장\" 으로 읽히고, 그건 거짓이다(관측이 없는 것과 0 건은 다르다).\n * 이 화면이 관측 열에서 내내 지켜 온 구분을 카드에서도 지킨다.\n */\n if (axisSource(a.axis) === 'state') {\n /* 스냅샷은 `{revision, state}` 봉투다 — 벗기지 않으면 축이 한 겹 안에 있어 언제나 `null` 이\n 된다(커널은 도는데 카드가 \"모름\" 으로 보였다). 벗기는 규칙의 집은 `warm-start` 하나다. */\n /*\n * 관측 중인지는 **런타임이 있는지**로 본다 — 캐시된 스냅샷은 관측이 아니다.\n *\n * 동작하지 않으면 저널을 접어 답한다(라이브=권위 · 히스토리=파생). 카드는 그 차이를 `basis` 로\n * 말한다: 파생을 권위처럼 내면 다시 계산한 수를 \"지금 이만큼\" 으로 읽게 된다.\n */\n const { snap, folded, observing } = await stateOnce()\n /* 축이 사는 자리는 **커널 선언이 말한다**(`path`) — 축 이름으로만 읽으면 중첩된 축이\n 언제나 「관측 없음」이 된다(에너지의 수요 구간이 그렇다). 규칙은 `axisValueOf` 한 곳. */\n const bucket = snap ? axisValueOf(snap, a) ?? null : null\n return {\n axis: a.axis,\n label: a.label,\n kind: a.kind,\n container: false,\n standardClass: a.standardClass,\n typeRole: a.typeRole ?? null,\n count: Array.isArray(bucket) ? bucket.length : null,\n /*\n * **비었으면 왜 비었는지 말한다.** 0 과 `null` 만 내면 화면은 \"없다\" 까지만 말하고, 사용자는\n * \"안 적어 놨나 · 아직 안 일어났나 · 못 세었나\" 를 구별할 수 없다. 이 화면이 관측 열에서\n * 내내 지켜 온 구분(§observedAbsence)을 카드에서도 지킨다.\n */\n ...(Array.isArray(bucket) ? (bucket.length ? {} : { emptyBecause: 'none-yet' }) : { emptyBecause: 'not-observed' }),\n basis: folded ? 'journal' : 'state',\n observing,\n /* 선언이 아니라 관측인 축 — 화면이 \"원본이 침묵했다\" 로 읽지 않게 사실로 말한다. */\n observedOnly: true,\n standardUndeclared: !a.standardClass?.isa95 && !a.standardClass?.epcis && !a.standardClass?.iso55000\n }\n }\n return {\n axis: a.axis,\n label: a.label,\n kind: a.kind,\n container: !Array.isArray(raw) && raw != null,\n standardClass: a.standardClass,\n typeRole: a.typeRole ?? null,\n count: entity ? inRows : inDocument,\n /*\n * 비었으면 왜 비었는지 — 선언 쪽은 세 갈래다.\n * · `not-declared` 이 트윈의 모델이 그 축을 아예 말하지 않는다(대개 템플릿이 안 적었다)\n * · `declared-empty` 선언은 있는데 목록이 비어 있다(적었다가 지운 것도 사실이다)\n * · `not-projected` 문서엔 있는데 행에 없다 — 투영을 거치지 않은 트윈이다(`stale` 과 같은 사실)\n *\n * 셋을 \"0\" 하나로 합치면 사용자가 손볼 곳을 찾지 못한다: 첫째는 템플릿, 둘째는 원본,\n * 셋째는 다시 읽기다. 셋 다 다른 일이다.\n */\n ...((entity ? inRows : inDocument)\n ? {}\n : {\n emptyBecause:\n entity && inDocument > 0 ? 'not-projected' : raw == null ? 'not-declared' : 'declared-empty'\n }),\n /* 'rows' = DB 가 셌다(질의 가능) · 'document' = 문서를 세었다(아직 이주 전). */\n basis: entity ? 'rows' : 'document',\n ...(entity ? { documentCount: inDocument, stale } : {}),\n /* 표준 투영이 없는 축은 **숨기지 않고** 그 사실을 낸다(ADR-0031). */\n standardUndeclared: !a.standardClass?.isa95 && !a.standardClass?.epcis && !a.standardClass?.iso55000\n }\n })\n )\n\n /*\n * **원본이 모르는데 실행 중인 것** — 커널은 마스터에 없던 자원도 이벤트로 알게 되고\n * `origin: 'observed'` 를 단다(`observed-reducer`). 그것을 표에 밀어 넣지 않는다 — 그러면\n * 틱 루프에서 DB 로 쓰는 **두 번째 쓰기 경로**가 생기고, 실패하면 조용히 어긋난다\n * (쓰기 경로는 인제스트 하나, ADR-0032-D).\n *\n * 대신 **여기서 비교해 낸다.** 이 차이는 숨겨야 할 불일치가 아니라 인스펙션이 답해야 할\n * 질문이다 — \"원본이 말한 적 없는 설비가 실제로 실행 중이다.\"\n *\n * 트윈이 안 실행 중이면 비교할 상대가 없다 — 그때는 `null` 이다(0 이 아니다. 없는 것과\n * 세어 보지 못한 것은 다르다).\n */\n const snap = TwinEngine.snapshot(domainId, instanceId)\n const observedOnly = snap\n ? {\n equipment: (snap.equipment ?? []).filter((e: any) => e?.origin === 'observed').map((e: any) => e.id),\n locations: (snap.locations ?? []).filter((l: any) => l?.origin === 'observed').map((l: any) => l.id),\n persons: (snap.persons ?? []).filter((p: any) => p?.origin === 'observed').map((p: any) => p.id)\n }\n : null\n\n /* 지도의 선 — 선언된 관계 중 **양끝이 이 트윈에 실재하는 것만**. 없는 선을 그리지 않는다. */\n /* 선의 양끝 판정은 **문서든 행이든 있으면 있는 것**으로 본다 — 투영이 낡았다고 관계가 없어지진 않는다. */\n const nonEmpty = new Set(concepts.filter(c => (c.count ?? 0) > 0 || ((c as any).documentCount ?? 0) > 0).map(c => c.axis))\n /* 선은 **얼마나 이어졌고 얼마나 끊겼는지**까지 말한다 — 관계에서 가장 중요한 정보는 대개 부재다. */\n const edges = await Promise.all(\n TWIN_RELATIONS.filter((r: any) => r.target.kind === 'axis' && nonEmpty.has(r.from) && nonEmpty.has(r.target.axis)).map(\n async (r: any) => ({ from: r.from, to: r.target.axis, via: r.via, optional: !!r.optional, ...this.edgeFill(model, r) })\n )\n )\n\n return {\n instanceId,\n /* 사람이 부르는 이름 — 결선 줄이 업무키만 보이지 않게(업무키는 곁에 남는다). */\n name: inst.name ?? null,\n /*\n * **생산 선언에서 갈 곳이 없는 두 값** — GS1 발행자와 대표 산출물.\n *\n * 나머지 선언(자재·레시피·라우트·공정)은 각자 개념 축이 있고, 자재의 GS1 참조는 자재가 보인다.\n * 이 둘만 자기 축이 없다: 회사 접두사는 이 트윈의 **정체**(발행자)이고, 대표 레시피는 이 공장이\n * 무엇을 만드는 곳인가다. 둘 다 결선 줄(어느 원본을 반영하나)과 같은 성격이라 그 줄에 둔다.\n */\n /*\n * **표준 적합성** — 채운 것과 빠진 것을 함께.\n *\n * 이 화면은 \"이 트윈이 표준 모델의 어디를 채우고 있나\" 를 묻는다. 그런데 지금까지 답은 우리\n * 축만 세었다 — 없는 것은 화면에 없으므로 아무도 묻지 않았다. 기준(표준의 목록)을 함께 내면\n * 그 질문이 실제로 답을 갖는다(`isa95-coverage.ts` 의 머리말 참조).\n */\n /* 종류가 기대는 표준을 **모두** 낸다 — 하나만 말하면 그 트윈의 다른 기준이 보이지 않는다. */\n standards: standardsForKind(inst.kind).map(id => COVERAGE[id]((TWIN_AXES as any[]).map(a => a.axis))),\n production: (() => {\n const spec: any = (inst.model as any)?.productionSpec\n if (!spec) return null\n const key = spec.recipeKey ?? null\n const material = (spec.definition?.materials ?? []).find((m: any) => (m.key ?? m.id) === key)\n return {\n companyPrefix: spec.companyPrefix ?? null,\n recipeKey: key,\n /* 이름은 자재 선언이 말한다(i18n 키일 수 있으므로 번역은 화면의 일). */\n recipeLabel: material?.label ?? material?.name ?? null\n }\n })(),\n description: inst.description ?? null,\n kind: inst.kind,\n realityMode: inst.realityMode,\n source: ref\n ? { referenceId: ref.source, adapterType: ref.adapterType, status: ref.status, lastSyncedAt: ref.lastSyncedAt }\n : null,\n /*\n * **다시 읽을 수 있는가** — 이 트윈이 자기 원본을 기억하고 있나(`TwinInstance.origin`).\n *\n * 없는 트윈이 실제로 있다(이 자리가 생기기 전에 만들어진 것). 그때 화면이 버튼을 그대로\n * 내면 누를 때마다 실패하고, 사용자는 무엇이 잘못됐는지 알 수 없다. 그래서 **할 수 있는지를\n * 먼저 답한다** — 못 하는 것을 못 한다고 말하는 것도 이 화면의 일이다.\n */\n origin: inst.origin ?? null,\n canResync: !!(inst.origin as any)?.kind,\n concepts,\n edges,\n /* 원본이 모르는데 관측에는 있는 것. `null` = 트윈이 실행 중이 아니어서 비교하지 못했다. */\n observedOnly,\n /*\n * 축 셋만 행이고 나머지는 문서에서 셌다는 사실. 축마다 `basis` 가 있지만 여기서도 한 번 말한다 —\n * 화면이 \"이 숫자는 무엇으로 셌나\" 를 한눈에 보여야 규모 한계가 드러난다(ADR-0032 이주 압력).\n */\n basis: {\n rows: Object.keys(ROW_BACKED),\n document: TWIN_AXES.filter((a: any) => !ROW_BACKED[a.axis]).map((a: any) => a.axis),\n /* 투영이 낡은 축 — 있으면 이 트윈은 인제스트를 한 번 더 태워야 한다. */\n stale: concepts.filter((c: any) => c.stale).map((c: any) => c.axis)\n }\n }\n }\n /**\n * **이 트윈에 무엇을 선언할 수 있나** — 값을 넣는 화면이 도메인의 모양을 알지 않게.\n *\n * 커널이 정한 명세(속성 id·단위·범위·설명)를 그대로 낸다. 화면이 「에너지 트윈이면 계약전력·요금\n * 단가…」를 알고 있으면 도메인이 늘 때마다 화면을 고쳐야 하고 단위가 두 곳에 적힌다(ADR-0031).\n *\n * 지금 무엇이 선언돼 있는지도 함께 낸다(`declared`) — 화면이 두 번 묻지 않게, 그리고 「원천이 말한\n * 값」과 「우리가 정한 값」을 한자리에서 견줄 수 있게(`PropertySource`, ADR-0034).\n *\n * 어휘가 없는 종류는 `properties: []` 다 — 「아직 없다」와 「아무 값이나 받는다」는 다르다.\n */\n @Query(returns => ScalarObject, {\n nullable: true,\n description:\n \"What can be declared on this twin and what already is: the kernel's property specification (id, unit, range, note) plus the current local declarations at both scopes — this twin and its facility — each with the value it overrode. Empty properties mean this kind has no declaration vocabulary yet — not that anything goes.\"\n })\n async twinDeclarations(@Arg('instanceId') instanceId: string, @Ctx() context: ResolverContext): Promise<any | null> {\n const domainId = context.state.domain.id\n const inst = await getRepository(TwinInstance).findOne({ where: { domain: { id: domainId }, instanceId } })\n if (!inst) return null\n const model = (inst.model ?? {}) as any\n /*\n * 겹이 둘이다 — **현장의 것과 이 트윈의 것**. 화면이 둘을 함께 봐야 「이 값이 어느 범위의 것인가」를\n * 말할 수 있고, 철회 버튼이 어느 겹을 겨눌지 고를 수 있다(요금을 현장에 넣었는데 트윈에서 철회하려\n * 하면 「겹에 없다」로 끝난다 — 사용자는 이유를 알 수 없다).\n */\n const space = inst.spaceId\n ? await getRepository(TwinSpace).findOne({ where: { domain: { id: domainId }, spaceId: inst.spaceId } })\n : null\n /*\n * 돌고 있으면 커널이 「무엇으로 돌고 있나」를 스스로 답한다. 그 보고는 **작업이 새로 생길 때** 채워지\n * 므로 재기동 직후에는 비는데, 이력에서 배운 종류는 그때도 남아 있다(실측으로 이 빈칸에 부딪혔다).\n */\n const operations = operationBasis(\n model,\n TwinEngine.kernel(domainId, instanceId),\n await TwinEngine.measuredOperationKinds(domainId, instanceId)\n )\n return {\n instanceId,\n kind: inst.kind,\n spaceId: inst.spaceId,\n spaceName: space?.name,\n properties: declarablePropertySpecs(inst.kind),\n declared: (inst.localDeclarations ?? []) as any[],\n declaredAtSpace: (space?.localDeclarations ?? []) as any[],\n /* 어디에 얹을 수 있나 — 모델의 자리·설비·공정. 화면이 대상을 지어내지 않게 목록으로 준다. */\n targets: [\n ...(model.locations ?? []).map((l: any) => ({ target: 'location', id: l.id, name: l.name, type: l.type, properties: l.properties ?? [] })),\n ...(readBoardEquipment(model) ?? []).map((e: any) => ({ target: 'equipment', id: e.id, name: e.name, type: e.type ?? e.kind, properties: e.properties ?? [] })),\n /*\n * 공정은 **자원이 아니다** — 값은 종류에 붙고, 그 종류에 명세 행이 없어도 된다(창고·야드에는 행이\n * 0개다). 그래서 대상 목록을 커널의 자기보고까지 합쳐 만든다(`operationBasis`).\n *\n * 지금 값은 **겹에서** 든다(사용자가 넣은 수와 단위 그대로). 모델에는 ISO 8601 로 구워지지만\n * 화면에 「PT240S」를 보이면 자기가 넣은 값인지 알아볼 수 없다.\n */\n ...operations.map(op => ({\n target: 'operation',\n id: op.kind,\n name: op.label,\n properties: declaredDurationProperty(op.kind, inst.localDeclarations as any[], space?.localDeclarations as any[])\n }))\n ],\n /*\n * 이 트윈이 돌리는 공정과 **그 시간이 어디서 오나** — 선언 화면이 「무엇이 상수로 도나」를 보여야\n * 사람이 채울 곳을 안다. `basis` 가 비면 아직 돌지 않은 것이다(공정이 없는 것이 아니다).\n */\n operations\n }\n }\n /**\n * **이 현장에 무엇을 선언할 수 있나** — 현장 값은 트윈의 것이 아니므로 트윈 없이 묻는다.\n *\n * ── 왜 트윈으로 묻지 않나 (2026-08-18) ─────────────────────────────────────\n * 처음에는 트윈 화면에서 범위만 골라 현장 값을 넣게 했다. 그런데 **현장 값을 넣으려고 트윈을 골라야\n * 하는 것**은 개념이 거꾸로다 — 트윈은 그 값을 읽는 쪽이다(사용자가 그 어긋남을 지적했다).\n *\n * 어휘와 대상은 **그 현장 트윈들의 합집합**이다: 한 현장에 여러 종류가 살고(에너지·창고·야드), 요금은\n * 에너지 트윈만 읽는다. 합집합으로 내되 **어느 트윈이 그 대상을 가졌는지** 함께 낸다 — 아무 트윈도\n * 읽지 않는 값을 넣는 일을 화면이 미리 보여 줄 수 있어야 한다.\n */\n @Query(returns => ScalarObject, {\n nullable: true,\n description:\n 'What can be declared for a whole facility (space) and what already is: the union of the property specifications of the twin kinds living there, the union of their declarable targets (with which twins carry each), and the current facility-scope declarations.'\n })\n async spaceDeclarations(@Arg('spaceId') spaceId: string, @Ctx() context: ResolverContext): Promise<any | null> {\n const domainId = context.state.domain.id\n const space = await getRepository(TwinSpace).findOne({ where: { domain: { id: domainId }, spaceId } })\n if (!space) return null\n const twins = await getRepository(TwinInstance).find({ where: { domain: { id: domainId }, spaceId } })\n\n /* 어휘 합집합 — 같은 속성이 여러 종류에 있어도 한 번만 낸다(명세는 커널 하나에서 온다). */\n const byId = new Map<string, any>()\n for (const t of twins) for (const spec of declarablePropertySpecs(t.kind)) if (!byId.has(spec.id)) byId.set(spec.id, spec)\n\n /* 대상 합집합 — 같은 id 가 여러 트윈에 있으면 **그 트윈들을 함께** 든다(현장 값은 그 전부에 얹힌다). */\n const targets = new Map<string, { target: string; id: string; name?: string; type?: string; twins: string[] }>()\n for (const t of twins) {\n const model = (t.model ?? {}) as any\n const add = (target: 'location' | 'equipment' | 'operation', e: any) => {\n const key = `${target}:${e.id}`\n const cur = targets.get(key)\n if (cur) { cur.twins.push(t.instanceId); return }\n targets.set(key, { target, id: e.id, name: e.name, type: e.type ?? e.kind, twins: [t.instanceId] })\n }\n for (const l of model.locations ?? []) add('location', l)\n for (const e of readBoardEquipment(model) ?? []) add('equipment', e)\n /*\n * 공정도 대상이다 — 「우리 현장의 적재는 4분」은 그 현장 창고 전부가 읽어야 하는 수다. 목록은 각\n * 트윈의 것을 합집합으로 든다(돌고 있으면 커널이 스스로 답한다).\n */\n for (const op of operationBasis(\n model,\n TwinEngine.kernel(domainId, t.instanceId),\n await TwinEngine.measuredOperationKinds(domainId, t.instanceId)\n ))\n add('operation', { id: op.kind, name: op.label })\n }\n\n return {\n spaceId,\n spaceName: space.name,\n properties: [...byId.values()],\n declared: (space.localDeclarations ?? []) as any[],\n targets: [...targets.values()],\n /* 이 현장에 사는 트윈들 — 화면이 「누가 이 값을 읽나」를 말할 수 있게. */\n twins: twins.map(t => ({ instanceId: t.instanceId, kind: t.kind, name: t.name }))\n }\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"twin-model-query.js","sourceRoot":"","sources":["../../../server/service/twin-model/twin-model-query.ts"],"names":[],"mappings":"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","sourcesContent":["import { Arg, Ctx, Query, Resolver } from 'type-graphql'\n\nimport { ScalarObject, getRepository } from '@things-factory/shell'\n\nimport { TwinEngine } from '../../engine/index.js'\nimport { readBoardEquipment } from '@operato/twin-kernel'\nimport { TwinInstance } from '../twin-instance/twin-instance.js'\nimport { TwinSpace } from '../twin-space/twin-space.js'\nimport { TwinReference } from '../reference/twin-reference.js'\nimport { declarablePropertySpecs } from '../../engine/local-declarations.js'\nimport { operationBasis } from '../../engine/operation-basis.js'\nimport { TwinLocation } from './twin-location.js'\nimport { TwinEquipment } from './twin-equipment.js'\nimport { TwinOperation } from './twin-operation.js'\nimport { standardsForKind, type StandardId } from './standard-coverage.js'\nimport { isa95Coverage } from './isa95-coverage.js'\nimport { iec61850Coverage } from './iec61850-coverage.js'\nimport { epcisCoverage } from './epcis-coverage.js'\n\n/**\n * 이 공정에 선언된 시간을 **겹에서** 든다 — 좁은 겹이 이긴다(트윈 먼저, 없으면 현장).\n *\n * 모델에는 ISO 8601 로 구워지지만 화면에는 **사용자가 넣은 수와 단위**를 보인다: 「PT240S」를 보이면\n * 자기가 넣은 값인지 알아볼 수 없다. 선언이 없으면 빈 배열이다(빈 값을 지어내지 않는다).\n */\nfunction declaredDurationProperty(kind: string, instanceLayer?: any[], spaceLayer?: any[]): any[] {\n const find = (layer?: any[]) => {\n const d = (layer ?? []).find(x => x?.target === 'operation' && String(x?.id ?? '') === kind)\n return (d?.properties ?? []).find((p: any) => p?.id === 'operation.duration')\n }\n const p = find(instanceLayer) ?? find(spaceLayer)\n if (!p) return []\n return [{ id: 'operation.duration', value: String(p.value ?? ''), ...(p.uom ? { uom: String(p.uom) } : {}), source: 'local' }]\n}\n\n/** 표준 id → 그 표의 함수. 정책은 `standardsForKind` 가, 표는 각 파일이 가진다. */\nconst COVERAGE: Record<StandardId, (axes: string[]) => any> = { isa95: isa95Coverage, epcis: epcisCoverage, iec61850: iec61850Coverage }\nimport { unwrapState } from '../../engine/warm-start.js'\n/* 공유 모듈은 빌드 산출물로 참조한다 — 서버·클라이언트가 같은 규칙을 쓰는 자리다(같은 관행: entity-delta). */\nimport { axisValueOf } from '@things-factory/headless-twin/dist-shared/axis-read.js'\n\n/* 커널 런타임 로드 — CJS 번들(엔진과 같은 방식). 축·관계 선언의 SSOT 다. */\nconst { TWIN_AXES, TWIN_RELATIONS, axisInfo, axisSource, axisAppliesTo } = require('@operato/twin-kernel')\n\n/**\n * 축 → 그 축이 이미 **행으로 옮겨졌나**. 옮겨진 축은 DB 가 세고, 아직인 축은 문서를 세어야 한다.\n *\n * 이 표가 곧 `ADR-0032` 이주의 진척이다 — 여기 없는 축은 아직 `TwinModelDef` 문서 안에 산다.\n */\nconst ROW_BACKED: Record<string, any> = {\n locations: TwinLocation,\n equipment: TwinEquipment,\n operations: TwinOperation\n}\n\n/** `productionSpec.definition.recipes` 같은 경로를 문서에서 따라간다. */\nconst at = (o: any, path: string) => path.split('.').reduce((a: any, k: string) => (a == null ? undefined : a[k]), o)\n\nfunction countInDocument(model: any, path: string): number {\n const v = at(model, path)\n if (Array.isArray(v)) return v.length\n return v ? 1 : 0\n}\n\n/*\n * 트윈 모델 조회 — **이 트윈은 표준 모델의 어디를 채우고 있나**(`plans/twin-inspection.md` §2 ②).\n *\n * ── 답의 크기가 개념 수에 비례한다 ─────────────────────────────────────────\n * 엔티티를 **절대 싣지 않는다.** 실으면 계약이 곧 무너지고, 규모가 오는 날 이 질의가 그대로\n * 병목이 된다. 항목은 별도 질의(`twinModelEntities`)가 페이징으로 낸다.\n *\n * ── 개념 목록을 이 코드가 갖지 않는다 ───────────────────────────────────────\n * 축·관계는 **커널 카탈로그가 선언**한다(`TWIN_AXES`·`TWIN_RELATIONS`). 그래서 야드의 `dock-door`\n * 나 에너지의 계량기가 늘어도 여기 손댈 것이 없다 — 도메인마다 화면·질의가 하나씩 늘어나던\n * 구조를 끊는 것이 이 설계의 요점이다(`ADR-0031`).\n *\n * ── 어떻게 셌는지 밝힌다 ───────────────────────────────────────────────────\n * 지금은 축 셋만 행이고 나머지는 문서 안에 있다. 그 사실을 `basis` 로 **축마다** 말한다.\n * 밝히지 않으면 \"이 트윈은 3개 축뿐\" 으로 읽히고, 이주가 어디까지 왔는지도 아무도 모른다.\n */\n/** `inputs[].material` 처럼 배열을 지나는 선언 경로를 따라간다 — 배열 표기가 관계 선언의 문법이다. */\nfunction valuesAt(obj: any, field: string): any[] {\n let cur: any[] = [obj]\n for (const seg of field.split('.')) {\n const key = seg.endsWith('[]') ? seg.slice(0, -2) : seg\n const next: any[] = []\n for (const c of cur) {\n if (c == null) continue\n const v = c[key]\n if (v == null) continue\n if (Array.isArray(v)) next.push(...v)\n else next.push(v)\n }\n cur = next\n }\n return cur.filter(v => v != null)\n}\n\nconst idOf = (o: any): string | null => (typeof o === 'string' ? o : o?.id ?? o?.key ?? o?.gtin ?? null)\n\n@Resolver()\nexport class TwinModelQuery {\n /**\n * 이 선이 **얼마나 이어져 있나** — 그리고 **얼마나 끊겨 있나.**\n *\n * 선이 \"선언되었고 양끝이 존재한다\" 만 말하면 지도는 그림일 뿐이다. 관계에서 가장 중요한 정보는\n * 대개 **부재**다 — 기준 자리가 비어 있는 설비 열두 대, 어느 라우트에도 안 실린 공정. 그것이\n * 조감도에서 보여야 사람이 끊긴 데를 찾아다니지 않는다.\n *\n * 셋을 센다:\n * · `linked` — 값이 있고 그 값이 상대편에 **실제로 있다**\n * · `dangling` — 값은 있는데 **가리키는 것이 없다**(끊어진 참조 — 가장 나쁜 부류)\n * · `missing` — 값 자체가 없다(선택 관계면 정상, 필수 관계면 결손)\n *\n * 문서 어휘로 푼다 — 관계 선언의 필드 이름이 원본 문서의 어휘이기 때문이다(행은 자기 컬럼\n * 이름을 쓴다). 그 사실을 `fillBasis` 로 함께 낸다.\n */\n private edgeFill(model: any, r: any): any {\n const fromInfo = axisInfo(r.from)\n const toInfo = axisInfo(r.target.axis)\n if (!fromInfo || !toInfo) return {}\n\n /*\n * **문서로 셀 수 없는 관계는 세지 않는다.** 선 채움(이어짐·끊어짐·빈 것)은 두 축을 문서에서\n * 살펴 센다. 그런데 일정·실적은 문서에 없다(관측 축) — 그 관계를 문서로 세면 언제나 0 이 나오고,\n * 화면은 그것을 \"전부 끊어졌다\" 로 읽는다. 셀 수 없으면 **수를 내지 않는 편이 정직하다.**\n */\n if (axisSource(r.from) !== 'document' || axisSource(r.target.axis) !== 'document') return {}\n\n const listOf = (info: any) => {\n const raw = at(model, info.path)\n return Array.isArray(raw) ? raw : raw ? [raw] : []\n }\n const targets = new Set(listOf(toInfo).map(idOf).filter(Boolean) as string[])\n let linked = 0\n let dangling = 0\n let missing = 0\n for (const item of listOf(fromInfo)) {\n const vals = valuesAt(item, r.field).map(v => (typeof v === 'object' ? idOf(v) : v)).filter(v => v != null).map(String)\n if (!vals.length) { missing++; continue }\n /* 한 항목이 여럿을 가리킬 수 있다(`steps[]`). 하나라도 끊겼으면 끊긴 쪽으로 센다 —\n \"대체로 이어져 있다\" 는 진단이 아니다. */\n if (vals.every(v => targets.has(v))) linked++\n else dangling++\n }\n return { linked, dangling, missing, fillBasis: 'document' }\n }\n\n @Query(returns => ScalarObject, {\n nullable: true,\n description:\n 'What this twin reflects and where it fills the standard model: per-concept counts, how each was counted, and the declared relations between concepts. Never carries entities — use twinModelEntities for items.'\n })\n async twinModelSummary(@Arg('instanceId') instanceId: string, @Ctx() context: ResolverContext): Promise<any | null> {\n const domainId = context.state.domain.id\n const inst = await getRepository(TwinInstance).findOne({ where: { domain: { id: domainId }, instanceId } })\n if (!inst) return null\n\n /* 어느 원본을 반영하나 — 없을 수 있다(템플릿으로 세운 트윈). 그 사실도 사실이다. */\n const ref = await getRepository(TwinReference)\n .findOne({ where: { domain: { id: domainId }, source: instanceId } })\n .catch(() => null)\n\n const model = inst.model ?? {}\n /*\n * **한 요청에 저널은 한 번만 접는다.**\n *\n * 관측 축(`orders`·`tasks`)은 상태에서 센다. 그 둘이 각자 `recover()` 를 부르고 있었고, 커널이\n * 멈춰 있으면 그것은 **저널 전체를 다시 접는 일**이다(23,731건짜리 트윈에서 4.6초, 두 번이면 곱절).\n * 트윈을 전부 멈춰도 이 화면이 5~7초 걸린 이유가 이것이다 — 부하가 아니라 같은 일을 두 번 한 것이다.\n *\n * 한 번만 접어 나눠 쓴다. 값이 같아지는 것은 덤이 아니라 **옳음**이다: 두 번 접으면 그 사이에\n * 들어온 이벤트 때문에 오더와 작업이 서로 다른 시점의 사실이 될 수 있다.\n */\n let statePromise: Promise<{ snap: any; folded: boolean; observing: boolean }> | null = null\n const stateOnce = () =>\n (statePromise ??= (async () => {\n const observing = TwinEngine.isRunning(domainId, instanceId)\n let snap = observing ? unwrapState(TwinEngine.snapshot(domainId, instanceId)) : null\n let folded = false\n if (!snap) {\n try {\n snap = unwrapState(await TwinEngine.recover(domainId, instanceId))\n folded = !!snap\n } catch {\n /* 다시 계산할 것이 없다 — 0 으로 메우지 않는다(카드는 \"모름\" 을 낸다). */\n }\n }\n return { snap, folded, observing }\n })())\n const concepts = await Promise.all(\n TWIN_AXES.map(async (a: any) => {\n /*\n * **이 종류에 없는 개념인가** — 「아직 없음」과 「해당 없음」은 다른 말이다.\n *\n * 에너지 트윈에는 옮겨 다니는 물건도, 배정할 사람도, 공정도 없다(커널이 축마다 선언한다).\n * 그것을 「로트 0」으로 그리면 사용자는 채우러 갈 곳을 찾고, 적합성 표는 채우지 못한 것으로\n * 센다 — 둘 다 거짓이다. 수는 `null`(모름이 아니라 셀 대상이 아님)이고 이유를 코드로 낸다.\n *\n * 세지도 않는다: 없는 개념을 행에서 세는 것은 질의만 늘리고 답을 바꾸지 않는다.\n */\n if (!axisAppliesTo(a.axis, inst.kind)) {\n return {\n axis: a.axis,\n label: a.label,\n kind: a.kind,\n container: false,\n standardClass: a.standardClass,\n typeRole: a.typeRole ?? null,\n count: null,\n notApplicable: true,\n emptyBecause: 'not-applicable',\n basis: 'declaration'\n }\n }\n const entity = ROW_BACKED[a.axis]\n /* 문서 축만 문서로 센다 — 경로 없는 축을 세면 모델 루트를 훑는다(상태 축은 아래에서 센다). */\n const inDocument = axisSource(a.axis) === 'document' ? countInDocument(model, a.path) : 0\n const inRows = entity\n ? await getRepository(entity).count({ where: { domain: { id: domainId } as any, instance: { id: inst.id } as any } })\n : undefined\n /*\n * **둘을 함께 세고 어긋나면 말한다.**\n *\n * 행으로 옮긴 축이라도 그 트윈이 투영을 거치지 않았으면 행은 0이다(인제스트 없이\n * warm-start 된 트윈이 그렇다). 행만 보고 답하면 문서에 자리 6개가 있는데 화면은\n * **0 이라고 말한다** — 조용한 거짓이고, 이 프로젝트가 내내 잡아 온 바로 그 부류다.\n *\n * 그래서 어느 하나로 메우지 않는다. 행 수를 답하되 문서 수를 함께 내고, 다르면\n * `stale` 로 표시한다 — 그 차이 자체가 \"이 트윈은 투영 이후 인제스트되지 않았다\" 는 정보다.\n */\n const stale = entity !== undefined && inRows !== inDocument\n /*\n * **그릇인가** — 문서에 객체 하나로 있는 축(`productionSpec`).\n *\n * `kind` 로는 갈리지 않는다: 커널은 공정·레시피·라우트도 `spec` 이라 부른다(같은 선언 부류라는\n * 뜻이다). 그릇인지 아닌지는 **모양**이 말한다 — 목록이 되는 축은 배열이고, 한 벌뿐인 선언은\n * 객체 하나다. 개념 지도는 개체를 세는 면이므로 그릇은 자기 면에서 통째로 읽는다.\n */\n const raw = axisSource(a.axis) === 'document' ? at(model, a.path) : undefined\n /*\n * **관측 축은 상태가 센다**(일정·실적 — ISA-95 Part 4). 커널이 멈춰 있으면 `null` 이다:\n * 0 으로 내면 \"작업이 없는 공장\" 으로 읽히고, 그건 거짓이다(관측이 없는 것과 0 건은 다르다).\n * 이 화면이 관측 열에서 내내 지켜 온 구분을 카드에서도 지킨다.\n */\n if (axisSource(a.axis) === 'state') {\n /* 스냅샷은 `{revision, state}` 봉투다 — 벗기지 않으면 축이 한 겹 안에 있어 언제나 `null` 이\n 된다(커널은 도는데 카드가 \"모름\" 으로 보였다). 벗기는 규칙의 집은 `warm-start` 하나다. */\n /*\n * 관측 중인지는 **런타임이 있는지**로 본다 — 캐시된 스냅샷은 관측이 아니다.\n *\n * 동작하지 않으면 저널을 접어 답한다(라이브=권위 · 히스토리=파생). 카드는 그 차이를 `basis` 로\n * 말한다: 파생을 권위처럼 내면 다시 계산한 수를 \"지금 이만큼\" 으로 읽게 된다.\n */\n const { snap, folded, observing } = await stateOnce()\n /* 축이 사는 자리는 **커널 선언이 말한다**(`path`) — 축 이름으로만 읽으면 중첩된 축이\n 언제나 「관측 없음」이 된다(에너지의 수요 구간이 그렇다). 규칙은 `axisValueOf` 한 곳. */\n const bucket = snap ? axisValueOf(snap, a) ?? null : null\n return {\n axis: a.axis,\n label: a.label,\n kind: a.kind,\n container: false,\n standardClass: a.standardClass,\n typeRole: a.typeRole ?? null,\n count: Array.isArray(bucket) ? bucket.length : null,\n /*\n * **비었으면 왜 비었는지 말한다.** 0 과 `null` 만 내면 화면은 \"없다\" 까지만 말하고, 사용자는\n * \"안 적어 놨나 · 아직 안 일어났나 · 못 세었나\" 를 구별할 수 없다. 이 화면이 관측 열에서\n * 내내 지켜 온 구분(§observedAbsence)을 카드에서도 지킨다.\n */\n ...(Array.isArray(bucket) ? (bucket.length ? {} : { emptyBecause: 'none-yet' }) : { emptyBecause: 'not-observed' }),\n basis: folded ? 'journal' : 'state',\n observing,\n /* 선언이 아니라 관측인 축 — 화면이 \"원본이 침묵했다\" 로 읽지 않게 사실로 말한다. */\n observedOnly: true,\n standardUndeclared: !a.standardClass?.isa95 && !a.standardClass?.epcis && !a.standardClass?.iso55000\n }\n }\n return {\n axis: a.axis,\n label: a.label,\n kind: a.kind,\n container: !Array.isArray(raw) && raw != null,\n standardClass: a.standardClass,\n typeRole: a.typeRole ?? null,\n count: entity ? inRows : inDocument,\n /*\n * 비었으면 왜 비었는지 — 선언 쪽은 세 갈래다.\n * · `not-declared` 이 트윈의 모델이 그 축을 아예 말하지 않는다(대개 템플릿이 안 적었다)\n * · `declared-empty` 선언은 있는데 목록이 비어 있다(적었다가 지운 것도 사실이다)\n * · `not-projected` 문서엔 있는데 행에 없다 — 투영을 거치지 않은 트윈이다(`stale` 과 같은 사실)\n *\n * 셋을 \"0\" 하나로 합치면 사용자가 손볼 곳을 찾지 못한다: 첫째는 템플릿, 둘째는 원본,\n * 셋째는 다시 읽기다. 셋 다 다른 일이다.\n */\n ...((entity ? inRows : inDocument)\n ? {}\n : {\n emptyBecause:\n entity && inDocument > 0 ? 'not-projected' : raw == null ? 'not-declared' : 'declared-empty'\n }),\n /* 'rows' = DB 가 셌다(질의 가능) · 'document' = 문서를 세었다(아직 이주 전). */\n basis: entity ? 'rows' : 'document',\n ...(entity ? { documentCount: inDocument, stale } : {}),\n /* 표준 투영이 없는 축은 **숨기지 않고** 그 사실을 낸다(ADR-0031). */\n standardUndeclared: !a.standardClass?.isa95 && !a.standardClass?.epcis && !a.standardClass?.iso55000\n }\n })\n )\n\n /*\n * **원본이 모르는데 실행 중인 것** — 커널은 마스터에 없던 자원도 이벤트로 알게 되고\n * `origin: 'observed'` 를 단다(`observed-reducer`). 그것을 표에 밀어 넣지 않는다 — 그러면\n * 틱 루프에서 DB 로 쓰는 **두 번째 쓰기 경로**가 생기고, 실패하면 조용히 어긋난다\n * (쓰기 경로는 인제스트 하나, ADR-0032-D).\n *\n * 대신 **여기서 비교해 낸다.** 이 차이는 숨겨야 할 불일치가 아니라 인스펙션이 답해야 할\n * 질문이다 — \"원본이 말한 적 없는 설비가 실제로 실행 중이다.\"\n *\n * 트윈이 안 실행 중이면 비교할 상대가 없다 — 그때는 `null` 이다(0 이 아니다. 없는 것과\n * 세어 보지 못한 것은 다르다).\n */\n const snap = TwinEngine.snapshot(domainId, instanceId)\n const observedOnly = snap\n ? {\n equipment: (snap.equipment ?? []).filter((e: any) => e?.origin === 'observed').map((e: any) => e.id),\n locations: (snap.locations ?? []).filter((l: any) => l?.origin === 'observed').map((l: any) => l.id),\n persons: (snap.persons ?? []).filter((p: any) => p?.origin === 'observed').map((p: any) => p.id)\n }\n : null\n\n /* 지도의 선 — 선언된 관계 중 **양끝이 이 트윈에 실재하는 것만**. 없는 선을 그리지 않는다. */\n /* 선의 양끝 판정은 **문서든 행이든 있으면 있는 것**으로 본다 — 투영이 낡았다고 관계가 없어지진 않는다. */\n const nonEmpty = new Set(concepts.filter(c => (c.count ?? 0) > 0 || ((c as any).documentCount ?? 0) > 0).map(c => c.axis))\n /* 선은 **얼마나 이어졌고 얼마나 끊겼는지**까지 말한다 — 관계에서 가장 중요한 정보는 대개 부재다. */\n const edges = await Promise.all(\n TWIN_RELATIONS.filter((r: any) => r.target.kind === 'axis' && nonEmpty.has(r.from) && nonEmpty.has(r.target.axis)).map(\n async (r: any) => ({ from: r.from, to: r.target.axis, via: r.via, optional: !!r.optional, ...this.edgeFill(model, r) })\n )\n )\n\n return {\n instanceId,\n /* 사람이 부르는 이름 — 결선 줄이 업무키만 보이지 않게(업무키는 곁에 남는다). */\n name: inst.name ?? null,\n /*\n * **생산 선언에서 갈 곳이 없는 두 값** — GS1 발행자와 대표 산출물.\n *\n * 나머지 선언(자재·레시피·라우트·공정)은 각자 개념 축이 있고, 자재의 GS1 참조는 자재가 보인다.\n * 이 둘만 자기 축이 없다: 회사 접두사는 이 트윈의 **정체**(발행자)이고, 대표 레시피는 이 공장이\n * 무엇을 만드는 곳인가다. 둘 다 결선 줄(어느 원본을 반영하나)과 같은 성격이라 그 줄에 둔다.\n */\n /*\n * **표준 적합성** — 채운 것과 빠진 것을 함께.\n *\n * 이 화면은 \"이 트윈이 표준 모델의 어디를 채우고 있나\" 를 묻는다. 그런데 지금까지 답은 우리\n * 축만 세었다 — 없는 것은 화면에 없으므로 아무도 묻지 않았다. 기준(표준의 목록)을 함께 내면\n * 그 질문이 실제로 답을 갖는다(`isa95-coverage.ts` 의 머리말 참조).\n */\n /* 종류가 기대는 표준을 **모두** 낸다 — 하나만 말하면 그 트윈의 다른 기준이 보이지 않는다. */\n standards: standardsForKind(inst.kind).map(id => COVERAGE[id]((TWIN_AXES as any[]).map(a => a.axis))),\n production: (() => {\n const spec: any = (inst.model as any)?.productionSpec\n if (!spec) return null\n const key = spec.recipeKey ?? null\n const material = (spec.definition?.materials ?? []).find((m: any) => (m.key ?? m.id) === key)\n return {\n companyPrefix: spec.companyPrefix ?? null,\n recipeKey: key,\n /* 이름은 자재 선언이 말한다(i18n 키일 수 있으므로 번역은 화면의 일). */\n recipeLabel: material?.label ?? material?.name ?? null\n }\n })(),\n description: inst.description ?? null,\n kind: inst.kind,\n restartPolicy: inst.restartPolicy,\n source: ref\n ? { referenceId: ref.source, adapterType: ref.adapterType, status: ref.status, lastSyncedAt: ref.lastSyncedAt }\n : null,\n /*\n * **다시 읽을 수 있는가** — 이 트윈이 자기 원본을 기억하고 있나(`TwinInstance.origin`).\n *\n * 없는 트윈이 실제로 있다(이 자리가 생기기 전에 만들어진 것). 그때 화면이 버튼을 그대로\n * 내면 누를 때마다 실패하고, 사용자는 무엇이 잘못됐는지 알 수 없다. 그래서 **할 수 있는지를\n * 먼저 답한다** — 못 하는 것을 못 한다고 말하는 것도 이 화면의 일이다.\n */\n origin: inst.origin ?? null,\n canResync: !!(inst.origin as any)?.kind,\n concepts,\n edges,\n /* 원본이 모르는데 관측에는 있는 것. `null` = 트윈이 실행 중이 아니어서 비교하지 못했다. */\n observedOnly,\n /*\n * 축 셋만 행이고 나머지는 문서에서 셌다는 사실. 축마다 `basis` 가 있지만 여기서도 한 번 말한다 —\n * 화면이 \"이 숫자는 무엇으로 셌나\" 를 한눈에 보여야 규모 한계가 드러난다(ADR-0032 이주 압력).\n */\n basis: {\n rows: Object.keys(ROW_BACKED),\n document: TWIN_AXES.filter((a: any) => !ROW_BACKED[a.axis]).map((a: any) => a.axis),\n /* 투영이 낡은 축 — 있으면 이 트윈은 인제스트를 한 번 더 태워야 한다. */\n stale: concepts.filter((c: any) => c.stale).map((c: any) => c.axis)\n }\n }\n }\n /**\n * **이 트윈에 무엇을 선언할 수 있나** — 값을 넣는 화면이 도메인의 모양을 알지 않게.\n *\n * 커널이 정한 명세(속성 id·단위·범위·설명)를 그대로 낸다. 화면이 「에너지 트윈이면 계약전력·요금\n * 단가…」를 알고 있으면 도메인이 늘 때마다 화면을 고쳐야 하고 단위가 두 곳에 적힌다(ADR-0031).\n *\n * 지금 무엇이 선언돼 있는지도 함께 낸다(`declared`) — 화면이 두 번 묻지 않게, 그리고 「원천이 말한\n * 값」과 「우리가 정한 값」을 한자리에서 견줄 수 있게(`PropertySource`, ADR-0034).\n *\n * 어휘가 없는 종류는 `properties: []` 다 — 「아직 없다」와 「아무 값이나 받는다」는 다르다.\n */\n @Query(returns => ScalarObject, {\n nullable: true,\n description:\n \"What can be declared on this twin and what already is: the kernel's property specification (id, unit, range, note) plus the current local declarations at both scopes — this twin and its facility — each with the value it overrode. Empty properties mean this kind has no declaration vocabulary yet — not that anything goes.\"\n })\n async twinDeclarations(@Arg('instanceId') instanceId: string, @Ctx() context: ResolverContext): Promise<any | null> {\n const domainId = context.state.domain.id\n const inst = await getRepository(TwinInstance).findOne({ where: { domain: { id: domainId }, instanceId } })\n if (!inst) return null\n const model = (inst.model ?? {}) as any\n /*\n * 겹이 둘이다 — **현장의 것과 이 트윈의 것**. 화면이 둘을 함께 봐야 「이 값이 어느 범위의 것인가」를\n * 말할 수 있고, 철회 버튼이 어느 겹을 겨눌지 고를 수 있다(요금을 현장에 넣었는데 트윈에서 철회하려\n * 하면 「겹에 없다」로 끝난다 — 사용자는 이유를 알 수 없다).\n */\n const space = inst.spaceId\n ? await getRepository(TwinSpace).findOne({ where: { domain: { id: domainId }, spaceId: inst.spaceId } })\n : null\n /*\n * 돌고 있으면 커널이 「무엇으로 돌고 있나」를 스스로 답한다. 그 보고는 **작업이 새로 생길 때** 채워지\n * 므로 재기동 직후에는 비는데, 이력에서 배운 종류는 그때도 남아 있다(실측으로 이 빈칸에 부딪혔다).\n */\n const operations = operationBasis(\n model,\n TwinEngine.kernel(domainId, instanceId),\n await TwinEngine.measuredOperationKinds(domainId, instanceId)\n )\n return {\n instanceId,\n kind: inst.kind,\n spaceId: inst.spaceId,\n spaceName: space?.name,\n properties: declarablePropertySpecs(inst.kind),\n declared: (inst.localDeclarations ?? []) as any[],\n declaredAtSpace: (space?.localDeclarations ?? []) as any[],\n /* 어디에 얹을 수 있나 — 모델의 자리·설비·공정. 화면이 대상을 지어내지 않게 목록으로 준다. */\n targets: [\n ...(model.locations ?? []).map((l: any) => ({ target: 'location', id: l.id, name: l.name, type: l.type, properties: l.properties ?? [] })),\n ...(readBoardEquipment(model) ?? []).map((e: any) => ({ target: 'equipment', id: e.id, name: e.name, type: e.type ?? e.kind, properties: e.properties ?? [] })),\n /*\n * 공정은 **자원이 아니다** — 값은 종류에 붙고, 그 종류에 명세 행이 없어도 된다(창고·야드에는 행이\n * 0개다). 그래서 대상 목록을 커널의 자기보고까지 합쳐 만든다(`operationBasis`).\n *\n * 지금 값은 **겹에서** 든다(사용자가 넣은 수와 단위 그대로). 모델에는 ISO 8601 로 구워지지만\n * 화면에 「PT240S」를 보이면 자기가 넣은 값인지 알아볼 수 없다.\n */\n ...operations.map(op => ({\n target: 'operation',\n id: op.kind,\n name: op.label,\n properties: declaredDurationProperty(op.kind, inst.localDeclarations as any[], space?.localDeclarations as any[])\n }))\n ],\n /*\n * 이 트윈이 돌리는 공정과 **그 시간이 어디서 오나** — 선언 화면이 「무엇이 상수로 도나」를 보여야\n * 사람이 채울 곳을 안다. `basis` 가 비면 아직 돌지 않은 것이다(공정이 없는 것이 아니다).\n */\n operations\n }\n }\n /**\n * **이 현장에 무엇을 선언할 수 있나** — 현장 값은 트윈의 것이 아니므로 트윈 없이 묻는다.\n *\n * ── 왜 트윈으로 묻지 않나 (2026-08-18) ─────────────────────────────────────\n * 처음에는 트윈 화면에서 범위만 골라 현장 값을 넣게 했다. 그런데 **현장 값을 넣으려고 트윈을 골라야\n * 하는 것**은 개념이 거꾸로다 — 트윈은 그 값을 읽는 쪽이다(사용자가 그 어긋남을 지적했다).\n *\n * 어휘와 대상은 **그 현장 트윈들의 합집합**이다: 한 현장에 여러 종류가 살고(에너지·창고·야드), 요금은\n * 에너지 트윈만 읽는다. 합집합으로 내되 **어느 트윈이 그 대상을 가졌는지** 함께 낸다 — 아무 트윈도\n * 읽지 않는 값을 넣는 일을 화면이 미리 보여 줄 수 있어야 한다.\n */\n @Query(returns => ScalarObject, {\n nullable: true,\n description:\n 'What can be declared for a whole facility (space) and what already is: the union of the property specifications of the twin kinds living there, the union of their declarable targets (with which twins carry each), and the current facility-scope declarations.'\n })\n async spaceDeclarations(@Arg('spaceId') spaceId: string, @Ctx() context: ResolverContext): Promise<any | null> {\n const domainId = context.state.domain.id\n const space = await getRepository(TwinSpace).findOne({ where: { domain: { id: domainId }, spaceId } })\n if (!space) return null\n const twins = await getRepository(TwinInstance).find({ where: { domain: { id: domainId }, spaceId } })\n\n /* 어휘 합집합 — 같은 속성이 여러 종류에 있어도 한 번만 낸다(명세는 커널 하나에서 온다). */\n const byId = new Map<string, any>()\n for (const t of twins) for (const spec of declarablePropertySpecs(t.kind)) if (!byId.has(spec.id)) byId.set(spec.id, spec)\n\n /* 대상 합집합 — 같은 id 가 여러 트윈에 있으면 **그 트윈들을 함께** 든다(현장 값은 그 전부에 얹힌다). */\n const targets = new Map<string, { target: string; id: string; name?: string; type?: string; twins: string[] }>()\n for (const t of twins) {\n const model = (t.model ?? {}) as any\n const add = (target: 'location' | 'equipment' | 'operation', e: any) => {\n const key = `${target}:${e.id}`\n const cur = targets.get(key)\n if (cur) { cur.twins.push(t.instanceId); return }\n targets.set(key, { target, id: e.id, name: e.name, type: e.type ?? e.kind, twins: [t.instanceId] })\n }\n for (const l of model.locations ?? []) add('location', l)\n for (const e of readBoardEquipment(model) ?? []) add('equipment', e)\n /*\n * 공정도 대상이다 — 「우리 현장의 적재는 4분」은 그 현장 창고 전부가 읽어야 하는 수다. 목록은 각\n * 트윈의 것을 합집합으로 든다(돌고 있으면 커널이 스스로 답한다).\n */\n for (const op of operationBasis(\n model,\n TwinEngine.kernel(domainId, t.instanceId),\n await TwinEngine.measuredOperationKinds(domainId, t.instanceId)\n ))\n add('operation', { id: op.kind, name: op.label })\n }\n\n return {\n spaceId,\n spaceName: space.name,\n properties: [...byId.values()],\n declared: (space.localDeclarations ?? []) as any[],\n targets: [...targets.values()],\n /* 이 현장에 사는 트윈들 — 화면이 「누가 이 값을 읽나」를 말할 수 있게. */\n twins: twins.map(t => ({ instanceId: t.instanceId, kind: t.kind, name: t.name }))\n }\n }\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@things-factory/headless-twin",
|
|
3
|
-
"version": "10.0.
|
|
3
|
+
"version": "10.0.12",
|
|
4
4
|
"main": "dist-server/index.js",
|
|
5
5
|
"things-factory": true,
|
|
6
6
|
"author": "heartyoh <heartyoh@hatiolab.com>",
|
|
@@ -27,11 +27,11 @@
|
|
|
27
27
|
"clean:shared": "rm -rf dist-shared tsconfig.shared.tsbuildinfo"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@operato/twin-kernel": "^0.7.
|
|
30
|
+
"@operato/twin-kernel": "^0.7.38",
|
|
31
31
|
"@things-factory/auth-base": "^10.0.10",
|
|
32
32
|
"@things-factory/cache-service": "^10.0.10",
|
|
33
33
|
"@things-factory/env": "^10.0.8",
|
|
34
34
|
"@things-factory/shell": "^10.0.10"
|
|
35
35
|
},
|
|
36
|
-
"gitHead": "
|
|
36
|
+
"gitHead": "3cc678c07e9031a820662d43410f93b796ca012f"
|
|
37
37
|
}
|
|
@@ -1,12 +1,16 @@
|
|
|
1
|
-
import { ingest, ingestEnergyRecords, ingestEnergyEquipmentRecords,
|
|
2
|
-
isEnergyEquipmentRecord, type AdapterRule, type EnergyRecord,
|
|
3
|
-
type EnergyEquipmentRecord, type IngestResult } from '@operato/twin-kernel'
|
|
1
|
+
import { ingest, ingestEnergyRecords, ingestEnergyEquipmentRecords, ingestOperationalRecords, isAggregationRecord,
|
|
2
|
+
isEnergyRecord, isEnergyEquipmentRecord, isOperationalRecord, type AdapterRule, type EnergyRecord,
|
|
3
|
+
type EnergyEquipmentRecord, type OperationalRecord, type IngestResult } from '@operato/twin-kernel'
|
|
4
4
|
|
|
5
5
|
/*
|
|
6
6
|
* 정규 레코드 → 커널 검증 ACL → CanonicalEnvelope (P1 seam 코어).
|
|
7
7
|
* 순수(커널만 의존; shell·integration-base 무의존 → node:test 검증 가능).
|
|
8
8
|
* 매핑(레거시 스키마 → 정규)은 이 앞 단계(jsonata step 또는 커넥터 openLiveFeed)가 끝낸다.
|
|
9
|
-
* 여기 도착하는 레코드는 이미 정규
|
|
9
|
+
* 여기 도착하는 레코드는 이미 정규 필드다 — 어휘마다 자기 필드 이름이 계약이다:
|
|
10
|
+
* EPCIS 품목 { epc, action, bizStep, disposition?, readPoint?, bizLocation? }
|
|
11
|
+
* 에너지 계량 { meterId, kW?, kWh?, … } · 설비 에너지 { equipmentId, generatedKW?, soc?, … }
|
|
12
|
+
* 담김(적재) { parentID, childEPCs[] | childQuantityList[], action, bizStep, … }
|
|
13
|
+
* 운영 사실 { taskId | moverId | personId | assetId | orderId, status, … } // vocabulary-guard: allow 저널 와이어 필드
|
|
10
14
|
* 커널은 objectEvent 구성 + EPCIS 검증(위반=오염 차단) + 봉투 래핑만(방언 없음).
|
|
11
15
|
*
|
|
12
16
|
* 두 소비처가 이 단일 코어를 공유(중복 금지):
|
|
@@ -15,7 +19,7 @@ import { ingest, ingestEnergyRecords, ingestEnergyEquipmentRecords, isEnergyReco
|
|
|
15
19
|
* SoT: design/integration/face2-adapters.md §5(seam)·§7(매핑=밖, 검증=커널).
|
|
16
20
|
*/
|
|
17
21
|
|
|
18
|
-
/** 고정 정규 매핑(방언 아님 — 필드명 자체가 정규 계약). sourceType
|
|
22
|
+
/** 고정 정규 매핑(방언 아님 — 필드명 자체가 정규 계약). `sourceType` 으로 어느 품목 사실인지 고른다. */
|
|
19
23
|
const TWIN_INGEST_RULES: AdapterRule[] = [
|
|
20
24
|
{
|
|
21
25
|
sourceType: 'twin',
|
|
@@ -28,9 +32,30 @@ const TWIN_INGEST_RULES: AdapterRule[] = [
|
|
|
28
32
|
readPoint: '$.readPoint',
|
|
29
33
|
bizLocation: '$.bizLocation'
|
|
30
34
|
}
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
/*
|
|
38
|
+
* **담김의 사실** — 무엇이 무엇에 실렸나(팔레트·상자). 오래 비어 있던 자리다: 관측 리듀서는 이 사실을
|
|
39
|
+
* 받으면 자식 물품에 `parent` 를 붙이는데, 문이 `ObjectEvent` 하나만 알아서 원본은 적재를 말할 길이
|
|
40
|
+
* 없었다(시뮬 원본을 붙여 돌려 보고서야 로그로 드러났다).
|
|
41
|
+
*/
|
|
42
|
+
sourceType: 'twin-aggregation',
|
|
43
|
+
mapping: {
|
|
44
|
+
type: 'AggregationEvent',
|
|
45
|
+
action: '$.action',
|
|
46
|
+
bizStep: '$.bizStep',
|
|
47
|
+
disposition: '$.disposition',
|
|
48
|
+
parentID: '$.parentID',
|
|
49
|
+
childEPCs: '$.childEPCs',
|
|
50
|
+
/* 낱개 식별자 없이 **수량으로** 담긴 자식(「이 품번 40개」) — 입고·포장이 이 모양으로 온다. */
|
|
51
|
+
childQuantityList: '$.childQuantityList',
|
|
52
|
+
readPoint: '$.readPoint',
|
|
53
|
+
bizLocation: '$.bizLocation'
|
|
54
|
+
}
|
|
31
55
|
}
|
|
32
56
|
]
|
|
33
57
|
|
|
58
|
+
/** 개체 관측 — 물건 하나가 어디서 무엇이 됐나. */
|
|
34
59
|
export interface CanonicalRecord {
|
|
35
60
|
epc: string
|
|
36
61
|
action: string // ADD | OBSERVE | DELETE
|
|
@@ -40,13 +65,34 @@ export interface CanonicalRecord {
|
|
|
40
65
|
bizLocation?: string
|
|
41
66
|
}
|
|
42
67
|
|
|
68
|
+
/**
|
|
69
|
+
* 담김 관측 — **부모와 자식**으로 말한다. 자리 필드는 개체 관측과 같은 이름이다(문자열).
|
|
70
|
+
*
|
|
71
|
+
* 이름은 표준의 것을 그대로 쓴다(`parentID`·`childEPCs`) — 여기서 다시 지으면 매핑이 번역이 되고,
|
|
72
|
+
* 같은 사실에 두 어휘가 생긴다.
|
|
73
|
+
*/
|
|
74
|
+
export interface CanonicalAggregationRecord {
|
|
75
|
+
parentID: string
|
|
76
|
+
/** 개체로 담긴 자식. 수량으로만 담겼으면 없다(둘 중 하나는 있어야 한다). */
|
|
77
|
+
childEPCs?: string[]
|
|
78
|
+
/** 수량으로 담긴 자식 — `{ epcClass, quantity, uom? }`. 값의 옳고 그름은 커널 검증이 본다. */
|
|
79
|
+
childQuantityList?: { epcClass: string; quantity?: number; uom?: string }[]
|
|
80
|
+
action: string // ADD(담다) | DELETE(풀다) | OBSERVE
|
|
81
|
+
bizStep: string
|
|
82
|
+
disposition?: string
|
|
83
|
+
readPoint?: string
|
|
84
|
+
bizLocation?: string
|
|
85
|
+
}
|
|
86
|
+
|
|
43
87
|
/** 정규 레코드[] → 검증된 CanonicalEnvelope[] (+ 거부분). 순수 함수. 단일→객체도 배열로 정규화. */
|
|
44
88
|
export function ingestCanonicalRecords(
|
|
45
89
|
records:
|
|
46
90
|
| CanonicalRecord
|
|
91
|
+
| CanonicalAggregationRecord
|
|
47
92
|
| EnergyRecord
|
|
48
93
|
| EnergyEquipmentRecord
|
|
49
|
-
|
|
|
94
|
+
| OperationalRecord
|
|
95
|
+
| (CanonicalRecord | CanonicalAggregationRecord | EnergyRecord | EnergyEquipmentRecord | OperationalRecord)[]
|
|
50
96
|
| undefined
|
|
51
97
|
| null,
|
|
52
98
|
tenantId: string,
|
|
@@ -54,29 +100,47 @@ export function ingestCanonicalRecords(
|
|
|
54
100
|
): IngestResult {
|
|
55
101
|
const arr = Array.isArray(records) ? records : records ? [records] : []
|
|
56
102
|
/*
|
|
57
|
-
* **어휘가
|
|
58
|
-
* 그래서 계측을 이 길로 넣으면 `epc` 가 없어 거부되거나(그나마 나은 쪽)
|
|
103
|
+
* **어휘가 넷이다** — 물류(EPCIS)·에너지 계량·설비 에너지·운영 사실. 예전에는 이 함수가 무조건
|
|
104
|
+
* `ObjectEvent` 를 만들었고, 그래서 계측을 이 길로 넣으면 `epc` 가 없어 거부되거나(그나마 나은 쪽)
|
|
105
|
+
* 엉뚱한 물품 관측이 됐다.
|
|
106
|
+
*
|
|
107
|
+
* 물류 어휘 안에는 모양이 둘이다(개체 관측 `epc` / 담김 `parentID`+`childEPCs`).
|
|
59
108
|
*
|
|
60
|
-
* 판정은 커널이 한 곳에서 한다(`isEnergyRecord`) — 여기서 다시 짐작하면
|
|
61
|
-
* 봉투는 같은 것을 쓰므로 아래 소비처(미러·저널·방송)는 이 갈림을 알
|
|
109
|
+
* 판정은 커널이 한 곳에서 한다(`isEnergyRecord`·`isOperationalRecord`·`isAggregationRecord`) — 여기서 다시 짐작하면
|
|
110
|
+
* 소비처마다 답이 달라진다. 봉투는 같은 것을 쓰므로 아래 소비처(미러·저널·방송)는 이 갈림을 알
|
|
111
|
+
* 필요가 없다.
|
|
62
112
|
*/
|
|
63
113
|
const energy = arr.filter(isEnergyRecord) as EnergyRecord[]
|
|
64
114
|
/* 세 번째 어휘 — 설비가 낸 자기 에너지 상태(발전·저장·감축 여지·개폐 위치). 계량과 갈라 두는
|
|
65
115
|
이유는 커널에 적혀 있다: 계량은 구간에 누적되고, 이것들은 그 설비의 지금이다. */
|
|
66
116
|
const energyEquipment = arr.filter(isEnergyEquipmentRecord) as unknown as EnergyEquipmentRecord[]
|
|
67
|
-
|
|
117
|
+
/*
|
|
118
|
+
* 네 번째 어휘 — **운영 사실**(작업·설비·사람·자산·오더·품질). 관측 리듀서는 이것을 오래전부터
|
|
119
|
+
* 접었는데 **들어올 문이 없었다**: 원본이 「이 작업이 끝났다」를 말할 길이 없어 시뮬 커널만 그것을
|
|
120
|
+
* 아는 상태가 남았다(같은 화면이 두 구동에서 다른 것을 말한다).
|
|
121
|
+
*/
|
|
122
|
+
const operational = arr.filter(isOperationalRecord) as OperationalRecord[]
|
|
123
|
+
const epcis = arr.filter(r => !isEnergyRecord(r) && !isEnergyEquipmentRecord(r) && !isOperationalRecord(r)) as (CanonicalRecord | CanonicalAggregationRecord)[]
|
|
68
124
|
|
|
125
|
+
/* 품목 어휘 안에서도 갈림이 있다(개체 관측 / 담김) — 그 판정도 커널의 것을 부른다. */
|
|
69
126
|
const epcisResult = epcis.length
|
|
70
|
-
? ingest(
|
|
127
|
+
? ingest(
|
|
128
|
+
epcis.map(r => ({ ...r, sourceType: isAggregationRecord(r) ? 'twin-aggregation' : 'twin' })),
|
|
129
|
+
TWIN_INGEST_RULES,
|
|
130
|
+
{ tenantId, defaultEventTime }
|
|
131
|
+
)
|
|
71
132
|
: { accepted: [], rejected: [] }
|
|
72
133
|
const energyResult = energy.length ? ingestEnergyRecords(energy, { tenantId, defaultEventTime }) : { accepted: [], rejected: [] }
|
|
73
134
|
const equipmentResult = energyEquipment.length
|
|
74
135
|
? ingestEnergyEquipmentRecords(energyEquipment, { tenantId, defaultEventTime })
|
|
75
136
|
: { accepted: [], rejected: [] }
|
|
137
|
+
const operationalResult = operational.length
|
|
138
|
+
? ingestOperationalRecords(operational, { tenantId, defaultEventTime })
|
|
139
|
+
: { accepted: [], rejected: [] }
|
|
76
140
|
|
|
77
141
|
/* 거부분을 합쳐 돌려준다 — 어느 어휘에서 거부됐는지는 이유 문구가 말한다. */
|
|
78
142
|
return {
|
|
79
|
-
accepted: [...epcisResult.accepted, ...energyResult.accepted, ...equipmentResult.accepted],
|
|
80
|
-
rejected: [...epcisResult.rejected, ...energyResult.rejected, ...equipmentResult.rejected]
|
|
143
|
+
accepted: [...epcisResult.accepted, ...energyResult.accepted, ...equipmentResult.accepted, ...operationalResult.accepted],
|
|
144
|
+
rejected: [...epcisResult.rejected, ...energyResult.rejected, ...equipmentResult.rejected, ...operationalResult.rejected]
|
|
81
145
|
}
|
|
82
146
|
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* **자극은 원본이 선언한다** — 트윈이 아니라.
|
|
3
|
+
*
|
|
4
|
+
* ── 무엇이 났나 (2026-08-19) ────────────────────────────────────────────────
|
|
5
|
+
* 데모의 자극(시나리오)은 시드 코드가 **메모리에만** 실었다: `runtime.scenario.load(...)`. 그래서 트윈을
|
|
6
|
+
* 재기동하면 자극이 사라지고, 구조만 서 있는 트윈이 남는다 — 작업 0·오더 0. 「살아 있는 데모」가
|
|
7
|
+
* **첫 재기동까지만** 사는 것이다(오늘 서버를 여러 번 띄우는 동안 실제로 그 상태였다).
|
|
8
|
+
*
|
|
9
|
+
* ── 왜 트윈에 저장하지 않나 (ADR-0029 · plans/simulator-as-source.md §4) ────
|
|
10
|
+
* 자극은 **원본의 거동**이다: 무엇이 들어오고 무슨 주문이 나는지는 시뮬레이터(원본)가 정하는 사실이고,
|
|
11
|
+
* 트윈은 그것을 반영한다. 트윈에 새 축을 만들면 ADR-0029 가 나중에 걷어내야 하는 표면이 하나 늘고
|
|
12
|
+
* (그 계획서의 §2·§5 가 이미 그 부담을 적어 두었다), 원본에 두면 그 계획의 **첫 조각 앞자리**가 된다:
|
|
13
|
+
* 지금은 호스트가 읽어 커널에 싣고, 나중에 `openLiveFeed` 뒤로 옮기면 같은 선언이 그대로 쓰인다.
|
|
14
|
+
*
|
|
15
|
+
* 그래서 집은 `TwinReference.connectionConfig.scenario` — 어댑터별 설정 자리(이미 `simple-json`)다.
|
|
16
|
+
*
|
|
17
|
+
* ── 판정을 순수 함수로 (이 파일) ────────────────────────────────────────────
|
|
18
|
+
* 「이 트윈에 저장된 자극을 실을 것인가」에는 사람이 틀리기 쉬운 갈림이 셋 있다: 미러에 실으면 **현실을
|
|
19
|
+
* 오염**시키고, 잘못된 선언을 실으면 **다음 틱에서 서버가 내려가고**(실제로 그 이력이 있다), 시나리오
|
|
20
|
+
* 엔진이 없는 런타임에 실으면 조용히 아무 일도 없다. 그래서 판정을 값으로 만들고 시험한다.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/** 자극을 실을지 말지 — 그리고 왜. 부르는 쪽은 이 답만 보고 움직인다. */
|
|
24
|
+
export type StimulusPlan =
|
|
25
|
+
/** 실어라 — 검사를 통과한 선언. */
|
|
26
|
+
| { action: 'load'; scenario: any }
|
|
27
|
+
/**
|
|
28
|
+
* 싣지 않는다 — 그 이유.
|
|
29
|
+
* · `none` — 원본이 자극을 선언하지 않았다(정상: 정적 구조만 반영하는 트윈)
|
|
30
|
+
* · `mirror` — 미러다. 외부 실물이 진실인 트윈에 우리가 자극을 만들면 그것은 현실이 아니다
|
|
31
|
+
* · `no-engine` — 이 런타임에 시나리오 엔진이 없다(관측 구동 커널)
|
|
32
|
+
* · `invalid` — 선언이 커널 규약을 어긴다. **싣지 않고 말한다**(다음 틱에서 터지게 두지 않는다)
|
|
33
|
+
*/
|
|
34
|
+
| { action: 'skip'; reason: 'none' | 'observed' | 'no-engine' | 'invalid'; detail?: string }
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 저장된 자극 선언을 읽어 **실을지 판정한다** — 커널 검사기를 그대로 쓴다(규칙을 두 벌로 만들지 않는다).
|
|
38
|
+
*
|
|
39
|
+
* `validate` 는 커널의 `validateScenario` 를 넘긴다(호스트가 커널 타입에 묶이지 않게 함수로 받는다).
|
|
40
|
+
* 검사기가 없으면 **싣지 않는다**: 검사할 수 없는 선언을 태우는 것이 가장 비싼 선택이다(서버가 내려간
|
|
41
|
+
* 이력이 있다).
|
|
42
|
+
*/
|
|
43
|
+
export function planStimulus(
|
|
44
|
+
config: any,
|
|
45
|
+
runtime: { hasScenarioEngine: boolean; mode?: 'sim' | 'live' },
|
|
46
|
+
validate?: (scenario: any) => { ok: boolean; errorCode?: string; errorParams?: any } | { ok: false; errorCode: string }
|
|
47
|
+
): StimulusPlan {
|
|
48
|
+
const scenario = config?.scenario
|
|
49
|
+
if (!scenario || typeof scenario !== 'object') return { action: 'skip', reason: 'none' }
|
|
50
|
+
/* 관측 트윈을 먼저 막는다 — 「엔진이 없다」로 답하면 이유가 흐려진다(애초에 실을 수 없는 트윈이다). */
|
|
51
|
+
if (runtime.mode === 'live') return { action: 'skip', reason: 'observed' }
|
|
52
|
+
if (!runtime.hasScenarioEngine) return { action: 'skip', reason: 'no-engine' }
|
|
53
|
+
if (!validate) return { action: 'skip', reason: 'invalid', detail: 'no validator available' }
|
|
54
|
+
const v = validate(scenario)
|
|
55
|
+
if (!v?.ok) return { action: 'skip', reason: 'invalid', detail: (v as any)?.errorCode ?? 'rejected' }
|
|
56
|
+
return { action: 'load', scenario }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 선언을 원본 설정에 얹는다(순수) — **기존 설정을 지우지 않는다.**
|
|
61
|
+
*
|
|
62
|
+
* 어댑터 설정에는 접속 정보가 함께 산다. 자극을 넣으면서 그 옆을 덮으면 다음 재동기가 원본을 못 찾는다.
|
|
63
|
+
*/
|
|
64
|
+
export function withStimulus(config: any, scenario: any): any {
|
|
65
|
+
return { ...(config ?? {}), scenario }
|
|
66
|
+
}
|
package/server/engine/index.ts
CHANGED
|
@@ -89,6 +89,15 @@ export interface KpiGroup {
|
|
|
89
89
|
waitTime: DurationStats
|
|
90
90
|
/** 이 축에 붙어 있던 시간의 합(자원 축에서 특히 의미 있다). */
|
|
91
91
|
busyMs: number
|
|
92
|
+
/**
|
|
93
|
+
* **품질 판정이 있었던 건수만** 센다 (2026-08-19).
|
|
94
|
+
*
|
|
95
|
+
* 판정이 없는 작업(이동·체류·중간 스테이션)은 어느 쪽에도 세지 않는다 — 모름을 양품으로 세면 양품률이
|
|
96
|
+
* 조용히 100% 로 올라간다. 그래서 `good + scrap` 은 `tasks` 보다 작을 수 있고, 그것이 정상이다.
|
|
97
|
+
*
|
|
98
|
+
* 판정이 하나도 없으면 **필드를 만들지 않는다**(0/0 을 「양품률 0%」로 읽히게 두지 않는다).
|
|
99
|
+
*/
|
|
100
|
+
quality?: { good: number; scrap: number }
|
|
92
101
|
}
|
|
93
102
|
|
|
94
103
|
export interface KpiResult {
|
|
@@ -517,6 +526,24 @@ function facetKey(rec: CompletionRecord, options: KpiFoldOptions): string {
|
|
|
517
526
|
}
|
|
518
527
|
}
|
|
519
528
|
|
|
529
|
+
/**
|
|
530
|
+
* 이 축의 품질 — **판정이 있었던 것만** 센다(없으면 필드를 만들지 않는다).
|
|
531
|
+
*
|
|
532
|
+
* 양품률의 이력이 여기서 나온다: 커널이 작업 완료에 결과를 적고(`TaskStatusDelta.outcome`), 폴드가 축으로
|
|
533
|
+
* 들고(`TaskFacets.outcome`), 여기서 종류별로 센다. 그 사슬이 없으면 「이 공정의 양품률」은 저널에
|
|
534
|
+
* 남지 않는다(설비 누적 카운터는 종류를 모른다).
|
|
535
|
+
*/
|
|
536
|
+
function qualityOf(rows: CompletionRecord[]): { quality?: { good: number; scrap: number } } {
|
|
537
|
+
let good = 0
|
|
538
|
+
let scrap = 0
|
|
539
|
+
for (const r of rows) {
|
|
540
|
+
const o = r.facets.outcome
|
|
541
|
+
if (o === 'good') good++
|
|
542
|
+
else if (o === 'scrap') scrap++
|
|
543
|
+
}
|
|
544
|
+
return good + scrap > 0 ? { quality: { good, scrap } } : {}
|
|
545
|
+
}
|
|
546
|
+
|
|
520
547
|
/**
|
|
521
548
|
* 기록을 축으로 묶어 축별 통계를 낸다 — **전체와 같은 규칙**으로(같은 stats 함수, 같은 창).
|
|
522
549
|
*
|
|
@@ -543,7 +570,8 @@ function groupRecords(
|
|
|
543
570
|
leadTime: stats(rows.map(r => r.leadMs).filter((v): v is number => v !== undefined)),
|
|
544
571
|
workTime: stats(rows.map(r => r.workMs).filter((v): v is number => v !== undefined)),
|
|
545
572
|
waitTime: stats(rows.map(r => r.waitMs).filter((v): v is number => v !== undefined)),
|
|
546
|
-
busyMs: rows.reduce((a, r) => a + (r.workMs ?? 0), 0)
|
|
573
|
+
busyMs: rows.reduce((a, r) => a + (r.workMs ?? 0), 0),
|
|
574
|
+
...qualityOf(rows)
|
|
547
575
|
}))
|
|
548
576
|
.sort((a, b) => b.tasks - a.tasks || (a.key < b.key ? -1 : 1))
|
|
549
577
|
|
|
@@ -135,11 +135,11 @@ export interface TwinKpiOutput extends Omit<Partial<KpiResult>, 'window'> {
|
|
|
135
135
|
/**
|
|
136
136
|
* 무엇을 집계했나 — 공간이면 합쳐 접은 트윈 목록.
|
|
137
137
|
*
|
|
138
|
-
* `
|
|
139
|
-
*
|
|
138
|
+
* `restartPolicies` 는 **이 숫자가 무엇의 성과인지**를 말한다: mirror(실제 시스템을 반영) ·
|
|
139
|
+
* `resume`(이어지는 가상 세계) · `reset`(재현 실험 사본). 같은 "345건" 이 현실의 실적일 수도, 시뮬레이션의
|
|
140
140
|
* 산출일 수도 있다 — 그 구별 없이 숫자만 보여주면 사용자가 시뮬 결과를 실적으로 착각한다.
|
|
141
141
|
*/
|
|
142
|
-
scope: { spaceId?: string; instanceIds: string[];
|
|
142
|
+
scope: { spaceId?: string; instanceIds: string[]; restartPolicies: string[] }
|
|
143
143
|
/** 구간별 값(요청했을 때만) — 추세를 그리기 위한 최소 정보. */
|
|
144
144
|
buckets?: { fromTime: string; toTime: string; tasks: number; orders: number; workP50Ms: number }[]
|
|
145
145
|
/**
|
|
@@ -508,7 +508,7 @@ async function fetchEvents(domainId: string, instanceIds: string[], fromMs: numb
|
|
|
508
508
|
* 공간이 주어지면 **가동 중이고 운영 목적인** 것만 모은다 — 벤치 사본은 사용자가 보는 현장이 아니다.
|
|
509
509
|
* 트윈이 지정되면 그것 하나. 둘 다 없으면 무엇을 재야 할지 알 수 없으므로 명시 실패한다.
|
|
510
510
|
*/
|
|
511
|
-
async function resolveTargets(input: TwinKpiInput): Promise<{ instanceIds: string[];
|
|
511
|
+
async function resolveTargets(input: TwinKpiInput): Promise<{ instanceIds: string[]; restartPolicies: string[] }> {
|
|
512
512
|
if (!input.instanceId && !input.spaceId) throw new Error('either instanceId or spaceId is required')
|
|
513
513
|
const list = await TwinEngine.list(input.domainId)
|
|
514
514
|
const rows = input.instanceId
|
|
@@ -517,8 +517,8 @@ async function resolveTargets(input: TwinKpiInput): Promise<{ instanceIds: strin
|
|
|
517
517
|
/* 목록에서 못 찾아도(정지·비등록) 지정된 트윈은 그대로 조회한다 — 저널은 남아 있을 수 있다. */
|
|
518
518
|
const instanceIds = rows.length ? rows.map((x: any) => x.instanceId as string) : input.instanceId ? [input.instanceId] : []
|
|
519
519
|
/* 컬럼이 NOT NULL 이라 'unknown' 으로 메울 일이 없다 — 비면 계약이 깨진 것이지 모르는 상태가 아니다. */
|
|
520
|
-
const
|
|
521
|
-
return { instanceIds,
|
|
520
|
+
const restartPolicies = [...new Set(rows.map((x: any) => String(x.restartPolicy)))]
|
|
521
|
+
return { instanceIds, restartPolicies }
|
|
522
522
|
}
|
|
523
523
|
|
|
524
524
|
/**
|
|
@@ -541,8 +541,8 @@ export async function computeTwinKpi(input: TwinKpiInput): Promise<TwinKpiOutput
|
|
|
541
541
|
const minutes = Math.max(1, Math.min(24 * 60, Number(input.windowMinutes) || 60))
|
|
542
542
|
const lookbackMs = Math.max(0, Math.min(24 * 60, input.lookbackMinutes ?? 60)) * 60_000
|
|
543
543
|
|
|
544
|
-
const { instanceIds,
|
|
545
|
-
const scope = { spaceId: input.spaceId, instanceIds,
|
|
544
|
+
const { instanceIds, restartPolicies } = await resolveTargets(input)
|
|
545
|
+
const scope = { spaceId: input.spaceId, instanceIds, restartPolicies }
|
|
546
546
|
|
|
547
547
|
/* 끝 시각 — 지정이 없으면 대상들의 마지막 기록. 그것도 없으면 측정할 것이 없다. */
|
|
548
548
|
let basis: 'given' | 'latest-event' = 'given'
|
|
@@ -51,8 +51,8 @@ export function liveFeedAttached(instanceId: string): boolean {
|
|
|
51
51
|
*/
|
|
52
52
|
export type LiveFeedState = 'attached' | 'detached' | 'not-applicable'
|
|
53
53
|
|
|
54
|
-
export function liveFeedStateOf(args: {
|
|
55
|
-
if (args.
|
|
54
|
+
export function liveFeedStateOf(args: { restartPolicy?: string; running?: boolean; instanceId: string }): LiveFeedState {
|
|
55
|
+
if (args.restartPolicy !== 'resync') return 'not-applicable'
|
|
56
56
|
if (!args.running) return 'not-applicable'
|
|
57
57
|
return liveFeedAttached(args.instanceId) ? 'attached' : 'detached'
|
|
58
58
|
}
|