@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,19 @@
|
|
|
1
|
+
import { ListParam } from '@things-factory/shell';
|
|
2
|
+
export declare class TwinActuationRuleResolver {
|
|
3
|
+
twinActuationRules(params: ListParam, context: ResolverContext): Promise<any>;
|
|
4
|
+
/**
|
|
5
|
+
* 규칙을 세우거나 고친다.
|
|
6
|
+
*
|
|
7
|
+
* **낼 수 없는 종류는 저장하지 않는다.** 저장은 되는데 안 도는 규칙이 생기면 사람은 규칙이 걸린 줄로
|
|
8
|
+
* 읽는다. 판정과 같은 검사를 지난다(§`commandTypeGap`) — 두 벌이면 어긋난다.
|
|
9
|
+
*/
|
|
10
|
+
saveTwinActuationRule(patch: any, context: ResolverContext): Promise<any>;
|
|
11
|
+
deleteTwinActuationRule(id: string, context: ResolverContext): Promise<any>;
|
|
12
|
+
/**
|
|
13
|
+
* **지금 한 번 돌린다** — 무엇을 냈고 무엇을 왜 안 냈는지 그대로 답한다.
|
|
14
|
+
*
|
|
15
|
+
* 낸 조치는 제안 상태로 남는다. 결재선에 올리는 것은 사람이 따로 한다(§`raiseTwinCommand`) — 규칙이
|
|
16
|
+
* 올리지 않는 이유가 여기에도 그대로 적용된다(결재선은 올린 사람 기준으로 풀린다).
|
|
17
|
+
*/
|
|
18
|
+
runTwinActuationRules(instanceId: string, context: ResolverContext): Promise<any>;
|
|
19
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TwinActuationRuleResolver = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
/*
|
|
6
|
+
* 규칙을 만들고 끄는 자리, 그리고 **지금 한 번 돌려 보는 자리**.
|
|
7
|
+
*
|
|
8
|
+
* ── 왜 「지금 돌린다」가 있나 ────────────────────────────────────────────
|
|
9
|
+
* 규칙을 만든 사람이 가장 먼저 묻는 것은 「이게 걸리나」다. 주기를 기다려 보라고 하면 그 사이에 무엇이
|
|
10
|
+
* 잘못됐는지 알 수 없고, 조건이 그때까지 살아 있을지도 모른다. 돌려 보면 **무엇을 냈고 무엇을 왜 안
|
|
11
|
+
* 냈는지**가 그 자리에서 답으로 온다.
|
|
12
|
+
*/
|
|
13
|
+
const type_graphql_1 = require("type-graphql");
|
|
14
|
+
const ops_contract_1 = require("@operato/ops-contract");
|
|
15
|
+
const shell_1 = require("@things-factory/shell");
|
|
16
|
+
const twin_engine_js_1 = require("../../engine/twin-engine.js");
|
|
17
|
+
const actuation_rule_js_1 = require("./actuation-rule.js");
|
|
18
|
+
const rule_evaluate_js_1 = require("./rule-evaluate.js");
|
|
19
|
+
const rule_runner_js_1 = require("./rule-runner.js");
|
|
20
|
+
const command_store_js_1 = require("./command-store.js");
|
|
21
|
+
const rule_wiring_js_1 = require("./rule-wiring.js");
|
|
22
|
+
let TwinActuationRuleResolver = class TwinActuationRuleResolver {
|
|
23
|
+
async twinActuationRules(params, context) {
|
|
24
|
+
const domain = context.state.domain;
|
|
25
|
+
if (!domain?.id)
|
|
26
|
+
return { items: [], total: 0 };
|
|
27
|
+
const qb = (0, shell_1.getQueryBuilderFromListParams)({
|
|
28
|
+
repository: (0, shell_1.getRepository)(actuation_rule_js_1.TwinActuationRule),
|
|
29
|
+
params,
|
|
30
|
+
domain,
|
|
31
|
+
alias: 'r',
|
|
32
|
+
searchables: ['instanceId', 'name', 'attentionKind', 'commandType', 'lastSkipReason'],
|
|
33
|
+
sortables: ['createdAt', 'updatedAt', 'instanceId', 'attentionKind', 'commandType', 'lastRunAt'],
|
|
34
|
+
defaultLimit: 50,
|
|
35
|
+
maxLimit: 200
|
|
36
|
+
});
|
|
37
|
+
const [items, total] = await qb.getManyAndCount();
|
|
38
|
+
return { items, total };
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* 규칙을 세우거나 고친다.
|
|
42
|
+
*
|
|
43
|
+
* **낼 수 없는 종류는 저장하지 않는다.** 저장은 되는데 안 도는 규칙이 생기면 사람은 규칙이 걸린 줄로
|
|
44
|
+
* 읽는다. 판정과 같은 검사를 지난다(§`commandTypeGap`) — 두 벌이면 어긋난다.
|
|
45
|
+
*/
|
|
46
|
+
async saveTwinActuationRule(patch, context) {
|
|
47
|
+
const { domain, user, tx } = context.state;
|
|
48
|
+
const instanceId = String(patch?.instanceId ?? '').trim();
|
|
49
|
+
if (!instanceId)
|
|
50
|
+
throw new Error('어느 트윈의 규칙인지 말해야 합니다 — 전역 규칙은 두지 않습니다');
|
|
51
|
+
const attentionKind = String(patch?.attentionKind ?? '').trim();
|
|
52
|
+
if (!attentionKind)
|
|
53
|
+
throw new Error('어느 관측에 걸 것인지 말해야 합니다');
|
|
54
|
+
const commandType = String(patch?.commandType ?? '').trim();
|
|
55
|
+
const gap = (0, rule_evaluate_js_1.commandTypeGap)(commandType, ops_contract_1.MES_COMMANDS, Object.values(ops_contract_1.CMD));
|
|
56
|
+
if (gap)
|
|
57
|
+
throw new Error(gap);
|
|
58
|
+
const repo = (0, shell_1.getRepository)(actuation_rule_js_1.TwinActuationRule, tx);
|
|
59
|
+
const saved = await repo.save({
|
|
60
|
+
...(patch.id ? { id: patch.id } : {}),
|
|
61
|
+
domain,
|
|
62
|
+
instanceId,
|
|
63
|
+
name: patch.name ?? null,
|
|
64
|
+
attentionKind,
|
|
65
|
+
minSeverity: patch.minSeverity ?? null,
|
|
66
|
+
minHeldMs: patch.minHeldMs ?? null,
|
|
67
|
+
/* 비워 두면 기본값(한 시간)이 걸린다. 0 은 「기다리지 않는다」는 뜻이라 그대로 존중한다. */
|
|
68
|
+
cooldownMs: patch.cooldownMs ?? null,
|
|
69
|
+
commandType,
|
|
70
|
+
payloadTemplate: patch.payloadTemplate ?? null,
|
|
71
|
+
/* 비어 있음을 「끔」으로 읽지 않는다 — 새로 만든 규칙은 켜져 있는 것이 사람의 뜻이다. */
|
|
72
|
+
enabled: patch.enabled === false ? false : true,
|
|
73
|
+
...(patch.id ? {} : { creator: user }),
|
|
74
|
+
updater: user
|
|
75
|
+
});
|
|
76
|
+
return saved;
|
|
77
|
+
}
|
|
78
|
+
async deleteTwinActuationRule(id, context) {
|
|
79
|
+
const { domain, tx } = context.state;
|
|
80
|
+
const result = await (0, shell_1.getRepository)(actuation_rule_js_1.TwinActuationRule, tx).delete({ id, domain: { id: domain.id } });
|
|
81
|
+
if (!result.affected)
|
|
82
|
+
throw new Error(`모르는 규칙입니다 (${id})`);
|
|
83
|
+
return { deleted: id };
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* **지금 한 번 돌린다** — 무엇을 냈고 무엇을 왜 안 냈는지 그대로 답한다.
|
|
87
|
+
*
|
|
88
|
+
* 낸 조치는 제안 상태로 남는다. 결재선에 올리는 것은 사람이 따로 한다(§`raiseTwinCommand`) — 규칙이
|
|
89
|
+
* 올리지 않는 이유가 여기에도 그대로 적용된다(결재선은 올린 사람 기준으로 풀린다).
|
|
90
|
+
*/
|
|
91
|
+
async runTwinActuationRules(instanceId, context) {
|
|
92
|
+
const domainId = context.state.domain?.id;
|
|
93
|
+
if (!domainId)
|
|
94
|
+
throw new Error('테넌트를 알 수 없습니다');
|
|
95
|
+
/*
|
|
96
|
+
* 트랜잭션을 넘기지 않는다 — 이 자리는 여러 트윈을 돌고, 한 트윈이 실패해도 앞의 트윈이 낸 것은
|
|
97
|
+
* 남아야 한다. 한 덩어리로 묶으면 마지막 트윈의 실패가 앞의 결과를 전부 물린다.
|
|
98
|
+
*/
|
|
99
|
+
const store = (0, command_store_js_1.databaseCommandStore)();
|
|
100
|
+
const owned = twin_engine_js_1.TwinEngine.ownedTwins(domainId).filter(t => !instanceId || t.instanceId === instanceId);
|
|
101
|
+
if (instanceId && !owned.length) {
|
|
102
|
+
throw new Error(`이 서버가 들고 있지 않은 트윈입니다 (${instanceId}) — 멈춰 있거나 다른 서버가 돌고 있습니다`);
|
|
103
|
+
}
|
|
104
|
+
const reports = await (0, rule_runner_js_1.runRulesOnce)({
|
|
105
|
+
ownedTwins: () => owned,
|
|
106
|
+
rulesOf: twin => (0, rule_wiring_js_1.rulesOf)(twin),
|
|
107
|
+
attentionsOf: rule_wiring_js_1.attentionsOf,
|
|
108
|
+
modeOf: rule_wiring_js_1.modeOf,
|
|
109
|
+
store,
|
|
110
|
+
specs: ops_contract_1.MES_COMMANDS,
|
|
111
|
+
nowMs: () => Date.now(),
|
|
112
|
+
noteRun: rule_wiring_js_1.noteRun
|
|
113
|
+
});
|
|
114
|
+
return { reports, proposed: reports.reduce((n, r) => n + r.proposed, 0) };
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
exports.TwinActuationRuleResolver = TwinActuationRuleResolver;
|
|
118
|
+
tslib_1.__decorate([
|
|
119
|
+
(0, type_graphql_1.Query)(returns => shell_1.ScalarObject, {
|
|
120
|
+
nullable: true,
|
|
121
|
+
description: 'List actuation rules with the standard list contract.'
|
|
122
|
+
}),
|
|
123
|
+
tslib_1.__param(0, (0, type_graphql_1.Args)(type => shell_1.ListParam)),
|
|
124
|
+
tslib_1.__param(1, (0, type_graphql_1.Ctx)()),
|
|
125
|
+
tslib_1.__metadata("design:type", Function),
|
|
126
|
+
tslib_1.__metadata("design:paramtypes", [shell_1.ListParam, Object]),
|
|
127
|
+
tslib_1.__metadata("design:returntype", Promise)
|
|
128
|
+
], TwinActuationRuleResolver.prototype, "twinActuationRules", null);
|
|
129
|
+
tslib_1.__decorate([
|
|
130
|
+
(0, type_graphql_1.Directive)('@transaction'),
|
|
131
|
+
(0, type_graphql_1.Directive)('@privilege(category: "twin", privilege: "mutation", domainOwnerGranted: true, superUserGranted: true)'),
|
|
132
|
+
(0, type_graphql_1.Mutation)(returns => shell_1.ScalarObject, { description: 'Create or update one actuation rule.' }),
|
|
133
|
+
tslib_1.__param(0, (0, type_graphql_1.Arg)('patch', type => shell_1.ScalarObject)),
|
|
134
|
+
tslib_1.__param(1, (0, type_graphql_1.Ctx)()),
|
|
135
|
+
tslib_1.__metadata("design:type", Function),
|
|
136
|
+
tslib_1.__metadata("design:paramtypes", [Object, Object]),
|
|
137
|
+
tslib_1.__metadata("design:returntype", Promise)
|
|
138
|
+
], TwinActuationRuleResolver.prototype, "saveTwinActuationRule", null);
|
|
139
|
+
tslib_1.__decorate([
|
|
140
|
+
(0, type_graphql_1.Directive)('@transaction'),
|
|
141
|
+
(0, type_graphql_1.Directive)('@privilege(category: "twin", privilege: "mutation", domainOwnerGranted: true, superUserGranted: true)'),
|
|
142
|
+
(0, type_graphql_1.Mutation)(returns => shell_1.ScalarObject, { description: 'Delete one actuation rule.' }),
|
|
143
|
+
tslib_1.__param(0, (0, type_graphql_1.Arg)('id')),
|
|
144
|
+
tslib_1.__param(1, (0, type_graphql_1.Ctx)()),
|
|
145
|
+
tslib_1.__metadata("design:type", Function),
|
|
146
|
+
tslib_1.__metadata("design:paramtypes", [String, Object]),
|
|
147
|
+
tslib_1.__metadata("design:returntype", Promise)
|
|
148
|
+
], TwinActuationRuleResolver.prototype, "deleteTwinActuationRule", null);
|
|
149
|
+
tslib_1.__decorate([
|
|
150
|
+
(0, type_graphql_1.Directive)('@privilege(category: "twin", privilege: "mutation", domainOwnerGranted: true, superUserGranted: true)'),
|
|
151
|
+
(0, type_graphql_1.Mutation)(returns => shell_1.ScalarObject, { description: 'Evaluate actuation rules once now and report what happened.' }),
|
|
152
|
+
tslib_1.__param(0, (0, type_graphql_1.Arg)('instanceId', { nullable: true })),
|
|
153
|
+
tslib_1.__param(1, (0, type_graphql_1.Ctx)()),
|
|
154
|
+
tslib_1.__metadata("design:type", Function),
|
|
155
|
+
tslib_1.__metadata("design:paramtypes", [String, Object]),
|
|
156
|
+
tslib_1.__metadata("design:returntype", Promise)
|
|
157
|
+
], TwinActuationRuleResolver.prototype, "runTwinActuationRules", null);
|
|
158
|
+
exports.TwinActuationRuleResolver = TwinActuationRuleResolver = tslib_1.__decorate([
|
|
159
|
+
(0, type_graphql_1.Resolver)()
|
|
160
|
+
], TwinActuationRuleResolver);
|
|
161
|
+
//# sourceMappingURL=actuation-rule-resolver.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"actuation-rule-resolver.js","sourceRoot":"","sources":["../../../server/service/actuation/actuation-rule-resolver.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * 규칙을 만들고 끄는 자리, 그리고 **지금 한 번 돌려 보는 자리**.\n *\n * ── 왜 「지금 돌린다」가 있나 ────────────────────────────────────────────\n * 규칙을 만든 사람이 가장 먼저 묻는 것은 「이게 걸리나」다. 주기를 기다려 보라고 하면 그 사이에 무엇이\n * 잘못됐는지 알 수 없고, 조건이 그때까지 살아 있을지도 모른다. 돌려 보면 **무엇을 냈고 무엇을 왜 안\n * 냈는지**가 그 자리에서 답으로 온다.\n */\nimport { Arg, Args, Ctx, Directive, Mutation, Query, Resolver } from 'type-graphql'\n\nimport { CMD, MES_COMMANDS } from '@operato/ops-contract'\n\nimport { ListParam, ScalarObject, getQueryBuilderFromListParams, getRepository } from '@things-factory/shell'\n\nimport { TwinEngine } from '../../engine/twin-engine.js'\n\nimport { TwinActuationRule } from './actuation-rule.js'\nimport { commandTypeGap } from './rule-evaluate.js'\nimport { runRulesOnce } from './rule-runner.js'\nimport { databaseCommandStore } from './command-store.js'\nimport { rulesOf, noteRun, attentionsOf, modeOf } from './rule-wiring.js'\n\n@Resolver()\nexport class TwinActuationRuleResolver {\n @Query(returns => ScalarObject, {\n nullable: true,\n description: 'List actuation rules with the standard list contract.'\n })\n async twinActuationRules(\n @Args(type => ListParam) params: ListParam,\n @Ctx() context: ResolverContext\n ): Promise<any> {\n const domain = context.state.domain\n if (!domain?.id) return { items: [], total: 0 }\n\n const qb = getQueryBuilderFromListParams({\n repository: getRepository(TwinActuationRule),\n params,\n domain,\n alias: 'r',\n searchables: ['instanceId', 'name', 'attentionKind', 'commandType', 'lastSkipReason'],\n sortables: ['createdAt', 'updatedAt', 'instanceId', 'attentionKind', 'commandType', 'lastRunAt'],\n defaultLimit: 50,\n maxLimit: 200\n })\n\n const [items, total] = await qb.getManyAndCount()\n return { items, total }\n }\n\n /**\n * 규칙을 세우거나 고친다.\n *\n * **낼 수 없는 종류는 저장하지 않는다.** 저장은 되는데 안 도는 규칙이 생기면 사람은 규칙이 걸린 줄로\n * 읽는다. 판정과 같은 검사를 지난다(§`commandTypeGap`) — 두 벌이면 어긋난다.\n */\n @Directive('@transaction')\n @Directive('@privilege(category: \"twin\", privilege: \"mutation\", domainOwnerGranted: true, superUserGranted: true)')\n @Mutation(returns => ScalarObject, { description: 'Create or update one actuation rule.' })\n async saveTwinActuationRule(\n @Arg('patch', type => ScalarObject) patch: any,\n @Ctx() context: ResolverContext\n ): Promise<any> {\n const { domain, user, tx } = context.state\n\n const instanceId = String(patch?.instanceId ?? '').trim()\n if (!instanceId) throw new Error('어느 트윈의 규칙인지 말해야 합니다 — 전역 규칙은 두지 않습니다')\n\n const attentionKind = String(patch?.attentionKind ?? '').trim()\n if (!attentionKind) throw new Error('어느 관측에 걸 것인지 말해야 합니다')\n\n const commandType = String(patch?.commandType ?? '').trim()\n const gap = commandTypeGap(commandType, MES_COMMANDS, Object.values(CMD) as string[])\n if (gap) throw new Error(gap)\n\n const repo = getRepository(TwinActuationRule, tx)\n const saved = await repo.save({\n ...(patch.id ? { id: patch.id } : {}),\n domain,\n instanceId,\n name: patch.name ?? null,\n attentionKind,\n minSeverity: patch.minSeverity ?? null,\n minHeldMs: patch.minHeldMs ?? null,\n /* 비워 두면 기본값(한 시간)이 걸린다. 0 은 「기다리지 않는다」는 뜻이라 그대로 존중한다. */\n cooldownMs: patch.cooldownMs ?? null,\n commandType,\n payloadTemplate: patch.payloadTemplate ?? null,\n /* 비어 있음을 「끔」으로 읽지 않는다 — 새로 만든 규칙은 켜져 있는 것이 사람의 뜻이다. */\n enabled: patch.enabled === false ? false : true,\n ...(patch.id ? {} : { creator: user }),\n updater: user\n } as any)\n\n return saved\n }\n\n @Directive('@transaction')\n @Directive('@privilege(category: \"twin\", privilege: \"mutation\", domainOwnerGranted: true, superUserGranted: true)')\n @Mutation(returns => ScalarObject, { description: 'Delete one actuation rule.' })\n async deleteTwinActuationRule(@Arg('id') id: string, @Ctx() context: ResolverContext): Promise<any> {\n const { domain, tx } = context.state\n const result = await getRepository(TwinActuationRule, tx).delete({ id, domain: { id: domain.id } } as any)\n if (!result.affected) throw new Error(`모르는 규칙입니다 (${id})`)\n return { deleted: id }\n }\n\n /**\n * **지금 한 번 돌린다** — 무엇을 냈고 무엇을 왜 안 냈는지 그대로 답한다.\n *\n * 낸 조치는 제안 상태로 남는다. 결재선에 올리는 것은 사람이 따로 한다(§`raiseTwinCommand`) — 규칙이\n * 올리지 않는 이유가 여기에도 그대로 적용된다(결재선은 올린 사람 기준으로 풀린다).\n */\n @Directive('@privilege(category: \"twin\", privilege: \"mutation\", domainOwnerGranted: true, superUserGranted: true)')\n @Mutation(returns => ScalarObject, { description: 'Evaluate actuation rules once now and report what happened.' })\n async runTwinActuationRules(\n @Arg('instanceId', { nullable: true }) instanceId: string,\n @Ctx() context: ResolverContext\n ): Promise<any> {\n const domainId = context.state.domain?.id\n if (!domainId) throw new Error('테넌트를 알 수 없습니다')\n\n /*\n * 트랜잭션을 넘기지 않는다 — 이 자리는 여러 트윈을 돌고, 한 트윈이 실패해도 앞의 트윈이 낸 것은\n * 남아야 한다. 한 덩어리로 묶으면 마지막 트윈의 실패가 앞의 결과를 전부 물린다.\n */\n const store = databaseCommandStore()\n\n const owned = TwinEngine.ownedTwins(domainId).filter(t => !instanceId || t.instanceId === instanceId)\n if (instanceId && !owned.length) {\n throw new Error(`이 서버가 들고 있지 않은 트윈입니다 (${instanceId}) — 멈춰 있거나 다른 서버가 돌고 있습니다`)\n }\n\n const reports = await runRulesOnce({\n ownedTwins: () => owned,\n rulesOf: twin => rulesOf(twin),\n attentionsOf,\n modeOf,\n store,\n specs: MES_COMMANDS,\n nowMs: () => Date.now(),\n noteRun\n })\n\n return { reports, proposed: reports.reduce((n, r) => n + r.proposed, 0) }\n }\n}\n"]}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Domain } from '@things-factory/shell';
|
|
2
|
+
import { User } from '@things-factory/auth-base';
|
|
3
|
+
export declare class TwinActuationRule {
|
|
4
|
+
readonly id: string;
|
|
5
|
+
domain?: Domain;
|
|
6
|
+
domainId?: string;
|
|
7
|
+
/**
|
|
8
|
+
* **어느 트윈인가** — 전역 규칙을 두지 않는다.
|
|
9
|
+
*
|
|
10
|
+
* 라인이 공장마다 다르므로 한 규칙이 모든 트윈에 걸리면 어느 공장에서는 반드시 틀린다. 트윈을 지목하면
|
|
11
|
+
* 그 사실이 행에 드러나고, 화면이 「이 공장에 걸린 규칙」을 그대로 보여 줄 수 있다.
|
|
12
|
+
*/
|
|
13
|
+
instanceId: string;
|
|
14
|
+
name?: string;
|
|
15
|
+
/** 어느 관측인가 — 커널의 안정 코드(`Attention.kind`). */
|
|
16
|
+
attentionKind: string;
|
|
17
|
+
minSeverity?: string;
|
|
18
|
+
/**
|
|
19
|
+
* **조건이 이만큼 이어졌을 때** 올린다.
|
|
20
|
+
*
|
|
21
|
+
* 없으면 처음 본 주기에 올린다. 깜빡이는 조건에 현장 지시가 나가는 것을 막는 유일한 칸이다.
|
|
22
|
+
*/
|
|
23
|
+
minHeldMs?: number;
|
|
24
|
+
/**
|
|
25
|
+
* 무엇을 내는가 — **선언된 조치 종류**(`CommandTypeSpec.type`).
|
|
26
|
+
*
|
|
27
|
+
* 커널 자극(`CMD`)이 아니다. 자극을 적으면 판정이 「선언되지 않은 조치 종류」로 거절한다 — 그 값은
|
|
28
|
+
* 모델 상태를 바꾸는 것이고 현장에 나가는 지시가 아니다.
|
|
29
|
+
*/
|
|
30
|
+
commandType: string;
|
|
31
|
+
/**
|
|
32
|
+
* 지시의 내용 — `{이름}` 을 관측의 값으로 바꿔 넣는다.
|
|
33
|
+
*
|
|
34
|
+
* 바꿀 값은 `Attention.params` 와 `anchor` 에서 온다. 바꿀 것이 없으면 그 칸을 비우고, 요구하는 칸이
|
|
35
|
+
* 비면 판정이 거절한다 — 지어낸 값으로 현장 지시를 만들지 않는다.
|
|
36
|
+
*/
|
|
37
|
+
payloadTemplate?: any;
|
|
38
|
+
/**
|
|
39
|
+
* **한 번 낸 근거에 다시 내기까지 기다리는 시간(ms).**
|
|
40
|
+
*
|
|
41
|
+
* 비워 두면 기본값이 걸린다(한 시간). 0 으로 두면 기다리지 않는데, 그러면 승인하는 순간 같은 것이
|
|
42
|
+
* 다시 올라온다 — 걸어 보고 실제로 그 상태를 만들었다.
|
|
43
|
+
*/
|
|
44
|
+
cooldownMs?: number;
|
|
45
|
+
/** 지우지 않고 끌 수 있어야 한다 — 잘못 낸 것을 본 사람이 가장 먼저 하려는 일이다. */
|
|
46
|
+
enabled?: boolean;
|
|
47
|
+
lastRunAt?: string;
|
|
48
|
+
proposedCount?: number;
|
|
49
|
+
/** 마지막에 왜 안 올렸나 — 사람 말로. */
|
|
50
|
+
lastSkipReason?: string;
|
|
51
|
+
creator?: User;
|
|
52
|
+
updater?: User;
|
|
53
|
+
createdAt?: Date;
|
|
54
|
+
updatedAt?: Date;
|
|
55
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TwinActuationRule = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
/*
|
|
6
|
+
* **어느 관측에 어느 지시를 붙이는가** — 선언 한 행.
|
|
7
|
+
*
|
|
8
|
+
* ── 왜 표인가 (2026-09-01) ───────────────────────────────────────────────
|
|
9
|
+
* 규칙은 현장이 정한다. 「병목이 세 시간 넘으면 추가 생산을 올린다」는 그 공장의 정책이고, 코드에 박으면
|
|
10
|
+
* 공장마다 포크가 생긴다. 그리고 **끌 수 있어야 한다** — 규칙이 잘못 낸 것을 본 사람이 가장 먼저 하려는
|
|
11
|
+
* 일이 그것이다.
|
|
12
|
+
*
|
|
13
|
+
* ── 판정은 여기 없다 ─────────────────────────────────────────────────────
|
|
14
|
+
* 무엇을 올릴지는 `rule-evaluate.ts` 가 든다. 이 파일은 값을 담기만 한다 — 판정이 두 벌이 되면 화면과
|
|
15
|
+
* 서버가 다른 답을 낸다.
|
|
16
|
+
*
|
|
17
|
+
* ── 계약에 넣지 않았다 ───────────────────────────────────────────────────
|
|
18
|
+
* 이 규칙이 가리키는 두 값(`Attention.kind` · `CommandTypeSpec.type`)은 계약의 어휘다. 그러나 「그 둘을
|
|
19
|
+
* 어떻게 잇는가」는 이 호스트의 배선이다 — 커널은 규칙을 읽지 않고, 커널을 쓰는 다른 소비처가 이 표를
|
|
20
|
+
* 알 이유가 없다.
|
|
21
|
+
*/
|
|
22
|
+
const typeorm_1 = require("typeorm");
|
|
23
|
+
const type_graphql_1 = require("type-graphql");
|
|
24
|
+
const shell_1 = require("@things-factory/shell");
|
|
25
|
+
const auth_base_1 = require("@things-factory/auth-base");
|
|
26
|
+
let TwinActuationRule = class TwinActuationRule {
|
|
27
|
+
};
|
|
28
|
+
exports.TwinActuationRule = TwinActuationRule;
|
|
29
|
+
tslib_1.__decorate([
|
|
30
|
+
(0, typeorm_1.PrimaryGeneratedColumn)('uuid'),
|
|
31
|
+
(0, type_graphql_1.Field)(type => type_graphql_1.ID, { description: 'Unique identifier of the rule.' }),
|
|
32
|
+
tslib_1.__metadata("design:type", String)
|
|
33
|
+
], TwinActuationRule.prototype, "id", void 0);
|
|
34
|
+
tslib_1.__decorate([
|
|
35
|
+
(0, typeorm_1.ManyToOne)(type => shell_1.Domain),
|
|
36
|
+
(0, type_graphql_1.Field)(type => shell_1.Domain, { nullable: true, description: 'Tenant this rule belongs to.' }),
|
|
37
|
+
tslib_1.__metadata("design:type", shell_1.Domain)
|
|
38
|
+
], TwinActuationRule.prototype, "domain", void 0);
|
|
39
|
+
tslib_1.__decorate([
|
|
40
|
+
(0, typeorm_1.RelationId)((e) => e.domain),
|
|
41
|
+
tslib_1.__metadata("design:type", String)
|
|
42
|
+
], TwinActuationRule.prototype, "domainId", void 0);
|
|
43
|
+
tslib_1.__decorate([
|
|
44
|
+
(0, typeorm_1.Column)(),
|
|
45
|
+
(0, type_graphql_1.Field)({ description: 'Twin instance this rule applies to. There are no global rules.' }),
|
|
46
|
+
tslib_1.__metadata("design:type", String)
|
|
47
|
+
], TwinActuationRule.prototype, "instanceId", void 0);
|
|
48
|
+
tslib_1.__decorate([
|
|
49
|
+
(0, typeorm_1.Column)({ nullable: true }),
|
|
50
|
+
(0, type_graphql_1.Field)({ nullable: true, description: 'What a person calls this rule.' }),
|
|
51
|
+
tslib_1.__metadata("design:type", String)
|
|
52
|
+
], TwinActuationRule.prototype, "name", void 0);
|
|
53
|
+
tslib_1.__decorate([
|
|
54
|
+
(0, typeorm_1.Column)(),
|
|
55
|
+
(0, type_graphql_1.Field)({ description: 'Observed condition kind in kernel vocabulary (Attention.kind).' }),
|
|
56
|
+
tslib_1.__metadata("design:type", String)
|
|
57
|
+
], TwinActuationRule.prototype, "attentionKind", void 0);
|
|
58
|
+
tslib_1.__decorate([
|
|
59
|
+
(0, typeorm_1.Column)({ nullable: true }),
|
|
60
|
+
(0, type_graphql_1.Field)({ nullable: true, description: 'Only at this severity or above: low, medium, high, critical.' }),
|
|
61
|
+
tslib_1.__metadata("design:type", String)
|
|
62
|
+
], TwinActuationRule.prototype, "minSeverity", void 0);
|
|
63
|
+
tslib_1.__decorate([
|
|
64
|
+
(0, typeorm_1.Column)({ type: 'int', nullable: true }),
|
|
65
|
+
(0, type_graphql_1.Field)({ nullable: true, description: 'Propose only after the condition has held this long (ms).' }),
|
|
66
|
+
tslib_1.__metadata("design:type", Number)
|
|
67
|
+
], TwinActuationRule.prototype, "minHeldMs", void 0);
|
|
68
|
+
tslib_1.__decorate([
|
|
69
|
+
(0, typeorm_1.Column)(),
|
|
70
|
+
(0, type_graphql_1.Field)({ description: 'Declared command type to propose. Not a kernel stimulus.' }),
|
|
71
|
+
tslib_1.__metadata("design:type", String)
|
|
72
|
+
], TwinActuationRule.prototype, "commandType", void 0);
|
|
73
|
+
tslib_1.__decorate([
|
|
74
|
+
(0, typeorm_1.Column)({ type: 'simple-json', nullable: true }),
|
|
75
|
+
(0, type_graphql_1.Field)(type => shell_1.ScalarObject, { nullable: true, description: 'Command payload template. {name} is filled from the observation.' }),
|
|
76
|
+
tslib_1.__metadata("design:type", Object)
|
|
77
|
+
], TwinActuationRule.prototype, "payloadTemplate", void 0);
|
|
78
|
+
tslib_1.__decorate([
|
|
79
|
+
(0, typeorm_1.Column)({ type: 'int', nullable: true }),
|
|
80
|
+
(0, type_graphql_1.Field)({ nullable: true, description: 'Wait this long before proposing again for the same cause (ms).' }),
|
|
81
|
+
tslib_1.__metadata("design:type", Number)
|
|
82
|
+
], TwinActuationRule.prototype, "cooldownMs", void 0);
|
|
83
|
+
tslib_1.__decorate([
|
|
84
|
+
(0, typeorm_1.Column)({ default: true }),
|
|
85
|
+
(0, type_graphql_1.Field)({ nullable: true, description: 'A disabled rule is not evaluated. Turning off is not deleting.' }),
|
|
86
|
+
tslib_1.__metadata("design:type", Boolean)
|
|
87
|
+
], TwinActuationRule.prototype, "enabled", void 0);
|
|
88
|
+
tslib_1.__decorate([
|
|
89
|
+
(0, typeorm_1.Column)({ nullable: true }),
|
|
90
|
+
(0, type_graphql_1.Field)({ nullable: true, description: 'When this rule was last evaluated (ISO 8601).' }),
|
|
91
|
+
tslib_1.__metadata("design:type", String)
|
|
92
|
+
], TwinActuationRule.prototype, "lastRunAt", void 0);
|
|
93
|
+
tslib_1.__decorate([
|
|
94
|
+
(0, typeorm_1.Column)({ type: 'int', nullable: true }),
|
|
95
|
+
(0, type_graphql_1.Field)({ nullable: true, description: 'How many commands this rule has proposed in total.' }),
|
|
96
|
+
tslib_1.__metadata("design:type", Number)
|
|
97
|
+
], TwinActuationRule.prototype, "proposedCount", void 0);
|
|
98
|
+
tslib_1.__decorate([
|
|
99
|
+
(0, typeorm_1.Column)({ nullable: true }),
|
|
100
|
+
(0, type_graphql_1.Field)({ nullable: true, description: 'Why the last evaluation proposed nothing, when it did not.' }),
|
|
101
|
+
tslib_1.__metadata("design:type", String)
|
|
102
|
+
], TwinActuationRule.prototype, "lastSkipReason", void 0);
|
|
103
|
+
tslib_1.__decorate([
|
|
104
|
+
(0, typeorm_1.ManyToOne)(type => auth_base_1.User, { nullable: true }),
|
|
105
|
+
(0, type_graphql_1.Field)(type => auth_base_1.User, { nullable: true }),
|
|
106
|
+
tslib_1.__metadata("design:type", auth_base_1.User)
|
|
107
|
+
], TwinActuationRule.prototype, "creator", void 0);
|
|
108
|
+
tslib_1.__decorate([
|
|
109
|
+
(0, typeorm_1.ManyToOne)(type => auth_base_1.User, { nullable: true }),
|
|
110
|
+
(0, type_graphql_1.Field)(type => auth_base_1.User, { nullable: true }),
|
|
111
|
+
tslib_1.__metadata("design:type", auth_base_1.User)
|
|
112
|
+
], TwinActuationRule.prototype, "updater", void 0);
|
|
113
|
+
tslib_1.__decorate([
|
|
114
|
+
(0, typeorm_1.CreateDateColumn)(),
|
|
115
|
+
(0, type_graphql_1.Field)({ nullable: true }),
|
|
116
|
+
tslib_1.__metadata("design:type", Date)
|
|
117
|
+
], TwinActuationRule.prototype, "createdAt", void 0);
|
|
118
|
+
tslib_1.__decorate([
|
|
119
|
+
(0, typeorm_1.UpdateDateColumn)(),
|
|
120
|
+
(0, type_graphql_1.Field)({ nullable: true }),
|
|
121
|
+
tslib_1.__metadata("design:type", Date)
|
|
122
|
+
], TwinActuationRule.prototype, "updatedAt", void 0);
|
|
123
|
+
exports.TwinActuationRule = TwinActuationRule = tslib_1.__decorate([
|
|
124
|
+
(0, typeorm_1.Entity)()
|
|
125
|
+
/* 판정이 이 두 칸으로 묻는다 — 이 테넌트의 · 이 트윈의 규칙. */
|
|
126
|
+
,
|
|
127
|
+
(0, typeorm_1.Index)('ix_twin_actuation_rule_0', (e) => [e.domain, e.instanceId]),
|
|
128
|
+
(0, type_graphql_1.ObjectType)({ description: 'One rule mapping an observed condition to a field instruction that needs approval.' })
|
|
129
|
+
], TwinActuationRule);
|
|
130
|
+
//# sourceMappingURL=actuation-rule.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"actuation-rule.js","sourceRoot":"","sources":["../../../server/service/actuation/actuation-rule.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * **어느 관측에 어느 지시를 붙이는가** — 선언 한 행.\n *\n * ── 왜 표인가 (2026-09-01) ───────────────────────────────────────────────\n * 규칙은 현장이 정한다. 「병목이 세 시간 넘으면 추가 생산을 올린다」는 그 공장의 정책이고, 코드에 박으면\n * 공장마다 포크가 생긴다. 그리고 **끌 수 있어야 한다** — 규칙이 잘못 낸 것을 본 사람이 가장 먼저 하려는\n * 일이 그것이다.\n *\n * ── 판정은 여기 없다 ─────────────────────────────────────────────────────\n * 무엇을 올릴지는 `rule-evaluate.ts` 가 든다. 이 파일은 값을 담기만 한다 — 판정이 두 벌이 되면 화면과\n * 서버가 다른 답을 낸다.\n *\n * ── 계약에 넣지 않았다 ───────────────────────────────────────────────────\n * 이 규칙이 가리키는 두 값(`Attention.kind` · `CommandTypeSpec.type`)은 계약의 어휘다. 그러나 「그 둘을\n * 어떻게 잇는가」는 이 호스트의 배선이다 — 커널은 규칙을 읽지 않고, 커널을 쓰는 다른 소비처가 이 표를\n * 알 이유가 없다.\n */\nimport { Column, CreateDateColumn, Entity, Index, ManyToOne, PrimaryGeneratedColumn, RelationId, UpdateDateColumn } from 'typeorm'\nimport { Field, ID, ObjectType } from 'type-graphql'\n\nimport { Domain, ScalarObject } from '@things-factory/shell'\nimport { User } from '@things-factory/auth-base'\n\n@Entity()\n/* 판정이 이 두 칸으로 묻는다 — 이 테넌트의 · 이 트윈의 규칙. */\n@Index('ix_twin_actuation_rule_0', (e: TwinActuationRule) => [e.domain, e.instanceId])\n@ObjectType({ description: 'One rule mapping an observed condition to a field instruction that needs approval.' })\nexport class TwinActuationRule {\n @PrimaryGeneratedColumn('uuid')\n @Field(type => ID, { description: 'Unique identifier of the rule.' })\n readonly id: string\n\n @ManyToOne(type => Domain)\n @Field(type => Domain, { nullable: true, description: 'Tenant this rule belongs to.' })\n domain?: Domain\n\n @RelationId((e: TwinActuationRule) => e.domain)\n domainId?: string\n\n /**\n * **어느 트윈인가** — 전역 규칙을 두지 않는다.\n *\n * 라인이 공장마다 다르므로 한 규칙이 모든 트윈에 걸리면 어느 공장에서는 반드시 틀린다. 트윈을 지목하면\n * 그 사실이 행에 드러나고, 화면이 「이 공장에 걸린 규칙」을 그대로 보여 줄 수 있다.\n */\n @Column()\n @Field({ description: 'Twin instance this rule applies to. There are no global rules.' })\n instanceId: string\n\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'What a person calls this rule.' })\n name?: string\n\n /** 어느 관측인가 — 커널의 안정 코드(`Attention.kind`). */\n @Column()\n @Field({ description: 'Observed condition kind in kernel vocabulary (Attention.kind).' })\n attentionKind: string\n\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'Only at this severity or above: low, medium, high, critical.' })\n minSeverity?: string\n\n /**\n * **조건이 이만큼 이어졌을 때** 올린다.\n *\n * 없으면 처음 본 주기에 올린다. 깜빡이는 조건에 현장 지시가 나가는 것을 막는 유일한 칸이다.\n */\n @Column({ type: 'int', nullable: true })\n @Field({ nullable: true, description: 'Propose only after the condition has held this long (ms).' })\n minHeldMs?: number\n\n /**\n * 무엇을 내는가 — **선언된 조치 종류**(`CommandTypeSpec.type`).\n *\n * 커널 자극(`CMD`)이 아니다. 자극을 적으면 판정이 「선언되지 않은 조치 종류」로 거절한다 — 그 값은\n * 모델 상태를 바꾸는 것이고 현장에 나가는 지시가 아니다.\n */\n @Column()\n @Field({ description: 'Declared command type to propose. Not a kernel stimulus.' })\n commandType: string\n\n /**\n * 지시의 내용 — `{이름}` 을 관측의 값으로 바꿔 넣는다.\n *\n * 바꿀 값은 `Attention.params` 와 `anchor` 에서 온다. 바꿀 것이 없으면 그 칸을 비우고, 요구하는 칸이\n * 비면 판정이 거절한다 — 지어낸 값으로 현장 지시를 만들지 않는다.\n */\n @Column({ type: 'simple-json', nullable: true })\n @Field(type => ScalarObject, { nullable: true, description: 'Command payload template. {name} is filled from the observation.' })\n payloadTemplate?: any\n\n /**\n * **한 번 낸 근거에 다시 내기까지 기다리는 시간(ms).**\n *\n * 비워 두면 기본값이 걸린다(한 시간). 0 으로 두면 기다리지 않는데, 그러면 승인하는 순간 같은 것이\n * 다시 올라온다 — 걸어 보고 실제로 그 상태를 만들었다.\n */\n @Column({ type: 'int', nullable: true })\n @Field({ nullable: true, description: 'Wait this long before proposing again for the same cause (ms).' })\n cooldownMs?: number\n\n /** 지우지 않고 끌 수 있어야 한다 — 잘못 낸 것을 본 사람이 가장 먼저 하려는 일이다. */\n @Column({ default: true })\n @Field({ nullable: true, description: 'A disabled rule is not evaluated. Turning off is not deleting.' })\n enabled?: boolean\n\n /* ── 마지막 판정의 기록 ─────────────────────────────────────────────────\n * 규칙이 **아무것도 올리지 않을 때** 사람이 알 수 있어야 한다. 조용히 안 도는 규칙과 조건이 없어서 안\n * 올리는 규칙은 화면에서 똑같이 보이고, 그 둘을 구별하지 못하면 사람은 규칙이 도는 줄 안다.\n */\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'When this rule was last evaluated (ISO 8601).' })\n lastRunAt?: string\n\n @Column({ type: 'int', nullable: true })\n @Field({ nullable: true, description: 'How many commands this rule has proposed in total.' })\n proposedCount?: number\n\n /** 마지막에 왜 안 올렸나 — 사람 말로. */\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'Why the last evaluation proposed nothing, when it did not.' })\n lastSkipReason?: string\n\n @ManyToOne(type => User, { nullable: true })\n @Field(type => User, { nullable: true })\n creator?: User\n\n @ManyToOne(type => User, { nullable: true })\n @Field(type => User, { nullable: true })\n updater?: User\n\n @CreateDateColumn()\n @Field({ nullable: true })\n createdAt?: Date\n\n @UpdateDateColumn()\n @Field({ nullable: true })\n updatedAt?: Date\n}\n"]}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { StoredCommand } from './command-dispatcher.js';
|
|
2
|
+
/** 결재선에 올리는 자리 — 앱이 구현한다. */
|
|
3
|
+
export interface ApprovalGateway {
|
|
4
|
+
/** 화면과 오류 문장에 쓰는 이름. 「어디서 승인하는가」를 사람에게 말해야 한다. */
|
|
5
|
+
label: string;
|
|
6
|
+
/**
|
|
7
|
+
* 이 커맨드를 결재선에 올린다.
|
|
8
|
+
*
|
|
9
|
+
* 돌려주는 것은 **올라간 자리의 식별자**다. 이 값이 커맨드 행에 적히고, 그 뒤로는 직접 결정이 막힌다
|
|
10
|
+
* — 문이 두 벌이면 한쪽 기록에만 남는 승인이 생긴다.
|
|
11
|
+
*
|
|
12
|
+
* `context` 는 리졸버 문맥 그대로 넘긴다(트랜잭션·로그인한 사람·테넌트). 이 패키지가 그 모양을 알면
|
|
13
|
+
* 앱의 인증 구조를 여기서 다시 정의하게 된다.
|
|
14
|
+
*/
|
|
15
|
+
request(command: StoredCommand, context: any): Promise<string>;
|
|
16
|
+
}
|
|
17
|
+
/** 앱이 부팅에서 한 번 부른다. */
|
|
18
|
+
export declare function setApprovalGateway(next: ApprovalGateway): void;
|
|
19
|
+
/** 등록된 것이 없으면 `undefined` — 없음을 없음으로 답한다. */
|
|
20
|
+
export declare function approvalGateway(): ApprovalGateway | undefined;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.setApprovalGateway = setApprovalGateway;
|
|
4
|
+
exports.approvalGateway = approvalGateway;
|
|
5
|
+
let gateway;
|
|
6
|
+
/** 앱이 부팅에서 한 번 부른다. */
|
|
7
|
+
function setApprovalGateway(next) {
|
|
8
|
+
gateway = next;
|
|
9
|
+
}
|
|
10
|
+
/** 등록된 것이 없으면 `undefined` — 없음을 없음으로 답한다. */
|
|
11
|
+
function approvalGateway() {
|
|
12
|
+
return gateway;
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=approval-gateway.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"approval-gateway.js","sourceRoot":"","sources":["../../../server/service/actuation/approval-gateway.ts"],"names":[],"mappings":";;AAoCA,gDAEC;AAGD,0CAEC;AAVD,IAAI,OAAoC,CAAA;AAExC,uBAAuB;AACvB,SAAgB,kBAAkB,CAAC,IAAqB;IACtD,OAAO,GAAG,IAAI,CAAA;AAChB,CAAC;AAED,6CAA6C;AAC7C,SAAgB,eAAe;IAC7B,OAAO,OAAO,CAAA;AAChB,CAAC","sourcesContent":["/*\n * **승인을 누가 받는가** — 그 자리를 밖으로 낸다.\n *\n * ── 왜 시임인가 (2026-08-31) ─────────────────────────────────────────────\n * 승인은 이미 이 저장소에 있다. `@things-factory/worklist` 가 결재선·위임·상신·「내가 승인할 것」 목록과\n * 승인 화면까지 들고 있다. 그것을 다시 만들면 두 벌이 되고, 사고 뒤에 「어느 쪽 기록이 진짜인가」를\n * 묻게 된다.\n *\n * 그런데 headless-twin 은 worklist 를 부를 수 없다 — worklist 는 조직도(Employee·Department·Role)와\n * 화면 27쪽을 데리고 오는 업무 모듈이고, 이 패키지는 커널을 안는 헤드리스 서버다. 의존을 그 방향으로\n * 걸면 커널을 쓰는 모든 배포가 조직도를 지게 된다.\n *\n * 그래서 **이 패키지는 자리만 만들고, 앱이 채운다**(operato-twin). 채우지 않은 배포에서는 게이트가\n * 사라지지 않는다 — 승인 없이 지나가는 것이 아니라, 아래 §`twinCommands` 의 직접 결정이 유일한 문이 된다.\n */\nimport type { StoredCommand } from './command-dispatcher.js'\n\n/** 결재선에 올리는 자리 — 앱이 구현한다. */\nexport interface ApprovalGateway {\n /** 화면과 오류 문장에 쓰는 이름. 「어디서 승인하는가」를 사람에게 말해야 한다. */\n label: string\n /**\n * 이 커맨드를 결재선에 올린다.\n *\n * 돌려주는 것은 **올라간 자리의 식별자**다. 이 값이 커맨드 행에 적히고, 그 뒤로는 직접 결정이 막힌다\n * — 문이 두 벌이면 한쪽 기록에만 남는 승인이 생긴다.\n *\n * `context` 는 리졸버 문맥 그대로 넘긴다(트랜잭션·로그인한 사람·테넌트). 이 패키지가 그 모양을 알면\n * 앱의 인증 구조를 여기서 다시 정의하게 된다.\n */\n request(command: StoredCommand, context: any): Promise<string>\n}\n\nlet gateway: ApprovalGateway | undefined\n\n/** 앱이 부팅에서 한 번 부른다. */\nexport function setApprovalGateway(next: ApprovalGateway): void {\n gateway = next\n}\n\n/** 등록된 것이 없으면 `undefined` — 없음을 없음으로 답한다. */\nexport function approvalGateway(): ApprovalGateway | undefined {\n return gateway\n}\n"]}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { CommandStore } from './command-dispatcher.js';
|
|
2
|
+
export interface CatchUpResult {
|
|
3
|
+
/** 효과를 기다리던 조치 수. */
|
|
4
|
+
awaiting: number;
|
|
5
|
+
/** 이번에 효과를 적은 조치 수. */
|
|
6
|
+
recorded: number;
|
|
7
|
+
/** 저널에 보고가 아직 없던 조치 수 — 저쪽이 아직 아무 말도 안 한 것이다. */
|
|
8
|
+
silent: number;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* 이 도메인에서 놓친 보고를 한 번 backfill 한다.
|
|
12
|
+
*
|
|
13
|
+
* **던지지 않는다.** 주기에서 부르고, 여기서 터지면 그 주기가 멈춘다. 그리고 이 일은 다음 바퀴에
|
|
14
|
+
* 다시 해도 같은 결과가 나온다 — 급할 것이 없다.
|
|
15
|
+
*/
|
|
16
|
+
export declare function catchUpEffects(domainId: string, store: CommandStore): Promise<CatchUpResult>;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.catchUpEffects = catchUpEffects;
|
|
4
|
+
/*
|
|
5
|
+
* **놓친 보고를 backfill 한다** — 유입 자리에서 잇는 길(§`settle-effects.ts`)의 짝.
|
|
6
|
+
*
|
|
7
|
+
* ── 왜 필요한가 (2026-09-02, 실제로 하나 놓쳤다) ──────────────────────────────
|
|
8
|
+
* 유입 자리에서만 이으면, **우리가 못 듣고 있던 동안 온 보고는 영원히 사라진다.**
|
|
9
|
+
*
|
|
10
|
+
* 10:13 MES 가 WO-20260902-005 를 현장에 냄. order.status 가 트윈에 도착
|
|
11
|
+
* 19:21 잇는 배선을 산출물에 넣음
|
|
12
|
+
* → 그 보고는 저널에 남았는데 조치에는 안 이어졌다. 다시 오지 않는다
|
|
13
|
+
*
|
|
14
|
+
* 같은 일이 트윈이 내려가 있는 사이, 커넥터를 고치는 사이, 프로세스가 죽었다 서는 사이에 난다.
|
|
15
|
+
* 관측은 되풀이해 주지 않는다.
|
|
16
|
+
*
|
|
17
|
+
* ── 왜 저널을 읽나 ───────────────────────────────────────────────────────────
|
|
18
|
+
* 저널에 `orderId` 가 **컬럼으로** 있고 인덱스도 있다(`ix_twin_event_4 (domain, instanceId, orderId)`
|
|
19
|
+
* — 「이 오더가 어디까지 갔나」). 그래서 「우리가 낸 지시들의 마지막 보고」를 값싸게 물을 수 있다.
|
|
20
|
+
* JSON 안을 뒤지지 않으므로 드라이버 다섯에서 같이 돈다.
|
|
21
|
+
*
|
|
22
|
+
* ── 왜 방향이 반대인가 ───────────────────────────────────────────────────────
|
|
23
|
+
* 유입 쪽은 **보고에서 조치를 찾는다**(보고가 많고 조치가 적다 — 그래서 식별자를 모아 한 번 묻는다).
|
|
24
|
+
* 이쪽은 **조치에서 보고를 찾는다.** 효과를 기다리는 조치는 몇 건이고, 그 몇 건의 식별자로 저널을
|
|
25
|
+
* 물으면 끝난다. 저널 전체를 주사하지 않는다.
|
|
26
|
+
*/
|
|
27
|
+
const ops_contract_1 = require("@operato/ops-contract");
|
|
28
|
+
const shell_1 = require("@things-factory/shell");
|
|
29
|
+
const log_js_1 = require("../../engine/log.js");
|
|
30
|
+
const twin_event_js_1 = require("../twin-event/twin-event.js");
|
|
31
|
+
const effect_match_js_1 = require("./effect-match.js");
|
|
32
|
+
/**
|
|
33
|
+
* 이 도메인에서 놓친 보고를 한 번 backfill 한다.
|
|
34
|
+
*
|
|
35
|
+
* **던지지 않는다.** 주기에서 부르고, 여기서 터지면 그 주기가 멈춘다. 그리고 이 일은 다음 바퀴에
|
|
36
|
+
* 다시 해도 같은 결과가 나온다 — 급할 것이 없다.
|
|
37
|
+
*/
|
|
38
|
+
async function catchUpEffects(domainId, store) {
|
|
39
|
+
const none = { awaiting: 0, recorded: 0, silent: 0 };
|
|
40
|
+
if (!store.awaitingEffect || !store.recordEffects) {
|
|
41
|
+
(0, log_js_1.twinWarn)('[actuation] 놓친 보고를 backfill 할 수 없다 — 저장소에 awaitingEffect 나 recordEffects 가 없다. ' +
|
|
42
|
+
'못 듣고 있던 동안 온 보고는 영원히 이어지지 않는다.');
|
|
43
|
+
return none;
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
const commands = await store.awaitingEffect(domainId);
|
|
47
|
+
if (!commands.length)
|
|
48
|
+
return none;
|
|
49
|
+
/* 트윈마다 묶는다 — 저널 질의가 트윈 단위로 인덱스를 탄다. */
|
|
50
|
+
const byTwin = new Map();
|
|
51
|
+
for (const c of commands) {
|
|
52
|
+
if (!c.dispatchRef)
|
|
53
|
+
continue;
|
|
54
|
+
const list = byTwin.get(c.instanceId) ?? [];
|
|
55
|
+
list.push(c);
|
|
56
|
+
byTwin.set(c.instanceId, list);
|
|
57
|
+
}
|
|
58
|
+
let recorded = 0;
|
|
59
|
+
let silent = 0;
|
|
60
|
+
for (const [instanceId, list] of byTwin) {
|
|
61
|
+
const observations = await latestOrderReports(domainId, instanceId, list.map(c => c.dispatchRef));
|
|
62
|
+
const seen = new Set(observations.map(o => o.orderId));
|
|
63
|
+
silent += list.filter(c => !seen.has(c.dispatchRef)).length;
|
|
64
|
+
const { updates } = (0, effect_match_js_1.matchEffects)({
|
|
65
|
+
observations,
|
|
66
|
+
byRef: new Map(list.map(c => [c.dispatchRef, { id: c.id, dispatchRef: c.dispatchRef, ...(c.effect ? { effect: c.effect } : {}) }]))
|
|
67
|
+
});
|
|
68
|
+
/*
|
|
69
|
+
* **이미 적힌 것과 같으면 적지 않는다.** 주기마다 부르므로, 안 걸러 내면 같은 보고로 `seen` 이
|
|
70
|
+
* 영원히 는다 — 「몇 번 보고됐나」가 「몇 번 backfill 했나」가 된다.
|
|
71
|
+
*/
|
|
72
|
+
const changed = updates.filter(u => {
|
|
73
|
+
const before = list.find(c => c.id === u.commandId)?.effect;
|
|
74
|
+
return !before || before.status !== u.status || before.at !== u.at;
|
|
75
|
+
});
|
|
76
|
+
if (changed.length) {
|
|
77
|
+
await store.recordEffects(domainId, changed);
|
|
78
|
+
recorded += changed.length;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return { awaiting: commands.length, recorded, silent };
|
|
82
|
+
}
|
|
83
|
+
catch (e) {
|
|
84
|
+
(0, log_js_1.twinWarn)(`[actuation] 놓친 보고를 backfill하다 실패했다 — ${String(e?.message ?? e).slice(0, 200)}`);
|
|
85
|
+
return none;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* 이 트윈에서 그 오더들의 **마지막** 상태 보고를 읽는다.
|
|
90
|
+
*
|
|
91
|
+
* ── 왜 오더마다 따로 묻나 ──────────────────────────────────────────────────
|
|
92
|
+
* 한 번에 묶어 물으면 그 오더들의 보고를 **전부** 받게 된다. 오더 하나가 수백 번 보고될 수 있고
|
|
93
|
+
* (초안 → 나감 → 진행 → 완료, 그리고 수량이 바뀔 때마다), 그러면 상한 없이 저널을 읽는 자리가 된다.
|
|
94
|
+
*
|
|
95
|
+
* 그래서 오더마다 `take: 1` 로 최신 하나만 읽는다. 질의 수가 조치 수만큼 늘지만, **효과를 기다리는
|
|
96
|
+
* 조치는 몇 건**이다(넘긴 것만, 그것도 식별자가 있는 것만). 몇 건에 몇 질의는 값이 아니고, 상한 없이
|
|
97
|
+
* 원장을 읽는 것은 값이다.
|
|
98
|
+
*
|
|
99
|
+
* `revision` 내림차순이 「나중」의 정본이다 — 시각이 같은 보고가 여럿 올 수 있고(같은 밀리초에 둘이
|
|
100
|
+
* 실제로 있었다), 그때 무엇이 나중인지는 원장의 순서가 답한다.
|
|
101
|
+
*/
|
|
102
|
+
async function latestOrderReports(domainId, instanceId, refs) {
|
|
103
|
+
const out = [];
|
|
104
|
+
const repo = (0, shell_1.getRepository)(twin_event_js_1.TwinEvent);
|
|
105
|
+
for (const orderId of new Set(refs)) {
|
|
106
|
+
const rows = await repo.find({
|
|
107
|
+
where: { domain: { id: domainId }, instanceId, eventType: ops_contract_1.OP_EVENT.order, orderId },
|
|
108
|
+
order: { revision: 'DESC' },
|
|
109
|
+
take: 1
|
|
110
|
+
});
|
|
111
|
+
const row = rows[0];
|
|
112
|
+
const data = row?.payload?.data;
|
|
113
|
+
if (!data)
|
|
114
|
+
continue;
|
|
115
|
+
/* 봉투에 적힌 시각을 쓴다 — 저장 시각이 아니다. 없으면 판정이 거른다. */
|
|
116
|
+
out.push({ orderId, status: String(data.status ?? ''), at: row.payload?.eventTime });
|
|
117
|
+
}
|
|
118
|
+
return out;
|
|
119
|
+
}
|
|
120
|
+
//# sourceMappingURL=catch-up-effects.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"catch-up-effects.js","sourceRoot":"","sources":["../../../server/service/actuation/catch-up-effects.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * **놓친 보고를 backfill 한다** — 유입 자리에서 잇는 길(§`settle-effects.ts`)의 짝.\n *\n * ── 왜 필요한가 (2026-09-02, 실제로 하나 놓쳤다) ──────────────────────────────\n * 유입 자리에서만 이으면, **우리가 못 듣고 있던 동안 온 보고는 영원히 사라진다.**\n *\n * 10:13 MES 가 WO-20260902-005 를 현장에 냄. order.status 가 트윈에 도착\n * 19:21 잇는 배선을 산출물에 넣음\n * → 그 보고는 저널에 남았는데 조치에는 안 이어졌다. 다시 오지 않는다\n *\n * 같은 일이 트윈이 내려가 있는 사이, 커넥터를 고치는 사이, 프로세스가 죽었다 서는 사이에 난다.\n * 관측은 되풀이해 주지 않는다.\n *\n * ── 왜 저널을 읽나 ───────────────────────────────────────────────────────────\n * 저널에 `orderId` 가 **컬럼으로** 있고 인덱스도 있다(`ix_twin_event_4 (domain, instanceId, orderId)`\n * — 「이 오더가 어디까지 갔나」). 그래서 「우리가 낸 지시들의 마지막 보고」를 값싸게 물을 수 있다.\n * JSON 안을 뒤지지 않으므로 드라이버 다섯에서 같이 돈다.\n *\n * ── 왜 방향이 반대인가 ───────────────────────────────────────────────────────\n * 유입 쪽은 **보고에서 조치를 찾는다**(보고가 많고 조치가 적다 — 그래서 식별자를 모아 한 번 묻는다).\n * 이쪽은 **조치에서 보고를 찾는다.** 효과를 기다리는 조치는 몇 건이고, 그 몇 건의 식별자로 저널을\n * 물으면 끝난다. 저널 전체를 주사하지 않는다.\n */\nimport { OP_EVENT } from '@operato/ops-contract'\n\nimport { getRepository } from '@things-factory/shell'\n\nimport { twinWarn } from '../../engine/log.js'\nimport { TwinEvent } from '../twin-event/twin-event.js'\n\nimport type { CommandStore, StoredCommand } from './command-dispatcher.js'\nimport { matchEffects, type OrderObservation } from './effect-match.js'\n\nexport interface CatchUpResult {\n /** 효과를 기다리던 조치 수. */\n awaiting: number\n /** 이번에 효과를 적은 조치 수. */\n recorded: number\n /** 저널에 보고가 아직 없던 조치 수 — 저쪽이 아직 아무 말도 안 한 것이다. */\n silent: number\n}\n\n/**\n * 이 도메인에서 놓친 보고를 한 번 backfill 한다.\n *\n * **던지지 않는다.** 주기에서 부르고, 여기서 터지면 그 주기가 멈춘다. 그리고 이 일은 다음 바퀴에\n * 다시 해도 같은 결과가 나온다 — 급할 것이 없다.\n */\nexport async function catchUpEffects(domainId: string, store: CommandStore): Promise<CatchUpResult> {\n const none: CatchUpResult = { awaiting: 0, recorded: 0, silent: 0 }\n\n if (!store.awaitingEffect || !store.recordEffects) {\n twinWarn(\n '[actuation] 놓친 보고를 backfill 할 수 없다 — 저장소에 awaitingEffect 나 recordEffects 가 없다. ' +\n '못 듣고 있던 동안 온 보고는 영원히 이어지지 않는다.'\n )\n return none\n }\n\n try {\n const commands = await store.awaitingEffect(domainId)\n if (!commands.length) return none\n\n /* 트윈마다 묶는다 — 저널 질의가 트윈 단위로 인덱스를 탄다. */\n const byTwin = new Map<string, StoredCommand[]>()\n for (const c of commands) {\n if (!c.dispatchRef) continue\n const list = byTwin.get(c.instanceId) ?? []\n list.push(c)\n byTwin.set(c.instanceId, list)\n }\n\n let recorded = 0\n let silent = 0\n\n for (const [instanceId, list] of byTwin) {\n const observations = await latestOrderReports(\n domainId,\n instanceId,\n list.map(c => c.dispatchRef!)\n )\n\n const seen = new Set(observations.map(o => o.orderId))\n silent += list.filter(c => !seen.has(c.dispatchRef!)).length\n\n const { updates } = matchEffects({\n observations,\n byRef: new Map(\n list.map(c => [c.dispatchRef!, { id: c.id, dispatchRef: c.dispatchRef!, ...(c.effect ? { effect: c.effect } : {}) }])\n )\n })\n\n /*\n * **이미 적힌 것과 같으면 적지 않는다.** 주기마다 부르므로, 안 걸러 내면 같은 보고로 `seen` 이\n * 영원히 는다 — 「몇 번 보고됐나」가 「몇 번 backfill 했나」가 된다.\n */\n const changed = updates.filter(u => {\n const before = list.find(c => c.id === u.commandId)?.effect\n return !before || before.status !== u.status || before.at !== u.at\n })\n\n if (changed.length) {\n await store.recordEffects(domainId, changed)\n recorded += changed.length\n }\n }\n\n return { awaiting: commands.length, recorded, silent }\n } catch (e: any) {\n twinWarn(`[actuation] 놓친 보고를 backfill하다 실패했다 — ${String(e?.message ?? e).slice(0, 200)}`)\n return none\n }\n}\n\n/**\n * 이 트윈에서 그 오더들의 **마지막** 상태 보고를 읽는다.\n *\n * ── 왜 오더마다 따로 묻나 ──────────────────────────────────────────────────\n * 한 번에 묶어 물으면 그 오더들의 보고를 **전부** 받게 된다. 오더 하나가 수백 번 보고될 수 있고\n * (초안 → 나감 → 진행 → 완료, 그리고 수량이 바뀔 때마다), 그러면 상한 없이 저널을 읽는 자리가 된다.\n *\n * 그래서 오더마다 `take: 1` 로 최신 하나만 읽는다. 질의 수가 조치 수만큼 늘지만, **효과를 기다리는\n * 조치는 몇 건**이다(넘긴 것만, 그것도 식별자가 있는 것만). 몇 건에 몇 질의는 값이 아니고, 상한 없이\n * 원장을 읽는 것은 값이다.\n *\n * `revision` 내림차순이 「나중」의 정본이다 — 시각이 같은 보고가 여럿 올 수 있고(같은 밀리초에 둘이\n * 실제로 있었다), 그때 무엇이 나중인지는 원장의 순서가 답한다.\n */\nasync function latestOrderReports(\n domainId: string,\n instanceId: string,\n refs: readonly string[]\n): Promise<OrderObservation[]> {\n const out: OrderObservation[] = []\n const repo = getRepository(TwinEvent)\n\n for (const orderId of new Set(refs)) {\n const rows = await repo.find({\n where: { domain: { id: domainId }, instanceId, eventType: OP_EVENT.order, orderId },\n order: { revision: 'DESC' },\n take: 1\n } as any)\n\n const row: any = rows[0]\n const data = row?.payload?.data\n if (!data) continue\n\n /* 봉투에 적힌 시각을 쓴다 — 저장 시각이 아니다. 없으면 판정이 거른다. */\n out.push({ orderId, status: String(data.status ?? ''), at: row.payload?.eventTime })\n }\n return out\n}\n"]}
|