@things-factory/headless-twin 10.0.18 → 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 +19 -16
- 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 +230 -0
- package/dist-server/service/actuation/command-dispatcher.js +269 -0
- package/dist-server/service/actuation/command-dispatcher.js.map +1 -0
- package/dist-server/service/actuation/command-store.d.ts +11 -0
- package/dist-server/service/actuation/command-store.js +219 -0
- package/dist-server/service/actuation/command-store.js.map +1 -0
- 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 +25 -0
- package/dist-server/service/actuation/index.js +30 -0
- package/dist-server/service/actuation/index.js.map +1 -0
- 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 +111 -0
- package/dist-server/service/actuation/twin-command.js +170 -0
- package/dist-server/service/actuation/twin-command.js.map +1 -0
- 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 +28 -17
- 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 +149 -22
- package/dist-server/service/reference/hook-contract.js +236 -23
- 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/hook-store.d.ts +3 -0
- package/dist-server/service/reference/hook-store.js +28 -0
- package/dist-server/service/reference/hook-store.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 +355 -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 +64 -6
- package/dist-server/service/reference/reference-hook.js +265 -27
- 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 +19 -20
- 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 +511 -0
- package/server/service/actuation/command-store.ts +236 -0
- 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 +27 -0
- 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 +232 -0
- package/server/service/actuation/twin-now.ts +97 -0
- package/server/service/index.ts +11 -0
- 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 +280 -22
- package/server/service/reference/hook-rejected.ts +53 -0
- package/server/service/reference/hook-store.ts +28 -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 +392 -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 +355 -32
- 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 +209 -0
- 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 +333 -0
- 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 +114 -23
- 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
|
@@ -142,6 +142,20 @@ export interface RefTestSpecification {
|
|
|
142
142
|
export interface RefEquipment {
|
|
143
143
|
id: string
|
|
144
144
|
kind: string
|
|
145
|
+
/**
|
|
146
|
+
* 속한 **등급들** — 계약 선언의 `equipment[].equipmentClassIds` 로 그대로 간다.
|
|
147
|
+
*
|
|
148
|
+
* ── 왜 빠져 있었나 (2026-09-04) ─────────────────────────────────────────
|
|
149
|
+
* 커널에 이 축을 여는 날 모델·상태·저널·미러까지 배선했는데 **접수면이 빠졌다.** 사람과 자산은
|
|
150
|
+
* 받는데(`personnelClassIds`·`assetClassIds`) 설비만 없었다.
|
|
151
|
+
*
|
|
152
|
+
* 그래서 원본이 설비 22대에 등급을 다 채워 보냈는데 **문턱에서 버려졌다** — 거절도 경고도 없이.
|
|
153
|
+
* 등급 목록은 서고 소속은 아무도 없어서, 화면에서 등급을 골라도 설비가 안 나왔다.
|
|
154
|
+
*
|
|
155
|
+
* 소속은 이 선언과 `kind` 를 합친 것이다(§`equipmentClassMembership`) — 판정하는 쪽이 그 함수를
|
|
156
|
+
* 지난다.
|
|
157
|
+
*/
|
|
158
|
+
equipmentClassIds?: string[]
|
|
145
159
|
/** 사람이 부르는 이름 — 없으면 화면이 id 를 보인다(id 를 이름으로 베끼지 않는다). */
|
|
146
160
|
name?: string
|
|
147
161
|
/** 시험 결과 — 자격이 성립하는지는 이것이 말한다(§RefTestResult). */
|
|
@@ -273,6 +287,33 @@ export interface RefPerson {
|
|
|
273
287
|
name?: string
|
|
274
288
|
/** 시험 결과 — 자격이 성립하는지는 이것이 말한다(§RefTestResult). */
|
|
275
289
|
testResults?: RefTestResult[]
|
|
290
|
+
/**
|
|
291
|
+
* **이 사람의 전역 정체성** — 커널 선언의 `personnel[].identity` 로 간다.
|
|
292
|
+
*
|
|
293
|
+
* ── 왜 필요한가 (인티그레이션 레인이 물음, 2026-09-05) ─────────────────────
|
|
294
|
+
* 설비에서 이 자리를 만든 이유가 사람에게도 그대로 걸린다: **지역 이름은 그 현장 안에서만
|
|
295
|
+
* 고유하다.** 인버터 `001` 이 두 발전소에 있어서 함께 집계하니 한쪽 발전량이 사라졌다.
|
|
296
|
+
* `E-1001` 도 다른 공장에 있을 수 있고, 여러 현장을 함께 보는 순간 같은 일이 난다.
|
|
297
|
+
*
|
|
298
|
+
* ── **여기에 개인정보를 넣지 않는다** ──────────────────────────────────────
|
|
299
|
+
* 설비와 사람이 갈리는 자리가 이것이다. 설비의 정체성은 하드웨어 일련번호이고 아무에게도 해가
|
|
300
|
+
* 없다. 사람의 「전역에서 고유한 값」은 사원번호·출입카드·주민등록처럼 **그 사람에 관한 사실**
|
|
301
|
+
* 이고, 저널은 되풀이 읽히고 미러로 복제되고 구독으로 방송된다 — 한 번 들어가면 지워지지 않는다.
|
|
302
|
+
*
|
|
303
|
+
* 그래서 이 칸이 요구하는 것은 **현장을 앞에 붙여 전역에서 고유해진 이름**이다. 그 이상이 아니다.
|
|
304
|
+
*
|
|
305
|
+
* ```
|
|
306
|
+
* 맞다 urn:hatiolab:mes-line-a:obj:E-1001 (§`objectUri` 가 만든다)
|
|
307
|
+
* https://hatiotrailer.com/twin/obj/E-1001
|
|
308
|
+
* 아니다 카드번호 · 사원번호 원문 · 주민등록번호 · 생체 식별자
|
|
309
|
+
* ```
|
|
310
|
+
*
|
|
311
|
+
* 「이 사람이 누구인가」는 인사 시스템이 답할 물음이고 트윈의 물음이 아니다. 트윈은 **같은
|
|
312
|
+
* 사람인지 다른 사람인지**만 알면 된다 — 그 일에 개인정보는 필요 없다.
|
|
313
|
+
*
|
|
314
|
+
* 값이 없으면 비운다. 지금 원본이 그런 값을 주지 않으므로 당분간 비어 있는 것이 정상이다.
|
|
315
|
+
*/
|
|
316
|
+
identity?: string
|
|
276
317
|
/**
|
|
277
318
|
* 속한 등급들 — **표준 `Person.PersonnelClassID`, `maxOccurs="unbounded"`**.
|
|
278
319
|
* 복수인 것이 표준이고 그것이 자격 표현이다: 한 사람이 용접 자격과 지게차 자격을 함께 갖는다.
|
|
@@ -319,8 +360,21 @@ export interface RefAsset {
|
|
|
319
360
|
assetClassIds?: string[]
|
|
320
361
|
/** 기본 위치. */
|
|
321
362
|
homeLocationId?: string
|
|
322
|
-
/**
|
|
323
|
-
|
|
363
|
+
/**
|
|
364
|
+
* **이 자산의 전역 정체성** — 설비와 같은 이름이다(§`RefEquipment.identity`).
|
|
365
|
+
*
|
|
366
|
+
* ── 왜 이름을 바꿨나 (2026-09-04) ───────────────────────────────────────
|
|
367
|
+
* `gs1Id` 였다. 설비에서 그 이름을 바꾼 이유가 여기에도 그대로 적용된다 — **GS1 키가 아닌 것이
|
|
368
|
+
* 정상이다.** 자산의 실측 값도 제조번호·사내 관리번호이고, GS1 을 요구하는 이름을 두면 그것을
|
|
369
|
+
* 채우는 쪽이 없는 키를 지어내거나 칸을 비워 둔다.
|
|
370
|
+
*
|
|
371
|
+
* 그리고 마스터는 이미 `identity` 로 낸다(설비·자산 둘 다). 이 자리만 다른 이름이라 **원본이
|
|
372
|
+
* 채우는 순간 조용히 사라졌다.** 지금 자산 26대가 다 비어 있어 버려지는 값은 없었다 —
|
|
373
|
+
* 인티그레이션 레인이 그것을 재서 「누가 채우는 순간」이라고 짚었다.
|
|
374
|
+
*
|
|
375
|
+
* 자리 쪽(`RefLocation.gs1Id`)은 그대로다 — 거기는 실제로 SGLN 이 온다.
|
|
376
|
+
*/
|
|
377
|
+
identity?: string
|
|
324
378
|
/** 자원 속성 — 표준 `PhysicalAssetProperty`(적재 한도·규격 등). */
|
|
325
379
|
properties?: RefResourceProperty[]
|
|
326
380
|
/** 적격을 검증한 시험 명세들 — 표준 `TestSpecificationID`. */
|
|
@@ -747,9 +801,27 @@ export function masterToTwin(master: ReferenceMaster, catalog?: any): IngestedTw
|
|
|
747
801
|
return { id: l.id, type: l.type, ...(l.name ? { name: l.name } : {}), ...(l.description ? { description: l.description } : {}), capacity: l.capacity ?? 0, ...(l.parallelism !== undefined ? { parallelism: l.parallelism } : {}), ...(l.level ? { level: l.level } : {}), ...(l.properties?.length ? { properties: l.properties } : {}), parentId: l.parentId, ...(l.upstreamId ? { upstreamId: l.upstreamId } : {}), gs1Id: l.gs1Id }
|
|
748
802
|
})
|
|
749
803
|
|
|
804
|
+
/*
|
|
805
|
+
* ── 설비가 구역에 서 있을 수 있다 (2026-09-06) ──────────────────────────────
|
|
806
|
+
* 마스터는 자리와 구역을 **한 목록**에 담고 `role` 로 가른다(§`locationEntries` · `areaLocs`).
|
|
807
|
+
* 그래서 설비의 `homeLocationId` 가 구역을 가리킬 수 있다 — 실제로 `EQ-CAM-SHOWER-01` 이
|
|
808
|
+
* `LINE-CAM`(구역)에 서 있고, 그것이 정상이다(에어샤워는 라인 전체에 딸린다).
|
|
809
|
+
*
|
|
810
|
+
* 계약이 그 구분을 위해 `homeLocationKind` 를 두었는데(`HomeKind = 'location' | 'area'`)
|
|
811
|
+
* **아무도 안 채우고 있었다.** 그래서 인제스트가 그 참조를 「끊어졌다」고 경고했다.
|
|
812
|
+
*
|
|
813
|
+
* 짐작하지 않는다 — 마스터가 이미 `role` 로 말하고 있으므로 그것을 읽는다.
|
|
814
|
+
*/
|
|
815
|
+
const areaIds = new Set(master.locations.filter(l => l.role === 'area').map(l => l.id))
|
|
816
|
+
|
|
750
817
|
const equipment = (master.equipment ?? []).map(e => ({
|
|
751
818
|
id: e.id, kind: e.kind, ...(e.name ? { name: e.name } : {}), ...(e.description ? { description: e.description } : {}), ...(e.testResults?.length ? { testResults: e.testResults } : {}),
|
|
752
|
-
|
|
819
|
+
/* 등급 소속 — 사람·자산과 같은 자리. 빠뜨리면 원본이 채운 등급이 문턱에서 조용히 버려진다. */
|
|
820
|
+
...(e.equipmentClassIds?.length ? { equipmentClassIds: e.equipmentClassIds } : {}),
|
|
821
|
+
homeLocation: e.homeLocationId,
|
|
822
|
+
/* 자리면 안 적는다 — 계약의 기본값이 `location` 이고, 없는 칸을 만들면 화면이 그것을 그린다. */
|
|
823
|
+
...(e.homeLocationId && areaIds.has(e.homeLocationId) ? { homeLocationKind: 'area' as const } : {}),
|
|
824
|
+
mtbfMs: e.mtbfMs, mttrMs: e.mttrMs, ...(e.identity ? { identity: e.identity } : {}),
|
|
753
825
|
...(e.window ? { window: e.window } : {}),
|
|
754
826
|
/* 속성은 손대지 않고 통과 — 이동시간 추정기가 kind 별 속도를 여기서 읽는다(단위 코드 그대로). */
|
|
755
827
|
...(e.properties?.length ? { properties: e.properties } : {}), ...(e.workCalendar?.length ? { workCalendar: e.workCalendar } : {}), ...(e.testSpecificationIds?.length ? { testSpecificationIds: e.testSpecificationIds } : {}),
|
|
@@ -773,6 +845,18 @@ export function masterToTwin(master: ReferenceMaster, catalog?: any): IngestedTw
|
|
|
773
845
|
if (master.space.timezone && utcOffsetMinutes === undefined) {
|
|
774
846
|
warnings.push(unknownTimezone(String(master.space.timezone)))
|
|
775
847
|
}
|
|
848
|
+
/*
|
|
849
|
+
* ── 이름도 함께 싣는다 (2026-09-07) ────────────────────────────────────────
|
|
850
|
+
* 위에서 푼 오프셋은 **이 순간의** 값이고, 이 모델은 저장된다. 그래서 그 수는 마스터를 받은 날의
|
|
851
|
+
* 계절로 굳는다 — `America/Tijuana`(PST −480 / PDT −420)가 자료에 있고, 지난 구간을 교대별로 접으면
|
|
852
|
+
* 그 안의 전환이 사실을 한 시간 밀어 **다른 교대로** 세게 한다. 오류는 나지 않는다.
|
|
853
|
+
*
|
|
854
|
+
* 이름이 함께 있으면 소비처가 순간마다 다시 풀 수 있다(§`shiftContext`). 커널은 이 값을 쓰지 않는다.
|
|
855
|
+
*
|
|
856
|
+
* 오프셋이 안 풀린 이름은 싣지 않는다 — 그 경우는 위에서 경고로 말했고, 풀리지 않는 이름을 실어
|
|
857
|
+
* 두면 소비처가 그것을 믿고 매번 실패한다.
|
|
858
|
+
*/
|
|
859
|
+
const timezone = utcOffsetMinutes !== undefined ? master.space.timezone : undefined
|
|
776
860
|
/*
|
|
777
861
|
* 오퍼레이션 명세를 model 에 통과시킨다 — 커널이 `loadOperations` 로 소비한다(TwinModelDef 확장 필드,
|
|
778
862
|
* 모르는 커널은 무시). 여기서 값을 손대지 않는다: 기간은 표준 표기(ISO 8601)로 커널까지 그대로 간다.
|
|
@@ -784,9 +868,9 @@ export function masterToTwin(master: ReferenceMaster, catalog?: any): IngestedTw
|
|
|
784
868
|
}
|
|
785
869
|
}
|
|
786
870
|
/* 사람도 손대지 않고 통과 — 등급·교대 그대로 커널까지 간다(경계에서 어휘를 갈지 않는다). */
|
|
787
|
-
const persons = master.persons?.map(p => ({ id: p.id, ...(p.name ? { name: p.name } : {}), ...(p.testResults?.length ? { testResults: p.testResults } : {}), personnelClassIds: p.personnelClassIds, ...(p.window ? { window: p.window } : {}), ...(p.workCalendar?.length ? { workCalendar: p.workCalendar } : {}), ...(p.homeLocationId ? { homeLocation: p.homeLocationId } : {}), ...(p.properties?.length ? { properties: p.properties } : {}), ...(p.testSpecificationIds?.length ? { testSpecificationIds: p.testSpecificationIds } : {}), ...(p.effectiveStart ? { effectiveStart: p.effectiveStart } : {}), ...(p.effectiveEnd ? { effectiveEnd: p.effectiveEnd } : {}) }))
|
|
871
|
+
const persons = master.persons?.map(p => ({ id: p.id, ...(p.name ? { name: p.name } : {}), ...(p.testResults?.length ? { testResults: p.testResults } : {}), personnelClassIds: p.personnelClassIds, ...(p.identity ? { identity: p.identity } : {}), ...(p.window ? { window: p.window } : {}), ...(p.workCalendar?.length ? { workCalendar: p.workCalendar } : {}), ...(p.homeLocationId ? { homeLocation: p.homeLocationId } : {}), ...(p.properties?.length ? { properties: p.properties } : {}), ...(p.testSpecificationIds?.length ? { testSpecificationIds: p.testSpecificationIds } : {}), ...(p.effectiveStart ? { effectiveStart: p.effectiveStart } : {}), ...(p.effectiveEnd ? { effectiveEnd: p.effectiveEnd } : {}) }))
|
|
788
872
|
/* 자산도 손대지 않고 통과 — 등급·자리 그대로 커널까지. */
|
|
789
|
-
const assets = master.assets?.map(a => ({ id: a.id, ...(a.name ? { name: a.name } : {}), ...(a.testResults?.length ? { testResults: a.testResults } : {}), assetClassIds: a.assetClassIds, homeLocation: a.homeLocationId,
|
|
873
|
+
const assets = master.assets?.map(a => ({ id: a.id, ...(a.name ? { name: a.name } : {}), ...(a.testResults?.length ? { testResults: a.testResults } : {}), assetClassIds: a.assetClassIds, homeLocation: a.homeLocationId, ...(a.identity ? { identity: a.identity } : {}), ...(a.properties?.length ? { properties: a.properties } : {}), ...(a.testSpecificationIds?.length ? { testSpecificationIds: a.testSpecificationIds } : {}), ...(a.effectiveStart ? { effectiveStart: a.effectiveStart } : {}), ...(a.effectiveEnd ? { effectiveEnd: a.effectiveEnd } : {}) }))
|
|
790
874
|
/* 품목 정의·등급도 손대지 않고 통과 — 경계에서 어휘를 갈지 않는다(환산 계수는 커널이 읽는다). */
|
|
791
875
|
const materialDefinitions = master.materialDefinitions?.map(d => ({
|
|
792
876
|
id: d.id,
|
|
@@ -833,6 +917,7 @@ export function masterToTwin(master: ReferenceMaster, catalog?: any): IngestedTw
|
|
|
833
917
|
...(materialDefinitions?.length ? { materialDefinitions } : {}),
|
|
834
918
|
...(materialClasses?.length ? { materialClasses } : {}),
|
|
835
919
|
...(utcOffsetMinutes !== undefined ? { utcOffsetMinutes } : {}),
|
|
920
|
+
...(timezone ? { timezone } : {}),
|
|
836
921
|
...(operations?.length ? { operations } : {}),
|
|
837
922
|
...(testSpecifications?.length ? { testSpecifications } : {}),
|
|
838
923
|
...(personnelClasses?.length ? { personnelClasses } : {}),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { pubsub } from '@things-factory/shell'
|
|
2
2
|
import { HOST_STEPS, type ReferenceProgress } from './reference-progress.js'
|
|
3
3
|
import { assessMaster } from './reference-assessment.js'
|
|
4
|
-
import { Arg, Ctx, Mutation, Query, Resolver, Directive} from 'type-graphql'
|
|
4
|
+
import { Arg, Ctx, Int, Mutation, Query, Resolver, Directive } from 'type-graphql'
|
|
5
5
|
|
|
6
6
|
import { ScalarObject, getRepository } from '@things-factory/shell'
|
|
7
7
|
|
|
@@ -14,6 +14,7 @@ import type { KnobDef } from './template-registry.js'
|
|
|
14
14
|
import { knobDefaultsFilled, pickOriginSite, siteEquipmentAdopted } from './reference-master.js'
|
|
15
15
|
import { TwinInstance } from '../twin-instance/twin-instance.js'
|
|
16
16
|
import { TwinReference } from './twin-reference.js'
|
|
17
|
+
import { runFill, seqReportOf } from './reference-fill-run.js'
|
|
17
18
|
import { listReferences, getReference } from './reference-registry.js'
|
|
18
19
|
import { getAdapter, listAdapterTypes, listAdapters } from './reference-adapter.js'
|
|
19
20
|
import {
|
|
@@ -193,6 +194,67 @@ export class TwinReferenceResolver {
|
|
|
193
194
|
* `origin` 이 없는 트윈(이 자리가 생기기 전에 만들어진 것)은 다시 읽을 수 없다. 짐작으로
|
|
194
195
|
* 템플릿을 고르거나 첫 레퍼런스를 집지 않는다 — 엉뚱한 원본으로 현장을 덮는 것이 훨씬 나쁘다.
|
|
195
196
|
*/
|
|
197
|
+
/**
|
|
198
|
+
* **그 번호들이 무엇이었나** — 구멍을 만났을 때 원본에 묻는다.
|
|
199
|
+
*
|
|
200
|
+
* `fillTwinScope` 가 「418 다음이 420」이라고 멈추면, 그 419 가 무엇이었는지 알아야 다음을
|
|
201
|
+
* 정할 수 있다. 그 길이 없어서 사람이 원본 DB 를 손으로 뒤졌다.
|
|
202
|
+
*
|
|
203
|
+
* **판정하지 않는다.** 원본이 아는 것을 그대로 낸다 — `lost` 는 「번호는 나갔는데 행이 없다」이지
|
|
204
|
+
* 「포기해도 된다」가 아니다. 그 판단은 되돌릴 수 없어서(커서가 지나가면 끝이다) 사람의 몫이다.
|
|
205
|
+
*/
|
|
206
|
+
/*
|
|
207
|
+
* **읽기인데 쓰기와 같은 문을 쓴다.** 이 문은 원본의 봉투를 그대로 냅니다 — 그 시스템이 무엇을
|
|
208
|
+
* 언제 보냈는지가 드러납니다. `twinConnectionBundle` 과 같은 무게이고, 이 저장소는 그 무게를
|
|
209
|
+
* 「쓰기는 GATE 하나」로 통일해 두었습니다(ADR-0027).
|
|
210
|
+
*/
|
|
211
|
+
@Directive('@privilege(category: "twin", privilege: "mutation", domainOwnerGranted: true, superUserGranted: true)')
|
|
212
|
+
@Query(returns => ScalarObject, {
|
|
213
|
+
description:
|
|
214
|
+
'Ask the source what it knows about a range of sequence numbers. Each number comes back as present · lost · unissued · unknown — the source reports, the human decides.'
|
|
215
|
+
})
|
|
216
|
+
async twinSeqReport(
|
|
217
|
+
@Arg('instanceId') instanceId: string,
|
|
218
|
+
@Arg('scope') scope: string,
|
|
219
|
+
@Arg('from', type => Int) from: number,
|
|
220
|
+
@Arg('to', type => Int) to: number,
|
|
221
|
+
@Ctx() context: ResolverContext
|
|
222
|
+
): Promise<any> {
|
|
223
|
+
return seqReportOf({ domainId: context.state.domain.id, instanceId, scope, from, to })
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* **빠진 구간을 다시 받는다** — 훅은 반드시 놓칩니다: 우리가 내려가 있을 때, 그쪽이 못 보냈을 때.
|
|
228
|
+
*
|
|
229
|
+
* ── 왜 문이 필요한가 ──────────────────────────────────────────────────────
|
|
230
|
+
* 번호가 비면 `takeInSequence` 가 알아챕니다. 그런데 **알아챈 뒤 그 사이를 채우는 길이 없었습니다.**
|
|
231
|
+
* `fillSince` 는 2026-08-31 부터 있었고 부르는 곳이 0곳이었습니다.
|
|
232
|
+
*
|
|
233
|
+
* 실제로 plant 아웃박스의 1~20 을 잃고 되찾을 방법이 없는 상태를 만났습니다.
|
|
234
|
+
*
|
|
235
|
+
* ── `from` 은 잃은 구간을 되찾을 때만 ──────────────────────────────────────
|
|
236
|
+
* 주지 않으면 커서부터 이어 받습니다. 주면 **그 번호부터 다시 묻습니다** — 이미 들어간 것은 유입
|
|
237
|
+
* 경계가 되풀이로 거르므로 두 번 앉지 않습니다.
|
|
238
|
+
*
|
|
239
|
+
* 저장된 커서를 되돌리지 않습니다. `fillSince` 는 번호 검사를 지난 만큼만 적으므로, 되찾은 뒤
|
|
240
|
+
* 커서가 원래 자리보다 앞서지 않습니다.
|
|
241
|
+
*/
|
|
242
|
+
@Directive('@privilege(category: "twin", privilege: "mutation", domainOwnerGranted: true, superUserGranted: true)')
|
|
243
|
+
@Mutation(returns => ScalarObject, {
|
|
244
|
+
description:
|
|
245
|
+
'Fetch the stretch this twin missed for one scope and ingest it through the same checks as the push path. Without `from` it continues from the cursor; with `from` it asks again from that number — already-ingested facts are filtered as duplicates.'
|
|
246
|
+
})
|
|
247
|
+
async fillTwinScope(
|
|
248
|
+
@Arg('instanceId') instanceId: string,
|
|
249
|
+
@Arg('scope', { description: 'The unit the source numbers by — the same name its envelopes carry (e.g. EVENTS).' }) scope: string,
|
|
250
|
+
@Ctx() context: ResolverContext,
|
|
251
|
+
@Arg('from', type => Int, { nullable: true, description: 'Ask again from this sequence number. Omit to continue from the cursor.' })
|
|
252
|
+
from?: number,
|
|
253
|
+
@Arg('maxPages', type => Int, { nullable: true, description: 'Page limit for one run. Default 50.' }) maxPages?: number
|
|
254
|
+
): Promise<any> {
|
|
255
|
+
return runFill({ domainId: context.state.domain.id, instanceId, scope, from: from ?? undefined, maxPages: maxPages ?? undefined })
|
|
256
|
+
}
|
|
257
|
+
|
|
196
258
|
@Directive('@privilege(category: "twin", privilege: "mutation", domainOwnerGranted: true, superUserGranted: true)')
|
|
197
259
|
@Mutation(returns => ScalarObject, {
|
|
198
260
|
description:
|
|
@@ -265,7 +327,20 @@ export class TwinReferenceResolver {
|
|
|
265
327
|
await getRepository(TwinReference).update(
|
|
266
328
|
{ id: refRow.id },
|
|
267
329
|
{
|
|
268
|
-
scopeSpec: { ...prevSpec, assessments: { ...(prevSpec.assessments ?? {}), [siteId]: assessMaster(master, siteId) } }
|
|
330
|
+
scopeSpec: { ...prevSpec, assessments: { ...(prevSpec.assessments ?? {}), [siteId]: assessMaster(master, siteId) } },
|
|
331
|
+
/*
|
|
332
|
+
* ── 읽은 시각과 경고 수도 새로 남긴다 (2026-09-06) ──────────────────
|
|
333
|
+
* 처음 연결하는 경로는 이 둘을 쓰는데(§`connectReference`) 다시 읽는 경로는 총평만
|
|
334
|
+
* 썼다. 그래서 `lastSyncedAt` 이 「처음 트윈을 만든 시각」에 머물렀다.
|
|
335
|
+
*
|
|
336
|
+
* 이것이 실제로 잘못된 판단을 만들었다. 인티그레이션 레인이 커넥터를 고치고 재동기화를
|
|
337
|
+
* 청한 뒤 이 칸으로 확인했는데, 값이 그대로여서 「안 돌았다」고 결론 내렸다. 실제로는
|
|
338
|
+
* 돌았고 품목 106건이 들어와 있었다.
|
|
339
|
+
*
|
|
340
|
+
* 경고 수도 같다. 다시 읽으면서 경고가 줄거나 늘었는데 화면은 지난 수를 보인다.
|
|
341
|
+
*/
|
|
342
|
+
lastSyncedAt: new Date().toISOString(),
|
|
343
|
+
lastWarningCount: Array.isArray(res.warnings) ? res.warnings.length : null
|
|
269
344
|
} as any
|
|
270
345
|
)
|
|
271
346
|
}
|
|
@@ -23,6 +23,7 @@ export interface TwinEventKeys {
|
|
|
23
23
|
bizStep?: string
|
|
24
24
|
epc?: string
|
|
25
25
|
orderId?: string
|
|
26
|
+
bizTransaction?: string
|
|
26
27
|
locationId?: string
|
|
27
28
|
moverId?: string
|
|
28
29
|
}
|
|
@@ -80,15 +81,46 @@ export function epcOf(envelope: any): string | undefined {
|
|
|
80
81
|
* 없었다. 화면은 오더를 눌러도 「연결된 이벤트가 없습니다」를 냈고, 그 답은 질의 결과로는 정직했다 —
|
|
81
82
|
* 시점을 어디로 옮겨도 0 이었다.
|
|
82
83
|
*
|
|
83
|
-
* 순서가 중요하다: 운영 델타가 먼저다. EPCIS 의 `bizTransactionList` 는 **거래**(PO/SO)이고 오더와
|
|
84
|
-
* 같은 것이 아닐 수 있으므로, 오더를 스스로 말하는 사건은 그 말을 그대로 쓴다.
|
|
85
|
-
*
|
|
86
84
|
* **과거 행은 채워지지 않는다**(사용자 결정 2026-08-24, `moverId` 때와 같은 방식). 앞으로 들어오는
|
|
87
85
|
* 사건부터 조회된다.
|
|
86
|
+
*
|
|
87
|
+
* ── 한 칸에 두 낱말이 들어 있었다 (2026-09-05 실측) ────────────────────────
|
|
88
|
+
* 위 주석이 이미 알고 있었다 — 「EPCIS 의 `bizTransactionList` 는 **거래**(PO/SO)이고 오더와 같은
|
|
89
|
+
* 것이 아닐 수 있다」. 그런데 그 둘을 **같은 칸에** 넣고 있었다. 그래서 이렇게 됐다.
|
|
90
|
+
*
|
|
91
|
+
* order.status order-1 내부 오더 id
|
|
92
|
+
* epcis.TransformationEvent urn:epc:id:gdti:9521321.403.1 GS1 거래 식별자
|
|
93
|
+
* epcis.TransactionEvent urn:epc:id:gdti:9521321.403.1
|
|
94
|
+
*
|
|
95
|
+
* 한 오더의 이력이 **색인 안에서 두 쪽으로 갈라진다.** 어느 이름으로 물어도 절반만 나오고,
|
|
96
|
+
* 오류는 나지 않는다. 실측: 어휘가 섞인 행 10,004개.
|
|
97
|
+
*
|
|
98
|
+
* 이것이 실제로 막은 것 — 「이 개체를 어느 레시피로 만들었나」다. 답은 기록되어 있다(오더 사건이
|
|
99
|
+
* `recipeKey` 를 든다, 실측 328,021/328,021 = 100%). 변환 사건은 그 오더를 GDTI 로 가리키는데
|
|
100
|
+
* 오더 사건은 `order-1` 로 적혀 있어 **색인으로 이을 수가 없었다.**
|
|
101
|
+
*
|
|
102
|
+
* 그래서 칸을 갈랐다(사용자 결정 2026-09-05).
|
|
103
|
+
*
|
|
104
|
+
* orderId 내부 오더 id — 읽기 모델의 이음쇠
|
|
105
|
+
* bizTransaction GS1 거래 식별자 — 표준이 말하는 그 거래
|
|
106
|
+
*
|
|
107
|
+
* 오더 사건은 **둘 다** 든다(`order.status` 의 payload 에 `orderId` 와 `bizTransaction` 이 함께
|
|
108
|
+
* 있다). 그래서 EPCIS 사건의 거래 식별자로 오더를 찾을 수 있다 — 이 두 칸이 그 다리다.
|
|
88
109
|
*/
|
|
89
110
|
export function orderOf(envelope: any): string | undefined {
|
|
90
111
|
const d = envelope?.data ?? envelope ?? {}
|
|
91
|
-
return d.orderId ??
|
|
112
|
+
return d.orderId ?? undefined
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* **그 사건이 가리키는 거래** — EPCIS `bizTransactionList` 의 첫 항목, 또는 운영 델타가 직접 든 값.
|
|
117
|
+
*
|
|
118
|
+
* 오더가 아니다. 표준이 말하는 거래(PO/SO/생산오더)이고, 한 오더가 여러 거래에 걸릴 수도 있다.
|
|
119
|
+
* 그 구별을 지키려고 `orderId` 와 갈라 두었다 — 합치면 위 주석의 그 일이 다시 난다.
|
|
120
|
+
*/
|
|
121
|
+
export function bizTransactionOf(envelope: any): string | undefined {
|
|
122
|
+
const d = envelope?.data ?? envelope ?? {}
|
|
123
|
+
return d.bizTransactionList?.[0]?.bizTransaction ?? d.bizTransaction ?? undefined
|
|
92
124
|
}
|
|
93
125
|
|
|
94
126
|
/**
|
|
@@ -143,6 +175,7 @@ export function twinEventKeys(envelope: any): TwinEventKeys {
|
|
|
143
175
|
bizStep: clip(bizStepOf(envelope), 'bizStep'),
|
|
144
176
|
epc: clip(epcOf(envelope), 'epc'),
|
|
145
177
|
orderId: clip(orderOf(envelope), 'orderId'),
|
|
178
|
+
bizTransaction: clip(bizTransactionOf(envelope), 'bizTransaction'),
|
|
146
179
|
locationId: clip(locationOf(envelope), 'locationId'),
|
|
147
180
|
moverId: clip(equipmentIdOf(envelope), 'moverId')
|
|
148
181
|
}
|
|
@@ -20,6 +20,7 @@ import { Domain, ScalarObject } from '@things-factory/shell'
|
|
|
20
20
|
* ix_2 (domain, instanceId, eventType, revision) 타입 필터 + 정렬 동시 충족(스케줄 화면 task/equipment)
|
|
21
21
|
* ix_3 (domain, instanceId, epc) "이 물건의 이력" — Entity360 의 본질 질문
|
|
22
22
|
* ix_4 (domain, instanceId, orderId) "이 오더가 어디까지 갔나"
|
|
23
|
+
* ix_9 (domain, instanceId, bizTransaction) "이 거래에 무슨 일이 있었나" — EPCIS↔오더의 다리
|
|
23
24
|
*
|
|
24
25
|
* bizStep·locationId·moverId 는 컬럼만 두고 인덱스는 두지 않는다 — 한 트윈 안에서 카디널리티가
|
|
25
26
|
* 낮아(업무단계 몇 개, 위치 수백, 설비 수십) (domain,instanceId) 로 이미 좁혀진 뒤의 잔여 필터로
|
|
@@ -62,6 +63,8 @@ import { Domain, ScalarObject } from '@things-factory/shell'
|
|
|
62
63
|
* 정리는 한 창에 2초만 쓰도록 예산이 걸려 있어서, 그 2초를 주사에 다 쓰고 44건만 지우고 멈췄다.
|
|
63
64
|
*/
|
|
64
65
|
@Index('ix_twin_event_8', (e: TwinEvent) => [e.domain, e.instanceId, e.createdAt], { unique: false })
|
|
66
|
+
/* ix_9 — "이 거래에 무슨 일이 있었나". EPCIS 사건에서 오더로 건너가는 다리(§`bizTransaction`). */
|
|
67
|
+
@Index('ix_twin_event_9', (e: TwinEvent) => [e.domain, e.instanceId, e.bizTransaction], { unique: false })
|
|
65
68
|
@ObjectType({ description: 'Append-only twin event journal record (EPCIS event or operational delta).' })
|
|
66
69
|
export class TwinEvent {
|
|
67
70
|
@PrimaryGeneratedColumn('uuid')
|
|
@@ -174,9 +177,23 @@ export class TwinEvent {
|
|
|
174
177
|
epc?: string
|
|
175
178
|
|
|
176
179
|
@Column({ length: 255, nullable: true })
|
|
177
|
-
@Field({ nullable: true, description: '
|
|
180
|
+
@Field({ nullable: true, description: 'Internal order identifier, promoted from the payload for indexed lookup of one order history.' })
|
|
178
181
|
orderId?: string
|
|
179
182
|
|
|
183
|
+
/*
|
|
184
|
+
* **거래 식별자는 오더 식별자와 다른 것이다.**
|
|
185
|
+
*
|
|
186
|
+
* 전에는 둘이 한 칸(`orderId`)에 들어갔다. 그래서 한 오더의 이력이 색인 안에서 두 쪽으로
|
|
187
|
+
* 갈라졌다 — 운영 델타는 `order-1`, EPCIS 사건은 `urn:epc:id:gdti:…` 로 적혀 어느 이름으로
|
|
188
|
+
* 물어도 절반만 나왔다(실측 2026-09-05: 어휘가 섞인 행 10,004개, 오류는 나지 않음).
|
|
189
|
+
*
|
|
190
|
+
* 오더 사건은 두 값을 다 들고 있다. 그래서 이 칸이 **EPCIS 사건에서 오더로 건너가는 다리**가
|
|
191
|
+
* 된다 — 「이 개체를 어느 레시피로 만들었나」가 그 다리로 답해진다.
|
|
192
|
+
*/
|
|
193
|
+
@Column({ length: 255, nullable: true })
|
|
194
|
+
@Field({ nullable: true, description: 'Business transaction identifier (PO / SO / production order), promoted from the payload for indexed lookup of one transaction.' })
|
|
195
|
+
bizTransaction?: string
|
|
196
|
+
|
|
180
197
|
@Column({ length: 255, nullable: true })
|
|
181
198
|
@Field({ nullable: true, description: 'Location identifier (read point, business location, or the plain location an operational delta carries), promoted from the payload for indexed filtering.' })
|
|
182
199
|
locationId?: string
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* **어느 레시피로 만들었나** — 판정만. 순수하다.
|
|
3
|
+
*
|
|
4
|
+
* ── 왜 따로 있나 ───────────────────────────────────────────────────────────
|
|
5
|
+
* 계보 질의는 TypeORM 과 shell 을 끌어온다. 규칙만 재려는 시험이 그것까지 들고 오면 무겁고, 이
|
|
6
|
+
* 저장소에서 그 이유로 시험이 안 서는 일을 겪었다. 판단과 배선을 나눈다 — 여기는 값만 보고 답한다.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* 짐작하지 않는 규칙.
|
|
10
|
+
*
|
|
11
|
+
* order 오더가 말했다. 판이 둘이든 열이든 그것을 쓴다
|
|
12
|
+
* only-candidate 오더는 말 안 했지만 후보가 하나다 — 고를 것이 없으면 짐작이 아니다
|
|
13
|
+
* ambiguous 후보가 둘 이상인데 말해 주는 것이 없다 → **답하지 않는다**
|
|
14
|
+
* said-but-undeclared 오더는 말했는데 그 레시피가 선언에 없다 — 모르는 것이 아니라 어긋난 것이다
|
|
15
|
+
*
|
|
16
|
+
* 마지막 둘을 가르는 이유: 사람이 할 일이 다르다. 어긋남은 선언이나 원본을 고칠 일이고,
|
|
17
|
+
* 모호함은 그 개체가 어느 것으로 만들어졌는지 기록이 없는 일이다.
|
|
18
|
+
*/
|
|
19
|
+
export function chooseRecipe(
|
|
20
|
+
recipes: any[],
|
|
21
|
+
candidates: any[],
|
|
22
|
+
saidRecipeKey: string | undefined
|
|
23
|
+
): { recipe?: any; basis: 'order' | 'only-candidate' | 'ambiguous' | 'said-but-undeclared' | 'none' } {
|
|
24
|
+
if (saidRecipeKey) {
|
|
25
|
+
const said = recipes.find((r: any) => r?.key === saidRecipeKey)
|
|
26
|
+
return said ? { recipe: said, basis: 'order' } : { basis: 'said-but-undeclared' }
|
|
27
|
+
}
|
|
28
|
+
if (candidates.length === 1) return { recipe: candidates[0], basis: 'only-candidate' }
|
|
29
|
+
if (candidates.length > 1) return { basis: 'ambiguous' }
|
|
30
|
+
return { basis: 'none' }
|
|
31
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* **board 밖을 가리키는 관계를 푸는 자리** — 선언은 있고 코드가 0곳이던 이음새.
|
|
3
|
+
*
|
|
4
|
+
* ── 무엇이 있었나 (2026-09-05 실측) ────────────────────────────────────────
|
|
5
|
+
* 계약이 `target: { kind: 'external', entity: … }` 를 선언하고 주석에 「해소는 호스트가 한다」고
|
|
6
|
+
* 적어 두었다. 그런데 **`headless-twin/server` 어디에도 `kind === 'external'` 을 다루는 코드가
|
|
7
|
+
* 없었다**(0곳).
|
|
8
|
+
*
|
|
9
|
+
* `space.area` 가 도는 것처럼 보인 이유는 이음새가 있어서가 아니라 **호스트에서 그것이 평범한 FK
|
|
10
|
+
* 라서**다. TypeORM 메타데이터가 우연히 답해 준 것이지 선언이 길을 낸 것이 아니다.
|
|
11
|
+
*
|
|
12
|
+
* ── 왜 지금 채우나 ─────────────────────────────────────────────────────────
|
|
13
|
+
* 아침에는 「아무도 안 막혔으니 급하지 않다」로 적었다. **그 판단이 규모를 작게 잡은 것이었다.**
|
|
14
|
+
*
|
|
15
|
+
* 360(한 대상에 걸린 것을 한자리에)의 소비처는 무한히 늘어난다. 그러면 각 제품의 자기 저장소가
|
|
16
|
+
* 관계의 원천이 된다 — MES 의 실적·상태 구간·정비 계획이 그렇고, 다음 제품도 그럴 것이다.
|
|
17
|
+
*
|
|
18
|
+
* 그때 **선언은 「밖을 가리킨다」고 말하는데 푸는 길이 없으면 제품마다 자기 길을 낸다.** 그것이
|
|
19
|
+
* 정확히 「무한히 갈린다」다. 이 파일이 그 갈림을 막는다.
|
|
20
|
+
*
|
|
21
|
+
* ── 커널은 어느 표인지 모른다 ──────────────────────────────────────────────
|
|
22
|
+
* 계약은 「이 축이 밖을 가리킨다」까지만 안다. **「어느 표의 어느 칸」은 제품이 안다.** 그래서 제품이
|
|
23
|
+
* 부팅에서 해소기를 꽂는다 — 장부 기록기(`installIngestWindowWriter`)와 같은 규율이다.
|
|
24
|
+
*
|
|
25
|
+
* 커널이 표 이름을 알게 되면 한 제품의 저장 구조에 매이고, 다음 제품이 다른 표를 쓰면 커널을 또
|
|
26
|
+
* 고쳐야 한다.
|
|
27
|
+
*
|
|
28
|
+
* ── 없는 것을 숨기지 않는다 ────────────────────────────────────────────────
|
|
29
|
+
* 해소기가 없으면 **그 사실을 낸다**(`unresolved`). 링크를 지우면 화면은 「그런 관계가 없다」로
|
|
30
|
+
* 읽고, 실제로는 「아무도 답할 수 없다」다. 그 둘은 사람이 할 일이 다르다.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/** 밖에 사는 대상 하나 — 화면이 이름을 보이고 갈 곳을 정할 수 있게. */
|
|
34
|
+
export interface ExternalRef {
|
|
35
|
+
id: string
|
|
36
|
+
/** 사람이 읽는 이름. 없으면 화면이 id 를 보인다(지어내지 않는다). */
|
|
37
|
+
name?: string
|
|
38
|
+
/**
|
|
39
|
+
* 그 제품에서 이것을 열 수 있는 자리 — 없으면 화면이 링크를 만들지 않는다.
|
|
40
|
+
*
|
|
41
|
+
* 막다른 링크를 만들지 않는 것이 이 칸의 요점이다. 눌러서 아무 일도 안 일어나면 사람은 그것을
|
|
42
|
+
* 결함으로 읽는다.
|
|
43
|
+
*/
|
|
44
|
+
href?: string
|
|
45
|
+
/** 그 id 가 실제로 있나 — **없으면 없다고 말한다.** 답에서 빼지 않는다. */
|
|
46
|
+
missing?: boolean
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 밖에서 이 대상을 가리키는 것들 — 들어오는 관계 한 묶음.
|
|
51
|
+
*
|
|
52
|
+
* `tense` 를 **제품이 선언한다**(계약의 `RelationTense`). 실적·상태 구간은 지난 일이고 정비 계획·
|
|
53
|
+
* 지시는 앞의 일이다 — 「걸린 것이 열」이 두 뜻이 되는 자리이므로 묶음이 스스로 말해야 한다.
|
|
54
|
+
*/
|
|
55
|
+
export interface ExternalIncoming {
|
|
56
|
+
/** 관계의 이름(i18n 키) — 계약의 `via` 와 같은 규칙. 제품은 자기 접두를 쓴다. */
|
|
57
|
+
via: string
|
|
58
|
+
/** 어디서 오나 — 사람이 읽는 축 이름이 아니라 그 제품의 축 키다. */
|
|
59
|
+
from: string
|
|
60
|
+
/** 언제의 일인가. 안 주면 「선언이 말하지 않았다」다 — `standing` 으로 바꾸지 않는다. */
|
|
61
|
+
tense?: 'past' | 'future' | 'standing'
|
|
62
|
+
ids: string[]
|
|
63
|
+
/** 전체 수 — `ids` 가 잘렸어도 몇인지 말한다. */
|
|
64
|
+
total: number
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 한 external 대상을 푸는 제품 쪽 구현.
|
|
69
|
+
*
|
|
70
|
+
* 둘 다 선택이다. 이름만 대 줄 수 있는 제품과 들어오는 관계만 낼 수 있는 제품이 다 있다 — 하나만
|
|
71
|
+
* 꽂아도 그만큼은 답해진다.
|
|
72
|
+
*/
|
|
73
|
+
export interface ExternalResolver {
|
|
74
|
+
/** 계약의 `target.entity` 와 **같은 낱말**. 다르면 아무 관계에도 안 붙는다. */
|
|
75
|
+
entity: string
|
|
76
|
+
/** 이 id 들이 무엇인가. */
|
|
77
|
+
resolve?(domainId: string, ids: readonly string[]): Promise<ExternalRef[]>
|
|
78
|
+
/** 이 대상을 가리키는 것들. */
|
|
79
|
+
incoming?(domainId: string, instanceId: string, axis: string, itemId: string): Promise<ExternalIncoming[]>
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const resolvers = new Map<string, ExternalResolver>()
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 해소기를 꽂는다 — **부팅에서 한 번.**
|
|
86
|
+
*
|
|
87
|
+
* 같은 `entity` 를 두 번 꽂으면 뒤엣것이 이긴다. 그것이 개발 중 재적재의 정상 동작이고, 운영에서
|
|
88
|
+
* 일어나면 두 제품이 같은 대상을 자기 것이라 말하는 것이므로 **경고를 남긴다** — 조용히 덮으면
|
|
89
|
+
* 어느 쪽이 답하고 있는지 아무도 모른다.
|
|
90
|
+
*/
|
|
91
|
+
export function registerExternalResolver(r: ExternalResolver, warn?: (msg: string) => void): void {
|
|
92
|
+
const entity = String(r?.entity ?? '').trim()
|
|
93
|
+
if (!entity) {
|
|
94
|
+
warn?.('[external-resolver] entity 없는 해소기는 꽂지 않는다 — 아무 관계에도 안 붙는다')
|
|
95
|
+
return
|
|
96
|
+
}
|
|
97
|
+
if (resolvers.has(entity)) {
|
|
98
|
+
warn?.(`[external-resolver] '${entity}' 해소기를 덮어쓴다 — 두 제품이 같은 대상을 답하려 하면 그중 하나만 답한다`)
|
|
99
|
+
}
|
|
100
|
+
resolvers.set(entity, { ...r, entity })
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** 지금 꽂혀 있는 것들 — 「무엇을 풀 수 있나」를 밖에서 잴 수 있게. */
|
|
104
|
+
export function externalResolverEntities(): string[] {
|
|
105
|
+
return [...resolvers.keys()].sort()
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** 시험이 재기동을 넣을 수 있게. */
|
|
109
|
+
export function clearExternalResolvers(): void {
|
|
110
|
+
resolvers.clear()
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* 그 id 들을 푼다 — **못 풀면 못 푼다고 말한다.**
|
|
115
|
+
*
|
|
116
|
+
* 해소기가 터져도 360 전체를 죽이지 않는다. 한 관계가 답을 못 하는 것과 화면이 안 뜨는 것은 다른
|
|
117
|
+
* 사고이고, 뒤엣것이 훨씬 나쁘다.
|
|
118
|
+
*/
|
|
119
|
+
export async function resolveExternal(
|
|
120
|
+
entity: string,
|
|
121
|
+
domainId: string,
|
|
122
|
+
ids: readonly string[],
|
|
123
|
+
warn?: (msg: string) => void
|
|
124
|
+
): Promise<{ refs?: ExternalRef[]; unresolved?: 'no-resolver' | 'failed' }> {
|
|
125
|
+
const r = resolvers.get(entity)
|
|
126
|
+
if (!r?.resolve) return { unresolved: 'no-resolver' }
|
|
127
|
+
try {
|
|
128
|
+
return { refs: await r.resolve(domainId, ids) }
|
|
129
|
+
} catch (e: any) {
|
|
130
|
+
warn?.(`[external-resolver] '${entity}' 해소가 실패했다 — ${String(e?.message ?? e)}`)
|
|
131
|
+
return { unresolved: 'failed' }
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* 밖에서 이 대상을 가리키는 것들을 모은다.
|
|
137
|
+
*
|
|
138
|
+
* 꽂힌 것이 없으면 **빈 배열이지 오류가 아니다** — 밖에서 가리키는 것이 없는 대상이 정상이다.
|
|
139
|
+
* 다만 「해소기가 없어서 모른다」와 「없다」는 다르므로, 그 구별은 `externalResolverEntities()` 로
|
|
140
|
+
* 밖에서 잰다.
|
|
141
|
+
*/
|
|
142
|
+
export async function externalIncoming(
|
|
143
|
+
domainId: string,
|
|
144
|
+
instanceId: string,
|
|
145
|
+
axis: string,
|
|
146
|
+
itemId: string,
|
|
147
|
+
warn?: (msg: string) => void
|
|
148
|
+
): Promise<ExternalIncoming[]> {
|
|
149
|
+
const out: ExternalIncoming[] = []
|
|
150
|
+
for (const r of resolvers.values()) {
|
|
151
|
+
if (!r.incoming) continue
|
|
152
|
+
try {
|
|
153
|
+
const got = await r.incoming(domainId, instanceId, axis, itemId)
|
|
154
|
+
/* 빈 묶음은 싣지 않는다 — 여기서는 「그 제품이 이 축을 안 본다」는 뜻이고 할 말이 없다. */
|
|
155
|
+
for (const g of got ?? []) if (g && g.total > 0) out.push(g)
|
|
156
|
+
} catch (e: any) {
|
|
157
|
+
warn?.(`[external-resolver] '${r.entity}' 의 들어오는 관계 조회가 실패했다 — ${String(e?.message ?? e)}`)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return out
|
|
161
|
+
}
|
|
@@ -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
|
* **관측(가동률)은 여기 두지 않는다** — 그건 트윈이 저널에서 만든 것이라 성질이 다르고,
|