@things-factory/headless-twin 10.0.19 → 10.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-server/engine/canonical-ingest.d.ts +10 -1
- package/dist-server/engine/canonical-ingest.js +110 -33
- package/dist-server/engine/canonical-ingest.js.map +1 -1
- package/dist-server/engine/event-time.d.ts +25 -0
- package/dist-server/engine/event-time.js +98 -0
- package/dist-server/engine/event-time.js.map +1 -0
- package/dist-server/engine/index.d.ts +2 -0
- package/dist-server/engine/index.js +23 -0
- package/dist-server/engine/index.js.map +1 -1
- package/dist-server/engine/ingest-dedupe.d.ts +18 -6
- package/dist-server/engine/ingest-dedupe.js +201 -15
- package/dist-server/engine/ingest-dedupe.js.map +1 -1
- package/dist-server/engine/ingest-health.d.ts +60 -1
- package/dist-server/engine/ingest-health.js +43 -8
- package/dist-server/engine/ingest-health.js.map +1 -1
- package/dist-server/engine/kpi-fold.d.ts +19 -0
- package/dist-server/engine/kpi-fold.js +4 -1
- package/dist-server/engine/kpi-fold.js.map +1 -1
- package/dist-server/engine/kpi-query.js +23 -3
- package/dist-server/engine/kpi-query.js.map +1 -1
- package/dist-server/engine/oee-accumulator.d.ts +35 -2
- package/dist-server/engine/oee-accumulator.js +56 -4
- package/dist-server/engine/oee-accumulator.js.map +1 -1
- package/dist-server/engine/twin-engine.d.ts +72 -3
- package/dist-server/engine/twin-engine.js +118 -5
- package/dist-server/engine/twin-engine.js.map +1 -1
- package/dist-server/index.js +21 -0
- package/dist-server/index.js.map +1 -1
- package/dist-server/migrations/1786600000000-RenameOperatoMesAdapterType.d.ts +5 -0
- package/dist-server/migrations/1786600000000-RenameOperatoMesAdapterType.js +66 -0
- package/dist-server/migrations/1786600000000-RenameOperatoMesAdapterType.js.map +1 -0
- package/dist-server/migrations/1786700000000-AddDispatchRetryColumns.d.ts +5 -0
- package/dist-server/migrations/1786700000000-AddDispatchRetryColumns.js +79 -0
- package/dist-server/migrations/1786700000000-AddDispatchRetryColumns.js.map +1 -0
- package/dist-server/migrations/index.js +5 -1
- package/dist-server/migrations/index.js.map +1 -1
- package/dist-server/routes.js +12 -20
- package/dist-server/routes.js.map +1 -1
- package/dist-server/service/actuation/actuation-advisor.d.ts +60 -0
- package/dist-server/service/actuation/actuation-advisor.js +53 -0
- package/dist-server/service/actuation/actuation-advisor.js.map +1 -0
- package/dist-server/service/actuation/actuation-outcome.d.ts +52 -0
- package/dist-server/service/actuation/actuation-outcome.js +34 -0
- package/dist-server/service/actuation/actuation-outcome.js.map +1 -0
- package/dist-server/service/actuation/actuation-rule-resolver.d.ts +19 -0
- package/dist-server/service/actuation/actuation-rule-resolver.js +161 -0
- package/dist-server/service/actuation/actuation-rule-resolver.js.map +1 -0
- package/dist-server/service/actuation/actuation-rule.d.ts +55 -0
- package/dist-server/service/actuation/actuation-rule.js +130 -0
- package/dist-server/service/actuation/actuation-rule.js.map +1 -0
- package/dist-server/service/actuation/approval-gateway.d.ts +20 -0
- package/dist-server/service/actuation/approval-gateway.js +14 -0
- package/dist-server/service/actuation/approval-gateway.js.map +1 -0
- package/dist-server/service/actuation/catch-up-effects.d.ts +16 -0
- package/dist-server/service/actuation/catch-up-effects.js +120 -0
- package/dist-server/service/actuation/catch-up-effects.js.map +1 -0
- package/dist-server/service/actuation/command-dispatcher.d.ts +206 -8
- package/dist-server/service/actuation/command-dispatcher.js +191 -14
- package/dist-server/service/actuation/command-dispatcher.js.map +1 -1
- package/dist-server/service/actuation/command-store.d.ts +10 -2
- package/dist-server/service/actuation/command-store.js +168 -5
- package/dist-server/service/actuation/command-store.js.map +1 -1
- package/dist-server/service/actuation/dispatch-after-approval.d.ts +11 -0
- package/dist-server/service/actuation/dispatch-after-approval.js +73 -0
- package/dist-server/service/actuation/dispatch-after-approval.js.map +1 -0
- package/dist-server/service/actuation/effect-match.d.ts +53 -0
- package/dist-server/service/actuation/effect-match.js +65 -0
- package/dist-server/service/actuation/effect-match.js.map +1 -0
- package/dist-server/service/actuation/index.d.ts +21 -2
- package/dist-server/service/actuation/index.js +21 -2
- package/dist-server/service/actuation/index.js.map +1 -1
- package/dist-server/service/actuation/retry-plan.d.ts +63 -0
- package/dist-server/service/actuation/retry-plan.js +99 -0
- package/dist-server/service/actuation/retry-plan.js.map +1 -0
- package/dist-server/service/actuation/rule-evaluate.d.ts +127 -0
- package/dist-server/service/actuation/rule-evaluate.js +205 -0
- package/dist-server/service/actuation/rule-evaluate.js.map +1 -0
- package/dist-server/service/actuation/rule-loop.d.ts +5 -0
- package/dist-server/service/actuation/rule-loop.js +156 -0
- package/dist-server/service/actuation/rule-loop.js.map +1 -0
- package/dist-server/service/actuation/rule-runner.d.ts +52 -0
- package/dist-server/service/actuation/rule-runner.js +135 -0
- package/dist-server/service/actuation/rule-runner.js.map +1 -0
- package/dist-server/service/actuation/rule-wiring.d.ts +24 -0
- package/dist-server/service/actuation/rule-wiring.js +80 -0
- package/dist-server/service/actuation/rule-wiring.js.map +1 -0
- package/dist-server/service/actuation/settle-effects.d.ts +21 -0
- package/dist-server/service/actuation/settle-effects.js +101 -0
- package/dist-server/service/actuation/settle-effects.js.map +1 -0
- package/dist-server/service/actuation/sweep-dispatch.d.ts +37 -0
- package/dist-server/service/actuation/sweep-dispatch.js +125 -0
- package/dist-server/service/actuation/sweep-dispatch.js.map +1 -0
- package/dist-server/service/actuation/twin-command-resolver.d.ts +120 -0
- package/dist-server/service/actuation/twin-command-resolver.js +406 -0
- package/dist-server/service/actuation/twin-command-resolver.js.map +1 -0
- package/dist-server/service/actuation/twin-command.d.ts +81 -0
- package/dist-server/service/actuation/twin-command.js +60 -0
- package/dist-server/service/actuation/twin-command.js.map +1 -1
- package/dist-server/service/actuation/twin-now.d.ts +41 -0
- package/dist-server/service/actuation/twin-now.js +65 -0
- package/dist-server/service/actuation/twin-now.js.map +1 -0
- package/dist-server/service/index.d.ts +5 -2
- package/dist-server/service/index.js +8 -0
- package/dist-server/service/index.js.map +1 -1
- package/dist-server/service/reference/actuation-routing.d.ts +60 -0
- package/dist-server/service/reference/actuation-routing.js +126 -0
- package/dist-server/service/reference/actuation-routing.js.map +1 -0
- package/dist-server/service/reference/actuation-target.d.ts +65 -0
- package/dist-server/service/reference/actuation-target.js +94 -0
- package/dist-server/service/reference/actuation-target.js.map +1 -0
- package/dist-server/service/reference/connection-portability-resolver.d.ts +29 -0
- package/dist-server/service/reference/connection-portability-resolver.js +181 -0
- package/dist-server/service/reference/connection-portability-resolver.js.map +1 -0
- package/dist-server/service/reference/connection-portability.d.ts +138 -0
- package/dist-server/service/reference/connection-portability.js +230 -0
- package/dist-server/service/reference/connection-portability.js.map +1 -0
- package/dist-server/service/reference/fill-cadence.d.ts +17 -0
- package/dist-server/service/reference/fill-cadence.js +31 -0
- package/dist-server/service/reference/fill-cadence.js.map +1 -0
- package/dist-server/service/reference/fill-dedupe.d.ts +16 -0
- package/dist-server/service/reference/fill-dedupe.js +63 -0
- package/dist-server/service/reference/fill-dedupe.js.map +1 -0
- package/dist-server/service/reference/fill-loop.d.ts +35 -0
- package/dist-server/service/reference/fill-loop.js +187 -0
- package/dist-server/service/reference/fill-loop.js.map +1 -0
- package/dist-server/service/reference/fill-span.d.ts +10 -0
- package/dist-server/service/reference/fill-span.js +37 -0
- package/dist-server/service/reference/fill-span.js.map +1 -0
- package/dist-server/service/reference/hook-contract.d.ts +88 -4
- package/dist-server/service/reference/hook-contract.js +105 -4
- package/dist-server/service/reference/hook-contract.js.map +1 -1
- package/dist-server/service/reference/hook-rejected.d.ts +25 -0
- package/dist-server/service/reference/hook-rejected.js +48 -0
- package/dist-server/service/reference/hook-rejected.js.map +1 -0
- package/dist-server/service/reference/index.d.ts +4 -1
- package/dist-server/service/reference/index.js +5 -1
- package/dist-server/service/reference/index.js.map +1 -1
- package/dist-server/service/reference/live-cadence.d.ts +68 -0
- package/dist-server/service/reference/live-cadence.js +44 -0
- package/dist-server/service/reference/live-cadence.js.map +1 -0
- package/dist-server/service/reference/live-feed-lease.d.ts +15 -0
- package/dist-server/service/reference/live-feed-lease.js +56 -0
- package/dist-server/service/reference/live-feed-lease.js.map +1 -0
- package/dist-server/service/reference/live-ingest.d.ts +21 -0
- package/dist-server/service/reference/live-ingest.js +66 -0
- package/dist-server/service/reference/live-ingest.js.map +1 -0
- package/dist-server/service/reference/reference-adapter.d.ts +291 -2
- package/dist-server/service/reference/reference-adapter.js +19 -1
- package/dist-server/service/reference/reference-adapter.js.map +1 -1
- package/dist-server/service/reference/reference-fill-run.d.ts +50 -0
- package/dist-server/service/reference/reference-fill-run.js +142 -0
- package/dist-server/service/reference/reference-fill-run.js.map +1 -0
- package/dist-server/service/reference/reference-fill.d.ts +90 -0
- package/dist-server/service/reference/reference-fill.js +141 -0
- package/dist-server/service/reference/reference-fill.js.map +1 -0
- package/dist-server/service/reference/reference-hook.d.ts +22 -0
- package/dist-server/service/reference/reference-hook.js +135 -55
- package/dist-server/service/reference/reference-hook.js.map +1 -1
- package/dist-server/service/reference/reference-link-groups.d.ts +10 -0
- package/dist-server/service/reference/reference-link-groups.js +15 -0
- package/dist-server/service/reference/reference-link-groups.js.map +1 -0
- package/dist-server/service/reference/reference-links.d.ts +14 -0
- package/dist-server/service/reference/reference-links.js +73 -0
- package/dist-server/service/reference/reference-links.js.map +1 -0
- package/dist-server/service/reference/reference-live.d.ts +1 -0
- package/dist-server/service/reference/reference-live.js +32 -6
- package/dist-server/service/reference/reference-live.js.map +1 -1
- package/dist-server/service/reference/reference-master.d.ts +56 -2
- package/dist-server/service/reference/reference-master.js +33 -3
- package/dist-server/service/reference/reference-master.js.map +1 -1
- package/dist-server/service/reference/reference-resolver.d.ts +27 -0
- package/dist-server/service/reference/reference-resolver.js +79 -1
- package/dist-server/service/reference/reference-resolver.js.map +1 -1
- package/dist-server/service/twin-event/twin-event-keys.d.ts +31 -3
- package/dist-server/service/twin-event/twin-event-keys.js +36 -4
- package/dist-server/service/twin-event/twin-event-keys.js.map +1 -1
- package/dist-server/service/twin-event/twin-event.d.ts +1 -0
- package/dist-server/service/twin-event/twin-event.js +11 -2
- package/dist-server/service/twin-event/twin-event.js.map +1 -1
- package/dist-server/service/twin-model/choose-recipe.d.ts +15 -0
- package/dist-server/service/twin-model/choose-recipe.js +33 -0
- package/dist-server/service/twin-model/choose-recipe.js.map +1 -0
- package/dist-server/service/twin-model/external-resolver.d.ts +76 -0
- package/dist-server/service/twin-model/external-resolver.js +109 -0
- package/dist-server/service/twin-model/external-resolver.js.map +1 -0
- package/dist-server/service/twin-model/project-structure.js +16 -3
- package/dist-server/service/twin-model/project-structure.js.map +1 -1
- package/dist-server/service/twin-model/twin-equipment.d.ts +17 -0
- package/dist-server/service/twin-model/twin-equipment.js +10 -0
- package/dist-server/service/twin-model/twin-equipment.js.map +1 -1
- package/dist-server/service/twin-model/twin-lineage-query.js +75 -1
- package/dist-server/service/twin-model/twin-lineage-query.js.map +1 -1
- package/dist-server/service/twin-model/twin-model-item-query.js +94 -5
- package/dist-server/service/twin-model/twin-model-item-query.js.map +1 -1
- package/dist-server/service/twin-model/twin-model-tree-query.js.map +1 -1
- package/dist-server/service/twin-space/twin-space.d.ts +29 -0
- package/dist-server/service/twin-space/twin-space.js +13 -0
- package/dist-server/service/twin-space/twin-space.js.map +1 -1
- package/dist-server/service/twin-subject/event-subjects.d.ts +3 -1
- package/dist-server/service/twin-subject/event-subjects.js +8 -1
- package/dist-server/service/twin-subject/event-subjects.js.map +1 -1
- package/dist-shared/entity-delta.js +1 -1
- package/dist-shared/entity-delta.js.map +1 -1
- package/package.json +7 -7
- package/server/engine/canonical-ingest.ts +119 -33
- package/server/engine/event-time.ts +96 -0
- package/server/engine/index.ts +20 -0
- package/server/engine/ingest-dedupe.ts +203 -27
- package/server/engine/ingest-health.ts +91 -6
- package/server/engine/kpi-fold.ts +24 -1
- package/server/engine/kpi-query.ts +23 -4
- package/server/engine/oee-accumulator.ts +80 -8
- package/server/engine/twin-engine.ts +119 -5
- package/server/index.ts +23 -0
- package/server/migrations/1786600000000-RenameOperatoMesAdapterType.ts +66 -0
- package/server/migrations/1786700000000-AddDispatchRetryColumns.ts +82 -0
- package/server/migrations/index.ts +5 -1
- package/server/routes.ts +12 -24
- package/server/service/actuation/actuation-advisor.ts +124 -0
- package/server/service/actuation/actuation-outcome.ts +97 -0
- package/server/service/actuation/actuation-rule-resolver.ts +147 -0
- package/server/service/actuation/actuation-rule.ts +139 -0
- package/server/service/actuation/approval-gateway.ts +44 -0
- package/server/service/actuation/catch-up-effects.ts +152 -0
- package/server/service/actuation/command-dispatcher.ts +401 -20
- package/server/service/actuation/command-store.ts +182 -8
- package/server/service/actuation/dispatch-after-approval.ts +71 -0
- package/server/service/actuation/effect-match.ts +124 -0
- package/server/service/actuation/index.ts +21 -2
- package/server/service/actuation/retry-plan.ts +145 -0
- package/server/service/actuation/rule-evaluate.ts +352 -0
- package/server/service/actuation/rule-loop.ts +160 -0
- package/server/service/actuation/rule-runner.ts +216 -0
- package/server/service/actuation/rule-wiring.ts +84 -0
- package/server/service/actuation/settle-effects.ts +115 -0
- package/server/service/actuation/sweep-dispatch.ts +179 -0
- package/server/service/actuation/twin-command-resolver.ts +387 -0
- package/server/service/actuation/twin-command.ts +128 -1
- package/server/service/actuation/twin-now.ts +97 -0
- package/server/service/index.ts +9 -1
- package/server/service/reference/actuation-routing.ts +165 -0
- package/server/service/reference/actuation-target.ts +163 -0
- package/server/service/reference/connection-portability-resolver.ts +171 -0
- package/server/service/reference/connection-portability.ts +285 -0
- package/server/service/reference/fill-cadence.ts +29 -0
- package/server/service/reference/fill-dedupe.ts +66 -0
- package/server/service/reference/fill-loop.ts +153 -0
- package/server/service/reference/fill-span.ts +31 -0
- package/server/service/reference/hook-contract.ts +185 -5
- package/server/service/reference/hook-rejected.ts +53 -0
- package/server/service/reference/index.ts +5 -1
- package/server/service/reference/live-cadence.ts +129 -0
- package/server/service/reference/live-feed-lease.ts +56 -0
- package/server/service/reference/live-ingest.ts +94 -0
- package/server/service/reference/reference-adapter.ts +327 -5
- package/server/service/reference/reference-fill-run.ts +165 -0
- package/server/service/reference/reference-fill.ts +223 -0
- package/server/service/reference/reference-hook.ts +162 -56
- package/server/service/reference/reference-link-groups.ts +21 -0
- package/server/service/reference/reference-links.ts +77 -0
- package/server/service/reference/reference-live.ts +30 -5
- package/server/service/reference/reference-master.ts +90 -5
- package/server/service/reference/reference-resolver.ts +77 -2
- package/server/service/twin-event/twin-event-keys.ts +37 -4
- package/server/service/twin-event/twin-event.ts +18 -1
- package/server/service/twin-model/choose-recipe.ts +31 -0
- package/server/service/twin-model/external-resolver.ts +161 -0
- package/server/service/twin-model/project-structure.ts +16 -3
- package/server/service/twin-model/twin-equipment.ts +22 -0
- package/server/service/twin-model/twin-lineage-query.ts +82 -1
- package/server/service/twin-model/twin-model-item-query.ts +95 -4
- package/server/service/twin-model/twin-model-tree-query.ts +1 -1
- package/server/service/twin-space/twin-space.ts +39 -0
- package/server/service/twin-subject/event-subjects.ts +8 -1
- package/shared/entity-delta.ts +1 -1
- package/test/ack-shape-alignment.test.ts +97 -0
- package/test/actuation-approval-door.test.ts +421 -0
- package/test/actuation-dispatch.test.ts +16 -3
- package/test/actuation-effect.test.ts +162 -0
- package/test/actuation-outcome.test.ts +152 -0
- package/test/actuation-rule-runner.test.ts +281 -0
- package/test/actuation-rule.test.ts +266 -0
- package/test/actuation-seam.test.ts +249 -0
- package/test/actuation-settle.test.ts +164 -0
- package/test/actuation-target.test.ts +145 -0
- package/test/canonical-ingest-vocabularies.test.ts +49 -2
- package/test/capability-mapping.test.ts +1 -1
- package/test/choose-recipe.test.ts +117 -0
- package/test/command-store-writes-columns.test.ts +126 -0
- package/test/connection-portability-doors.test.ts +87 -0
- package/test/connector-capability-declaration.test.ts +54 -4
- package/test/declaration-reaches-model.test.ts +43 -0
- package/test/dedupe-transformation.test.ts +112 -0
- package/test/entity-delta.test.ts +1 -1
- package/test/event-subjects.test.ts +2 -1
- package/test/external-resolver.test.ts +171 -0
- package/test/fill-dedupe.test.ts +59 -0
- package/test/fill-loop.test.ts +35 -0
- package/test/first-envelope.test.ts +153 -0
- package/test/fold-shift-dst.test.ts +110 -0
- package/test/home-in-area.test.ts +90 -0
- package/test/hook-rejected-shape.test.ts +86 -0
- package/test/hook-response-sum.test.ts +193 -0
- package/test/hook-sequence.test.ts +1 -1
- package/test/ingest-dedupe.test.ts +56 -0
- package/test/ingest-expected-quiet.test.ts +111 -0
- package/test/ingest-idempotent.test.ts +79 -0
- package/test/ingest-reconcile-four-ways.test.ts +97 -0
- package/test/ingest-rules-header.test.ts +100 -0
- package/test/ingest-window-rolls-unobserved.test.ts +72 -0
- package/test/intake-mapping-parity.test.ts +175 -0
- package/test/live-cadence.test.ts +170 -0
- package/test/live-feed-lease.test.ts +63 -0
- package/test/live-mirror-parity.test.ts +1 -1
- package/test/oee-accumulator.test.ts +3 -3
- package/test/oee-site-calendar.test.ts +146 -0
- package/test/order-and-transaction-split.test.ts +107 -0
- package/test/reference-fill.test.ts +235 -0
- package/test/reference-hook.test.ts +1 -1
- package/test/reference-links.test.ts +56 -0
- package/test/retry-plan.test.ts +139 -0
- package/test/sweep-dispatch.test.ts +191 -0
- package/test/twin-event-keys.test.ts +20 -6
- package/test/twin-model-item-db.test.ts +6 -6
- package/test/twin-model-tree-db.test.ts +6 -6
- package/tsconfig.shared.tsbuildinfo +1 -1
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* **규칙을 주기로 돈다** — 판정을 저장소에 잇는 자리.
|
|
3
|
+
*
|
|
4
|
+
* ── 왜 틱이 아닌가 (2026-09-01) ──────────────────────────────────────────
|
|
5
|
+
* 커널 틱 안에 넣으려다 `tickGuarded` 를 읽고 접었다. 그 자리는 **예외가 나면 트윈을 멈추고**, 굶김
|
|
6
|
+
* 감시가 느린 틱 셋에 트윈을 내린다. 규칙 판정은 DB 를 두 번 건드리므로 그 감시에 정확히 걸린다.
|
|
7
|
+
*
|
|
8
|
+
* 그리고 필요하지도 않다. 「병목이 세 시간째」를 1초마다 다시 물을 이유가 없다.
|
|
9
|
+
*
|
|
10
|
+
* ── 규칙은 결재선에 올리지 않는다 ────────────────────────────────────────
|
|
11
|
+
* 이것이 이 파일의 가장 중요한 규율이다.
|
|
12
|
+
*
|
|
13
|
+
* 업무 목록의 결재선은 **올린 사람 기준**으로 풀린다 — `Myself` · `MyDepartment` · `MySupervisor` 가
|
|
14
|
+
* 전부 `context.state.user` 를 본다(§`getSystemUserFromOrgMemberItem`). 배경 루프에는 「나」가 없다.
|
|
15
|
+
* 시스템 사용자를 지어내 넘기면 그 셋이 **시스템 사용자를 가리키게 되고**, 결재선이 뜻을 잃는다.
|
|
16
|
+
*
|
|
17
|
+
* 그래서 규칙은 **내는 것까지** 한다. 결재선에 올리는 것은 사람이 한다(§`raiseTwinCommand`) — 그 사람이
|
|
18
|
+
* 「이것을 승인자들 앞에 놓았다」의 주체가 되고, 그 사람 기준으로 결재선이 풀린다.
|
|
19
|
+
*
|
|
20
|
+
* 올리기 전까지 그 조치는 나가지 못한다. 게이트는 그대로 서 있다.
|
|
21
|
+
*/
|
|
22
|
+
import type { CommandTypeSpec } from '@operato/ops-contract'
|
|
23
|
+
|
|
24
|
+
import { twinError, twinWarn } from '../../engine/log.js'
|
|
25
|
+
|
|
26
|
+
import { propose, type CommandStore } from './command-dispatcher.js'
|
|
27
|
+
import { evaluateRules, type ActuationRuleSpec, type AttentionLike, type RuleSkip } from './rule-evaluate.js'
|
|
28
|
+
|
|
29
|
+
/** 이 프로세스가 든 트윈 하나. */
|
|
30
|
+
export interface OwnedTwin {
|
|
31
|
+
domainId: string
|
|
32
|
+
instanceId: string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** 한 트윈을 돌고 나서 남는 사실. */
|
|
36
|
+
export interface TwinRunReport {
|
|
37
|
+
domainId: string
|
|
38
|
+
instanceId: string
|
|
39
|
+
/** 실제로 적힌 커맨드 수. */
|
|
40
|
+
proposed: number
|
|
41
|
+
/** 규칙마다 몇 개를 냈나 — 규칙 행에 누계를 적으려면 나누어 세어야 한다. */
|
|
42
|
+
proposedByRule: Record<string, number>
|
|
43
|
+
/** 왜 안 올렸나 — 규칙이 조용히 안 도는 것과 조건이 없어서 안 내는 것을 구별한다. */
|
|
44
|
+
skips: RuleSkip[]
|
|
45
|
+
/** 상한에 닿아 두고 온 수. */
|
|
46
|
+
withheld?: number
|
|
47
|
+
/** 이 트윈을 아예 못 돌았으면 그 이유. */
|
|
48
|
+
failed?: string
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** 밖에서 받는 것들 — 저장소도 엔진도 시계도 인자다. 그래야 시험이 재기동과 상한을 걸을 수 있다. */
|
|
52
|
+
export interface RuleRunnerDeps {
|
|
53
|
+
/** 이 프로세스가 든 트윈들. 남의 프로세스가 든 트윈은 그쪽이 돈다. */
|
|
54
|
+
ownedTwins(): OwnedTwin[]
|
|
55
|
+
/** 이 트윈에 걸린 규칙 — 없으면 스냅샷을 읽지 않는다. */
|
|
56
|
+
rulesOf(twin: OwnedTwin): Promise<ActuationRuleSpec[]>
|
|
57
|
+
/** 지금 서 있는 주의들. */
|
|
58
|
+
attentionsOf(twin: OwnedTwin): AttentionLike[] | undefined
|
|
59
|
+
/**
|
|
60
|
+
* 이 트윈이 **무엇을 비추고 있나** — 관측(`live`)인가 가정(`sim`)인가.
|
|
61
|
+
*
|
|
62
|
+
* 이 값이 없거나 `live` 가 아니면 조치를 내지 않는다(§`runOneTwin`). 모름을 통과로 읽지 않는다.
|
|
63
|
+
*/
|
|
64
|
+
modeOf(twin: OwnedTwin): 'live' | 'sim' | undefined
|
|
65
|
+
/** 커맨드가 사는 자리. */
|
|
66
|
+
store: CommandStore
|
|
67
|
+
/** 선언된 조치 종류 — 없는 종류를 가리키는 규칙은 판정이 걸러 낸다. */
|
|
68
|
+
specs: readonly CommandTypeSpec[]
|
|
69
|
+
nowMs(): number
|
|
70
|
+
/** 돈 결과를 규칙 행에 남긴다 — 화면이 「이 규칙이 돌고 있나」를 말할 수 있게. */
|
|
71
|
+
noteRun?(twin: OwnedTwin, report: TwinRunReport): Promise<void>
|
|
72
|
+
maxPerRun?: number
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 한 바퀴 돈다 — **한 트윈이 실패해도 나머지는 돈다.**
|
|
77
|
+
*
|
|
78
|
+
* 트윈 하나의 규칙이 잘못됐다고 다른 공장의 규칙이 멈추면, 고치는 동안 전체가 멈춘 셈이 된다.
|
|
79
|
+
*/
|
|
80
|
+
export async function runRulesOnce(deps: RuleRunnerDeps): Promise<TwinRunReport[]> {
|
|
81
|
+
const out: TwinRunReport[] = []
|
|
82
|
+
|
|
83
|
+
for (const twin of deps.ownedTwins()) {
|
|
84
|
+
const report: TwinRunReport = { ...twin, proposed: 0, proposedByRule: {}, skips: [] }
|
|
85
|
+
try {
|
|
86
|
+
Object.assign(report, await runOneTwin(twin, deps))
|
|
87
|
+
} catch (e: any) {
|
|
88
|
+
report.failed = String(e?.message ?? e)
|
|
89
|
+
twinError(`[twin-rule] "${twin.instanceId}" 규칙을 돌지 못했다 — ${report.failed}`)
|
|
90
|
+
}
|
|
91
|
+
out.push(report)
|
|
92
|
+
|
|
93
|
+
if (deps.noteRun) {
|
|
94
|
+
await deps.noteRun(twin, report).catch(err =>
|
|
95
|
+
twinWarn(`[twin-rule] "${twin.instanceId}" 규칙 기록을 남기지 못했다 — ${err?.message ?? err}`)
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return out
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function runOneTwin(twin: OwnedTwin, deps: RuleRunnerDeps): Promise<Partial<TwinRunReport>> {
|
|
104
|
+
const rules = await deps.rulesOf(twin)
|
|
105
|
+
/* 규칙이 없으면 스냅샷도 읽지 않는다 — 아무것도 안 할 일에 값을 만들지 않는다. */
|
|
106
|
+
if (!rules.length) return {}
|
|
107
|
+
|
|
108
|
+
/*
|
|
109
|
+
* **이미 올라가 있는 근거를 저장소에서 읽는다.**
|
|
110
|
+
*
|
|
111
|
+
* 저장소가 그 길을 내주지 않으면 **판정하지 않는다.** 빈 집합을 만들어 주면 매 주기 같은 조건이 다시
|
|
112
|
+
* 올라가 대기 줄에 같은 지시가 쌓인다 — 그것을 조용히 하지 않는다.
|
|
113
|
+
*/
|
|
114
|
+
if (!deps.store.openCauses) {
|
|
115
|
+
return {
|
|
116
|
+
skips: [
|
|
117
|
+
{
|
|
118
|
+
ruleId: '(전체)',
|
|
119
|
+
reason:
|
|
120
|
+
'이 저장소는 이미 올라간 근거를 말해 주지 않는다 — 판정하면 같은 조건이 주기마다 다시 올라간다. ' +
|
|
121
|
+
'CommandStore.openCauses 가 필요하다'
|
|
122
|
+
}
|
|
123
|
+
]
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/*
|
|
128
|
+
* ── **시늉된 조건에서 현장 지시를 내지 않는다** (2026-09-01) ────────────
|
|
129
|
+
*
|
|
130
|
+
* 시뮬 트윈의 상태는 **가정**이다. 커널이 스스로 물품을 옮겨 만든 병목이고, 현장에서 관측된 것이
|
|
131
|
+
* 아니다. 그 위에서 생산 지시를 내면 **지어낸 조건이 실제 지시가 된다** — 오늘 자극과 조치를 갈라
|
|
132
|
+
* 놓은 것과 같은 부류의 잘못이고, 이쪽이 더 나쁘다(자극은 모델 안에서 끝나지만 이것은 현장으로 나간다).
|
|
133
|
+
*
|
|
134
|
+
* 실제로 그렇게 걸었다. 오늘 규칙을 확인한 트윈이 전부 `sim` 이었고, 시늉된 병목에서 생산 지시가
|
|
135
|
+
* 올라왔다. 배선은 옳게 돌았지만 근거는 가정이었다.
|
|
136
|
+
*
|
|
137
|
+
* 예측을 근거로 미리 지시를 내는 것 자체는 있을 수 있는 일이다. 다만 그것은 **선언해서** 할 일이고,
|
|
138
|
+
* 승인하는 사람이 「이것은 예측이다」를 알아야 한다. 그 선언이 아직 없으므로 지금은 막고, 왜 막았는지
|
|
139
|
+
* 말한다 — 그 문장이 필요해진 사람이 무엇을 요청해야 하는지 알려 준다.
|
|
140
|
+
*/
|
|
141
|
+
const mode = deps.modeOf(twin)
|
|
142
|
+
if (mode !== 'live') {
|
|
143
|
+
return {
|
|
144
|
+
skips: [
|
|
145
|
+
{
|
|
146
|
+
ruleId: '(전체)',
|
|
147
|
+
reason:
|
|
148
|
+
mode === 'sim'
|
|
149
|
+
? '이 트윈은 시뮬레이션이다 — 시늉된 조건에서 현장 지시를 내지 않는다. ' +
|
|
150
|
+
'예측을 근거로 미리 내려면 그것을 선언하는 자리가 필요하고, 아직 없다'
|
|
151
|
+
: '이 트윈이 관측을 비추는지 알 수 없다 — 모름을 「관측이다」로 읽지 않는다'
|
|
152
|
+
}
|
|
153
|
+
]
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const attentions = deps.attentionsOf(twin)
|
|
158
|
+
if (attentions === undefined) {
|
|
159
|
+
/*
|
|
160
|
+
* 스냅샷이 없다 — 멈춘 트윈이거나 아직 안 선 트윈이다. **주의가 0건인 것과 다르다.** 0건으로 읽으면
|
|
161
|
+
* 「조건이 하나도 없다」가 되고, 규칙이 도는 줄로 보인다.
|
|
162
|
+
*/
|
|
163
|
+
return {
|
|
164
|
+
skips: [{ ruleId: '(전체)', reason: '이 트윈의 상태를 읽을 수 없다 — 멈춰 있거나 아직 서지 않았다' }]
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const openCauses = await deps.store.openCauses(twin.domainId, twin.instanceId)
|
|
169
|
+
/* 없으면 넘기지 않는다 — 판정이 「받지 못했다」와 「낸 적이 없다」를 구별해야 한다. */
|
|
170
|
+
const lastProposedAt = await deps.store.lastProposedAt?.(twin.domainId, twin.instanceId)
|
|
171
|
+
|
|
172
|
+
const verdict = evaluateRules({
|
|
173
|
+
rules,
|
|
174
|
+
attentions,
|
|
175
|
+
specs: deps.specs,
|
|
176
|
+
openCauses,
|
|
177
|
+
...(lastProposedAt ? { lastProposedAt } : {}),
|
|
178
|
+
nowMs: deps.nowMs(),
|
|
179
|
+
...(deps.maxPerRun ? { maxPerRun: deps.maxPerRun } : {})
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
const skips = [...verdict.skips]
|
|
183
|
+
const proposedByRule: Record<string, number> = {}
|
|
184
|
+
let proposed = 0
|
|
185
|
+
|
|
186
|
+
for (const p of verdict.proposals) {
|
|
187
|
+
try {
|
|
188
|
+
await propose(deps.store, twin.domainId, deps.specs, {
|
|
189
|
+
id: newCommandId(),
|
|
190
|
+
instanceId: twin.instanceId,
|
|
191
|
+
type: p.commandType,
|
|
192
|
+
/* 사람이 아니다. 그래서 낸 사람이 없고, 아무 승인자든 두 번째 사람이 된다. */
|
|
193
|
+
origin: 'rule',
|
|
194
|
+
proposedAt: new Date(deps.nowMs()).toISOString(),
|
|
195
|
+
payload: p.payload,
|
|
196
|
+
causeRef: p.attentionId,
|
|
197
|
+
ruleRef: p.ruleId
|
|
198
|
+
})
|
|
199
|
+
proposed++
|
|
200
|
+
proposedByRule[p.ruleId] = (proposedByRule[p.ruleId] ?? 0) + 1
|
|
201
|
+
} catch (e: any) {
|
|
202
|
+
/*
|
|
203
|
+
* 하나가 거절돼도 나머지는 낸다. 그리고 **거절을 삼키지 않는다** — 규칙이 무언가를 내려 했는데
|
|
204
|
+
* 못 냈다는 사실이 사라지면, 사람은 그 조건에 규칙이 안 걸린 줄로 읽는다.
|
|
205
|
+
*/
|
|
206
|
+
skips.push({ ruleId: p.ruleId, attentionId: p.attentionId, reason: String(e?.message ?? e) })
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return { proposed, proposedByRule, skips, ...(verdict.withheld ? { withheld: verdict.withheld } : {}) }
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** 식별자 — 이 파일이 만든다. 저장소가 만들면 커맨드를 적기 전에 근거를 붙일 수 없다. */
|
|
214
|
+
function newCommandId(): string {
|
|
215
|
+
return globalThis.crypto.randomUUID()
|
|
216
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 규칙 판정이 **실제 엔진·저장소에 닿는 자리** — 한 벌.
|
|
3
|
+
*
|
|
4
|
+
* 리졸버(「지금 돌려 본다」)와 주기 루프가 같은 배선을 써야 한다. 두 벌이면 화면에서 돌려 봤을 때와
|
|
5
|
+
* 주기가 돌 때가 다르게 동작하고, 사람은 「돌려 봤을 때는 됐는데」를 겪는다.
|
|
6
|
+
*/
|
|
7
|
+
import { TwinEngine } from '../../engine/twin-engine.js'
|
|
8
|
+
import { getRepository } from '@things-factory/shell'
|
|
9
|
+
|
|
10
|
+
import { TwinActuationRule } from './actuation-rule.js'
|
|
11
|
+
import type { ActuationRuleSpec, AttentionLike } from './rule-evaluate.js'
|
|
12
|
+
import type { OwnedTwin, TwinRunReport } from './rule-runner.js'
|
|
13
|
+
|
|
14
|
+
/** 이 트윈에 걸린 규칙 — 꺼 둔 것도 함께 준다(판정이 거른다. 「꺼져 있다」는 판정의 규율이다). */
|
|
15
|
+
export async function rulesOf(twin: OwnedTwin): Promise<ActuationRuleSpec[]> {
|
|
16
|
+
const rows = await getRepository(TwinActuationRule).find({
|
|
17
|
+
where: { domain: { id: twin.domainId }, instanceId: twin.instanceId }
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
return rows.map(r => ({
|
|
21
|
+
id: r.id,
|
|
22
|
+
instanceId: r.instanceId,
|
|
23
|
+
attentionKind: r.attentionKind,
|
|
24
|
+
...(r.minSeverity ? { minSeverity: r.minSeverity as any } : {}),
|
|
25
|
+
...(r.minHeldMs ? { minHeldMs: r.minHeldMs } : {}),
|
|
26
|
+
...(r.cooldownMs === null || r.cooldownMs === undefined ? {} : { cooldownMs: r.cooldownMs }),
|
|
27
|
+
commandType: r.commandType,
|
|
28
|
+
...(r.payloadTemplate ? { payloadTemplate: r.payloadTemplate } : {}),
|
|
29
|
+
enabled: r.enabled !== false
|
|
30
|
+
}))
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 이 트윈이 관측을 비추나 가정을 돌리나 — 조치를 낼 수 있는지가 여기서 갈린다.
|
|
35
|
+
*
|
|
36
|
+
* 판정과 같은 술어를 쓴다(`TwinEngine.modeOf`). 두 벌이면 어긋난다.
|
|
37
|
+
*/
|
|
38
|
+
export function modeOf(twin: OwnedTwin): 'live' | 'sim' | undefined {
|
|
39
|
+
return TwinEngine.modeOf(twin.domainId, twin.instanceId)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 지금 서 있는 주의들 — **못 읽으면 `undefined`.**
|
|
44
|
+
*
|
|
45
|
+
* 빈 배열로 답하지 않는다. 멈춘 트윈을 「조건이 하나도 없다」로 읽으면 규칙이 도는 줄로 보인다.
|
|
46
|
+
* 주의 질의와 같은 원천을 쓴다(§`twin-attention-query.ts`) — 두 곳이 다른 것을 보면 화면에 보이는
|
|
47
|
+
* 조건과 규칙이 보는 조건이 어긋난다.
|
|
48
|
+
*/
|
|
49
|
+
export function attentionsOf(twin: OwnedTwin): AttentionLike[] | undefined {
|
|
50
|
+
if (!TwinEngine.owns(twin.domainId, twin.instanceId)) return undefined
|
|
51
|
+
|
|
52
|
+
const kernel = TwinEngine.kernel(twin.domainId, twin.instanceId)
|
|
53
|
+
const snap = kernel?.getSnapshot?.() ?? TwinEngine.snapshot(twin.domainId, twin.instanceId)
|
|
54
|
+
if (!snap) return undefined
|
|
55
|
+
|
|
56
|
+
return (snap.attentions ?? []) as AttentionLike[]
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 돈 결과를 규칙 행에 남긴다 — **조용히 안 도는 규칙과 조건이 없어서 안 내는 규칙을 구별하려면 필요하다.**
|
|
61
|
+
*
|
|
62
|
+
* 이 트윈의 모든 규칙에 `lastRunAt` 을 찍는다. 이유는 그 규칙 것만 적는다.
|
|
63
|
+
*/
|
|
64
|
+
export async function noteRun(twin: OwnedTwin, report: TwinRunReport): Promise<void> {
|
|
65
|
+
const repo = getRepository(TwinActuationRule)
|
|
66
|
+
const rows = await repo.find({ where: { domain: { id: twin.domainId }, instanceId: twin.instanceId } })
|
|
67
|
+
if (!rows.length) return
|
|
68
|
+
|
|
69
|
+
const at = new Date().toISOString()
|
|
70
|
+
const reasonOf = (id: string) => report.skips.filter(s => s.ruleId === id).map(s => s.reason)[0] ?? null
|
|
71
|
+
|
|
72
|
+
await repo.save(
|
|
73
|
+
rows.map(row => ({
|
|
74
|
+
...row,
|
|
75
|
+
lastRunAt: at,
|
|
76
|
+
proposedCount: (row.proposedCount ?? 0) + (report.proposedByRule[row.id] ?? 0),
|
|
77
|
+
/*
|
|
78
|
+
* 이유는 그 규칙 것만 적는다. 트윈 전체가 못 돈 경우(§`failed`)는 모든 규칙에 같은 이유가 붙는다
|
|
79
|
+
* — 그때는 규칙 하나의 문제가 아니기 때문이다.
|
|
80
|
+
*/
|
|
81
|
+
lastSkipReason: report.failed ?? reasonOf(row.id)
|
|
82
|
+
})) as any
|
|
83
|
+
)
|
|
84
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* **관측이 들어올 때 나간 지시와 잇는다** — 폐루프의 배선. 판단은 `effect-match.ts` 가 한다.
|
|
3
|
+
*
|
|
4
|
+
* ── 무엇이 실측으로 확인됐나 (2026-09-02) ─────────────────────────────────────
|
|
5
|
+
* 인티그레이션 레인이 조치 둘을 MES 까지 걸었다. 트윈 쪽 두 행이 **구별되지 않았다.**
|
|
6
|
+
*
|
|
7
|
+
* 325e9bb1 acked WO-20260902-004 지시서 붙음 MES 에서 DRAFT
|
|
8
|
+
* 57428362 acked WO-20260902-005 지시서 못 붙음 MES 에서 DRAFT
|
|
9
|
+
*
|
|
10
|
+
* `state`·`dispatch_ref`·`error` 가 셋 다 같다. 그래서 화면은 둘 다 「나갔다」로 읽는데, 둘 다 아직
|
|
11
|
+
* 현장에 안 갔다. `acked` 의 뜻은 **「저쪽이 받았다」**까지다.
|
|
12
|
+
*
|
|
13
|
+
* 빠져나오는 것은 관측이다 — 저쪽이 그 오더를 현장에 내면 `order.status` 가 우리가 받은 식별자로
|
|
14
|
+
* 돌아온다. 그것을 이 파일이 잇는다.
|
|
15
|
+
*
|
|
16
|
+
* ── 왜 유입 자리인가 ─────────────────────────────────────────────────────────
|
|
17
|
+
* 관측의 **시각**이 봉투에만 있다. 나중에 커널 상태에서 읽으면 「지금」밖에 알 수 없고, 그것은 보고된
|
|
18
|
+
* 시각이 아니라 우리가 처리한 시각이다. 없는 사실을 만들지 않는다.
|
|
19
|
+
*
|
|
20
|
+
* ── 어디서 부르나 ───────────────────────────────────────────────────────────
|
|
21
|
+
*
|
|
22
|
+
* routes.ts 훅으로 밀려 들어온 것
|
|
23
|
+
* reference-live.ts 커넥터가 이어 내는 것 · 폴링으로 물어 온 것
|
|
24
|
+
*
|
|
25
|
+
* **백필(`backfill-period-facts.ts`)에서는 부르지 않는다.** 그것은 마감된 구간의 저널을 채우는
|
|
26
|
+
* 일이고, 이 자리가 답하는 것은 「방금 보낸 지시가 어떻게 됐나」다. 저널을 고치는 작업이 조치 기록을
|
|
27
|
+
* 바꾸게 두면, 백필을 다시 돌릴 때마다 같은 보고가 다시 세어진다(`seen` 이 는다).
|
|
28
|
+
*
|
|
29
|
+
* 그래서 지난 기록에서 효과가 확인되는 경우는 지금 다루지 않는다. 필요해지면 그때는 **백필 전용
|
|
30
|
+
* 경로**로 만들 일이다 — 몇 번 세었나가 아니라 「그때 이랬다」만 적는 자리다.
|
|
31
|
+
*
|
|
32
|
+
* ── 값은 얼마나 드나 ─────────────────────────────────────────────────────────
|
|
33
|
+
* 오더 보고가 든 묶음마다 질의 하나다. 조치가 하나도 안 나간 도메인에서도 그 질의가 돈다 — 지금
|
|
34
|
+
* 규모에서 그 값은 작지만 공짜라고 말하지 않는다. 더 줄이려면 「효과를 기다리는 식별자」를 프로세스가
|
|
35
|
+
* 들고 있어야 하고, 그러면 재기동 때 저장소에서 되세우는 자리가 하나 더 생긴다. 그것이 필요해지는
|
|
36
|
+
* 시점은 **오더 보고가 초당 수백 건 오는 도메인**이다.
|
|
37
|
+
*/
|
|
38
|
+
import { OP_EVENT } from '@operato/ops-contract'
|
|
39
|
+
|
|
40
|
+
import { twinWarn } from '../../engine/log.js'
|
|
41
|
+
|
|
42
|
+
import type { CommandStore } from './command-dispatcher.js'
|
|
43
|
+
import { matchEffects, refsToLookUp, type OrderObservation } from './effect-match.js'
|
|
44
|
+
|
|
45
|
+
/** 봉투에서 오더 보고만 골라낸다 — 다른 어휘는 보지 않는다. */
|
|
46
|
+
export function orderObservationsIn(envelopes: readonly any[]): OrderObservation[] {
|
|
47
|
+
const out: OrderObservation[] = []
|
|
48
|
+
for (const e of envelopes ?? []) {
|
|
49
|
+
if (e?.eventType !== OP_EVENT.order) continue
|
|
50
|
+
const data = e?.data
|
|
51
|
+
if (!data?.orderId) continue
|
|
52
|
+
/*
|
|
53
|
+
* 시각은 **봉투의 것**을 쓴다. 보고한 쪽이 말한 시각이고, 우리가 처리한 시각이 아니다.
|
|
54
|
+
* 없으면 `matchEffects` 가 거른다 — 여기서 「지금」으로 채우지 않는다.
|
|
55
|
+
*/
|
|
56
|
+
out.push({ orderId: String(data.orderId), status: String(data.status ?? ''), at: e?.eventTime })
|
|
57
|
+
}
|
|
58
|
+
return out
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface SettleEffectsResult {
|
|
62
|
+
/** 효과를 적은 조치 수. */
|
|
63
|
+
recorded: number
|
|
64
|
+
/** 이을 수 없던 보고 수 — 대개 우리가 내지 않은 오더다. */
|
|
65
|
+
unmatched: number
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 이 묶음의 오더 보고를 나간 지시와 잇는다 — **아무것도 못 이어도 조용히 지난다.**
|
|
70
|
+
*
|
|
71
|
+
* 유입 경로에서 부른다. 그래서 **던지지 않는다** — 효과를 적다 실패해서 관측 유입이 멈추면, 고치는
|
|
72
|
+
* 동안 트윈이 눈을 감는다. 효과는 다음 보고에 다시 이을 수 있고 관측은 다시 오지 않는다.
|
|
73
|
+
*/
|
|
74
|
+
export async function settleEffects(args: {
|
|
75
|
+
domainId: string
|
|
76
|
+
envelopes: readonly any[]
|
|
77
|
+
store: CommandStore
|
|
78
|
+
}): Promise<SettleEffectsResult> {
|
|
79
|
+
const { domainId, envelopes, store } = args
|
|
80
|
+
const none = { recorded: 0, unmatched: 0 }
|
|
81
|
+
|
|
82
|
+
const observations = orderObservationsIn(envelopes)
|
|
83
|
+
if (!observations.length) return none
|
|
84
|
+
|
|
85
|
+
/*
|
|
86
|
+
* 저장소가 두 자리를 안 갖추면 이을 길이 없다. **조용히 지나지 않고 말한다** — 이 상태에서는
|
|
87
|
+
* 넘긴 지시가 저쪽에서 어떻게 됐는지 영원히 모르고, 화면은 그 사실을 알 수 없다.
|
|
88
|
+
*/
|
|
89
|
+
if (!store.byDispatchRef || !store.recordEffects) {
|
|
90
|
+
twinWarn(
|
|
91
|
+
'[actuation] 나간 지시와 관측을 이을 수 없다 — 저장소에 byDispatchRef 나 recordEffects 가 없다. ' +
|
|
92
|
+
'넘긴 조치가 저쪽에서 무엇이 되었는지 알 수 없는 상태다.'
|
|
93
|
+
)
|
|
94
|
+
return none
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
const byRef = await store.byDispatchRef(domainId, refsToLookUp(observations))
|
|
99
|
+
if (!byRef.size) return { recorded: 0, unmatched: observations.length }
|
|
100
|
+
|
|
101
|
+
const { updates, skips } = matchEffects({
|
|
102
|
+
observations,
|
|
103
|
+
byRef: new Map(
|
|
104
|
+
[...byRef].map(([ref, c]) => [ref, { id: c.id, dispatchRef: ref, ...(c.effect ? { effect: c.effect } : {}) }])
|
|
105
|
+
)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
if (updates.length) await store.recordEffects(domainId, updates)
|
|
109
|
+
return { recorded: updates.length, unmatched: skips.length }
|
|
110
|
+
} catch (e: any) {
|
|
111
|
+
/* 관측 유입을 멈추지 않는다 — 위 §. 다만 조용히 넘기지도 않는다. */
|
|
112
|
+
twinWarn(`[actuation] 효과를 잇다 실패했다 — ${String(e?.message ?? e).slice(0, 200)}`)
|
|
113
|
+
return none
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* **남은 것을 줍는 그물** — 승인은 됐는데 안 나간 것, 실패해서 다시 해 볼 것.
|
|
3
|
+
*
|
|
4
|
+
* ── 왜 필요한가 (2026-09-04) ────────────────────────────────────────────────
|
|
5
|
+
* 승인 직후에 한 번 보내는 것만 있었다(§`dispatchAfterApproval`). 정상 경로는 그것으로 282ms 에
|
|
6
|
+
* 끝난다 — 실측이다. 그런데 두 자리가 비어 있었다.
|
|
7
|
+
*
|
|
8
|
+
* 커밋과 발신 사이에 프로세스가 죽으면 그 조치는 `approved` 로 남고 스스로 안 나간다
|
|
9
|
+
* 실패한 것을 다시 보내려면 사람이 시켜야 했다
|
|
10
|
+
*
|
|
11
|
+
* 2026-09-02 에 뒤엣것을 겪었다: 저쪽 인증 정보가 채워진 뒤 사람이 손으로 다시 넘겨서 나갔다.
|
|
12
|
+
*
|
|
13
|
+
* ── 왜 새 타이머를 세우지 않나 ──────────────────────────────────────────────
|
|
14
|
+
* 규칙 주기(`rule-loop`)에 얹는다. 그 파일이 이미 그 이유를 적어 두었다 — 「타이머를 둘 두면
|
|
15
|
+
* 내리고 세울 자리가 둘이 된다」. 놓친 보고를 backfill 하는 것(`catchUpEffects`)도 같은 자리에 있다.
|
|
16
|
+
*
|
|
17
|
+
* 30초면 충분하다. **아무도 안 기다린다** — 정상 경로가 이미 끝냈고, 이 그물은 남은 것만 본다.
|
|
18
|
+
*
|
|
19
|
+
* ── 판단은 여기 없다 ────────────────────────────────────────────────────────
|
|
20
|
+
* 「지금 넘길 것인가, 다음은 언제인가」는 순수 함수가 답한다(§`retryDecisionOf`). 이 파일은
|
|
21
|
+
* 표를 읽고 그 답대로 하고 결과를 적는 배선이다. 그렇게 갈라 둔 이유는 물러섬 규칙을 30분
|
|
22
|
+
* 기다리지 않고 값으로 잴 수 있어야 하기 때문이다.
|
|
23
|
+
*
|
|
24
|
+
* ── 던지지 않는다 ───────────────────────────────────────────────────────────
|
|
25
|
+
* 주기 안에서 돈다. 한 조치가 터져도 다음 조치를 봐야 하고, 이 그물이 터져도 규칙과 backfill 은
|
|
26
|
+
* 돌아야 한다. 조치를 못 넘기는 것과 트윈이 멈추는 것은 다른 사고다.
|
|
27
|
+
*/
|
|
28
|
+
import { nextCommandState, type ApprovedCommand } from '@operato/ops-contract'
|
|
29
|
+
|
|
30
|
+
import { twinWarn } from '../../engine/log.js'
|
|
31
|
+
import { dispatch, type CommandAck, type CommandStore, type StoredCommand } from './command-dispatcher.js'
|
|
32
|
+
import { retryDecisionOf } from './retry-plan.js'
|
|
33
|
+
|
|
34
|
+
/*
|
|
35
|
+
* **의존을 인자로 받는다** — 이 파일이 커넥터 등록소·연결 표를 직접 부르지 않는다.
|
|
36
|
+
*
|
|
37
|
+
* 규칙 실행이 이미 그 모양이다(`runRulesOnce({ store, rulesOf, attentionsOf … })`). 그렇게 두는
|
|
38
|
+
* 값이 둘이다: 배선을 시험이 굴릴 수 있고(직접 부르면 시험이 DB·모듈 부팅을 끌어온다 — 실제로
|
|
39
|
+
* 그렇게 났다), 그리고 이 파일이 무엇에 의존하는지가 서명에 적힌다.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
/** 한 바퀴에 볼 최대 수 — 대기 줄이 커도 한 주기를 독차지하지 않게. */
|
|
43
|
+
export const SWEEP_LIMIT = 50
|
|
44
|
+
|
|
45
|
+
export interface SweepReport {
|
|
46
|
+
/** 표에서 읽은 수. */
|
|
47
|
+
seen: number
|
|
48
|
+
/** 실제로 넘긴 수. */
|
|
49
|
+
dispatched: number
|
|
50
|
+
/** 넘기지 않은 이유별 수 — 목록이 그대로 보인다. */
|
|
51
|
+
skipped: Record<string, number>
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 이 테넌트의 대기 줄을 한 바퀴 훑는다.
|
|
56
|
+
*
|
|
57
|
+
* 저장소가 대기 줄을 못 내면 **아무것도 하지 않는다** — 그 능력이 없는 구현도 그대로 돌아야
|
|
58
|
+
* 한다(옛 거동).
|
|
59
|
+
*/
|
|
60
|
+
export interface SweepDeps {
|
|
61
|
+
store: CommandStore
|
|
62
|
+
/** 이 조치를 저쪽에 낸다 — 부르는 쪽이 커넥터 배선을 안다. */
|
|
63
|
+
/*
|
|
64
|
+
* `dispatch` 가 승인 기록을 확인한 뒤의 커맨드를 넘겨 준다(`ApprovedCommand`) — 그 타입을 그대로
|
|
65
|
+
* 받는다. 우리가 `StoredCommand` 로 넓히면 「승인 없이 나갈 수 있는 서명」이 된다.
|
|
66
|
+
*/
|
|
67
|
+
actuate: (command: ApprovedCommand) => Promise<CommandAck>
|
|
68
|
+
/** 이 연결이 두 번 보내도 지시가 하나인가 — **선언만 참으로 읽는다.** */
|
|
69
|
+
idempotent: (instanceId: string) => Promise<boolean>
|
|
70
|
+
/**
|
|
71
|
+
* 우리 쪽 설정만으로 답할 수 있는 준비 상태. 저쪽을 두드리지 않는 답만 준다.
|
|
72
|
+
*
|
|
73
|
+
* 없으면 앞점검을 하지 않는다(그냥 넘긴다) — 모르는 것을 거절로 접지 않는 규율이다.
|
|
74
|
+
*/
|
|
75
|
+
localReadiness?: (instanceId: string) => Promise<{ ready?: boolean; reason?: string; code?: string } | undefined>
|
|
76
|
+
nowMs?: () => number
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function sweepPendingDispatch(domainId: string, deps: SweepDeps): Promise<SweepReport> {
|
|
80
|
+
const { store } = deps
|
|
81
|
+
const now = deps.nowMs ?? (() => Date.now())
|
|
82
|
+
const report: SweepReport = { seen: 0, dispatched: 0, skipped: {} }
|
|
83
|
+
if (!store.pendingDispatch) return report
|
|
84
|
+
|
|
85
|
+
const pending = await store.pendingDispatch(domainId).catch(() => [] as readonly StoredCommand[])
|
|
86
|
+
report.seen = pending.length
|
|
87
|
+
|
|
88
|
+
const note = (why: string) => {
|
|
89
|
+
report.skipped[why] = (report.skipped[why] ?? 0) + 1
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
for (const command of pending.slice(0, SWEEP_LIMIT)) {
|
|
93
|
+
try {
|
|
94
|
+
const decision = retryDecisionOf({
|
|
95
|
+
state: command.state,
|
|
96
|
+
dispatchable: nextCommandState(command.state, 'dispatch') !== undefined,
|
|
97
|
+
retry: command.retry,
|
|
98
|
+
attempts: command.attempts,
|
|
99
|
+
nextAttemptAt: command.nextAttemptAt,
|
|
100
|
+
idempotent: await deps.idempotent(command.instanceId).catch(() => false),
|
|
101
|
+
nowMs: now()
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
if (!decision.take) {
|
|
105
|
+
note(decision.skip ?? 'unknown')
|
|
106
|
+
continue
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/*
|
|
110
|
+
* **넘기기 전에 우리 설정을 먼저 본다** — 아끼려는 것이 아니라 **이유를 지금 값으로
|
|
111
|
+
* 적으려는** 것이다(인티그레이션 레인이 제 첫 근거를 깼다: 그쪽 어댑터는 이미 `fetch` 전에
|
|
112
|
+
* 되돌아오므로 아낄 요청이 없다).
|
|
113
|
+
*
|
|
114
|
+
* 건너뛰면서 아무것도 안 적으면 **옛 `error` 가 남는다.** 저쪽 인증 정보 때문에 실패해 앉은
|
|
115
|
+
* 뒤 우리 쪽 설정이 지워지면, 목록에 계속 저쪽 얘기가 보이고 사람이 남의 서버를 보러 간다.
|
|
116
|
+
*
|
|
117
|
+
* 모르면(`undefined`) 그냥 넘긴다 — 모르는 것을 거절로 접지 않는다.
|
|
118
|
+
*/
|
|
119
|
+
const readiness = await deps.localReadiness?.(command.instanceId).catch(() => undefined)
|
|
120
|
+
if (readiness && readiness.ready === false) {
|
|
121
|
+
await noteBlocked(store, domainId, command, decision.nextAttemptAt, readiness.reason, readiness.code)
|
|
122
|
+
note('not-ready')
|
|
123
|
+
continue
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const settled = await dispatch(store, domainId, command.id, c => deps.actuate(c))
|
|
127
|
+
|
|
128
|
+
await recordAttempt(store, domainId, settled, decision.attempt ?? 1, decision.nextAttemptAt)
|
|
129
|
+
|
|
130
|
+
if (settled.state === 'failed') {
|
|
131
|
+
note('failed')
|
|
132
|
+
twinWarn(`[twin-sweep] 조치 ${command.id} 를 넘기지 못했다: ${settled.error ?? '(사유 없음)'}`)
|
|
133
|
+
} else {
|
|
134
|
+
report.dispatched++
|
|
135
|
+
}
|
|
136
|
+
} catch (e: any) {
|
|
137
|
+
/* 한 조치가 터져도 다음을 본다 — 가장 흔한 원인은 그 사이에 상태가 바뀐 것이다. */
|
|
138
|
+
note('threw')
|
|
139
|
+
twinWarn(`[twin-sweep] 조치 ${command.id} 에서 예외 — ${String(e?.message ?? e)}`)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return report
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* 못 나가는 이유를 **지금 값으로** 적는다 — 넘기지 않았으므로 상태는 그대로다.
|
|
148
|
+
*
|
|
149
|
+
* **바뀌었을 때만 쓴다.** 10분마다 같은 이유를 다시 쓰면 `updatedAt` 이 계속 튀고, 목록의
|
|
150
|
+
* 「언제부터 이랬나」가 사라진다(§`catch-up-effects` 에서 같은 규율을 지켰다).
|
|
151
|
+
*/
|
|
152
|
+
async function noteBlocked(
|
|
153
|
+
store: CommandStore,
|
|
154
|
+
domainId: string,
|
|
155
|
+
command: StoredCommand,
|
|
156
|
+
nextAttemptAt: string | undefined,
|
|
157
|
+
reason?: string,
|
|
158
|
+
code?: string
|
|
159
|
+
): Promise<void> {
|
|
160
|
+
const sentence = reason ?? code ?? '연결 설정이 조치를 낼 준비가 되지 않았습니다'
|
|
161
|
+
if (command.error === sentence && command.nextAttemptAt === nextAttemptAt) return
|
|
162
|
+
|
|
163
|
+
await store
|
|
164
|
+
.save(domainId, { ...command, error: sentence, ...(nextAttemptAt ? { nextAttemptAt } : {}) })
|
|
165
|
+
.catch(err => twinWarn(`[twin-sweep] 조치 ${command.id} 의 사유를 적지 못했다 — ${String(err?.message ?? err)}`))
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** 시도 횟수와 다음 시각을 적는다 — `dispatch` 가 쓴 상태를 덮지 않고 이 둘만 더한다. */
|
|
169
|
+
async function recordAttempt(
|
|
170
|
+
store: CommandStore,
|
|
171
|
+
domainId: string,
|
|
172
|
+
settled: StoredCommand,
|
|
173
|
+
attempt: number,
|
|
174
|
+
nextAttemptAt: string | undefined
|
|
175
|
+
): Promise<void> {
|
|
176
|
+
await store
|
|
177
|
+
.save(domainId, { ...settled, attempts: attempt, ...(nextAttemptAt ? { nextAttemptAt } : {}) })
|
|
178
|
+
.catch(err => twinWarn(`[twin-sweep] 조치 ${settled.id} 의 시도 기록을 적지 못했다 — ${String(err?.message ?? err)}`))
|
|
179
|
+
}
|