@things-factory/headless-twin 10.0.19 → 10.1.0
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 +10 -1
- package/dist-server/engine/canonical-ingest.js +110 -33
- package/dist-server/engine/canonical-ingest.js.map +1 -1
- package/dist-server/engine/event-time.d.ts +25 -0
- package/dist-server/engine/event-time.js +98 -0
- package/dist-server/engine/event-time.js.map +1 -0
- package/dist-server/engine/index.d.ts +2 -0
- package/dist-server/engine/index.js +23 -0
- package/dist-server/engine/index.js.map +1 -1
- package/dist-server/engine/ingest-dedupe.d.ts +18 -6
- package/dist-server/engine/ingest-dedupe.js +201 -15
- package/dist-server/engine/ingest-dedupe.js.map +1 -1
- package/dist-server/engine/ingest-health.d.ts +60 -1
- package/dist-server/engine/ingest-health.js +43 -8
- package/dist-server/engine/ingest-health.js.map +1 -1
- package/dist-server/engine/kpi-fold.d.ts +19 -0
- package/dist-server/engine/kpi-fold.js +4 -1
- package/dist-server/engine/kpi-fold.js.map +1 -1
- package/dist-server/engine/kpi-query.js +23 -3
- package/dist-server/engine/kpi-query.js.map +1 -1
- package/dist-server/engine/oee-accumulator.d.ts +35 -2
- package/dist-server/engine/oee-accumulator.js +56 -4
- package/dist-server/engine/oee-accumulator.js.map +1 -1
- package/dist-server/engine/twin-engine.d.ts +72 -3
- package/dist-server/engine/twin-engine.js +118 -5
- package/dist-server/engine/twin-engine.js.map +1 -1
- package/dist-server/index.js +21 -0
- package/dist-server/index.js.map +1 -1
- package/dist-server/migrations/1786600000000-RenameOperatoMesAdapterType.d.ts +5 -0
- package/dist-server/migrations/1786600000000-RenameOperatoMesAdapterType.js +66 -0
- package/dist-server/migrations/1786600000000-RenameOperatoMesAdapterType.js.map +1 -0
- package/dist-server/migrations/1786700000000-AddDispatchRetryColumns.d.ts +5 -0
- package/dist-server/migrations/1786700000000-AddDispatchRetryColumns.js +79 -0
- package/dist-server/migrations/1786700000000-AddDispatchRetryColumns.js.map +1 -0
- package/dist-server/migrations/index.js +5 -1
- package/dist-server/migrations/index.js.map +1 -1
- package/dist-server/routes.js +12 -20
- package/dist-server/routes.js.map +1 -1
- package/dist-server/service/actuation/actuation-advisor.d.ts +60 -0
- package/dist-server/service/actuation/actuation-advisor.js +53 -0
- package/dist-server/service/actuation/actuation-advisor.js.map +1 -0
- package/dist-server/service/actuation/actuation-outcome.d.ts +52 -0
- package/dist-server/service/actuation/actuation-outcome.js +34 -0
- package/dist-server/service/actuation/actuation-outcome.js.map +1 -0
- package/dist-server/service/actuation/actuation-rule-resolver.d.ts +19 -0
- package/dist-server/service/actuation/actuation-rule-resolver.js +161 -0
- package/dist-server/service/actuation/actuation-rule-resolver.js.map +1 -0
- package/dist-server/service/actuation/actuation-rule.d.ts +55 -0
- package/dist-server/service/actuation/actuation-rule.js +130 -0
- package/dist-server/service/actuation/actuation-rule.js.map +1 -0
- package/dist-server/service/actuation/approval-gateway.d.ts +20 -0
- package/dist-server/service/actuation/approval-gateway.js +14 -0
- package/dist-server/service/actuation/approval-gateway.js.map +1 -0
- package/dist-server/service/actuation/catch-up-effects.d.ts +16 -0
- package/dist-server/service/actuation/catch-up-effects.js +120 -0
- package/dist-server/service/actuation/catch-up-effects.js.map +1 -0
- package/dist-server/service/actuation/command-dispatcher.d.ts +206 -8
- package/dist-server/service/actuation/command-dispatcher.js +191 -14
- package/dist-server/service/actuation/command-dispatcher.js.map +1 -1
- package/dist-server/service/actuation/command-store.d.ts +10 -2
- package/dist-server/service/actuation/command-store.js +168 -5
- package/dist-server/service/actuation/command-store.js.map +1 -1
- package/dist-server/service/actuation/dispatch-after-approval.d.ts +11 -0
- package/dist-server/service/actuation/dispatch-after-approval.js +73 -0
- package/dist-server/service/actuation/dispatch-after-approval.js.map +1 -0
- package/dist-server/service/actuation/effect-match.d.ts +53 -0
- package/dist-server/service/actuation/effect-match.js +65 -0
- package/dist-server/service/actuation/effect-match.js.map +1 -0
- package/dist-server/service/actuation/index.d.ts +21 -2
- package/dist-server/service/actuation/index.js +21 -2
- package/dist-server/service/actuation/index.js.map +1 -1
- package/dist-server/service/actuation/retry-plan.d.ts +63 -0
- package/dist-server/service/actuation/retry-plan.js +99 -0
- package/dist-server/service/actuation/retry-plan.js.map +1 -0
- package/dist-server/service/actuation/rule-evaluate.d.ts +127 -0
- package/dist-server/service/actuation/rule-evaluate.js +205 -0
- package/dist-server/service/actuation/rule-evaluate.js.map +1 -0
- package/dist-server/service/actuation/rule-loop.d.ts +5 -0
- package/dist-server/service/actuation/rule-loop.js +156 -0
- package/dist-server/service/actuation/rule-loop.js.map +1 -0
- package/dist-server/service/actuation/rule-runner.d.ts +52 -0
- package/dist-server/service/actuation/rule-runner.js +135 -0
- package/dist-server/service/actuation/rule-runner.js.map +1 -0
- package/dist-server/service/actuation/rule-wiring.d.ts +24 -0
- package/dist-server/service/actuation/rule-wiring.js +80 -0
- package/dist-server/service/actuation/rule-wiring.js.map +1 -0
- package/dist-server/service/actuation/settle-effects.d.ts +21 -0
- package/dist-server/service/actuation/settle-effects.js +101 -0
- package/dist-server/service/actuation/settle-effects.js.map +1 -0
- package/dist-server/service/actuation/sweep-dispatch.d.ts +37 -0
- package/dist-server/service/actuation/sweep-dispatch.js +125 -0
- package/dist-server/service/actuation/sweep-dispatch.js.map +1 -0
- package/dist-server/service/actuation/twin-command-resolver.d.ts +120 -0
- package/dist-server/service/actuation/twin-command-resolver.js +406 -0
- package/dist-server/service/actuation/twin-command-resolver.js.map +1 -0
- package/dist-server/service/actuation/twin-command.d.ts +81 -0
- package/dist-server/service/actuation/twin-command.js +60 -0
- package/dist-server/service/actuation/twin-command.js.map +1 -1
- package/dist-server/service/actuation/twin-now.d.ts +41 -0
- package/dist-server/service/actuation/twin-now.js +65 -0
- package/dist-server/service/actuation/twin-now.js.map +1 -0
- package/dist-server/service/index.d.ts +5 -2
- package/dist-server/service/index.js +8 -0
- package/dist-server/service/index.js.map +1 -1
- package/dist-server/service/reference/actuation-routing.d.ts +60 -0
- package/dist-server/service/reference/actuation-routing.js +126 -0
- package/dist-server/service/reference/actuation-routing.js.map +1 -0
- package/dist-server/service/reference/actuation-target.d.ts +65 -0
- package/dist-server/service/reference/actuation-target.js +94 -0
- package/dist-server/service/reference/actuation-target.js.map +1 -0
- package/dist-server/service/reference/connection-portability-resolver.d.ts +29 -0
- package/dist-server/service/reference/connection-portability-resolver.js +181 -0
- package/dist-server/service/reference/connection-portability-resolver.js.map +1 -0
- package/dist-server/service/reference/connection-portability.d.ts +138 -0
- package/dist-server/service/reference/connection-portability.js +230 -0
- package/dist-server/service/reference/connection-portability.js.map +1 -0
- package/dist-server/service/reference/fill-cadence.d.ts +17 -0
- package/dist-server/service/reference/fill-cadence.js +31 -0
- package/dist-server/service/reference/fill-cadence.js.map +1 -0
- package/dist-server/service/reference/fill-dedupe.d.ts +16 -0
- package/dist-server/service/reference/fill-dedupe.js +63 -0
- package/dist-server/service/reference/fill-dedupe.js.map +1 -0
- package/dist-server/service/reference/fill-loop.d.ts +35 -0
- package/dist-server/service/reference/fill-loop.js +187 -0
- package/dist-server/service/reference/fill-loop.js.map +1 -0
- package/dist-server/service/reference/fill-span.d.ts +10 -0
- package/dist-server/service/reference/fill-span.js +37 -0
- package/dist-server/service/reference/fill-span.js.map +1 -0
- package/dist-server/service/reference/hook-contract.d.ts +88 -4
- package/dist-server/service/reference/hook-contract.js +105 -4
- package/dist-server/service/reference/hook-contract.js.map +1 -1
- package/dist-server/service/reference/hook-rejected.d.ts +25 -0
- package/dist-server/service/reference/hook-rejected.js +48 -0
- package/dist-server/service/reference/hook-rejected.js.map +1 -0
- package/dist-server/service/reference/index.d.ts +4 -1
- package/dist-server/service/reference/index.js +5 -1
- package/dist-server/service/reference/index.js.map +1 -1
- package/dist-server/service/reference/live-cadence.d.ts +68 -0
- package/dist-server/service/reference/live-cadence.js +44 -0
- package/dist-server/service/reference/live-cadence.js.map +1 -0
- package/dist-server/service/reference/live-feed-lease.d.ts +15 -0
- package/dist-server/service/reference/live-feed-lease.js +56 -0
- package/dist-server/service/reference/live-feed-lease.js.map +1 -0
- package/dist-server/service/reference/live-ingest.d.ts +21 -0
- package/dist-server/service/reference/live-ingest.js +66 -0
- package/dist-server/service/reference/live-ingest.js.map +1 -0
- package/dist-server/service/reference/reference-adapter.d.ts +291 -2
- package/dist-server/service/reference/reference-adapter.js +19 -1
- package/dist-server/service/reference/reference-adapter.js.map +1 -1
- package/dist-server/service/reference/reference-fill-run.d.ts +50 -0
- package/dist-server/service/reference/reference-fill-run.js +142 -0
- package/dist-server/service/reference/reference-fill-run.js.map +1 -0
- package/dist-server/service/reference/reference-fill.d.ts +90 -0
- package/dist-server/service/reference/reference-fill.js +141 -0
- package/dist-server/service/reference/reference-fill.js.map +1 -0
- package/dist-server/service/reference/reference-hook.d.ts +22 -0
- package/dist-server/service/reference/reference-hook.js +135 -55
- package/dist-server/service/reference/reference-hook.js.map +1 -1
- package/dist-server/service/reference/reference-link-groups.d.ts +10 -0
- package/dist-server/service/reference/reference-link-groups.js +15 -0
- package/dist-server/service/reference/reference-link-groups.js.map +1 -0
- package/dist-server/service/reference/reference-links.d.ts +14 -0
- package/dist-server/service/reference/reference-links.js +73 -0
- package/dist-server/service/reference/reference-links.js.map +1 -0
- package/dist-server/service/reference/reference-live.d.ts +1 -0
- package/dist-server/service/reference/reference-live.js +32 -6
- package/dist-server/service/reference/reference-live.js.map +1 -1
- package/dist-server/service/reference/reference-master.d.ts +56 -2
- package/dist-server/service/reference/reference-master.js +33 -3
- package/dist-server/service/reference/reference-master.js.map +1 -1
- package/dist-server/service/reference/reference-resolver.d.ts +27 -0
- package/dist-server/service/reference/reference-resolver.js +79 -1
- package/dist-server/service/reference/reference-resolver.js.map +1 -1
- package/dist-server/service/twin-event/twin-event-keys.d.ts +31 -3
- package/dist-server/service/twin-event/twin-event-keys.js +36 -4
- package/dist-server/service/twin-event/twin-event-keys.js.map +1 -1
- package/dist-server/service/twin-event/twin-event.d.ts +1 -0
- package/dist-server/service/twin-event/twin-event.js +11 -2
- package/dist-server/service/twin-event/twin-event.js.map +1 -1
- package/dist-server/service/twin-model/choose-recipe.d.ts +15 -0
- package/dist-server/service/twin-model/choose-recipe.js +33 -0
- package/dist-server/service/twin-model/choose-recipe.js.map +1 -0
- package/dist-server/service/twin-model/external-resolver.d.ts +76 -0
- package/dist-server/service/twin-model/external-resolver.js +109 -0
- package/dist-server/service/twin-model/external-resolver.js.map +1 -0
- package/dist-server/service/twin-model/project-structure.js +16 -3
- package/dist-server/service/twin-model/project-structure.js.map +1 -1
- package/dist-server/service/twin-model/twin-equipment.d.ts +17 -0
- package/dist-server/service/twin-model/twin-equipment.js +10 -0
- package/dist-server/service/twin-model/twin-equipment.js.map +1 -1
- package/dist-server/service/twin-model/twin-lineage-query.js +75 -1
- package/dist-server/service/twin-model/twin-lineage-query.js.map +1 -1
- package/dist-server/service/twin-model/twin-model-item-query.js +94 -5
- package/dist-server/service/twin-model/twin-model-item-query.js.map +1 -1
- package/dist-server/service/twin-model/twin-model-tree-query.js.map +1 -1
- package/dist-server/service/twin-space/twin-space.d.ts +29 -0
- package/dist-server/service/twin-space/twin-space.js +13 -0
- package/dist-server/service/twin-space/twin-space.js.map +1 -1
- package/dist-server/service/twin-subject/event-subjects.d.ts +3 -1
- package/dist-server/service/twin-subject/event-subjects.js +8 -1
- package/dist-server/service/twin-subject/event-subjects.js.map +1 -1
- package/dist-shared/entity-delta.js +1 -1
- package/dist-shared/entity-delta.js.map +1 -1
- package/package.json +7 -7
- package/server/engine/canonical-ingest.ts +119 -33
- package/server/engine/event-time.ts +96 -0
- package/server/engine/index.ts +20 -0
- package/server/engine/ingest-dedupe.ts +203 -27
- package/server/engine/ingest-health.ts +91 -6
- package/server/engine/kpi-fold.ts +24 -1
- package/server/engine/kpi-query.ts +23 -4
- package/server/engine/oee-accumulator.ts +80 -8
- package/server/engine/twin-engine.ts +119 -5
- package/server/index.ts +23 -0
- package/server/migrations/1786600000000-RenameOperatoMesAdapterType.ts +66 -0
- package/server/migrations/1786700000000-AddDispatchRetryColumns.ts +82 -0
- package/server/migrations/index.ts +5 -1
- package/server/routes.ts +12 -24
- package/server/service/actuation/actuation-advisor.ts +124 -0
- package/server/service/actuation/actuation-outcome.ts +97 -0
- package/server/service/actuation/actuation-rule-resolver.ts +147 -0
- package/server/service/actuation/actuation-rule.ts +139 -0
- package/server/service/actuation/approval-gateway.ts +44 -0
- package/server/service/actuation/catch-up-effects.ts +152 -0
- package/server/service/actuation/command-dispatcher.ts +401 -20
- package/server/service/actuation/command-store.ts +182 -8
- package/server/service/actuation/dispatch-after-approval.ts +71 -0
- package/server/service/actuation/effect-match.ts +124 -0
- package/server/service/actuation/index.ts +21 -2
- package/server/service/actuation/retry-plan.ts +145 -0
- package/server/service/actuation/rule-evaluate.ts +352 -0
- package/server/service/actuation/rule-loop.ts +160 -0
- package/server/service/actuation/rule-runner.ts +216 -0
- package/server/service/actuation/rule-wiring.ts +84 -0
- package/server/service/actuation/settle-effects.ts +115 -0
- package/server/service/actuation/sweep-dispatch.ts +179 -0
- package/server/service/actuation/twin-command-resolver.ts +387 -0
- package/server/service/actuation/twin-command.ts +128 -1
- package/server/service/actuation/twin-now.ts +97 -0
- package/server/service/index.ts +9 -1
- package/server/service/reference/actuation-routing.ts +165 -0
- package/server/service/reference/actuation-target.ts +163 -0
- package/server/service/reference/connection-portability-resolver.ts +171 -0
- package/server/service/reference/connection-portability.ts +285 -0
- package/server/service/reference/fill-cadence.ts +29 -0
- package/server/service/reference/fill-dedupe.ts +66 -0
- package/server/service/reference/fill-loop.ts +153 -0
- package/server/service/reference/fill-span.ts +31 -0
- package/server/service/reference/hook-contract.ts +185 -5
- package/server/service/reference/hook-rejected.ts +53 -0
- package/server/service/reference/index.ts +5 -1
- package/server/service/reference/live-cadence.ts +129 -0
- package/server/service/reference/live-feed-lease.ts +56 -0
- package/server/service/reference/live-ingest.ts +94 -0
- package/server/service/reference/reference-adapter.ts +327 -5
- package/server/service/reference/reference-fill-run.ts +165 -0
- package/server/service/reference/reference-fill.ts +223 -0
- package/server/service/reference/reference-hook.ts +162 -56
- package/server/service/reference/reference-link-groups.ts +21 -0
- package/server/service/reference/reference-links.ts +77 -0
- package/server/service/reference/reference-live.ts +30 -5
- package/server/service/reference/reference-master.ts +90 -5
- package/server/service/reference/reference-resolver.ts +77 -2
- package/server/service/twin-event/twin-event-keys.ts +37 -4
- package/server/service/twin-event/twin-event.ts +18 -1
- package/server/service/twin-model/choose-recipe.ts +31 -0
- package/server/service/twin-model/external-resolver.ts +161 -0
- package/server/service/twin-model/project-structure.ts +16 -3
- package/server/service/twin-model/twin-equipment.ts +22 -0
- package/server/service/twin-model/twin-lineage-query.ts +82 -1
- package/server/service/twin-model/twin-model-item-query.ts +95 -4
- package/server/service/twin-model/twin-model-tree-query.ts +1 -1
- package/server/service/twin-space/twin-space.ts +39 -0
- package/server/service/twin-subject/event-subjects.ts +8 -1
- package/shared/entity-delta.ts +1 -1
- package/test/ack-shape-alignment.test.ts +97 -0
- package/test/actuation-approval-door.test.ts +421 -0
- package/test/actuation-dispatch.test.ts +16 -3
- package/test/actuation-effect.test.ts +162 -0
- package/test/actuation-outcome.test.ts +152 -0
- package/test/actuation-rule-runner.test.ts +281 -0
- package/test/actuation-rule.test.ts +266 -0
- package/test/actuation-seam.test.ts +249 -0
- package/test/actuation-settle.test.ts +164 -0
- package/test/actuation-target.test.ts +145 -0
- package/test/canonical-ingest-vocabularies.test.ts +49 -2
- package/test/capability-mapping.test.ts +1 -1
- package/test/choose-recipe.test.ts +117 -0
- package/test/command-store-writes-columns.test.ts +126 -0
- package/test/connection-portability-doors.test.ts +87 -0
- package/test/connector-capability-declaration.test.ts +54 -4
- package/test/declaration-reaches-model.test.ts +43 -0
- package/test/dedupe-transformation.test.ts +112 -0
- package/test/entity-delta.test.ts +1 -1
- package/test/event-subjects.test.ts +2 -1
- package/test/external-resolver.test.ts +171 -0
- package/test/fill-dedupe.test.ts +59 -0
- package/test/fill-loop.test.ts +35 -0
- package/test/first-envelope.test.ts +153 -0
- package/test/fold-shift-dst.test.ts +110 -0
- package/test/home-in-area.test.ts +90 -0
- package/test/hook-rejected-shape.test.ts +86 -0
- package/test/hook-response-sum.test.ts +193 -0
- package/test/hook-sequence.test.ts +1 -1
- package/test/ingest-dedupe.test.ts +56 -0
- package/test/ingest-expected-quiet.test.ts +111 -0
- package/test/ingest-idempotent.test.ts +79 -0
- package/test/ingest-reconcile-four-ways.test.ts +97 -0
- package/test/ingest-rules-header.test.ts +100 -0
- package/test/ingest-window-rolls-unobserved.test.ts +72 -0
- package/test/intake-mapping-parity.test.ts +175 -0
- package/test/live-cadence.test.ts +170 -0
- package/test/live-feed-lease.test.ts +63 -0
- package/test/live-mirror-parity.test.ts +1 -1
- package/test/oee-accumulator.test.ts +3 -3
- package/test/oee-site-calendar.test.ts +146 -0
- package/test/order-and-transaction-split.test.ts +107 -0
- package/test/reference-fill.test.ts +235 -0
- package/test/reference-hook.test.ts +1 -1
- package/test/reference-links.test.ts +56 -0
- package/test/retry-plan.test.ts +139 -0
- package/test/sweep-dispatch.test.ts +191 -0
- package/test/twin-event-keys.test.ts +20 -6
- package/test/twin-model-item-db.test.ts +6 -6
- package/test/twin-model-tree-db.test.ts +6 -6
- package/tsconfig.shared.tsbuildinfo +1 -1
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -130,10 +130,21 @@ export async function projectStructure(domainId: string, instanceId: string, mod
|
|
|
130
130
|
const equipment: TwinEquipment[] = (model?.equipment ?? [])
|
|
131
131
|
.filter((m: any) => m?.id)
|
|
132
132
|
.map((m: any) => {
|
|
133
|
+
/*
|
|
134
|
+
* ── 설비는 자리에도 구역에도 설 수 있다 (2026-09-06) ─────────────────────
|
|
135
|
+
* 자리 목록에서 먼저 찾고, 없으면 구역에서 찾는다. `locations.parentId` 가 이미 같은 규율을
|
|
136
|
+
* 쓴다(§`parentIsLocation`).
|
|
137
|
+
*
|
|
138
|
+
* 실물이 있다 — `EQ-CAM-SHOWER-01`(에어샤워)이 `LINE-CAM`(구역)에 딸려 있고, 라인 전체에
|
|
139
|
+
* 붙는 설비는 자리 하나를 고를 수 없다. 그동안 자리에서만 찾아서 **끊어진 참조로 보고**했다.
|
|
140
|
+
*
|
|
141
|
+
* 마스터가 `homeLocationKind: 'area'` 로 말해 주지만 여기서는 조회로 판정한다 — 옛 트윈의
|
|
142
|
+
* model 에는 그 칸이 없고, 있는 것을 못 찾았다고 말하는 것이 더 나쁘다.
|
|
143
|
+
*/
|
|
133
144
|
const home = m.homeLocation ? locOf.get(m.homeLocation) : undefined
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
if (m.homeLocation && !home) unresolvedHome.push(`${m.id}→${m.homeLocation}`)
|
|
145
|
+
const homeArea = m.homeLocation && !home ? areaOf.get(m.homeLocation) : undefined
|
|
146
|
+
/* 끊어진 기준 자리는 **이름을 대고** 보고한다 — 자리에도 구역에도 없을 때만이다. */
|
|
147
|
+
if (m.homeLocation && !home && !homeArea) unresolvedHome.push(`${m.id}→${m.homeLocation}`)
|
|
137
148
|
return eqRepo.create({
|
|
138
149
|
...scope,
|
|
139
150
|
equipmentId: m.id,
|
|
@@ -147,6 +158,8 @@ export async function projectStructure(domainId: string, instanceId: string, mod
|
|
|
147
158
|
/* 결과까지 — 참조만 있으면 "무엇으로 검증한다" 까지고, 자격 성립은 결과가 말한다. */
|
|
148
159
|
testResults: m.testResults ?? null,
|
|
149
160
|
homeLocation: home ? ({ id: home.id } as any) : null,
|
|
161
|
+
/* 둘 다 차는 일은 없다 — 자리에서 찾았으면 구역은 안 본다. */
|
|
162
|
+
homeArea: homeArea ? ({ id: homeArea.id } as any) : null,
|
|
150
163
|
mtbfMs: m.mtbfMs ?? null,
|
|
151
164
|
mttrMs: m.mttrMs ?? null,
|
|
152
165
|
/* 선언된 정체성 — 예전에는 선언에 없는 `gs1Id` 를 읽어 이 칸이 늘 비어 있었다. */
|
|
@@ -5,6 +5,7 @@ import { Domain, ScalarObject } from '@things-factory/shell'
|
|
|
5
5
|
|
|
6
6
|
import { TwinInstance } from '../twin-instance/twin-instance.js'
|
|
7
7
|
import { TwinLocation } from './twin-location.js'
|
|
8
|
+
import { TwinArea } from '../twin-space/twin-area.js'
|
|
8
9
|
|
|
9
10
|
/*
|
|
10
11
|
* TwinEquipment — 이 트윈이 반영하는 **설비**. ISA-95 `Equipment` / ISO 55000 `Asset` / EPCIS `GIAI`.
|
|
@@ -114,6 +115,27 @@ export class TwinEquipment {
|
|
|
114
115
|
@RelationId((e: TwinEquipment) => e.homeLocation)
|
|
115
116
|
homeLocationRowId?: string
|
|
116
117
|
|
|
118
|
+
/**
|
|
119
|
+
* 기준 **구역** — 설비가 자리가 아니라 구역에 서 있을 때.
|
|
120
|
+
*
|
|
121
|
+
* ── 왜 칸이 둘인가 (2026-09-06) ────────────────────────────────────────────
|
|
122
|
+
* 계약이 `homeLocationKind = 'location' | 'area'` 로 그 갈림을 말한다. 실물이 있다 —
|
|
123
|
+
* `EQ-CAM-SHOWER-01`(에어샤워)이 `LINE-CAM` 이라는 **구역**에 딸려 있고, 그것이 정상이다.
|
|
124
|
+
* 라인 전체에 붙는 설비는 자리 하나를 고를 수 없다.
|
|
125
|
+
*
|
|
126
|
+
* 그동안 이 칸이 없어서 인제스트가 그 참조를 **「끊어졌다」고 경고**했다. 자리 목록에서 못 찾으면
|
|
127
|
+
* 없는 것으로 봤기 때문이다.
|
|
128
|
+
*
|
|
129
|
+
* 한 칸에 두 대상을 담지 않는다 — FK 가 서로 다른 표를 가리키고, 합치면 DB 가 못 막는다.
|
|
130
|
+
* **둘 다 차는 일은 없다**(마스터의 `role` 이 하나를 정한다).
|
|
131
|
+
*/
|
|
132
|
+
@ManyToOne(type => TwinArea, { nullable: true, onDelete: 'SET NULL' })
|
|
133
|
+
@Field(type => TwinArea, { nullable: true, description: 'Home area this equipment belongs to, when it stands in an area rather than a location.' })
|
|
134
|
+
homeArea?: TwinArea
|
|
135
|
+
|
|
136
|
+
@RelationId((e: TwinEquipment) => e.homeArea)
|
|
137
|
+
homeAreaRowId?: string
|
|
138
|
+
|
|
117
139
|
/*
|
|
118
140
|
* 고장 모델 — 원본이 선언할 때만 있다. 없으면 고장 없는 설비로 동작한다(커널 계약).
|
|
119
141
|
* **관측(가동률)은 여기 두지 않는다** — 그건 트윈이 저널에서 만든 것이라 성질이 다르고,
|
|
@@ -3,6 +3,7 @@ import { Arg, Ctx, Int, Query, Resolver } from 'type-graphql'
|
|
|
3
3
|
import { ScalarObject, getRepository } from '@things-factory/shell'
|
|
4
4
|
|
|
5
5
|
import { itemRefOf, resolveByDeclaration } from './item-ref.js'
|
|
6
|
+
import { chooseRecipe } from './choose-recipe.js'
|
|
6
7
|
|
|
7
8
|
import { TwinInstance } from '../twin-instance/twin-instance.js'
|
|
8
9
|
import { TwinEvent } from '../twin-event/twin-event.js'
|
|
@@ -133,7 +134,39 @@ export class TwinLineageQuery {
|
|
|
133
134
|
|
|
134
135
|
const myMaterial = materialOf(epc)
|
|
135
136
|
const recipes: any[] = spec?.definition?.recipes ?? []
|
|
136
|
-
|
|
137
|
+
|
|
138
|
+
/*
|
|
139
|
+
* ── 「무엇으로 만들었나」는 **적혀 있다** (2026-09-05) ───────────────────────
|
|
140
|
+
*
|
|
141
|
+
* 예전에는 이 자리가 자재로 되짚어 `recipes.find(...)` 로 **먼저 걸리는 하나**를 골랐다. 배열
|
|
142
|
+
* 순서가 답을 정한다는 뜻이고, 한 자재에 레시피가 둘이면 틀린 답이 확신에 찬 얼굴로 나온다.
|
|
143
|
+
* 실제로 그런 자재가 있다 — `BOLT-M8` 에 `RCP-BOLT-STD` · `RCP-BOLT-FAST` 둘이다.
|
|
144
|
+
*
|
|
145
|
+
* 그런데 답은 이미 기록되어 있다. 만들 때 정해지는 사실이고 커널이 오더에 싣는다
|
|
146
|
+
* (`FlowOrder.recipeKey`). 변환 사건은 그 오더를 거래 식별자로 가리키고(`bizTransactionList`),
|
|
147
|
+
* 이 함수는 그 값을 이미 `tx` 로 꺼내 놓고도 쓰지 않았다.
|
|
148
|
+
*
|
|
149
|
+
* 세 자리가 다 있었고 마지막 연결만 없었다. 「가릴 수 없다」가 아니라 **안 읽고 있었다.**
|
|
150
|
+
*
|
|
151
|
+
* ── 규칙 셋 — 짐작하지 않는다 ───────────────────────────────────────────────
|
|
152
|
+
* 오더가 말한다 그것을 쓴다. 판이 둘이든 열이든 상관없다
|
|
153
|
+
* 오더가 말 안 하고
|
|
154
|
+
* 후보가 하나 그것을 쓴다 — 고를 것이 없으면 짐작이 아니다
|
|
155
|
+
* 후보가 둘 이상 **답하지 않는다.** 어느 것인지 모르는 채로 견주면 거짓 진단이 된다
|
|
156
|
+
*
|
|
157
|
+
* 마지막 줄이 이 함수가 식별자 되찾기에 이미 적용하던 규율이다(「후보가 둘 이상이면 답하지
|
|
158
|
+
* 않는다」). 두 줄 위에서만 안 지키고 있었다.
|
|
159
|
+
*/
|
|
160
|
+
const candidates = myMaterial
|
|
161
|
+
? recipes.filter((r: any) => (r.outputs ?? []).some((o: any) => o.material === myMaterial))
|
|
162
|
+
: []
|
|
163
|
+
|
|
164
|
+
/* 이 개체를 만든 변환이 가리키는 거래 — 그 오더가 레시피를 말한다. */
|
|
165
|
+
const madeTx = (madeBy.get(epc) ?? []).map(s => s.tx).find(Boolean)
|
|
166
|
+
const saidRecipeKey = madeTx ? await recipeKeyOfTransaction(domainId, instanceId, String(madeTx)) : undefined
|
|
167
|
+
|
|
168
|
+
const chosen = chooseRecipe(recipes, candidates, saidRecipeKey)
|
|
169
|
+
const recipe = chosen.recipe
|
|
137
170
|
|
|
138
171
|
/*
|
|
139
172
|
* ── 「모르는 개체」와 「대응이 없는 개체」는 다른 사실이다 (2026-08-23) ──────
|
|
@@ -164,6 +197,16 @@ export class TwinLineageQuery {
|
|
|
164
197
|
}
|
|
165
198
|
} else if (!myMaterial) {
|
|
166
199
|
conformance = { comparable: false, reason: 'item-not-bound' }
|
|
200
|
+
} else if (chosen.basis === 'said-but-undeclared') {
|
|
201
|
+
/* 오더는 말했는데 그 레시피가 모델의 선언에 없다 — 원본과 선언이 어긋난 것이지 모르는 것이 아니다. */
|
|
202
|
+
conformance = { comparable: false, reason: 'recipe-not-declared', recipe: saidRecipeKey }
|
|
203
|
+
} else if (chosen.basis === 'ambiguous') {
|
|
204
|
+
/* 짐작하지 않는다 — 어느 것으로 만들었는지 말해 주는 사실이 없다. 후보를 밝혀 사람이 판단하게 한다. */
|
|
205
|
+
conformance = {
|
|
206
|
+
comparable: false,
|
|
207
|
+
reason: 'many-recipes-for-material',
|
|
208
|
+
recipes: candidates.map((r: any) => r.key).sort()
|
|
209
|
+
}
|
|
167
210
|
} else if (!recipe) {
|
|
168
211
|
conformance = { comparable: false, reason: 'no-recipe-for-material' }
|
|
169
212
|
} else {
|
|
@@ -205,6 +248,8 @@ export class TwinLineageQuery {
|
|
|
205
248
|
comparable: true,
|
|
206
249
|
material: myMaterial,
|
|
207
250
|
recipe: recipe.key,
|
|
251
|
+
/* 그 레시피를 **어떻게 알았나** — 화면이 「기록으로 안다」와 「하나뿐이라 안다」를 구별할 수 있게. */
|
|
252
|
+
recipeBasis: chosen.basis,
|
|
208
253
|
/* 자재마다 선언·관측을 나란히. 어느 한쪽에만 있는 것도 사실이다(빠뜨리지 않는다). */
|
|
209
254
|
lines: keys.map(m => ({
|
|
210
255
|
material: m,
|
|
@@ -231,3 +276,39 @@ export class TwinLineageQuery {
|
|
|
231
276
|
}
|
|
232
277
|
}
|
|
233
278
|
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* 그 거래를 가리키는 오더가 **어느 레시피로** 만들었다고 말하는가.
|
|
282
|
+
*
|
|
283
|
+
* ── 왜 거래 식별자로 묻나 ───────────────────────────────────────────────────
|
|
284
|
+
* EPCIS 변환 사건에는 레시피가 없다 — 표준에 그 자리가 없고, 대신 생산 오더를 거래로 가리킨다
|
|
285
|
+
* (`bizTransactionList`). 오더 사건은 `recipeKey` 와 그 거래 식별자를 **둘 다** 든다. 그래서 이
|
|
286
|
+
* 두 칸이 EPCIS 에서 오더로 건너가는 다리다(§`twin-event-keys.ts`).
|
|
287
|
+
*
|
|
288
|
+
* ── 색인을 탄다 ─────────────────────────────────────────────────────────────
|
|
289
|
+
* `ix_twin_event_9 (domain, instanceId, bizTransaction)` 로 좁힌다. 이 칸이 없던 때는 payload 를
|
|
290
|
+
* 문자열로 훑어야 했고 32만 행에 3분이 걸렸다 — 화면이 기다릴 비용이 아니다.
|
|
291
|
+
*
|
|
292
|
+
* ── 못 찾으면 **답하지 않는다** ──────────────────────────────────────────────
|
|
293
|
+
* 옛 행에는 이 칸이 비어 있다(과거 행은 채우지 않는다는 규율). 그때는 `undefined` 를 내고, 부르는
|
|
294
|
+
* 쪽이 후보 수로 판단한다 — 여기서 아무거나 고르면 예전과 같은 결함이 된다.
|
|
295
|
+
*/
|
|
296
|
+
async function recipeKeyOfTransaction(
|
|
297
|
+
domainId: string,
|
|
298
|
+
instanceId: string,
|
|
299
|
+
bizTransaction: string
|
|
300
|
+
): Promise<string | undefined> {
|
|
301
|
+
const row = await getRepository(TwinEvent)
|
|
302
|
+
.createQueryBuilder('e')
|
|
303
|
+
.where('e.domain = :domainId', { domainId })
|
|
304
|
+
.andWhere('e.instanceId = :instanceId', { instanceId })
|
|
305
|
+
.andWhere('e.bizTransaction = :tx', { tx: bizTransaction })
|
|
306
|
+
.andWhere('e.eventType = :eventType', { eventType: 'order.status' })
|
|
307
|
+
/* 어느 줄이든 같은 오더를 말하므로 하나면 된다 — 가장 이른 것이 그 오더가 선 순간이다. */
|
|
308
|
+
.orderBy('e.eventTime', 'ASC')
|
|
309
|
+
.limit(1)
|
|
310
|
+
.getOne()
|
|
311
|
+
|
|
312
|
+
const said = (row?.payload as any)?.data?.recipeKey
|
|
313
|
+
return typeof said === 'string' && said ? said : undefined
|
|
314
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { twinWarn } from '../../engine/log.js'
|
|
1
2
|
import { Arg, Ctx, Int, Query, Resolver } from 'type-graphql'
|
|
2
3
|
|
|
3
4
|
import { ScalarObject, getRepository } from '@things-factory/shell'
|
|
@@ -17,7 +18,9 @@ import { TwinArea } from '../twin-space/twin-area.js'
|
|
|
17
18
|
import { at, idOf, nameIndex, namesFor } from './name-index.js'
|
|
18
19
|
|
|
19
20
|
/* 커널 런타임 로드 — 축·관계·속성 선언의 SSOT. 이 파일은 도메인 모양을 알지 않는다. */
|
|
20
|
-
const { TWIN_AXES, TWIN_RELATIONS, axisInfo, axisSource, documentPath, relationsFrom, relationsTo, propertiesOf } =
|
|
21
|
+
const { TWIN_AXES, TWIN_RELATIONS, axisInfo, axisSource, documentPath, isCacheableTense, relationsFrom, relationsTo, propertiesOf, tenseOf } =
|
|
22
|
+
require('@operato/ops-contract')
|
|
23
|
+
import { externalIncoming, resolveExternal } from './external-resolver.js'
|
|
21
24
|
|
|
22
25
|
/**
|
|
23
26
|
* 축 → 그 축이 이미 **행으로 옮겨졌나**, 그리고 행의 어느 컬럼이 id 인가.
|
|
@@ -31,6 +34,32 @@ const ROW_BACKED: Record<string, { entity: any; idColumn: string; order: any }>
|
|
|
31
34
|
operations: { entity: TwinOperation, idColumn: 'operationKey', order: { seq: 'ASC' } }
|
|
32
35
|
}
|
|
33
36
|
|
|
37
|
+
/*
|
|
38
|
+
* `productionSpec.binding` 의 키가 사는 축 — 자재가 둘로 갈려 있다.
|
|
39
|
+
*
|
|
40
|
+
* materials productionSpec.definition.materials 레시피의 투입 자재
|
|
41
|
+
* materialDefinitions materialDefinitions 마스터 품목 목록
|
|
42
|
+
*
|
|
43
|
+
* 예전에는 `materials` 하나만 봤다. 커넥터에 따라 `binding` 키가 어느 축의 id 인지 달라서, MES 연결
|
|
44
|
+
* 둘은 품목이 자기 GS1 참조를 한 건도 답하지 못했다. 오류는 안 나고 칸이 비어서 나온다.
|
|
45
|
+
*
|
|
46
|
+
* 실측(2026-09-06, 인티그레이션 레인이 찾음):
|
|
47
|
+
*
|
|
48
|
+
* 트윈 binding def.materials 맞음 materialDefinitions 맞음
|
|
49
|
+
* mes-miratek 106 0 106
|
|
50
|
+
* mes-line-a 21 0 21
|
|
51
|
+
* operato-chef-v1-pocheon 2789 1789 0
|
|
52
|
+
* rosarito-mes 20 20 0
|
|
53
|
+
*
|
|
54
|
+
* **두 축이 동시에 맞는 트윈은 없다**(양쪽 id 에 다 있으면서 binding 키인 것 0개). 그래서 두 축에
|
|
55
|
+
* 다 걸어도 어느 쪽을 붙일지 모호해지지 않는다. 조회가 판정한다 — `binding[id]` 는 키가 아닌 축에서
|
|
56
|
+
* 그냥 없다.
|
|
57
|
+
*
|
|
58
|
+
* 두 축을 한 이름으로 합치지 않는다. 레시피 투입 자재와 마스터 품목은 서로 다른 것이고, 합치면
|
|
59
|
+
* 둘 다 있는 트윈(chef)에서 어느 쪽 목록인지 알 수 없게 된다.
|
|
60
|
+
*/
|
|
61
|
+
const BINDING_AXES = new Set(['materials', 'materialDefinitions'])
|
|
62
|
+
|
|
34
63
|
|
|
35
64
|
/** 검색 — 항목의 값 어디든 걸리면 맞는 것으로 본다. 축마다 검색 컬럼을 정해 두면 축이 늘 때마다 여기를 고쳐야 한다. */
|
|
36
65
|
function matches(o: any, q: string): boolean {
|
|
@@ -272,7 +301,7 @@ export class TwinModelItemQuery {
|
|
|
272
301
|
* 값을 복제해 두면 두 곳이 어긋나고, 어긋난 쪽을 아무도 모른다.
|
|
273
302
|
*/
|
|
274
303
|
private withBinding(axis: string, model: any, fields: any): any {
|
|
275
|
-
if (axis
|
|
304
|
+
if (!BINDING_AXES.has(axis) || !fields || typeof fields !== 'object') return fields
|
|
276
305
|
const binding: Record<string, string> = model?.productionSpec?.binding ?? {}
|
|
277
306
|
const ref = binding[fields.key ?? fields.id]
|
|
278
307
|
/* 없으면 키를 만들지 않는다 — 빈 값이 있는 것과 선언되지 않은 것은 다르다. */
|
|
@@ -746,13 +775,55 @@ export class TwinModelItemQuery {
|
|
|
746
775
|
/* 대상이 축이면 그 축의 항목으로 갈 수 있다. 타입·외부(구역)면 **갈 곳이 다르다** —
|
|
747
776
|
화면이 링크를 만들지 말지 알아야 하므로 대상 종류를 그대로 낸다(막다른 링크 금지). */
|
|
748
777
|
target: r.target,
|
|
778
|
+
/*
|
|
779
|
+
* ── 언제의 일인가, 그리고 담아 둬도 되나 (2026-09-06) ────────────────
|
|
780
|
+
* 판정을 계약이 낸다. 화면이 `standing` 을 보고 스스로 정하면 값이 하나 늘 때 한 화면만
|
|
781
|
+
* 따라간다. UX 레인이 이 칸을 요청했고, 그쪽에 있던 규칙은 걷었다.
|
|
782
|
+
*
|
|
783
|
+
* `tense` 가 없는 관계도 `cacheable: false` 다. 모를 때는 다시 조회하는 쪽이 낫다 —
|
|
784
|
+
* 낡은 값을 자신 있게 보여 주는 것이 비어 있는 것보다 나쁘다. 그래서 `tense` 는 없을 수
|
|
785
|
+
* 있고 `cacheable` 은 언제나 있다.
|
|
786
|
+
*/
|
|
787
|
+
tense: tenseOf(r),
|
|
788
|
+
cacheable: isCacheableTense(tenseOf(r)),
|
|
749
789
|
ids: ids.slice(0, NEIGHBOR_MAX),
|
|
750
790
|
total: ids.length
|
|
751
791
|
}
|
|
752
|
-
})
|
|
792
|
+
})
|
|
793
|
+
|
|
794
|
+
/*
|
|
795
|
+
* ── 밖을 가리키는 관계를 푼다 (2026-09-05) ─────────────────────────────────
|
|
796
|
+
* 계약이 `kind: 'external'` 을 선언하며 「해소는 호스트가 한다」고 적어 두었는데 **부르는 곳이
|
|
797
|
+
* 0이었다.** 그래서 화면은 id 만 받고 그것이 무엇인지, 갈 수 있는지 몰랐다.
|
|
798
|
+
*
|
|
799
|
+
* 못 풀면 **못 푼다고 말한다** — 링크를 지우면 화면이 「그런 관계가 없다」로 읽고, 실제로는
|
|
800
|
+
* 「아무도 답할 수 없다」다. 사람이 할 일이 다르다.
|
|
801
|
+
*/
|
|
802
|
+
for (const l of out) {
|
|
803
|
+
if (l.target?.kind !== 'external' || !l.ids.length) continue
|
|
804
|
+
const got = await resolveExternal(l.target.entity, domainId, l.ids, m => twinWarn(m))
|
|
805
|
+
if (got.refs) (l as any).refs = got.refs
|
|
806
|
+
else (l as any).unresolved = got.unresolved
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
/*
|
|
810
|
+
* **비어 있는 관계도 낸다.** 예전에는 `total > 0` 만 냈다 — 그러면 사람은 없다는 것조차 모른다.
|
|
811
|
+
*
|
|
812
|
+
* 실측으로 그 값이 드러났다(2026-09-05): 설비 64대에 시험 규격이 전부 비어 있는 것을 「빈 묶음」
|
|
813
|
+
* 으로 세워 두니 **「이 설비에 검사 기준이 없다」와 「화면이 그것을 안 본다」가 구별됐다.** 지우면
|
|
814
|
+
* 그 구별이 사라진다.
|
|
815
|
+
*
|
|
816
|
+
* 선택이 아닌 관계(`optional: false`)가 비어 있으면 그것은 **끊어진 참조**이고, 그 사실이 화면에
|
|
817
|
+
* 닿아야 한다.
|
|
818
|
+
*/
|
|
753
819
|
|
|
754
820
|
/* 들어오는 쪽 — 나를 가리키는 항목들. 그 축이 행이면 DB 가 세고, 문서면 문서를 전수 조회한다. */
|
|
755
821
|
const incoming: any[] = []
|
|
822
|
+
/*
|
|
823
|
+
* 밖에서 오는 것을 함께 싣는다 — 제품이 자기 저장소에서 모은다(커널은 그 표를 모른다).
|
|
824
|
+
* 꽂힌 해소기가 없으면 빈 배열이고 그것은 정상이다.
|
|
825
|
+
*/
|
|
826
|
+
incoming.push(...(await externalIncoming(domainId, inst.instanceId, axis, itemId, m => twinWarn(m))))
|
|
756
827
|
for (const r of relationsTo(axis)) {
|
|
757
828
|
const fromInfo = axisInfo(r.from)
|
|
758
829
|
if (!fromInfo) continue
|
|
@@ -799,7 +870,27 @@ export class TwinModelItemQuery {
|
|
|
799
870
|
if (id) hits.push(id)
|
|
800
871
|
}
|
|
801
872
|
}
|
|
802
|
-
|
|
873
|
+
/*
|
|
874
|
+
* **들어오는 쪽도 빈 것을 낸다** — 나가는 쪽과 같은 이유다(위 주석).
|
|
875
|
+
*
|
|
876
|
+
* 「이 설비를 가리키는 작업이 없다」는 사실이고, 그것을 지우면 화면은 그 물음을 물어보지도
|
|
877
|
+
* 못한다. 축당 들어오는 관계는 0~8개다(실측 2026-09-05: 시험규격 8 · 자리 6 · 나머지 2 이하)
|
|
878
|
+
* — 다 내도 읽을 수 있는 분량이다.
|
|
879
|
+
*
|
|
880
|
+
* 값도 안 든다. 행 축은 0을 알아내려면 이미 `find` 를 돌렸고, 그 0을 말하는 비용은 없다.
|
|
881
|
+
*
|
|
882
|
+
* `externalIncoming` 은 여기서 예외다 — 그쪽의 빈 묶음은 「그 제품이 이 축을 안 본다」는
|
|
883
|
+
* 뜻이고 우리 선언에 없는 관계이므로 할 말이 없다.
|
|
884
|
+
*/
|
|
885
|
+
incoming.push({
|
|
886
|
+
via: r.via,
|
|
887
|
+
from: r.from,
|
|
888
|
+
field: r.field,
|
|
889
|
+
tense: tenseOf(r),
|
|
890
|
+
cacheable: isCacheableTense(tenseOf(r)),
|
|
891
|
+
ids: hits.slice(0, NEIGHBOR_MAX),
|
|
892
|
+
total: hits.length
|
|
893
|
+
})
|
|
803
894
|
}
|
|
804
895
|
|
|
805
896
|
return { out, incoming }
|
|
@@ -14,7 +14,7 @@ const { TWIN_RELATIONS, axisInfo } = require('@operato/ops-contract')
|
|
|
14
14
|
* 다단 BOM 을 "자재 안에 자재" 로 적으면 같은 부품이 여러 완제품에 들어갈 때마다 복제되고,
|
|
15
15
|
* 수량이 어디 붙는지도 애매해진다. 표준(ISA-95)이 택한 모양은 **자재와 레시피가 번갈아 나오는
|
|
16
16
|
* 그래프**다: 완제품 ←산출— 레시피 —투입→ 부품들, 그리고 그 부품이 또 어느 레시피의 산출이면
|
|
17
|
-
* 거기서 다시 내려간다. 우리 선언(`
|
|
17
|
+
* 거기서 다시 내려간다. 우리 선언(`ops.rel.input`·`ops.rel.output`)이 이미 그 두 홉이다.
|
|
18
18
|
*
|
|
19
19
|
* ── 패턴을 코드가 알지 않는다 ──────────────────────────────────────────────────
|
|
20
20
|
* "BOM 은 산출→투입" 이라고 적어 두면 그 순간 이 파일이 도메인을 알게 된다(ADR-0031 위반).
|
|
@@ -77,6 +77,45 @@ export class TwinSpace {
|
|
|
77
77
|
@Field({ nullable: true, description: 'IANA time zone of this physical site (e.g. America/Tijuana) — the base for reading shift times. Belongs to the space, not the tenant, because one tenant can run plants in different zones.' })
|
|
78
78
|
timezone?: string
|
|
79
79
|
|
|
80
|
+
/**
|
|
81
|
+
* **이 현장이 언제 도나** — 표준 `WorkCalendarEntry`(복수). 휴일 · 주말 · 정기보수가 여기 든다.
|
|
82
|
+
*
|
|
83
|
+
* ── 왜 공간에 있나 (2026-09-07) ──────────────────────────────────────────
|
|
84
|
+
* 이 칸이 없던 동안 설비와 사람만 달력을 가질 수 있었다. 그래서 **현장이 언제 쉬는지가 어디에도
|
|
85
|
+
* 없었고**, 계획 조업 시간이 「재기 시작한 뒤 흐른 시간 전부」였다 — 주말도 야간도 휴일도 계획 조업으로
|
|
86
|
+
* 세었다. [산술] 8시간 × 주5일이면 168시간 중 40시간이 조업이니 그 수가 4.2배 크다.
|
|
87
|
+
*
|
|
88
|
+
* 시간대와 같은 축이라 같은 자리에 둔다. 「이 현장의 06:00 이 언제인가」와 「이 현장이 언제 도나」는
|
|
89
|
+
* 둘 다 현장이 갖는 값이고, 테넌트가 아니다 — 한 테넌트가 다른 시간대·다른 조업일의 공장을 함께 가진다.
|
|
90
|
+
*
|
|
91
|
+
* `withSpaceTimeBase` 가 이 값을 모델에 얹고, 거기서 OEE 누적기가 계획 정지를 낸다. 얹는 자리를
|
|
92
|
+
* 시간대와 하나로 둔 이유는 **한쪽만 채워진 상태**를 만들지 않기 위해서다.
|
|
93
|
+
*
|
|
94
|
+
* 없으면 아무것도 가정하지 않는다 — 종일 가동으로 보는 것은 「제약이 없다」가 아니라 「선언되지
|
|
95
|
+
* 않았다」이고, 그 사실은 세어져야 한다(안 그러면 사람이 수를 되살리려고 아무 달력이나 채운다).
|
|
96
|
+
*/
|
|
97
|
+
@Column({ type: 'simple-json', nullable: true })
|
|
98
|
+
@Field(type => ScalarObject, {
|
|
99
|
+
nullable: true,
|
|
100
|
+
description:
|
|
101
|
+
'When this site runs — standard WorkCalendarEntry list (holidays, weekends, planned maintenance). Absent means undeclared, not unrestricted: planned busy time then counts the whole window.'
|
|
102
|
+
})
|
|
103
|
+
workCalendar?: any
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* 현장의 **우편 주소** — 사람이 읽고 서류에 적는 값.
|
|
107
|
+
*
|
|
108
|
+
* ── 왜 좌표와 따로 두나 (2026-09-07) ────────────────────────────────────
|
|
109
|
+
* `geo` 는 도면을 지구에 붙이는 값이고 화면이 쓴다. 주소는 **사람과 서류의 값**이다 — 납품·통관·
|
|
110
|
+
* 감사에 그 문자열이 그대로 들어가고, 좌표에서 되돌려 만들 수 없다(같은 좌표에 여러 주소가 있고
|
|
111
|
+
* 동·호가 좌표에 없다).
|
|
112
|
+
*
|
|
113
|
+
* 고객이 준 것을 그대로 적는다. 우편번호·동·호를 우리가 지어내거나 좌표에서 유추하지 않는다.
|
|
114
|
+
*/
|
|
115
|
+
@Column({ nullable: true })
|
|
116
|
+
@Field({ nullable: true, description: 'Postal address of this site as the customer states it. Kept apart from `geo`: a floor or unit number has no coordinate, and paperwork carries this string verbatim.' })
|
|
117
|
+
address?: string
|
|
118
|
+
|
|
80
119
|
@Column({ nullable: true })
|
|
81
120
|
@Field({ nullable: true, description: 'Id of the primary representation shown on the home/monitor.' })
|
|
82
121
|
primaryRepresentationId?: string
|
|
@@ -46,8 +46,10 @@ import { twinEventKeys } from '../twin-event/twin-event-keys.js'
|
|
|
46
46
|
* location 자리
|
|
47
47
|
* equipment 설비
|
|
48
48
|
* task 작업
|
|
49
|
+
* transaction 거래(PO/SO/생산오더) — 오더와 다르다. 전에는 오더 칸에 섞여 들어와 한 오더의
|
|
50
|
+
* 이력이 두 쪽으로 갈라졌다(2026-09-05)
|
|
49
51
|
*/
|
|
50
|
-
export const SUBJECT_KINDS = ['item', 'lot', 'order', 'location', 'equipment', 'task'] as const
|
|
52
|
+
export const SUBJECT_KINDS = ['item', 'lot', 'order', 'location', 'equipment', 'task', 'transaction'] as const
|
|
51
53
|
export type SubjectKind = (typeof SUBJECT_KINDS)[number]
|
|
52
54
|
|
|
53
55
|
export interface EventSubject {
|
|
@@ -133,6 +135,11 @@ export function subjectsOf(envelope: any, opts?: { itemOf?: ItemResolver }): Eve
|
|
|
133
135
|
put(out, 'item', item || classEpc)
|
|
134
136
|
}
|
|
135
137
|
put(out, 'order', keys.orderId)
|
|
138
|
+
/*
|
|
139
|
+
* 거래는 오더가 아니다 — 갈라 적는다. 합쳐 적으면 EPCIS 사건의 GDTI 가 오더 이름으로 앉고,
|
|
140
|
+
* 그러면 한 오더의 이력이 두 쪽이 된다(`twin-event-keys.ts` 의 그 실측).
|
|
141
|
+
*/
|
|
142
|
+
put(out, 'transaction', keys.bizTransaction)
|
|
136
143
|
put(out, 'location', keys.locationId)
|
|
137
144
|
put(out, 'equipment', keys.moverId)
|
|
138
145
|
|
package/shared/entity-delta.ts
CHANGED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* ★ **답의 모양이 세 곳에서 같아야 한다** — 어댑터가 답한 칸이 중간에서 사라지지 않게.
|
|
3
|
+
*
|
|
4
|
+
* ── 무엇이 있었나 (2026-09-04, 인티그레이션 레인이 찾음) ────────────────────
|
|
5
|
+
* 조치의 답이 세 타입을 지난다.
|
|
6
|
+
*
|
|
7
|
+
* ReferenceAdapter.actuate 반환 커넥터가 만든다
|
|
8
|
+
* ActuationAck routeActuation 이 되돌린다
|
|
9
|
+
* CommandAck dispatch 가 받아 표에 적는다
|
|
10
|
+
*
|
|
11
|
+
* 가운데가 `ok`·`ref`·`error` 셋만 들고 있었다. 그래서 **타입은 `note`·`noteNext`·`retry` 를
|
|
12
|
+
* 버린다고 말했다.** 값은 흘렀다 — JS 객체가 키를 안 버리므로. 실제로 `dispatch_note` 에 앉았다.
|
|
13
|
+
*
|
|
14
|
+
* 그런데 **커넥터를 만드는 사람이 그 칸이 있다는 것을 알 방법이 없었다.** 이 저장소에서 하루
|
|
15
|
+
* 종일 「자리는 있고 길이 없다」로 부른 것의 타입 판이다 — 동작은 하고 계약이 안 알려 준다.
|
|
16
|
+
*
|
|
17
|
+
* ── 왜 원문을 읽나 ──────────────────────────────────────────────────────────
|
|
18
|
+
* 타입은 런타임에 없다. 그래서 「세 모양이 같은가」를 값으로 잴 수 없고, 원문에서 읽는 것이
|
|
19
|
+
* 지금 할 수 있는 최선이다. 이 검사가 막으려는 것은 **한 곳에만 칸을 더하는 일**이다.
|
|
20
|
+
*/
|
|
21
|
+
import { test } from 'node:test'
|
|
22
|
+
import assert from 'node:assert/strict'
|
|
23
|
+
import { readFileSync } from 'node:fs'
|
|
24
|
+
|
|
25
|
+
/** 주석 속 낱말이 세어지지 않게 — 오늘 두 번 그 함정에 걸렸다. */
|
|
26
|
+
const bare = (p: string): string =>
|
|
27
|
+
readFileSync(new URL(p, import.meta.url), 'utf8')
|
|
28
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
29
|
+
.replace(/\/\/[^\n]*/g, '')
|
|
30
|
+
|
|
31
|
+
/** 답이 반드시 나를 칸 — 하나라도 중간에서 사라지면 커넥터가 적은 것이 버려진다. */
|
|
32
|
+
const CARRIED = ['note', 'noteNext', 'retry'] as const
|
|
33
|
+
|
|
34
|
+
const SOURCES = {
|
|
35
|
+
'어댑터 actuate 반환': '../server/service/reference/reference-adapter.ts',
|
|
36
|
+
ActuationAck: '../server/service/reference/actuation-routing.ts',
|
|
37
|
+
CommandAck: '../server/service/actuation/command-dispatcher.ts'
|
|
38
|
+
} as const
|
|
39
|
+
|
|
40
|
+
test('★ 세 타입이 같은 칸을 든다 — 한 곳에만 더하면 값이 중간에서 버려진다', () => {
|
|
41
|
+
const missing: string[] = []
|
|
42
|
+
|
|
43
|
+
for (const [label, path] of Object.entries(SOURCES)) {
|
|
44
|
+
const src = bare(path)
|
|
45
|
+
for (const field of CARRIED) {
|
|
46
|
+
/* `field?:` 로 선언된 자리를 찾는다 — 이름만 스치는 것은 세지 않는다. */
|
|
47
|
+
if (!new RegExp(`\\b${field}\\?:`).test(src)) missing.push(`${label} 에 ${field} 없음`)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
assert.deepEqual(
|
|
52
|
+
missing,
|
|
53
|
+
[],
|
|
54
|
+
'조치의 답이 지나는 세 타입 중 한 곳이 칸을 버린다. 값은 흐르지만 커넥터를 만드는 사람은\n' +
|
|
55
|
+
'그 칸이 있다는 것을 알 수 없다:\n ' + missing.join('\n ')
|
|
56
|
+
)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test('★ 갈래 셋은 한 곳에서 정의되고 나머지는 그것을 가리킨다 — 반복하면 갈라진다', () => {
|
|
60
|
+
/*
|
|
61
|
+
* `again`·`after-fix`·`never` 는 어댑터가 고르고 일꾼이 판정한다. 세 곳에 낱말을 각각 적으면
|
|
62
|
+
* 한쪽만 늘어나는 날 다른 쪽이 **모르는 값을 「말하지 않은 것」으로 읽어 조용히 집지 않는다.**
|
|
63
|
+
*
|
|
64
|
+
* 처음에 이 검사를 「세 곳에 낱말이 다 있는가」로 뒀다. 그것이 곧 **반복을 요구하는 검사**였고,
|
|
65
|
+
* 반복이 바로 갈라짐의 원인이다. 그래서 규칙을 바꿨다: 정의는 한 곳, 나머지는 그 이름을 가리킨다.
|
|
66
|
+
*/
|
|
67
|
+
const outlooks = ['again', 'after-fix', 'never'] as const
|
|
68
|
+
|
|
69
|
+
/* 정의하는 자리 — 셋을 다 안다. */
|
|
70
|
+
const plan = bare('../server/service/actuation/retry-plan.ts')
|
|
71
|
+
assert.match(plan, /export type RetryOutlook/, '갈래를 정의하는 자리가 없다')
|
|
72
|
+
for (const o of outlooks) assert.ok(plan.includes(`'${o}'`), `정의가 갈래 '${o}' 를 모른다`)
|
|
73
|
+
|
|
74
|
+
/* 선언하는 자리는 낱말을 다 적거나 그 이름을 가리켜야 한다. */
|
|
75
|
+
const loose: string[] = []
|
|
76
|
+
for (const [label, path] of Object.entries(SOURCES)) {
|
|
77
|
+
const src = bare(path)
|
|
78
|
+
if (!/\bretry\?:/.test(src)) continue
|
|
79
|
+
|
|
80
|
+
const namesType = /retry\?:\s*RetryOutlook/.test(src)
|
|
81
|
+
const listsAll = outlooks.every(o => src.includes(`'${o}'`))
|
|
82
|
+
if (!namesType && !listsAll) loose.push(`${label} 이 갈래를 좁히지 않는다`)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
assert.deepEqual(
|
|
86
|
+
loose,
|
|
87
|
+
[],
|
|
88
|
+
'갈래를 문자열로 열어 두면 커넥터가 모르는 낱말을 보내도 타입이 통과시킨다:\n ' + loose.join('\n ')
|
|
89
|
+
)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
test('★ 이 검사가 실제로 무언가를 잰다 — 원문을 못 읽으면 빨강', () => {
|
|
93
|
+
for (const [label, path] of Object.entries(SOURCES)) {
|
|
94
|
+
const src = bare(path)
|
|
95
|
+
assert.ok(src.length > 1000, `${label} 원문이 ${src.length}자다 — 경로가 틀렸다`)
|
|
96
|
+
}
|
|
97
|
+
})
|