@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
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* **나간 지시와 돌아온 관측을 잇는다** — 그 판정.
|
|
3
|
+
*
|
|
4
|
+
* `dispatchRef` 는 쓰는 곳만 있고 읽는 곳이 없었다. 그래서 조치를 넘긴 뒤 저쪽에서 그것이 어떻게
|
|
5
|
+
* 됐는지 트윈이 몰랐고, 화면은 「넘겼다」까지만 보여 주고 끝난 것처럼 읽혔다.
|
|
6
|
+
*
|
|
7
|
+
* 여기서 지키는 것은 셋이다.
|
|
8
|
+
*
|
|
9
|
+
* 이어지나 우리가 낸 지시의 식별자로 돌아온 보고를 찾는가
|
|
10
|
+
* 해석하지 않나 커널이 「released 는 나간 것」 같은 방언을 알지 않는가
|
|
11
|
+
* 버리지 않나 못 이은 보고의 이유가 남는가
|
|
12
|
+
*/
|
|
13
|
+
import { test } from 'node:test'
|
|
14
|
+
import assert from 'node:assert/strict'
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
matchEffects,
|
|
18
|
+
refsToLookUp,
|
|
19
|
+
type DispatchedCommand,
|
|
20
|
+
type OrderObservation
|
|
21
|
+
} from '../server/service/actuation/effect-match.js'
|
|
22
|
+
|
|
23
|
+
const AT = '2026-09-02T09:00:00.000Z'
|
|
24
|
+
const LATER = '2026-09-02T09:05:00.000Z'
|
|
25
|
+
|
|
26
|
+
const ob = (over: Partial<OrderObservation> = {}): OrderObservation => ({
|
|
27
|
+
orderId: 'WO-20260902-002',
|
|
28
|
+
status: 'released',
|
|
29
|
+
at: AT,
|
|
30
|
+
...over
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
function byRef(...commands: DispatchedCommand[]) {
|
|
34
|
+
return new Map(commands.map(c => [c.dispatchRef, c]))
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const cmd = (over: Partial<DispatchedCommand> = {}): DispatchedCommand => ({
|
|
38
|
+
id: 'cmd-1',
|
|
39
|
+
dispatchRef: 'WO-20260902-002',
|
|
40
|
+
...over
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
/* ── 이어진다 ───────────────────────────────────────────────────────────────── */
|
|
44
|
+
|
|
45
|
+
test('★ 우리가 낸 지시의 식별자로 돌아온 보고를 잇는다 — 이것이 폐루프의 마지막 칸이다', () => {
|
|
46
|
+
const out = matchEffects({ observations: [ob()], byRef: byRef(cmd()) })
|
|
47
|
+
|
|
48
|
+
assert.deepEqual(out.updates, [{ commandId: 'cmd-1', status: 'released', at: AT, seen: 1 }])
|
|
49
|
+
assert.deepEqual(out.skips, [])
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('★ 상태를 해석하지 않는다 — 커널은 어느 값이 「현장에 나갔다」인지 모른다', () => {
|
|
53
|
+
/*
|
|
54
|
+
* 오더 상태 어휘는 도메인 소유다(`OrderStatusDelta.status` 는 열린 문자열이고 코어가 강제하지
|
|
55
|
+
* 않는다). 커널이 `released` 를 특별히 알면 그 방언을 쓰지 않는 현장에서 폐루프가 조용히 안 닫힌다.
|
|
56
|
+
*
|
|
57
|
+
* 그래서 무엇이 와도 그대로 적는다 — 뜻은 그 어휘를 아는 쪽이 읽는다.
|
|
58
|
+
*/
|
|
59
|
+
for (const status of ['released', 'in-progress', '작업중', 'WIP', 'cancelled']) {
|
|
60
|
+
const out = matchEffects({ observations: [ob({ status })], byRef: byRef(cmd()) })
|
|
61
|
+
assert.equal(out.updates[0]?.status, status, `${status} 를 그대로 적지 않았다`)
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test('취소 보고도 효과다 — 저쪽에서 무엇이 되었나이지 잘 되었나가 아니다', () => {
|
|
66
|
+
const out = matchEffects({ observations: [ob({ status: 'cancelled' })], byRef: byRef(cmd()) })
|
|
67
|
+
|
|
68
|
+
assert.equal(out.updates.length, 1, '나쁜 소식을 걸러 내면 화면이 여전히 「넘겼다」로 끝난다')
|
|
69
|
+
assert.equal(out.updates[0]!.status, 'cancelled')
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
/* ── 몇 번 바뀌었나를 남긴다 ────────────────────────────────────────────────── */
|
|
73
|
+
|
|
74
|
+
test('★ 상태가 여러 번 바뀌면 마지막이 남고 센 수가 는다', () => {
|
|
75
|
+
const out = matchEffects({
|
|
76
|
+
observations: [ob({ status: 'released', at: AT }), ob({ status: 'in-progress', at: LATER })],
|
|
77
|
+
byRef: byRef(cmd())
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
assert.equal(out.updates.length, 1, '같은 조치에 두 줄을 냈다')
|
|
81
|
+
assert.deepEqual(out.updates[0], { commandId: 'cmd-1', status: 'in-progress', at: LATER, seen: 2 })
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
test('★ 이미 적힌 효과에서 이어 센다 — 다음 묶음이 1로 되돌리지 않는다', () => {
|
|
85
|
+
const out = matchEffects({
|
|
86
|
+
observations: [ob({ status: 'in-progress', at: LATER })],
|
|
87
|
+
byRef: byRef(cmd({ effect: { status: 'released', at: AT, seen: 3 } }))
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
assert.equal(out.updates[0]!.seen, 4)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
/* ── 못 이은 것은 이유가 남는다 ─────────────────────────────────────────────── */
|
|
94
|
+
|
|
95
|
+
test('★ 우리가 내지 않은 오더는 이유로 남는다 — 대개 이것이고, 결함이 아니다', () => {
|
|
96
|
+
/*
|
|
97
|
+
* 저쪽 시스템은 자기 오더를 다 보고하고 그중 우리가 낸 것은 일부다. 이 줄이 쌓이는 것을 결함으로
|
|
98
|
+
* 읽으면 안 된다 — 그래서 이유 문장이 그렇게 말한다.
|
|
99
|
+
*/
|
|
100
|
+
const out = matchEffects({ observations: [ob({ orderId: 'WO-남의것' })], byRef: byRef(cmd()) })
|
|
101
|
+
|
|
102
|
+
assert.deepEqual(out.updates, [])
|
|
103
|
+
assert.equal(out.skips.length, 1)
|
|
104
|
+
assert.match(out.skips[0]!.reason, /우리가 낸 지시가 아니다/)
|
|
105
|
+
assert.equal(out.skips[0]!.orderId, 'WO-남의것', '어느 오더였는지 말하지 않으면 되짚을 수 없다')
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
test('★ 시각이 없는 보고는 「지금」으로 채우지 않는다 — 처리 시각이 관측 시각이 된다', () => {
|
|
109
|
+
const out = matchEffects({ observations: [ob({ at: undefined })], byRef: byRef(cmd()) })
|
|
110
|
+
|
|
111
|
+
assert.deepEqual(out.updates, [])
|
|
112
|
+
assert.match(out.skips[0]!.reason, /지금으로 채우지 않는다/)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
test('★ 상태가 빈 보고는 효과로 적지 않는다 — 「아무 말도 안 했다」가 「효과 없음」이 된다', () => {
|
|
116
|
+
for (const status of ['', ' ']) {
|
|
117
|
+
const out = matchEffects({ observations: [ob({ status })], byRef: byRef(cmd()) })
|
|
118
|
+
assert.deepEqual(out.updates, [], `${JSON.stringify(status)} 를 효과로 적었다`)
|
|
119
|
+
assert.match(out.skips[0]!.reason, /빈 값을 효과로 적지 않는다/)
|
|
120
|
+
}
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
test('오더 식별자가 없는 보고도 이유로 남는다', () => {
|
|
124
|
+
const out = matchEffects({ observations: [ob({ orderId: ' ' })], byRef: byRef(cmd()) })
|
|
125
|
+
|
|
126
|
+
assert.deepEqual(out.updates, [])
|
|
127
|
+
assert.match(out.skips[0]!.reason, /식별자가 없는/)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
test('보고가 없으면 아무것도 만들지 않는다 — 빈 묶음에 이유를 만들지 않는다', () => {
|
|
131
|
+
const out = matchEffects({ observations: [], byRef: byRef(cmd()) })
|
|
132
|
+
|
|
133
|
+
assert.deepEqual(out.updates, [])
|
|
134
|
+
assert.deepEqual(out.skips, [])
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
/* ── 여러 조치 ──────────────────────────────────────────────────────────────── */
|
|
138
|
+
|
|
139
|
+
test('★ 한 묶음이 여러 조치에 닿으면 각각 적는다 — 하나만 잇고 끝내지 않는다', () => {
|
|
140
|
+
const out = matchEffects({
|
|
141
|
+
observations: [ob({ orderId: 'WO-1' }), ob({ orderId: 'WO-2', status: 'in-progress' }), ob({ orderId: 'WO-남의것' })],
|
|
142
|
+
byRef: byRef(cmd({ id: 'cmd-1', dispatchRef: 'WO-1' }), cmd({ id: 'cmd-2', dispatchRef: 'WO-2' }))
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
assert.equal(out.updates.length, 2)
|
|
146
|
+
assert.deepEqual(
|
|
147
|
+
out.updates.map(u => [u.commandId, u.status]).sort(),
|
|
148
|
+
[
|
|
149
|
+
['cmd-1', 'released'],
|
|
150
|
+
['cmd-2', 'in-progress']
|
|
151
|
+
]
|
|
152
|
+
)
|
|
153
|
+
assert.equal(out.skips.length, 1)
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
/* ── 저장소에 물을 것을 줄인다 ─────────────────────────────────────────────── */
|
|
157
|
+
|
|
158
|
+
test('★ 찾아볼 식별자만 골라 낸다 — 유입은 초당 수천 건이 지나는 자리다', () => {
|
|
159
|
+
const observations = [ob({ orderId: 'WO-1' }), ob({ orderId: 'WO-1' }), ob({ orderId: 'WO-2' }), ob({ orderId: ' ' })]
|
|
160
|
+
|
|
161
|
+
assert.deepEqual(refsToLookUp(observations).sort(), ['WO-1', 'WO-2'], '중복이나 빈 값을 저장소에 물었다')
|
|
162
|
+
})
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* ★ **조치가 어떻게 됐나** — 승인한 사람에게 돌려줄 사실, 그리고 그 안의 판정 하나.
|
|
3
|
+
*
|
|
4
|
+
* ── 이 판정이 왜 커널에 있나 (2026-09-04) ──────────────────────────────────
|
|
5
|
+
* 「저쪽에서 사람이 할 일이 남았나」의 답이 한 필드에 없다. `dispatchNote` 만 보면 틀린다 —
|
|
6
|
+
* 그것은 **넘길 때 저쪽이 한 말**이고, 사람이 나중에 지시서를 골라도 그대로 남는다. 저쪽에 다시
|
|
7
|
+
* 물을 일이 없어 갱신되지 않는다(인티그레이션 레인이 MES 코드를 읽어 찾았다).
|
|
8
|
+
*
|
|
9
|
+
* 그래서 답은 「말이 있고 효과가 아직 없을 때」다. 그 겹침을 화면마다 하게 두면 목록·상세·알림이
|
|
10
|
+
* 각자 겹치고 한 곳이 빠진 날 그 화면만 다른 말을 한다.
|
|
11
|
+
*/
|
|
12
|
+
import { test } from 'node:test'
|
|
13
|
+
import assert from 'node:assert/strict'
|
|
14
|
+
|
|
15
|
+
import { actuationOutcomeOf } from '../server/service/actuation/actuation-outcome.js'
|
|
16
|
+
import type { StoredCommand } from '../server/service/actuation/command-dispatcher.js'
|
|
17
|
+
|
|
18
|
+
const at = (over: Partial<StoredCommand> = {}): StoredCommand =>
|
|
19
|
+
({
|
|
20
|
+
id: 'cmd-1',
|
|
21
|
+
instanceId: 'mes-line-a',
|
|
22
|
+
kind: 'production.release',
|
|
23
|
+
proposedBy: 'rule',
|
|
24
|
+
origin: 'rule',
|
|
25
|
+
state: 'acked',
|
|
26
|
+
...over
|
|
27
|
+
}) as StoredCommand
|
|
28
|
+
|
|
29
|
+
/* ── 저쪽에 할 일이 남았나 ───────────────────────────────────────────────── */
|
|
30
|
+
|
|
31
|
+
test('★ 말이 있고 효과가 아직 없으면 저쪽에 할 일이 남았다', () => {
|
|
32
|
+
const out = actuationOutcomeOf(at({ dispatchNote: '지시서를 골라야 합니다' }), false)
|
|
33
|
+
|
|
34
|
+
assert.equal(out.pendingAtSource, true)
|
|
35
|
+
assert.equal(out.note, '지시서를 골라야 합니다')
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test('★ 효과가 오면 할 일이 끝났다 — 말은 그대로 남아 있어도', () => {
|
|
39
|
+
/*
|
|
40
|
+
* 이것이 이 판정의 존재 이유다. `dispatchNote` 는 갱신되지 않으므로 그것만 보면 사람이 지시서를
|
|
41
|
+
* 고른 뒤에도 「골라야 합니다」가 계속 보인다. 이미 처리된 일을 계속 시키는 화면이 된다.
|
|
42
|
+
*/
|
|
43
|
+
const out = actuationOutcomeOf(
|
|
44
|
+
at({ dispatchNote: '지시서를 골라야 합니다', effect: { status: 'released', at: '2026-09-04T01:00:00.000Z', seen: 1 } }),
|
|
45
|
+
false
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
assert.equal(out.pendingAtSource, false, '효과가 왔는데 아직 할 일이 남았다고 답했다')
|
|
49
|
+
assert.equal(out.note, '지시서를 골라야 합니다', '기록은 지우지 않는다 — 왜 늦었는지가 사라진다')
|
|
50
|
+
assert.equal(out.effectStatus, 'released')
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test('★ 다음에 할 일이 갈라져 오면 그대로 낸다 — 화면이 문장을 자르지 않게', () => {
|
|
54
|
+
const out = actuationOutcomeOf(
|
|
55
|
+
at({ dispatchNote: '접수됐지만 지시서가 붙지 않았습니다', dispatchNoteNext: 'MES 에서 지시서를 골라야 현장에 나갑니다' }),
|
|
56
|
+
false
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
assert.equal(out.note, '접수됐지만 지시서가 붙지 않았습니다')
|
|
60
|
+
assert.equal(out.noteNext, 'MES 에서 지시서를 골라야 현장에 나갑니다')
|
|
61
|
+
assert.equal(out.pendingAtSource, true)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test('★ 어댑터가 아직 안 갈랐으면 비운다 — 없는 줄을 만들지 않는다', () => {
|
|
65
|
+
/*
|
|
66
|
+
* 그때 `note` 는 두 문장이 붙은 한 덩이이고, 화면은 통째로 그려야 한다. 커널이 그것을 잘라
|
|
67
|
+
* 두 칸에 넣으면 **파싱 규칙이 커널에 생기고** 번역되면 깨진다.
|
|
68
|
+
*/
|
|
69
|
+
const out = actuationOutcomeOf(at({ dispatchNote: '원인과 다음 할 일이 붙은 한 문장입니다' }), false)
|
|
70
|
+
|
|
71
|
+
assert.equal(out.note, '원인과 다음 할 일이 붙은 한 문장입니다')
|
|
72
|
+
assert.equal('noteNext' in out, false)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
test('★ 말이 없으면 할 일이 없다', () => {
|
|
76
|
+
assert.equal(actuationOutcomeOf(at(), false).pendingAtSource, false)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
test('★ 아직 넘겨 보지 않았으면 판정하지 않는다 — 「없다」와 「모른다」는 다르다', () => {
|
|
80
|
+
/*
|
|
81
|
+
* `approved` 는 승인만 된 상태다. 저쪽에 물어본 적이 없으니 할 일이 남았는지 알 방법이 없다.
|
|
82
|
+
* `false` 로 접으면 화면이 「할 일 없음」으로 그리고, 그것은 넘긴 뒤와 구별되지 않는다.
|
|
83
|
+
*/
|
|
84
|
+
for (const state of ['proposed', 'approved', 'rejected']) {
|
|
85
|
+
const out = actuationOutcomeOf(at({ state, dispatchNote: '지시서를 골라야 합니다' }), true)
|
|
86
|
+
assert.equal(out.pendingAtSource, undefined, `${state} 인데 판정했다`)
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
test('넘긴 상태 셋에서는 판정한다', () => {
|
|
91
|
+
for (const state of ['dispatching', 'acked', 'failed']) {
|
|
92
|
+
const out = actuationOutcomeOf(at({ state }), false)
|
|
93
|
+
assert.equal(out.pendingAtSource, false, `${state} 인데 판정을 비웠다`)
|
|
94
|
+
}
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
/* ── 저쪽 낱말을 해석하지 않는다 ────────────────────────────────────────── */
|
|
98
|
+
|
|
99
|
+
test('★ 효과 상태를 그대로 낸다 — 어느 값이 「현장에 나갔다」인지는 도메인이 안다', () => {
|
|
100
|
+
for (const status of ['released', 'closed', 'WAITING', '작업중']) {
|
|
101
|
+
const out = actuationOutcomeOf(at({ effect: { status, at: '2026-09-04T01:00:00.000Z', seen: 2 } }), false)
|
|
102
|
+
assert.equal(out.effectStatus, status)
|
|
103
|
+
assert.equal(out.pendingAtSource, false, `${status} 를 「효과 없음」으로 읽었다`)
|
|
104
|
+
}
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
/* ── 되돌릴 수 있나 ─────────────────────────────────────────────────────── */
|
|
108
|
+
|
|
109
|
+
test('★ 되돌릴 수 있으면 그렇게 낸다 — 화면은 참일 때만 한 줄 낸다', () => {
|
|
110
|
+
assert.equal(actuationOutcomeOf(at({ reversible: true }), false).reversible, true)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
test('★ 모르면 비운다 — 「되돌릴 수 없습니다」를 커널이 단언하지 않는다', () => {
|
|
114
|
+
/*
|
|
115
|
+
* 늘 참인 경고는 사흘이면 안 읽힌다(UX 레인 판단). 그러니 흔한 쪽을 단언하지 않고, 드문 쪽만
|
|
116
|
+
* 값으로 낸다. 커널이 `false` 를 채워 보내면 화면이 그것을 그릴 유혹이 생긴다.
|
|
117
|
+
*/
|
|
118
|
+
assert.equal(actuationOutcomeOf(at(), false).reversible, undefined)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
test('돌이킬 수 없다고 선언된 것은 그대로 낸다', () => {
|
|
122
|
+
assert.equal(actuationOutcomeOf(at({ reversible: false }), false).reversible, false)
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
/* ── 나머지 사실 ────────────────────────────────────────────────────────── */
|
|
126
|
+
|
|
127
|
+
test('★ 나간 것은 저쪽 이름을 든다 — 없으면 무엇을 되돌릴지 지목할 수 없다', () => {
|
|
128
|
+
const out = actuationOutcomeOf(at({ dispatchRef: 'WO-20260903-001' }), false)
|
|
129
|
+
assert.equal(out.dispatchRef, 'WO-20260903-001')
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
test('★ 못 나간 것은 사유를 든다', () => {
|
|
133
|
+
const out = actuationOutcomeOf(at({ state: 'failed', error: 'MES 가 받지 않았습니다 — unknown material ITEM-1' }), true)
|
|
134
|
+
|
|
135
|
+
assert.equal(out.state, 'failed')
|
|
136
|
+
assert.match(out.error!, /unknown material/)
|
|
137
|
+
assert.equal(out.dispatchable, true, '다시 넘길 수 있는 상태다')
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
test('★ 종착이면 다시 넘기지 않는다', () => {
|
|
141
|
+
assert.equal(actuationOutcomeOf(at({ state: 'acked' }), false).dispatchable, false)
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
test('없는 값으로 키를 만들지 않는다 — 화면이 빈 줄을 그리지 않게', () => {
|
|
145
|
+
const out = actuationOutcomeOf(at(), false)
|
|
146
|
+
|
|
147
|
+
for (const k of ['dispatchRef', 'error', 'note', 'effectStatus', 'effectAt', 'reversible']) {
|
|
148
|
+
assert.equal(k in out, false, `${k} 가 빈 값으로 들어 있다`)
|
|
149
|
+
}
|
|
150
|
+
assert.equal(out.id, 'cmd-1')
|
|
151
|
+
assert.equal(out.state, 'acked')
|
|
152
|
+
})
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* **규칙 주기가 저장소에 닿는다** — 그 배선.
|
|
3
|
+
*
|
|
4
|
+
* 판정 자체는 `actuation-rule.test.ts` 가 지킨다. 여기서 보는 것은 **그 판정이 실제 경로에 서 있는가**다
|
|
5
|
+
* — 커맨드가 적혔는가, 근거가 남았는가, 다시 세운 뒤에도 중복이 안 나는가, 한 트윈이 실패해도 나머지가
|
|
6
|
+
* 도는가.
|
|
7
|
+
*
|
|
8
|
+
* 이 저장소에서 가장 자주 나는 결함이 「자리는 있고 길이 없다」이고, 규칙이 그 부류로 나면 두 결과 중
|
|
9
|
+
* 하나다 — 아무 지시도 안 나가거나, 같은 지시가 대기 줄에 쌓인다.
|
|
10
|
+
*/
|
|
11
|
+
import { test } from 'node:test'
|
|
12
|
+
import assert from 'node:assert/strict'
|
|
13
|
+
|
|
14
|
+
import { MES_COMMANDS } from '@operato/ops-contract'
|
|
15
|
+
|
|
16
|
+
import type { CommandStore, StoredCommand } from '../server/service/actuation/command-dispatcher.js'
|
|
17
|
+
import type { ActuationRuleSpec, AttentionLike } from '../server/service/actuation/rule-evaluate.js'
|
|
18
|
+
import { runRulesOnce, type RuleRunnerDeps } from '../server/service/actuation/rule-runner.js'
|
|
19
|
+
|
|
20
|
+
const NOW = Date.parse('2026-09-01T03:00:00.000Z')
|
|
21
|
+
const HOUR = 3_600_000
|
|
22
|
+
const TWIN = { domainId: 'd-1', instanceId: 'twin-a' }
|
|
23
|
+
const ORDER = MES_COMMANDS[0]!
|
|
24
|
+
|
|
25
|
+
const attention = (over: Partial<AttentionLike> = {}): AttentionLike => ({
|
|
26
|
+
id: 'bottleneck:paint-1',
|
|
27
|
+
kind: 'bottleneck',
|
|
28
|
+
severity: 'high',
|
|
29
|
+
state: 'active',
|
|
30
|
+
anchor: { locationId: 'paint-1' },
|
|
31
|
+
timeStamp: new Date(NOW - 3 * HOUR).toISOString(),
|
|
32
|
+
params: { ratioPct: 118 },
|
|
33
|
+
...over
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
const rule = (over: Partial<ActuationRuleSpec> = {}): ActuationRuleSpec => ({
|
|
37
|
+
id: 'rule-1',
|
|
38
|
+
instanceId: TWIN.instanceId,
|
|
39
|
+
attentionKind: 'bottleneck',
|
|
40
|
+
minHeldMs: 2 * HOUR,
|
|
41
|
+
commandType: ORDER.type,
|
|
42
|
+
payloadTemplate: { materialDefinitionId: 'urn:epc:idpat:sgtin:9521234.00001.*', quantity: 20, uom: 'EA' },
|
|
43
|
+
...over
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
/** 저장본 하나 — 프로세스가 아니라 여기 산다. 그래서 다시 세워도 남는다. */
|
|
47
|
+
function makeStore(seed: StoredCommand[] = []) {
|
|
48
|
+
const rows = new Map<string, StoredCommand>(seed.map(c => [c.id, c]))
|
|
49
|
+
const store: CommandStore & { rows: Map<string, StoredCommand> } = {
|
|
50
|
+
rows,
|
|
51
|
+
load: async (_d, id) => rows.get(id) ?? null,
|
|
52
|
+
save: async (_d, c) => void rows.set(c.id, JSON.parse(JSON.stringify(c))),
|
|
53
|
+
create: async (_d, c) => {
|
|
54
|
+
const copy: StoredCommand = JSON.parse(JSON.stringify(c))
|
|
55
|
+
rows.set(copy.id, copy)
|
|
56
|
+
return copy
|
|
57
|
+
},
|
|
58
|
+
openCauses: async (_d, instanceId) =>
|
|
59
|
+
new Set(
|
|
60
|
+
[...rows.values()]
|
|
61
|
+
.filter(c => c.instanceId === instanceId && c.state === 'proposed' && c.causeRef)
|
|
62
|
+
.map(c => c.causeRef!)
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
return store
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function deps(over: Partial<RuleRunnerDeps> = {}): RuleRunnerDeps {
|
|
69
|
+
return {
|
|
70
|
+
ownedTwins: () => [TWIN],
|
|
71
|
+
rulesOf: async () => [rule()],
|
|
72
|
+
attentionsOf: () => [attention()],
|
|
73
|
+
modeOf: () => 'live' as const,
|
|
74
|
+
store: makeStore(),
|
|
75
|
+
specs: MES_COMMANDS,
|
|
76
|
+
nowMs: () => NOW,
|
|
77
|
+
...over
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/* ── 실제로 적힌다 ────────────────────────────────────────────────────────── */
|
|
82
|
+
|
|
83
|
+
test('★ 규칙이 낸 조치가 저장소에 적힌다 — 판정만 있고 적히지 않으면 아무 일도 없다', async () => {
|
|
84
|
+
const store = makeStore()
|
|
85
|
+
const out = await runRulesOnce(deps({ store }))
|
|
86
|
+
|
|
87
|
+
assert.equal(out.length, 1)
|
|
88
|
+
assert.equal(out[0]!.proposed, 1)
|
|
89
|
+
assert.equal(store.rows.size, 1)
|
|
90
|
+
|
|
91
|
+
const row = [...store.rows.values()][0]!
|
|
92
|
+
assert.equal(row.state, 'proposed')
|
|
93
|
+
assert.equal(row.type, ORDER.type)
|
|
94
|
+
assert.equal(row.payload.quantity, 20)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
test('★ 근거가 커맨드에 남는다 — 이것이 없으면 다음 주기가 같은 것을 또 올린다', async () => {
|
|
98
|
+
const store = makeStore()
|
|
99
|
+
await runRulesOnce(deps({ store }))
|
|
100
|
+
const row = [...store.rows.values()][0]!
|
|
101
|
+
|
|
102
|
+
assert.equal(row.causeRef, 'bottleneck:paint-1')
|
|
103
|
+
assert.equal(row.ruleRef, 'rule-1')
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
test('★ 규칙이 낸 것에는 낸 사람이 없다 — 사람이 아니므로 아무 승인자든 두 번째 사람이 된다', async () => {
|
|
107
|
+
const store = makeStore()
|
|
108
|
+
await runRulesOnce(deps({ store }))
|
|
109
|
+
const row = [...store.rows.values()][0]!
|
|
110
|
+
|
|
111
|
+
assert.equal(row.origin, 'rule')
|
|
112
|
+
assert.equal(row.proposedBy, undefined)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
test('★ 규칙은 결재선에 올리지 않는다 — 배경 루프에는 「나」가 없어 Myself·MySupervisor 가 뜻을 잃는다', async () => {
|
|
116
|
+
const store = makeStore()
|
|
117
|
+
await runRulesOnce(deps({ store }))
|
|
118
|
+
const row = [...store.rows.values()][0]!
|
|
119
|
+
|
|
120
|
+
assert.equal(row.approvalRef, undefined, '올렸다면 결재선이 시스템 사용자 기준으로 풀린다')
|
|
121
|
+
assert.equal(row.state, 'proposed', '올리는 것은 사람이 한다 — 그때까지 게이트는 서 있다')
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
/* ── 시늉된 조건에서 현장 지시를 내지 않는다 ─────────────────────────────── */
|
|
125
|
+
|
|
126
|
+
test('★ 시뮬 트윈에는 조치를 내지 않는다 — 지어낸 조건이 현장 지시가 되지 않게', async () => {
|
|
127
|
+
/*
|
|
128
|
+
* 시뮬 트윈의 병목은 커널이 스스로 물품을 옮겨 만든 것이다. 현장에서 관측된 것이 아니다.
|
|
129
|
+
* 오늘 규칙을 확인한 트윈이 전부 `sim` 이었고 시늉된 병목에서 생산 지시가 올라왔다 — 배선은
|
|
130
|
+
* 옳게 돌았지만 근거가 가정이었다.
|
|
131
|
+
*/
|
|
132
|
+
const store = makeStore()
|
|
133
|
+
const out = await runRulesOnce(deps({ store, modeOf: () => 'sim' }))
|
|
134
|
+
|
|
135
|
+
assert.equal(out[0]!.proposed, 0)
|
|
136
|
+
assert.equal(store.rows.size, 0)
|
|
137
|
+
assert.match(out[0]!.skips[0]!.reason, /시뮬레이션이다/)
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
test('★ 모드를 모르면 내지 않는다 — 모름을 「관측이다」로 읽지 않는다', async () => {
|
|
141
|
+
const store = makeStore()
|
|
142
|
+
const out = await runRulesOnce(deps({ store, modeOf: () => undefined }))
|
|
143
|
+
|
|
144
|
+
assert.equal(out[0]!.proposed, 0)
|
|
145
|
+
assert.match(out[0]!.skips[0]!.reason, /알 수 없다/)
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
/* ── 두 번 올리지 않는다 ─────────────────────────────────────────────────── */
|
|
149
|
+
|
|
150
|
+
test('★ 두 번 돌아도 하나만 적힌다', async () => {
|
|
151
|
+
const store = makeStore()
|
|
152
|
+
const d = deps({ store })
|
|
153
|
+
|
|
154
|
+
await runRulesOnce(d)
|
|
155
|
+
const second = await runRulesOnce(d)
|
|
156
|
+
|
|
157
|
+
assert.equal(second[0]!.proposed, 0)
|
|
158
|
+
assert.equal(store.rows.size, 1, '같은 조건에 지시가 둘 적혔다')
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
test('★ 재기동해도 두 번 올리지 않는다 — 근거를 저장소에서 읽기 때문이다', async () => {
|
|
162
|
+
const first = makeStore()
|
|
163
|
+
await runRulesOnce(deps({ store: first }))
|
|
164
|
+
|
|
165
|
+
/* 프로세스가 새로 선 것으로 본다 — 저장본만 물려주고 판정 상태는 버린다. */
|
|
166
|
+
const saved = [...first.rows.values()].map(c => JSON.parse(JSON.stringify(c)) as StoredCommand)
|
|
167
|
+
const second = makeStore(saved)
|
|
168
|
+
const out = await runRulesOnce(deps({ store: second }))
|
|
169
|
+
|
|
170
|
+
assert.equal(out[0]!.proposed, 0)
|
|
171
|
+
assert.equal(second.rows.size, 1)
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
test('★ 결정이 끝난 것은 다시 막지 않는다 — 어제 거절한 조건이 오늘 또 성립하면 새 판단거리다', async () => {
|
|
175
|
+
const store = makeStore()
|
|
176
|
+
await runRulesOnce(deps({ store }))
|
|
177
|
+
/* 사람이 거절했다. */
|
|
178
|
+
const row = [...store.rows.values()][0]!
|
|
179
|
+
await store.save(TWIN.domainId, { ...row, state: 'rejected' })
|
|
180
|
+
|
|
181
|
+
const out = await runRulesOnce(deps({ store }))
|
|
182
|
+
assert.equal(out[0]!.proposed, 1, '거절된 것이 영구히 그 조건을 막고 있다')
|
|
183
|
+
assert.equal(store.rows.size, 2)
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
/* ── 저장소가 근거를 말하지 않으면 판정하지 않는다 ──────────────────────── */
|
|
187
|
+
|
|
188
|
+
test('★ 근거를 읽을 길이 없으면 아무것도 올리지 않고 말한다 — 조용히 중복을 내지 않는다', async () => {
|
|
189
|
+
const store = makeStore()
|
|
190
|
+
delete (store as any).openCauses
|
|
191
|
+
|
|
192
|
+
const out = await runRulesOnce(deps({ store }))
|
|
193
|
+
assert.equal(out[0]!.proposed, 0)
|
|
194
|
+
assert.equal(store.rows.size, 0)
|
|
195
|
+
assert.match(out[0]!.skips[0]!.reason, /openCauses/)
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
/* ── 상태를 못 읽는 트윈 ─────────────────────────────────────────────────── */
|
|
199
|
+
|
|
200
|
+
test('★ 스냅샷이 없는 트윈은 「주의 0건」이 아니다 — 멈춘 트윈을 조건 없음으로 읽지 않는다', async () => {
|
|
201
|
+
const out = await runRulesOnce(deps({ attentionsOf: () => undefined }))
|
|
202
|
+
assert.equal(out[0]!.proposed, 0)
|
|
203
|
+
assert.match(out[0]!.skips[0]!.reason, /멈춰 있거나 아직 서지 않았다/)
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
test('주의가 정말 0건이면 이유를 만들지 않는다 — 정상이다', async () => {
|
|
207
|
+
const out = await runRulesOnce(deps({ attentionsOf: () => [] }))
|
|
208
|
+
assert.equal(out[0]!.proposed, 0)
|
|
209
|
+
assert.deepEqual(out[0]!.skips, [])
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
test('규칙이 없으면 상태를 읽지도 않는다 — 아무것도 안 할 일에 값을 만들지 않는다', async () => {
|
|
213
|
+
let read = 0
|
|
214
|
+
const out = await runRulesOnce(
|
|
215
|
+
deps({
|
|
216
|
+
rulesOf: async () => [],
|
|
217
|
+
attentionsOf: () => {
|
|
218
|
+
read++
|
|
219
|
+
return []
|
|
220
|
+
}
|
|
221
|
+
})
|
|
222
|
+
)
|
|
223
|
+
assert.equal(read, 0)
|
|
224
|
+
assert.deepEqual(out[0]!.skips, [])
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
/* ── 한 트윈이 실패해도 나머지는 돈다 ───────────────────────────────────── */
|
|
228
|
+
|
|
229
|
+
test('★ 한 트윈의 규칙이 터져도 다른 트윈은 돈다 — 고치는 동안 전체가 멈추지 않게', async () => {
|
|
230
|
+
const other = { domainId: 'd-1', instanceId: 'twin-b' }
|
|
231
|
+
const store = makeStore()
|
|
232
|
+
const out = await runRulesOnce(
|
|
233
|
+
deps({
|
|
234
|
+
store,
|
|
235
|
+
ownedTwins: () => [TWIN, other],
|
|
236
|
+
rulesOf: async twin => {
|
|
237
|
+
if (twin.instanceId === TWIN.instanceId) throw new Error('규칙을 읽지 못했다')
|
|
238
|
+
return [rule({ instanceId: other.instanceId })]
|
|
239
|
+
}
|
|
240
|
+
})
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
assert.equal(out.length, 2)
|
|
244
|
+
assert.match(String(out[0]!.failed), /규칙을 읽지 못했다/)
|
|
245
|
+
assert.equal(out[1]!.proposed, 1, '앞 트윈이 터져서 뒤 트윈이 안 돌았다')
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
test('★ 한 지시가 거절돼도 나머지는 낸다 — 그리고 거절을 삼키지 않는다', async () => {
|
|
249
|
+
const store = makeStore()
|
|
250
|
+
const out = await runRulesOnce(
|
|
251
|
+
deps({
|
|
252
|
+
store,
|
|
253
|
+
/* 하나는 선언되지 않은 종류를 가리키고, 하나는 제대로 된 것. */
|
|
254
|
+
rulesOf: async () => [rule({ id: 'bad', commandType: 'line.shutdown' }), rule({ id: 'good' })],
|
|
255
|
+
attentionsOf: () => [attention()]
|
|
256
|
+
})
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
assert.equal(out[0]!.proposed, 1)
|
|
260
|
+
assert.equal(out[0]!.skips.length, 1)
|
|
261
|
+
assert.match(out[0]!.skips[0]!.reason, /선언되지 않은 조치 종류/)
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
/* ── 돈 결과를 남긴다 ───────────────────────────────────────────────────── */
|
|
265
|
+
|
|
266
|
+
test('★ 돈 결과를 밖으로 넘긴다 — 조용히 안 도는 규칙과 조건이 없어서 안 내는 규칙을 구별하려면 필요하다', async () => {
|
|
267
|
+
const notes: any[] = []
|
|
268
|
+
await runRulesOnce(deps({ noteRun: async (twin, report) => void notes.push({ twin, report }) }))
|
|
269
|
+
|
|
270
|
+
assert.equal(notes.length, 1)
|
|
271
|
+
assert.equal(notes[0].report.proposed, 1)
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
test('기록을 남기지 못해도 판정은 끝난 것이다 — 기록 실패로 조치를 되돌리지 않는다', async () => {
|
|
275
|
+
const store = makeStore()
|
|
276
|
+
const out = await runRulesOnce(
|
|
277
|
+
deps({ store, noteRun: async () => { throw new Error('기록 실패') } })
|
|
278
|
+
)
|
|
279
|
+
assert.equal(out[0]!.proposed, 1)
|
|
280
|
+
assert.equal(store.rows.size, 1)
|
|
281
|
+
})
|