@things-factory/headless-twin 10.1.28 → 10.1.29
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/service/reference/actuation-verification.d.ts +22 -0
- package/dist-server/service/reference/actuation-verification.js +37 -0
- package/dist-server/service/reference/actuation-verification.js.map +1 -1
- package/dist-server/service/reference/reference-resolver.js +32 -2
- package/dist-server/service/reference/reference-resolver.js.map +1 -1
- package/dist-server/service/twin-model/twin-lineage-query.js +14 -0
- package/dist-server/service/twin-model/twin-lineage-query.js.map +1 -1
- package/dist-server/service/twin-model/twin-model-item-query.js +11 -6
- package/dist-server/service/twin-model/twin-model-item-query.js.map +1 -1
- package/package.json +7 -7
- package/server/service/reference/actuation-verification.ts +42 -0
- package/server/service/reference/reference-resolver.ts +33 -2
- package/server/service/twin-model/twin-lineage-query.ts +13 -0
- package/server/service/twin-model/twin-model-item-query.ts +33 -6
- package/test/actuation-verification-state.test.ts +46 -1
- package/test/axis-declaration-passthrough-db.test.ts +43 -1
- package/test/lineage-survives-restart.test.ts +15 -0
- package/test/twin-model-item-db.test.ts +18 -1
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"twin-lineage-query.js","sourceRoot":"","sources":["../../../server/service/twin-model/twin-lineage-query.ts"],"names":[],"mappings":"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","sourcesContent":["import { Arg, Ctx, Int, Query, Resolver } from 'type-graphql'\n\nimport { ScalarObject, getRepository } from '@things-factory/shell'\n\nimport { itemRefOf, resolveByDeclaration } from './item-ref.js'\nimport { chooseRecipe } from './choose-recipe.js'\n\nimport { TwinInstance } from '../twin-instance/twin-instance.js'\nimport { TwinEvent } from '../twin-event/twin-event.js'\n\n/*\n * 계보 — **이 개체는 무엇에서 나왔나**, 그리고 **설계대로 만들어졌나.**\n *\n * ── 구성과 계보는 주어가 다르다 ────────────────────────────────────────────────\n * 구성(BOM)의 주어는 품목 **정의**다 — \"트레일러는 프레임 1개로 만든다\"(선언).\n * 계보의 주어는 품목 **개체**다 — \"이 트레일러는 저 프레임 로트에서 나왔다\"(관측).\n * 둘은 같은 모양이라 겹쳐 볼 수 있고, **그 차이가 곧 진단**이다: \"설계는 1개인데 이건 2개를 먹었다\".\n * 트윈이 아니면 못 하는 답이고, 이 질의가 그 답의 재료다.\n *\n * ── 어디서 읽나 ────────────────────────────────────────────────────────────────\n * 계보는 상태가 아니라 **저널**에 있다(EPCIS `TransformationEvent` 의 입력·산출 목록). 상태는\n * 지금 무엇이 있는지만 알고, 없어진 원자재가 무엇이었는지는 이벤트만 안다.\n *\n * ── 규모 ───────────────────────────────────────────────────────────────────────\n * 입력 목록은 **컬럼이 아니라 봉투 안**에 있어서 인덱스로 좁힐 수 없다. 그래서 변환 이벤트만\n * 창으로 끊어 읽고, **끊었으면 끊었다고 말한다.** 그 사실을 감추면 \"이 개체는 원자재가 없다\" 로\n * 읽히고, 계보에서 그 오해는 리콜 범위를 좁게 잡는 결과로 이어진다.\n */\n\n\n@Resolver()\nexport class TwinLineageQuery {\n @Query(returns => ScalarObject, {\n nullable: true,\n description:\n 'Where one item came from (observed) laid next to what its type declares (BOM). Walks TransformationEvent input/output lists in the journal; reports the window it read and whether it was truncated. The declared|observed difference is the diagnosis: \"the recipe says one frame, this one consumed two\".'\n })\n async twinItemLineage(\n @Arg('instanceId') instanceId: string,\n @Arg('epc') epc: string,\n @Ctx() context: ResolverContext,\n @Arg('maxDepth', type => Int, { nullable: true }) maxDepth?: number,\n @Arg('window', type => Int, { nullable: true }) window?: number\n ): 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 const depthCap = Math.min(Math.max(maxDepth ?? 6, 1), 12)\n const windowCap = Math.min(Math.max(window ?? 5000, 1), 50000)\n\n /*\n * 변환 이벤트만 읽는다. **최신부터** 읽는다 — 계보를 묻는 개체는 대개 최근에 만들어진 것이고,\n * 창을 넘겨 잘릴 때 잃는 쪽이 오래된 것이어야 덜 아프다.\n */\n /*\n * 관계를 **컬럼으로** 묻는다 (2026-08-28). `find({ where: { domain: { id } } })` 은 관계를 가진\n * 엔티티에서 두 단계 질의가 되고 **안쪽에 상한이 없다** — `take` 가 있어도 조건에 맞는 행을 전부\n * 만든 뒤 밖에서 자른다. 변환 사건이 적은 트윈에서는 표가 안 나지만, 변환을 많이 내는 트윈에서는\n * 이 한 번이 그 종류 전체를 만든다. 질의 빌더로 물으면 상한이 안쪽에 걸린다.\n *\n * 종류가 정확일치이므로 `(domain, instance, event_type, event_time)` 색인이 시각 순서까지 준다.\n */\n const rows: any[] = await getRepository(TwinEvent)\n .createQueryBuilder('e')\n .where('e.domain = :domainId', { domainId })\n .andWhere('e.instanceId = :instanceId', { instanceId })\n .andWhere('e.eventType = :eventType', { eventType: 'epcis.TransformationEvent' })\n .orderBy('e.eventTime', 'DESC')\n .limit(windowCap)\n .getMany()\n const truncated = rows.length >= windowCap\n\n /* 산출 → 그 변환. 한 개체는 한 번만 만들어지지만, 데이터가 그렇지 않을 수 있으므로 목록으로 든다. */\n const madeBy = new Map<string, { inputs: string[]; at?: string; tx?: string }[]>()\n for (const r of rows) {\n const d = r.payload?.data ?? {}\n const outs: string[] = d.outputEPCList ?? []\n const ins: string[] = d.inputEPCList ?? []\n const step = { inputs: ins, at: r.eventTime, tx: d.bizTransactionList?.[0]?.bizTransaction }\n for (const o of outs) {\n const list = madeBy.get(o) ?? []\n list.push(step)\n madeBy.set(o, list)\n }\n }\n\n /* 거슬러 올라간다 — 너비 우선, 지나온 것은 다시 파지 않는다(사이클·공유 부품). */\n const items: any[] = [{ epc, itemRef: itemRefOf(epc), depth: 0 }]\n const edges: any[] = []\n const seen = new Set<string>([epc])\n let depthTruncated = false\n let frontier = [{ epc, depth: 0 }]\n while (frontier.length) {\n const next: typeof frontier = []\n for (const cur of frontier) {\n if (cur.depth >= depthCap) { depthTruncated = true; continue }\n for (const step of madeBy.get(cur.epc) ?? []) {\n for (const input of step.inputs) {\n edges.push({ from: cur.epc, to: input, at: step.at, tx: step.tx, depth: cur.depth + 1 })\n if (seen.has(input)) continue\n seen.add(input)\n items.push({ epc: input, itemRef: itemRefOf(input), depth: cur.depth + 1 })\n next.push({ epc: input, depth: cur.depth + 1 })\n }\n }\n }\n frontier = next\n }\n\n /*\n * **겹쳐 본다.** 이 개체의 품목이 어느 레시피의 산출이면, 그 레시피가 선언한 투입과 실제로\n * 소비된 것을 자재별로 나란히 센다.\n *\n * 개체(EPC)와 자재 키를 잇는 것은 `productionSpec.binding`(자재 키 → GS1 품목 참조)이다.\n * 바인딩이 없으면 **비교하지 않는다** — 짐작으로 이으면 \"설계와 다르다\" 는 거짓 진단이 나오고,\n * 그건 아무 말도 안 하는 것보다 나쁘다.\n */\n const spec: any = (inst.model as any)?.productionSpec\n const binding: Record<string, string> = spec?.binding ?? {}\n const refToMaterial = new Map(Object.entries(binding).map(([mat, ref]) => [String(ref), mat]))\n /*\n * ── **화면은 끝마디로 묻는다** (2026-08-23) ─────────────────────────────────\n *\n * 서랍의 포커스 식별자는 클래스의 끝마디다(`net/class/product-M0000438`). 대응 표의 값은 전체\n * 식별자(`https://…/class/product-M0000438`)이므로 곧바로는 맞지 않고, 그래서 「선언된 자재가\n * 아닙니다」가 떴다 — 그 품목은 대응 표에 있었다.\n *\n * 끝마디로도 찾되 **하나뿐일 때만** 인정한다. 둘 이상이 같은 끝마디를 가지면 어느 것인지 모르므로\n * 아무 말도 하지 않는다 — 짐작으로 이으면 남의 레시피와 견주게 되고, 그것이 「설계와 다르다」는\n * 거짓 진단이 된다.\n */\n const materialOf = (e: string) => resolveByDeclaration(itemRefOf(e), refToMaterial) ?? null\n\n const myMaterial = materialOf(epc)\n const recipes: any[] = spec?.definition?.recipes ?? []\n\n /*\n * ── 「무엇으로 만들었나」는 **적혀 있다** (2026-09-05) ───────────────────────\n *\n * 예전에는 이 자리가 자재로 되짚어 `recipes.find(...)` 로 **먼저 걸리는 하나**를 골랐다. 배열\n * 순서가 답을 정한다는 뜻이고, 한 자재에 레시피가 둘이면 틀린 답이 확신에 찬 얼굴로 나온다.\n * 실제로 그런 자재가 있다 — `BOLT-M8` 에 `RCP-BOLT-STD` · `RCP-BOLT-FAST` 둘이다.\n *\n * 그런데 답은 이미 기록되어 있다. 만들 때 정해지는 사실이고 커널이 오더에 싣는다\n * (`FlowOrder.recipeKey`). 변환 사건은 그 오더를 거래 식별자로 가리키고(`bizTransactionList`),\n * 이 함수는 그 값을 이미 `tx` 로 꺼내 놓고도 쓰지 않았다.\n *\n * 세 자리가 다 있었고 마지막 연결만 없었다. 「가릴 수 없다」가 아니라 **안 읽고 있었다.**\n *\n * ── 규칙 셋 — 짐작하지 않는다 ───────────────────────────────────────────────\n * 오더가 말한다 그것을 쓴다. 판이 둘이든 열이든 상관없다\n * 오더가 말 안 하고\n * 후보가 하나 그것을 쓴다 — 고를 것이 없으면 짐작이 아니다\n * 후보가 둘 이상 **답하지 않는다.** 어느 것인지 모르는 채로 견주면 거짓 진단이 된다\n *\n * 마지막 줄이 이 함수가 식별자 되찾기에 이미 적용하던 규율이다(「후보가 둘 이상이면 답하지\n * 않는다」). 두 줄 위에서만 안 지키고 있었다.\n */\n const candidates = myMaterial\n ? recipes.filter((r: any) => (r.outputs ?? []).some((o: any) => o.material === myMaterial))\n : []\n\n /* 이 개체를 만든 변환이 가리키는 거래 — 그 오더가 레시피를 말한다. */\n const madeTx = (madeBy.get(epc) ?? []).map(s => s.tx).find(Boolean)\n const saidRecipeKey = madeTx ? await recipeKeyOfTransaction(domainId, instanceId, String(madeTx)) : undefined\n\n const chosen = chooseRecipe(recipes, candidates, saidRecipeKey)\n const recipe = chosen.recipe\n\n /*\n * ── 「모르는 개체」와 「대응이 없는 개체」는 다른 사실이다 (2026-08-23) ──────\n *\n * 화면이 「이 개체는 선언된 자재가 아닙니다(라인 내부의 중간 산물일 수 있습니다)」라고 말했는데,\n * 그 품목은 원본의 품목 선언에 **이름까지 있었다**(「냉동그린빈스」). 바인딩 표가 그 품목을 담지\n * 못한 것이고, 그것은 「중간 산물」과 전혀 다른 이야기다 — 사용자가 할 일도 다르다(원본을 볼 일이\n * 아니라 대응을 고칠 일이다).\n *\n * 그래서 개체가 가리키는 품목이 **모델의 품목 선언에 있는지** 함께 본다. 이름은 그 자리에 있으니\n * 함께 낸다(화면이 사람에게 무엇을 말하는지 알 수 있게).\n */\n const definitions: any[] = (inst.model as any)?.materialDefinitions ?? []\n const defByRef = new Map(definitions.filter(d => d?.id).map(d => [String(d.id), d]))\n /* 정의도 같은 문으로 되찾는다 — 규칙 하나, 후보가 둘 이상이면 답하지 않는다. */\n const myRef = itemRefOf(epc)\n const myDef = resolveByDeclaration(myRef, defByRef)\n\n let conformance: any = null\n if (!Object.keys(binding).length) {\n conformance = { comparable: false, reason: 'no-binding' }\n } else if (!myMaterial && myDef) {\n /* 원본은 이 품목을 안다 — 개체를 자재에 잇는 대응이 없어서 비교하지 못한다. */\n conformance = {\n comparable: false,\n reason: 'item-known-not-bound',\n ...(typeof myDef.name === 'string' && myDef.name ? { itemName: myDef.name } : {})\n }\n } else if (!myMaterial) {\n conformance = { comparable: false, reason: 'item-not-bound' }\n } else if (chosen.basis === 'said-but-undeclared') {\n /* 오더는 말했는데 그 레시피가 모델의 선언에 없다 — 원본과 선언이 어긋난 것이지 모르는 것이 아니다. */\n conformance = { comparable: false, reason: 'recipe-not-declared', recipe: saidRecipeKey }\n } else if (chosen.basis === 'ambiguous') {\n /* 짐작하지 않는다 — 어느 것으로 만들었는지 말해 주는 사실이 없다. 후보를 밝혀 사람이 판단하게 한다. */\n conformance = {\n comparable: false,\n reason: 'many-recipes-for-material',\n recipes: candidates.map((r: any) => r.key).sort()\n }\n } else if (!recipe) {\n conformance = { comparable: false, reason: 'no-recipe-for-material' }\n } else {\n /*\n * 관측: **바인딩된 자재까지 내려가 센다** — 중간 산물(WIP)은 통과한다.\n *\n * 다단 라인에서 완제품의 직접 투입은 원자재가 아니라 **WIP** 다: 원자재는 첫 공정에서\n * 소비되고 완제품은 마지막 공정에서 나온다. 그래서 직접 투입만 세면 레시피가 선언한 자재와\n * **영원히 안 맞는다**(검사가 이것을 잡았다).\n *\n * WIP 은 라우트의 내부 산물이고 선언된 자재가 아니다 — 레시피는 자재로 말하므로 비교도\n * 자재로 해야 한다. 바인딩에 있으면 거기서 멈추고, 없으면 그 위로 계속 따라간다.\n *\n * 다만 **다른 완제품까지 넘어가지 않는다**: 바인딩된 것을 만나면 멈추므로, 하위조립도\n * 자재로 선언돼 있으면 그 자리에서 세어진다(그 아래는 그 자재 자신의 레시피가 볼 일이다).\n */\n const observed = new Map<string, number>()\n let unbound = 0\n const parentsOf = (e: string) => edges.filter(x => x.from === e).map(x => x.to)\n const walked = new Set<string>([epc])\n let layer = parentsOf(epc)\n while (layer.length) {\n const deeper: string[] = []\n for (const e of layer) {\n if (walked.has(e)) continue\n walked.add(e)\n const mat = materialOf(e)\n if (mat) { observed.set(mat, (observed.get(mat) ?? 0) + 1); continue }\n const up = parentsOf(e)\n /* 바인딩에도 없고 더 올라갈 곳도 없다 — 정말 모르는 투입이다(수만 낸다). */\n if (!up.length) { unbound++; continue }\n deeper.push(...up)\n }\n layer = deeper\n }\n const declared = new Map<string, number>((recipe.inputs ?? []).map((i: any) => [i.material, i.qty ?? null]))\n const keys = [...new Set([...declared.keys(), ...observed.keys()])]\n conformance = {\n comparable: true,\n material: myMaterial,\n recipe: recipe.key,\n /* 그 레시피를 **어떻게 알았나** — 화면이 「기록으로 안다」와 「하나뿐이라 안다」를 구별할 수 있게. */\n recipeBasis: chosen.basis,\n /* 자재마다 선언·관측을 나란히. 어느 한쪽에만 있는 것도 사실이다(빠뜨리지 않는다). */\n lines: keys.map(m => ({\n material: m,\n declared: declared.has(m) ? declared.get(m) : null,\n observed: observed.get(m) ?? 0,\n matches: declared.get(m) === (observed.get(m) ?? 0)\n })),\n /* 바인딩에 없는 투입 — 설계에 없는 것이 들어갔을 수도, 바인딩이 덜 선언됐을 수도 있다.\n 어느 쪽인지 우리는 모르므로 **수만 낸다.** */\n unboundInputs: unbound\n }\n }\n\n return {\n instanceId,\n epc,\n itemRef: itemRefOf(epc),\n material: myMaterial,\n items,\n edges,\n conformance,\n /* 무엇을 근거로 답했나 — 창을 넘겨 끊겼으면 계보가 실제보다 얕게 보인다. */\n basis: { source: 'journal', transformationsRead: rows.length, truncated, depthTruncated, depthCap, windowCap }\n }\n }\n}\n\n/**\n * 그 거래를 가리키는 오더가 **어느 레시피로** 만들었다고 말하는가.\n *\n * ── 왜 거래 식별자로 묻나 ───────────────────────────────────────────────────\n * EPCIS 변환 사건에는 레시피가 없다 — 표준에 그 자리가 없고, 대신 생산 오더를 거래로 가리킨다\n * (`bizTransactionList`). 오더 사건은 `recipeKey` 와 그 거래 식별자를 **둘 다** 든다. 그래서 이\n * 두 칸이 EPCIS 에서 오더로 건너가는 다리다(§`twin-event-keys.ts`).\n *\n * ── 색인을 탄다 ─────────────────────────────────────────────────────────────\n * `ix_twin_event_9 (domain, instanceId, bizTransaction)` 로 좁힌다. 이 칸이 없던 때는 payload 를\n * 문자열로 훑어야 했고 32만 행에 3분이 걸렸다 — 화면이 기다릴 비용이 아니다.\n *\n * ── 못 찾으면 **답하지 않는다** ──────────────────────────────────────────────\n * 옛 행에는 이 칸이 비어 있다(과거 행은 채우지 않는다는 규율). 그때는 `undefined` 를 내고, 부르는\n * 쪽이 후보 수로 판단한다 — 여기서 아무거나 고르면 예전과 같은 결함이 된다.\n */\nasync function recipeKeyOfTransaction(\n domainId: string,\n instanceId: string,\n bizTransaction: string\n): Promise<string | undefined> {\n const row = await getRepository(TwinEvent)\n .createQueryBuilder('e')\n .where('e.domain = :domainId', { domainId })\n .andWhere('e.instanceId = :instanceId', { instanceId })\n .andWhere('e.bizTransaction = :tx', { tx: bizTransaction })\n .andWhere('e.eventType = :eventType', { eventType: 'order.status' })\n /* 어느 줄이든 같은 오더를 말하므로 하나면 된다 — 가장 이른 것이 그 오더가 선 순간이다. */\n .orderBy('e.eventTime', 'ASC')\n .limit(1)\n .getOne()\n\n const said = (row?.payload as any)?.data?.recipeKey\n return typeof said === 'string' && said ? said : undefined\n}\n"]}
|
|
1
|
+
{"version":3,"file":"twin-lineage-query.js","sourceRoot":"","sources":["../../../server/service/twin-model/twin-lineage-query.ts"],"names":[],"mappings":"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","sourcesContent":["import { Arg, Ctx, Int, Query, Resolver } from 'type-graphql'\n\nimport { ScalarObject, getRepository } from '@things-factory/shell'\n\nimport { itemRefOf, resolveByDeclaration } from './item-ref.js'\nimport { chooseRecipe } from './choose-recipe.js'\n\nimport { TwinInstance } from '../twin-instance/twin-instance.js'\nimport { TwinEvent } from '../twin-event/twin-event.js'\n\n/*\n * 계보 — **이 개체는 무엇에서 나왔나**, 그리고 **설계대로 만들어졌나.**\n *\n * ── 구성과 계보는 주어가 다르다 ────────────────────────────────────────────────\n * 구성(BOM)의 주어는 품목 **정의**다 — \"트레일러는 프레임 1개로 만든다\"(선언).\n * 계보의 주어는 품목 **개체**다 — \"이 트레일러는 저 프레임 로트에서 나왔다\"(관측).\n * 둘은 같은 모양이라 겹쳐 볼 수 있고, **그 차이가 곧 진단**이다: \"설계는 1개인데 이건 2개를 먹었다\".\n * 트윈이 아니면 못 하는 답이고, 이 질의가 그 답의 재료다.\n *\n * ── 어디서 읽나 ────────────────────────────────────────────────────────────────\n * 계보는 상태가 아니라 **저널**에 있다(EPCIS `TransformationEvent` 의 입력·산출 목록). 상태는\n * 지금 무엇이 있는지만 알고, 없어진 원자재가 무엇이었는지는 이벤트만 안다.\n *\n * ── 규모 ───────────────────────────────────────────────────────────────────────\n * 입력 목록은 **컬럼이 아니라 봉투 안**에 있어서 인덱스로 좁힐 수 없다. 그래서 변환 이벤트만\n * 창으로 끊어 읽고, **끊었으면 끊었다고 말한다.** 그 사실을 감추면 \"이 개체는 원자재가 없다\" 로\n * 읽히고, 계보에서 그 오해는 리콜 범위를 좁게 잡는 결과로 이어진다.\n */\n\n\n@Resolver()\nexport class TwinLineageQuery {\n @Query(returns => ScalarObject, {\n nullable: true,\n description:\n 'Where one item came from (observed) laid next to what its type declares (BOM). Walks TransformationEvent input/output lists in the journal; reports the window it read and whether it was truncated. The declared|observed difference is the diagnosis: \"the recipe says one frame, this one consumed two\".'\n })\n async twinItemLineage(\n @Arg('instanceId') instanceId: string,\n @Arg('epc') epc: string,\n @Ctx() context: ResolverContext,\n @Arg('maxDepth', type => Int, { nullable: true }) maxDepth?: number,\n @Arg('window', type => Int, { nullable: true }) window?: number\n ): 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 const depthCap = Math.min(Math.max(maxDepth ?? 6, 1), 12)\n const windowCap = Math.min(Math.max(window ?? 5000, 1), 50000)\n\n /*\n * 변환 이벤트만 읽는다. **최신부터** 읽는다 — 계보를 묻는 개체는 대개 최근에 만들어진 것이고,\n * 창을 넘겨 잘릴 때 잃는 쪽이 오래된 것이어야 덜 아프다.\n */\n /*\n * 관계를 **컬럼으로** 묻는다 (2026-08-28). `find({ where: { domain: { id } } })` 은 관계를 가진\n * 엔티티에서 두 단계 질의가 되고 **안쪽에 상한이 없다** — `take` 가 있어도 조건에 맞는 행을 전부\n * 만든 뒤 밖에서 자른다. 변환 사건이 적은 트윈에서는 표가 안 나지만, 변환을 많이 내는 트윈에서는\n * 이 한 번이 그 종류 전체를 만든다. 질의 빌더로 물으면 상한이 안쪽에 걸린다.\n *\n * 종류가 정확일치이므로 `(domain, instance, event_type, event_time)` 색인이 시각 순서까지 준다.\n */\n const rows: any[] = await getRepository(TwinEvent)\n .createQueryBuilder('e')\n .where('e.domain = :domainId', { domainId })\n .andWhere('e.instanceId = :instanceId', { instanceId })\n .andWhere('e.eventType = :eventType', { eventType: 'epcis.TransformationEvent' })\n .orderBy('e.eventTime', 'DESC')\n .limit(windowCap)\n .getMany()\n const truncated = rows.length >= windowCap\n\n /* 산출 → 그 변환. 한 개체는 한 번만 만들어지지만, 데이터가 그렇지 않을 수 있으므로 목록으로 든다. */\n const madeBy = new Map<string, { inputs: string[]; at?: string; tx?: string }[]>()\n for (const r of rows) {\n const d = r.payload?.data ?? {}\n const outs: string[] = d.outputEPCList ?? []\n const ins: string[] = d.inputEPCList ?? []\n const step = { inputs: ins, at: r.eventTime, tx: d.bizTransactionList?.[0]?.bizTransaction }\n for (const o of outs) {\n const list = madeBy.get(o) ?? []\n list.push(step)\n madeBy.set(o, list)\n }\n }\n\n /* 거슬러 올라간다 — 너비 우선, 지나온 것은 다시 파지 않는다(사이클·공유 부품). */\n const items: any[] = [{ epc, itemRef: itemRefOf(epc), depth: 0 }]\n const edges: any[] = []\n const seen = new Set<string>([epc])\n let depthTruncated = false\n let frontier = [{ epc, depth: 0 }]\n while (frontier.length) {\n const next: typeof frontier = []\n for (const cur of frontier) {\n if (cur.depth >= depthCap) { depthTruncated = true; continue }\n for (const step of madeBy.get(cur.epc) ?? []) {\n for (const input of step.inputs) {\n edges.push({ from: cur.epc, to: input, at: step.at, tx: step.tx, depth: cur.depth + 1 })\n if (seen.has(input)) continue\n seen.add(input)\n items.push({ epc: input, itemRef: itemRefOf(input), depth: cur.depth + 1 })\n next.push({ epc: input, depth: cur.depth + 1 })\n }\n }\n }\n frontier = next\n }\n\n /*\n * **겹쳐 본다.** 이 개체의 품목이 어느 레시피의 산출이면, 그 레시피가 선언한 투입과 실제로\n * 소비된 것을 자재별로 나란히 센다.\n *\n * 개체(EPC)와 자재 키를 잇는 것은 `productionSpec.binding`(자재 키 → GS1 품목 참조)이다.\n * 바인딩이 없으면 **비교하지 않는다** — 짐작으로 이으면 \"설계와 다르다\" 는 거짓 진단이 나오고,\n * 그건 아무 말도 안 하는 것보다 나쁘다.\n */\n const spec: any = (inst.model as any)?.productionSpec\n const binding: Record<string, string> = spec?.binding ?? {}\n const refToMaterial = new Map(Object.entries(binding).map(([mat, ref]) => [String(ref), mat]))\n /*\n * ── **화면은 끝마디로 묻는다** (2026-08-23) ─────────────────────────────────\n *\n * 서랍의 포커스 식별자는 클래스의 끝마디다(`net/class/product-M0000438`). 대응 표의 값은 전체\n * 식별자(`https://…/class/product-M0000438`)이므로 곧바로는 맞지 않고, 그래서 「선언된 자재가\n * 아닙니다」가 떴다 — 그 품목은 대응 표에 있었다.\n *\n * 끝마디로도 찾되 **하나뿐일 때만** 인정한다. 둘 이상이 같은 끝마디를 가지면 어느 것인지 모르므로\n * 아무 말도 하지 않는다 — 짐작으로 이으면 남의 레시피와 견주게 되고, 그것이 「설계와 다르다」는\n * 거짓 진단이 된다.\n */\n const materialOf = (e: string) => resolveByDeclaration(itemRefOf(e), refToMaterial) ?? null\n\n const myMaterial = materialOf(epc)\n const recipes: any[] = spec?.definition?.recipes ?? []\n\n /*\n * ── 「무엇으로 만들었나」는 **적혀 있다** (2026-09-05) ───────────────────────\n *\n * 예전에는 이 자리가 자재로 되짚어 `recipes.find(...)` 로 **먼저 걸리는 하나**를 골랐다. 배열\n * 순서가 답을 정한다는 뜻이고, 한 자재에 레시피가 둘이면 틀린 답이 확신에 찬 얼굴로 나온다.\n * 실제로 그런 자재가 있다 — `BOLT-M8` 에 `RCP-BOLT-STD` · `RCP-BOLT-FAST` 둘이다.\n *\n * 그런데 답은 이미 기록되어 있다. 만들 때 정해지는 사실이고 커널이 오더에 싣는다\n * (`FlowOrder.recipeKey`). 변환 사건은 그 오더를 거래 식별자로 가리키고(`bizTransactionList`),\n * 이 함수는 그 값을 이미 `tx` 로 꺼내 놓고도 쓰지 않았다.\n *\n * 세 자리가 다 있었고 마지막 연결만 없었다. 「가릴 수 없다」가 아니라 **안 읽고 있었다.**\n *\n * ── 규칙 셋 — 짐작하지 않는다 ───────────────────────────────────────────────\n * 오더가 말한다 그것을 쓴다. 판이 둘이든 열이든 상관없다\n * 오더가 말 안 하고\n * 후보가 하나 그것을 쓴다 — 고를 것이 없으면 짐작이 아니다\n * 후보가 둘 이상 **답하지 않는다.** 어느 것인지 모르는 채로 견주면 거짓 진단이 된다\n *\n * 마지막 줄이 이 함수가 식별자 되찾기에 이미 적용하던 규율이다(「후보가 둘 이상이면 답하지\n * 않는다」). 두 줄 위에서만 안 지키고 있었다.\n */\n const candidates = myMaterial\n ? recipes.filter((r: any) => (r.outputs ?? []).some((o: any) => o.material === myMaterial))\n : []\n\n /* 이 개체를 만든 변환이 가리키는 거래 — 그 오더가 레시피를 말한다. */\n const madeTx = (madeBy.get(epc) ?? []).map(s => s.tx).find(Boolean)\n const saidRecipeKey = madeTx ? await recipeKeyOfTransaction(domainId, instanceId, String(madeTx)) : undefined\n\n const chosen = chooseRecipe(recipes, candidates, saidRecipeKey)\n const recipe = chosen.recipe\n\n /*\n * ── 「모르는 개체」와 「대응이 없는 개체」는 다른 사실이다 (2026-08-23) ──────\n *\n * 화면이 「이 개체는 선언된 자재가 아닙니다(라인 내부의 중간 산물일 수 있습니다)」라고 말했는데,\n * 그 품목은 원본의 품목 선언에 **이름까지 있었다**(「냉동그린빈스」). 바인딩 표가 그 품목을 담지\n * 못한 것이고, 그것은 「중간 산물」과 전혀 다른 이야기다 — 사용자가 할 일도 다르다(원본을 볼 일이\n * 아니라 대응을 고칠 일이다).\n *\n * 그래서 개체가 가리키는 품목이 **모델의 품목 선언에 있는지** 함께 본다. 이름은 그 자리에 있으니\n * 함께 낸다(화면이 사람에게 무엇을 말하는지 알 수 있게).\n */\n const definitions: any[] = (inst.model as any)?.materialDefinitions ?? []\n const defByRef = new Map(definitions.filter(d => d?.id).map(d => [String(d.id), d]))\n /* 정의도 같은 문으로 되찾는다 — 규칙 하나, 후보가 둘 이상이면 답하지 않는다. */\n const myRef = itemRefOf(epc)\n const myDef = resolveByDeclaration(myRef, defByRef)\n\n let conformance: any = null\n if (!Object.keys(binding).length) {\n conformance = { comparable: false, reason: 'no-binding' }\n } else if (!myMaterial && myDef) {\n /* 원본은 이 품목을 안다 — 개체를 자재에 잇는 대응이 없어서 비교하지 못한다. */\n conformance = {\n comparable: false,\n reason: 'item-known-not-bound',\n ...(typeof myDef.name === 'string' && myDef.name ? { itemName: myDef.name } : {})\n }\n } else if (!myMaterial) {\n conformance = { comparable: false, reason: 'item-not-bound' }\n } else if (chosen.basis === 'said-but-undeclared') {\n /* 오더는 말했는데 그 레시피가 모델의 선언에 없다 — 원본과 선언이 어긋난 것이지 모르는 것이 아니다. */\n conformance = { comparable: false, reason: 'recipe-not-declared', recipe: saidRecipeKey }\n } else if (chosen.basis === 'ambiguous') {\n /* 짐작하지 않는다 — 어느 것으로 만들었는지 말해 주는 사실이 없다. 후보를 밝혀 사람이 판단하게 한다. */\n conformance = {\n comparable: false,\n reason: 'many-recipes-for-material',\n recipes: candidates.map((r: any) => r.key).sort()\n }\n } else if (!recipe) {\n conformance = { comparable: false, reason: 'no-recipe-for-material' }\n } else if (!(madeBy.get(epc) ?? []).length) {\n /*\n * ── **안 본 것을 「다르다」고 하지 않는다** (2026-09-18 · :3000 화면에서 확인) ─────────\n *\n * 이 개체를 만든 변환이 저널에 하나도 없으면 투입을 하나도 관측하지 못한 것이다. 그런데\n * 아래 셈은 그대로 돌아 관측을 전부 0 으로 적었고, 레시피가 요구한 자재 23 줄이 전부\n * 「안 맞는다」가 됐다 — 화면은 「레시피와 다릅니다」를 확신에 찬 얼굴로 그렸다. 실제로는\n * 원본이 변환을 아직 안 보냈을 뿐이다(`mes-miratek` 의 카메라 로트가 그 경우다).\n *\n * 이 파일의 규율이 이미 그것을 말한다 — 짐작으로 이으면 「설계와 다르다」는 거짓 진단이\n * 되고, 그건 아무 말도 안 하는 것보다 나쁘다. 관측이 **없는** 것도 같은 부류다.\n */\n conformance = { comparable: false, reason: 'made-not-observed', recipe: recipe.key }\n } else {\n /*\n * 관측: **바인딩된 자재까지 내려가 센다** — 중간 산물(WIP)은 통과한다.\n *\n * 다단 라인에서 완제품의 직접 투입은 원자재가 아니라 **WIP** 다: 원자재는 첫 공정에서\n * 소비되고 완제품은 마지막 공정에서 나온다. 그래서 직접 투입만 세면 레시피가 선언한 자재와\n * **영원히 안 맞는다**(검사가 이것을 잡았다).\n *\n * WIP 은 라우트의 내부 산물이고 선언된 자재가 아니다 — 레시피는 자재로 말하므로 비교도\n * 자재로 해야 한다. 바인딩에 있으면 거기서 멈추고, 없으면 그 위로 계속 따라간다.\n *\n * 다만 **다른 완제품까지 넘어가지 않는다**: 바인딩된 것을 만나면 멈추므로, 하위조립도\n * 자재로 선언돼 있으면 그 자리에서 세어진다(그 아래는 그 자재 자신의 레시피가 볼 일이다).\n */\n const observed = new Map<string, number>()\n let unbound = 0\n const parentsOf = (e: string) => edges.filter(x => x.from === e).map(x => x.to)\n const walked = new Set<string>([epc])\n let layer = parentsOf(epc)\n while (layer.length) {\n const deeper: string[] = []\n for (const e of layer) {\n if (walked.has(e)) continue\n walked.add(e)\n const mat = materialOf(e)\n if (mat) { observed.set(mat, (observed.get(mat) ?? 0) + 1); continue }\n const up = parentsOf(e)\n /* 바인딩에도 없고 더 올라갈 곳도 없다 — 정말 모르는 투입이다(수만 낸다). */\n if (!up.length) { unbound++; continue }\n deeper.push(...up)\n }\n layer = deeper\n }\n const declared = new Map<string, number>((recipe.inputs ?? []).map((i: any) => [i.material, i.qty ?? null]))\n const keys = [...new Set([...declared.keys(), ...observed.keys()])]\n conformance = {\n comparable: true,\n material: myMaterial,\n recipe: recipe.key,\n /* 그 레시피를 **어떻게 알았나** — 화면이 「기록으로 안다」와 「하나뿐이라 안다」를 구별할 수 있게. */\n recipeBasis: chosen.basis,\n /* 자재마다 선언·관측을 나란히. 어느 한쪽에만 있는 것도 사실이다(빠뜨리지 않는다). */\n lines: keys.map(m => ({\n material: m,\n declared: declared.has(m) ? declared.get(m) : null,\n observed: observed.get(m) ?? 0,\n matches: declared.get(m) === (observed.get(m) ?? 0)\n })),\n /* 바인딩에 없는 투입 — 설계에 없는 것이 들어갔을 수도, 바인딩이 덜 선언됐을 수도 있다.\n 어느 쪽인지 우리는 모르므로 **수만 낸다.** */\n unboundInputs: unbound\n }\n }\n\n return {\n instanceId,\n epc,\n itemRef: itemRefOf(epc),\n material: myMaterial,\n items,\n edges,\n conformance,\n /* 무엇을 근거로 답했나 — 창을 넘겨 끊겼으면 계보가 실제보다 얕게 보인다. */\n basis: { source: 'journal', transformationsRead: rows.length, truncated, depthTruncated, depthCap, windowCap }\n }\n }\n}\n\n/**\n * 그 거래를 가리키는 오더가 **어느 레시피로** 만들었다고 말하는가.\n *\n * ── 왜 거래 식별자로 묻나 ───────────────────────────────────────────────────\n * EPCIS 변환 사건에는 레시피가 없다 — 표준에 그 자리가 없고, 대신 생산 오더를 거래로 가리킨다\n * (`bizTransactionList`). 오더 사건은 `recipeKey` 와 그 거래 식별자를 **둘 다** 든다. 그래서 이\n * 두 칸이 EPCIS 에서 오더로 건너가는 다리다(§`twin-event-keys.ts`).\n *\n * ── 색인을 탄다 ─────────────────────────────────────────────────────────────\n * `ix_twin_event_9 (domain, instanceId, bizTransaction)` 로 좁힌다. 이 칸이 없던 때는 payload 를\n * 문자열로 훑어야 했고 32만 행에 3분이 걸렸다 — 화면이 기다릴 비용이 아니다.\n *\n * ── 못 찾으면 **답하지 않는다** ──────────────────────────────────────────────\n * 옛 행에는 이 칸이 비어 있다(과거 행은 채우지 않는다는 규율). 그때는 `undefined` 를 내고, 부르는\n * 쪽이 후보 수로 판단한다 — 여기서 아무거나 고르면 예전과 같은 결함이 된다.\n */\nasync function recipeKeyOfTransaction(\n domainId: string,\n instanceId: string,\n bizTransaction: string\n): Promise<string | undefined> {\n const row = await getRepository(TwinEvent)\n .createQueryBuilder('e')\n .where('e.domain = :domainId', { domainId })\n .andWhere('e.instanceId = :instanceId', { instanceId })\n .andWhere('e.bizTransaction = :tx', { tx: bizTransaction })\n .andWhere('e.eventType = :eventType', { eventType: 'order.status' })\n /* 어느 줄이든 같은 오더를 말하므로 하나면 된다 — 가장 이른 것이 그 오더가 선 순간이다. */\n .orderBy('e.eventTime', 'ASC')\n .limit(1)\n .getOne()\n\n const said = (row?.payload as any)?.data?.recipeKey\n return typeof said === 'string' && said ? said : undefined\n}\n"]}
|
|
@@ -109,7 +109,7 @@ let TwinModelItemQuery = class TwinModelItemQuery {
|
|
|
109
109
|
const repo = (0, shell_1.getRepository)(row.entity);
|
|
110
110
|
const where = { domain: { id: domainId }, instance: { id: inst.id } };
|
|
111
111
|
const [rows, total] = await repo.findAndCount({ where, order: row.order, skip, take });
|
|
112
|
-
|
|
112
|
+
const answer = {
|
|
113
113
|
axis,
|
|
114
114
|
label: info.label,
|
|
115
115
|
basis: 'rows',
|
|
@@ -119,22 +119,26 @@ let TwinModelItemQuery = class TwinModelItemQuery {
|
|
|
119
119
|
resourceClassLabels: this.classLabels(inst.model ?? {}),
|
|
120
120
|
items: rows.map((r) => this.rowItem(axis, r))
|
|
121
121
|
};
|
|
122
|
+
return answer;
|
|
122
123
|
}
|
|
123
124
|
if (row && q) {
|
|
124
125
|
/*
|
|
125
|
-
* 검색은 아직 DB 로 내리지 않았다. **그 사실을 밝힌다**
|
|
126
|
-
*
|
|
127
|
-
*
|
|
126
|
+
* 검색은 아직 DB 로 내리지 않았다. **그 사실을 밝힌다** — 흉내로 덮으면 규모가 오는 날까지
|
|
127
|
+
* 아무도 이 한계를 모른다. 훑는 건수를 상한으로 막고, 상한에 닿았으면 잘렸다고 말한다
|
|
128
|
+
* (알리지 않고 자르지 않는다).
|
|
129
|
+
*
|
|
130
|
+
* 말하는 자리는 `scanned` · `truncated` 다. 근거(`basis`)는 그대로 `rows` 이고 — 값이 어디서
|
|
131
|
+
* 왔는지는 훑었든 DB 가 골랐든 같은 행이다 — 계약이 닫아 둔 다섯 밖으로 나가지 않는다.
|
|
128
132
|
*/
|
|
129
133
|
const SCAN_MAX = 5000;
|
|
130
134
|
const repo = (0, shell_1.getRepository)(row.entity);
|
|
131
135
|
const where = { domain: { id: domainId }, instance: { id: inst.id } };
|
|
132
136
|
const rows = await repo.find({ where, order: row.order, take: SCAN_MAX });
|
|
133
137
|
const hit = rows.filter((r) => matches(this.rowItem(axis, r), q));
|
|
134
|
-
|
|
138
|
+
const answer = {
|
|
135
139
|
axis,
|
|
136
140
|
label: info.label,
|
|
137
|
-
basis: 'rows
|
|
141
|
+
basis: 'rows',
|
|
138
142
|
total: hit.length,
|
|
139
143
|
offset: skip,
|
|
140
144
|
resourceClassLabels: this.classLabels(inst.model ?? {}),
|
|
@@ -145,6 +149,7 @@ let TwinModelItemQuery = class TwinModelItemQuery {
|
|
|
145
149
|
return { items, names: (0, name_index_js_1.namesFor)(items, (0, name_index_js_1.nameIndex)(inst.model ?? {})) };
|
|
146
150
|
})()
|
|
147
151
|
};
|
|
152
|
+
return answer;
|
|
148
153
|
}
|
|
149
154
|
/*
|
|
150
155
|
* **선언이 아닌 축** — 일정·실적(ISA-95 Part 4). 이 축의 집은 트윈 모델이 아니라 **커널 상태**다
|