@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
package/server/service/index.ts
CHANGED
|
@@ -12,8 +12,10 @@ export * from './twin-state/twin-state-subscription.js'
|
|
|
12
12
|
export * from './reference/reference-master.js'
|
|
13
13
|
export * from './reference/reference-registry.js'
|
|
14
14
|
export * from './reference/reference-adapter.js'
|
|
15
|
+
export * from './reference/live-cadence.js'
|
|
15
16
|
export * from './reference/reference-live.js'
|
|
16
17
|
export * from './reference/twin-reference.js'
|
|
18
|
+
export * from './reference/connection-portability.js'
|
|
17
19
|
export * from './reference/template-registry.js'
|
|
18
20
|
/* 커넥터 점검 하네스 — 실 원본에 붙여 무엇이 채워지고 무엇이 비는지 센다(앱을 띄우지 않는다). */
|
|
19
21
|
export * from './reference/reference-probe.js'
|
|
@@ -21,6 +23,11 @@ export * from './reference/reference-probe.js'
|
|
|
21
23
|
export * from './reference/reference-assessment.js'
|
|
22
24
|
/* 도메인 어휘 카탈로그 조합(커널 SSOT 노출) — 서버측 소비자(board-ai 도구 등)가 같은 형태를 쓰도록. */
|
|
23
25
|
export * from './twin-lifecycle/domain-catalog.js'
|
|
26
|
+
/*
|
|
27
|
+
* 조치 — 앱이 결재선을 채우고(§`setApprovalGateway`) 판정을 옮긴다(§`settleByApproval`). 이 줄이 없으면
|
|
28
|
+
* 자리는 있는데 앱이 닿을 길이 없다.
|
|
29
|
+
*/
|
|
30
|
+
export * from './actuation/index.js'
|
|
24
31
|
|
|
25
32
|
/* IMPORT ENTITIES AND RESOLVERS */
|
|
26
33
|
import { entities as TwinEventEntities } from './twin-event/index.js'
|
|
@@ -32,7 +39,7 @@ import { entities as TwinSpaceEntities, resolvers as TwinSpaceResolvers } from '
|
|
|
32
39
|
import { entities as TwinTargetEntities, resolvers as TwinTargetResolvers } from './twin-target/index.js'
|
|
33
40
|
import { entities as TwinReferenceEntities, resolvers as TwinReferenceResolvers } from './reference/index.js'
|
|
34
41
|
/* 현장에 내리는 조치 — 승인이 재기동을 넘어 살아야 하므로 표로 든다(§`actuation/twin-command.ts`). */
|
|
35
|
-
import { entities as TwinActuationEntities } from './actuation/index.js'
|
|
42
|
+
import { entities as TwinActuationEntities, resolvers as TwinActuationResolvers } from './actuation/index.js'
|
|
36
43
|
import { resolvers as TwinStateResolvers } from './twin-state/index.js'
|
|
37
44
|
import { resolvers as TwinControlResolvers } from './twin-control/index.js'
|
|
38
45
|
import { entities as TwinModelEntities, resolvers as TwinModelResolvers } from './twin-model/index.js'
|
|
@@ -84,6 +91,7 @@ export const schema = {
|
|
|
84
91
|
...TwinSpaceResolvers,
|
|
85
92
|
...TwinTargetResolvers,
|
|
86
93
|
...TwinReferenceResolvers,
|
|
94
|
+
...TwinActuationResolvers,
|
|
87
95
|
...TwinSubjectResolvers
|
|
88
96
|
],
|
|
89
97
|
directives: {}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* **승인된 조치가 원본으로 나가는 길** — 판단은 `actuation-target.ts`, 여기는 배선.
|
|
3
|
+
*
|
|
4
|
+
* ── 무엇이 끊겨 있었나 (2026-09-02) ────────────────────────────────────────────
|
|
5
|
+
* MES 커넥터는 `actuate` 를 다 만들어 두었다. 서명·접수 판정·초안 상태까지 있었다. 그런데 넘기는
|
|
6
|
+
* 리졸버가 **어댑터를 찾지도 않고** 「구현이 없습니다」라고 답했다. 그 문장을 사실로 읽어서 설계
|
|
7
|
+
* 문서에도, 다른 레인에도 그렇게 알렸다 — 둘 다 틀렸다.
|
|
8
|
+
*
|
|
9
|
+
* 자리는 있고 길이 없는 상태다. 이 저장소에서 가장 자주 나는 결함 부류이고, 조치 경로에서 나면
|
|
10
|
+
* 결과가 하나다 — **승인은 쌓이는데 현장에는 아무것도 가지 않는다.**
|
|
11
|
+
*
|
|
12
|
+
* ── 왜 구동과 따로 두나 ───────────────────────────────────────────────────────
|
|
13
|
+
* 모양이 같아서 한 함수로 묶고 싶어진다. 묶지 않는다.
|
|
14
|
+
*
|
|
15
|
+
* 구동은 시뮬레이터를 움직인다 틀리면 시늉이 어긋난다
|
|
16
|
+
* 조치는 현장에 작업지시를 낸다 틀리면 만들지 않아도 될 것을 만든다
|
|
17
|
+
*/
|
|
18
|
+
import { getRepository } from '@things-factory/shell'
|
|
19
|
+
|
|
20
|
+
import type { ApprovedCommand } from '@operato/ops-contract'
|
|
21
|
+
|
|
22
|
+
import { TwinInstance } from '../twin-instance/twin-instance.js'
|
|
23
|
+
|
|
24
|
+
import { TwinReference } from './twin-reference.js'
|
|
25
|
+
import { capabilitiesOf, getAdapter, type ActuationReadiness, type SiteDescriptor } from './reference-adapter.js'
|
|
26
|
+
import { APPROVAL_STANDS, actuationTarget, isActuationAllowed, type ActuationTarget } from './actuation-target.js'
|
|
27
|
+
|
|
28
|
+
/** 어댑터가 답하는 모양 그대로 — 디스패처가 이 값을 커맨드 행에 적는다. */
|
|
29
|
+
/**
|
|
30
|
+
* 조치를 낸 결과 — **어댑터가 답한 것을 좁히지 않고 그대로 나른다.**
|
|
31
|
+
*
|
|
32
|
+
* ── 왜 넓혔나 (2026-09-04, 인티그레이션 레인이 찾음) ────────────────────────
|
|
33
|
+
* 이 타입이 `ok`·`ref`·`error` 셋만 들고 있었다. 그래서 `routeActuation` 이 어댑터의 답을
|
|
34
|
+
* 그대로 되돌리는데 **타입은 `note`·`noteNext`·`retry` 를 버린다고 말했다.**
|
|
35
|
+
*
|
|
36
|
+
* 값은 흘렀다 — JS 객체가 키를 안 버리므로. 어제 `dispatch_note` 에 실제로 앉았다. 그런데
|
|
37
|
+
* **커넥터를 만드는 사람이 그 칸이 있다는 것을 알 방법이 없었다.** 우리가 어제 하루 종일
|
|
38
|
+
* 「자리는 있고 길이 없다」로 부른 것의 타입 판이다.
|
|
39
|
+
*
|
|
40
|
+
* 좁히지 않는다: 세 칸을 다 실어 `CommandAck` 와 같은 모양으로 둔다. 커널이 어느 칸을 어떻게
|
|
41
|
+
* 쓰는지는 `dispatch` 가 정하고, 이 자리는 나르기만 한다.
|
|
42
|
+
*/
|
|
43
|
+
export interface ActuationAck {
|
|
44
|
+
ok: boolean
|
|
45
|
+
/** 저쪽 문서·작업 식별자. 없으면 무엇을 되돌릴지 지목할 수 없다. */
|
|
46
|
+
ref?: string
|
|
47
|
+
/** 받아들였으나 저쪽이 덧붙인 말 — **실패가 아니다.** 원인 쪽 문장. */
|
|
48
|
+
note?: string
|
|
49
|
+
/** 그 말 중 다음에 할 일 한 줄 — 붙여 보내지 않는다. */
|
|
50
|
+
noteNext?: string
|
|
51
|
+
error?: string
|
|
52
|
+
/** 다시 해서 될 일인가 — 실패에만. 말하지 않으면 일꾼이 집지 않는다. */
|
|
53
|
+
retry?: 'again' | 'after-fix' | 'never'
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 저장소를 **한 번만** 읽고, 판정과 함께 부를 것까지 돌려준다.
|
|
58
|
+
*
|
|
59
|
+
* 판정한 뒤 보낼 때 다시 읽으면 그 사이에 연결 설정이 바뀔 수 있다 — 판정한 커넥터와 실제로 부르는
|
|
60
|
+
* 커넥터가 달라진다. 그리고 같은 행을 두 번 읽을 이유가 없다.
|
|
61
|
+
*/
|
|
62
|
+
async function resolve(domainId: string, instanceId: string) {
|
|
63
|
+
const inst = await getRepository(TwinInstance).findOne({ where: { domain: { id: domainId }, instanceId } })
|
|
64
|
+
const ref = inst
|
|
65
|
+
? await getRepository(TwinReference).findOne({ where: { domain: { id: domainId }, source: instanceId } })
|
|
66
|
+
: null
|
|
67
|
+
const adapter = ref ? getAdapter(ref.adapterType) : undefined
|
|
68
|
+
|
|
69
|
+
const target = actuationTarget({
|
|
70
|
+
instanceId,
|
|
71
|
+
twinExists: !!inst,
|
|
72
|
+
siteId: (inst?.origin as any)?.siteId,
|
|
73
|
+
...(ref ? { ref: { source: ref.source, adapterType: ref.adapterType } } : {}),
|
|
74
|
+
adapterRegistered: !!adapter,
|
|
75
|
+
capabilities: capabilitiesOf(adapter),
|
|
76
|
+
declaresActuate: !!adapter?.capabilities?.includes('actuate'),
|
|
77
|
+
hasActuateImpl: typeof adapter?.actuate === 'function'
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
return { target, ref, adapter }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 이 트윈의 원본에 승인된 조치를 보낸다.
|
|
85
|
+
*
|
|
86
|
+
* **거절할 때는 무엇이 없어서인지 말한다.** 「보내지 못했습니다」만 답하면 사람이 연결 설정을 열어야
|
|
87
|
+
* 하는지, 커넥터를 고쳐야 하는지, 트윈을 다시 만들어야 하는지 알 수 없다.
|
|
88
|
+
*/
|
|
89
|
+
export async function routeActuation(
|
|
90
|
+
domainId: string,
|
|
91
|
+
instanceId: string,
|
|
92
|
+
command: ApprovedCommand
|
|
93
|
+
): Promise<ActuationAck> {
|
|
94
|
+
const { target, ref, adapter } = await resolve(domainId, instanceId)
|
|
95
|
+
if (!isActuationAllowed(target)) return { ok: false, error: `${target.detail}. ${APPROVAL_STANDS}` }
|
|
96
|
+
|
|
97
|
+
/*
|
|
98
|
+
* 여기까지 왔으면 판정이 그 둘의 존재를 이미 확인했다(§`actuationTarget`). 그래도 조용히 지나가지
|
|
99
|
+
* 않는다 — 판정과 배선이 어긋나면 그것이 결함이고, `!` 로 덮으면 그 결함이 「아무 일도 일어나지
|
|
100
|
+
* 않았다」로 나타난다.
|
|
101
|
+
*/
|
|
102
|
+
if (!adapter?.actuate) {
|
|
103
|
+
return { ok: false, error: `커넥터를 찾았는데 조치를 보낼 자리가 없습니다 (${target.adapterType}). ${APPROVAL_STANDS}` }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const site: SiteDescriptor = { siteId: target.siteId }
|
|
107
|
+
return adapter.actuate(ref?.connectionConfig ?? {}, site, command)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* 이 트윈이 조치를 받을 수 있나 — **넘기기 전에 화면이 먼저 알 수 있게.**
|
|
112
|
+
*
|
|
113
|
+
* 승인 화면이 「넘기기」를 보이기 전에 이것을 묻는다. 누를 수 없는 것을 보여 주고 눌렀을 때 거절하는
|
|
114
|
+
* 것보다, 왜 못 넘기는지 먼저 말하는 것이 낫다.
|
|
115
|
+
*/
|
|
116
|
+
export async function actuationTargetOf(domainId: string, instanceId: string): Promise<ActuationTarget> {
|
|
117
|
+
return (await resolve(domainId, instanceId)).target
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* **저쪽이 받을 준비가 됐나** — 넘기기 전에 묻는다. 화면이 사람을 헛걸음시키지 않게.
|
|
122
|
+
*
|
|
123
|
+
* ── 왜 `actuationTargetOf` 와 따로인가 ──────────────────────────────────────
|
|
124
|
+
* 그쪽은 **망을 타지 않는다.** 화면이 목록을 그릴 때마다 부를 수 있어야 하므로 그래야 한다.
|
|
125
|
+
* 이 함수는 커넥터가 저쪽에 물을 수 있고, 그래서 부르는 시점이 다르다 — 사람이 「넘기기」를
|
|
126
|
+
* 열었을 때다.
|
|
127
|
+
*
|
|
128
|
+
* ── 답이 문이 아니다 ────────────────────────────────────────────────────────
|
|
129
|
+
* **이 답으로 넘김을 막지 않는다.** 막으면 점검이 틀렸거나 잠깐 못 닿은 사이에 성공할 수 있었던
|
|
130
|
+
* 조치가 못 나간다. 화면이 미리 말하는 데까지가 이 함수의 일이다.
|
|
131
|
+
*
|
|
132
|
+
* 커넥터가 이 얼굴을 안 만들었으면 **「모른다」로 답한다** — 「준비됐다」가 아니다. 없는 확인을
|
|
133
|
+
* 통과로 읽으면 이 함수가 있으나 없으나 같아진다.
|
|
134
|
+
*/
|
|
135
|
+
export async function actuationReadinessOf(domainId: string, instanceId: string): Promise<ActuationReadiness> {
|
|
136
|
+
const { target, ref, adapter } = await resolve(domainId, instanceId)
|
|
137
|
+
|
|
138
|
+
/* 보낼 대상 자체가 없으면 저쪽에 물을 것도 없다 — 그 사실을 그대로 옮긴다. */
|
|
139
|
+
if (!isActuationAllowed(target)) {
|
|
140
|
+
return { checked: 'local', ready: false, reason: target.detail, code: target.code }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (!adapter?.actuationReadiness) {
|
|
144
|
+
/* 확인할 길이 없다. 「준비됐다」로 답하지 않는다 — 그러면 화면이 없는 확인을 통과로 읽는다. */
|
|
145
|
+
return {
|
|
146
|
+
checked: 'local',
|
|
147
|
+
reason: `커넥터 "${target.adapterType}" 는 저쪽이 받을 준비가 됐는지 알려 주지 않습니다 — 넘겨 보고 알게 됩니다`,
|
|
148
|
+
code: 'not-checkable'
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
return await adapter.actuationReadiness(ref?.connectionConfig ?? {}, { siteId: target.siteId })
|
|
154
|
+
} catch (e: any) {
|
|
155
|
+
/*
|
|
156
|
+
* 점검이 터진 것을 「저쪽이 죽었다」로 옮기지 않는다 — 우리 점검의 실패다. 그리고 던지지 않는다:
|
|
157
|
+
* 이 함수 때문에 승인 화면이 안 열리면, 알려 주려던 것보다 큰 것을 잃는다.
|
|
158
|
+
*/
|
|
159
|
+
return {
|
|
160
|
+
checked: 'local',
|
|
161
|
+
reason: `준비 상태를 확인하다 실패했습니다 — ${String(e?.message ?? e).slice(0, 160)}`,
|
|
162
|
+
code: 'check-failed'
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* **조치를 어디로 보낼 수 있나** — 판단만. 저장소도 어댑터 호출도 여기 없다.
|
|
3
|
+
*
|
|
4
|
+
* 배선(§`actuation-routing.ts`)에서 갈라 두는 이유는 규칙 쪽과 같다(§`rule-evaluate.ts`). 이 파일을
|
|
5
|
+
* 부르는 시험이 TypeORM 데코레이터를 지나면 시험이 서지 않고, 그러면 **거절 판정을 아무 시험도 걷지
|
|
6
|
+
* 않는다.** 조치 경로에서 그것은 승인이 쌓이는데 현장에 아무것도 안 가는 상태로 나타난다.
|
|
7
|
+
*/
|
|
8
|
+
import type { ReferenceCapability } from './reference-adapter.js'
|
|
9
|
+
|
|
10
|
+
/** 판단에 필요한 것만 — 엔티티 전체를 받지 않는다. */
|
|
11
|
+
export interface ActuationTargetInput {
|
|
12
|
+
/** 어느 트윈인가 — **거절 문장이 이것을 말한다.** 「모르는 트윈입니다」만으로는 찾을 수 없다. */
|
|
13
|
+
instanceId: string
|
|
14
|
+
/** 트윈이 있나. 없으면 `undefined`. */
|
|
15
|
+
twinExists: boolean
|
|
16
|
+
/** 이 트윈이 어느 사이트에서 왔나 — `TwinInstance.origin.siteId`. */
|
|
17
|
+
siteId?: string
|
|
18
|
+
/** 연결된 원본. 없으면 `undefined`(시뮬레이션으로만 세운 트윈). */
|
|
19
|
+
ref?: { source: string; adapterType: string }
|
|
20
|
+
/** 커넥터가 등록되어 있나. */
|
|
21
|
+
adapterRegistered: boolean
|
|
22
|
+
/**
|
|
23
|
+
* 그 커넥터가 실제로 낼 수 있는 능력 — `capabilitiesOf` 가 판정한 값.
|
|
24
|
+
*
|
|
25
|
+
* **낼 수 있나는 이 값만 본다.** 아래 둘은 못 낼 때 무엇을 고쳐야 하는지 말하기 위한 것이고,
|
|
26
|
+
* 판정을 다시 만들지 않는다.
|
|
27
|
+
*/
|
|
28
|
+
capabilities: readonly ReferenceCapability[]
|
|
29
|
+
/** 커넥터가 `capabilities` 에 조치를 적었나. */
|
|
30
|
+
declaresActuate: boolean
|
|
31
|
+
/** 커넥터에 `actuate` 함수가 있나. */
|
|
32
|
+
hasActuateImpl: boolean
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** 보낼 수 있다 — 어디로까지 정해진 상태. */
|
|
36
|
+
export interface ActuationAllowed {
|
|
37
|
+
ok: true
|
|
38
|
+
siteId: string
|
|
39
|
+
adapterType: string
|
|
40
|
+
source: string
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** 보낼 수 없다 — 왜인지 코드와 문장으로. */
|
|
44
|
+
export interface ActuationRefused {
|
|
45
|
+
ok: false
|
|
46
|
+
code: ActuationRefusal
|
|
47
|
+
detail: string
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type ActuationTarget = ActuationAllowed | ActuationRefused
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 보낼 수 있나 — **타입 가드로 둔다.**
|
|
54
|
+
*
|
|
55
|
+
* 이 저장소는 기본 설정이 `strict: false` 이고(`packages/tsconfig-base.json`), 그 상태에서는 `ok` 가
|
|
56
|
+
* `true`·`false` 로 갈린 유니온이 `if (!t.ok)` 로 좁혀지지 않는다. 부르는 쪽마다 캐스팅을 넣으면 그
|
|
57
|
+
* 캐스팅이 언젠가 틀린 쪽을 가리킨다. 판정을 여기 한 자리에 둔다.
|
|
58
|
+
*/
|
|
59
|
+
export function isActuationAllowed(target: ActuationTarget): target is ActuationAllowed {
|
|
60
|
+
return target.ok === true
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* 왜 못 보내나 — **코드로 답한다.**
|
|
65
|
+
*
|
|
66
|
+
* 화면이 이 코드로 다음 할 일을 가른다. 문장만 주면 화면은 그것을 그대로 띄우는 것밖에 못 하고,
|
|
67
|
+
* 사람은 연결 설정을 열어야 하는지 커넥터를 고쳐야 하는지 모른다.
|
|
68
|
+
*/
|
|
69
|
+
export type ActuationRefusal =
|
|
70
|
+
| 'unknown-twin'
|
|
71
|
+
| 'no-source' /* 원본이 없는 트윈 — 보낼 곳이 없다 */
|
|
72
|
+
| 'no-adapter' /* 커넥터가 등록되지 않았다 */
|
|
73
|
+
| 'not-declared' /* 구현은 있는데 선언이 없다 — 한 줄 넣으면 된다 */
|
|
74
|
+
| 'not-implemented' /* 선언은 있는데 구현이 없다 — 만들다 만 커넥터다 */
|
|
75
|
+
| 'not-supported' /* 선언도 구현도 없다 — 이 커넥터는 관측만 낸다 */
|
|
76
|
+
| 'no-site' /* 어느 사이트로 낼지 모른다 */
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 보낼 수 있나 — 보낼 수 있으면 **어디로**까지 답한다.
|
|
80
|
+
*
|
|
81
|
+
* 순서가 뜻을 갖는다. 앞의 것이 없으면 뒤를 물을 수 없다 — 원본을 모르면 능력을 물을 데가 없다.
|
|
82
|
+
*/
|
|
83
|
+
export function actuationTarget(input: ActuationTargetInput): ActuationTarget {
|
|
84
|
+
if (!input.twinExists) {
|
|
85
|
+
return { ok: false, code: 'unknown-twin', detail: `모르는 트윈입니다 (${input.instanceId})` }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (!input.ref) {
|
|
89
|
+
/*
|
|
90
|
+
* 시뮬레이션으로만 세운 트윈이 이 경우다. 규칙 경로가 시뮬 트윈을 이미 막지만
|
|
91
|
+
* (§`rule-runner.ts`), 사람이 손으로 낸 조치는 그 문을 지나지 않는다. 막는 지점이 두 경로에
|
|
92
|
+
* 각각 있는 것이고, 같은 판정을 두 벌 만든 것이 아니다.
|
|
93
|
+
*/
|
|
94
|
+
return {
|
|
95
|
+
ok: false,
|
|
96
|
+
code: 'no-source',
|
|
97
|
+
detail: `트윈 "${input.instanceId}" 에는 연결된 원본이 없습니다 — 보낼 곳이 없습니다`
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (!input.adapterRegistered) {
|
|
102
|
+
return { ok: false, code: 'no-adapter', detail: `등록되지 않은 커넥터입니다 (${input.ref.adapterType})` }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!input.capabilities.includes('actuate')) {
|
|
106
|
+
/*
|
|
107
|
+
* **왜 못 내는지를 셋으로 가른다** — 고칠 것이 셋 다 다르다.
|
|
108
|
+
*
|
|
109
|
+
* 선언 없음 · 구현 있음 한 줄 넣으면 된다
|
|
110
|
+
* 선언 있음 · 구현 없음 만들다 만 커넥터다. 등록 검사는 이쪽을 말하지 않는다
|
|
111
|
+
* 선언도 구현도 없음 이 커넥터는 조치를 받지 않는다
|
|
112
|
+
*
|
|
113
|
+
* 뭉개면 한 줄로 끝날 일에 커넥터를 다시 만들려 들거나, 반대로 「관측만 냅니다」를 믿고 조치를
|
|
114
|
+
* 포기한다. 가운데 것을 「관측만 냅니다」로 답하는 것은 **사실이 아니다** — 그 사람은 조치를
|
|
115
|
+
* 하겠다고 적어 두었다.
|
|
116
|
+
*/
|
|
117
|
+
const { adapterType, source } = input.ref
|
|
118
|
+
if (input.hasActuateImpl && !input.declaresActuate) {
|
|
119
|
+
return {
|
|
120
|
+
ok: false,
|
|
121
|
+
code: 'not-declared',
|
|
122
|
+
detail:
|
|
123
|
+
`커넥터 "${adapterType}" 가 조치를 보내는 구현은 있는데 능력으로 선언하지 않았습니다 — ` +
|
|
124
|
+
"connector 의 capabilities 에 'actuate' 를 넣어야 합니다"
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (input.declaresActuate && !input.hasActuateImpl) {
|
|
128
|
+
return {
|
|
129
|
+
ok: false,
|
|
130
|
+
code: 'not-implemented',
|
|
131
|
+
detail:
|
|
132
|
+
`커넥터 "${adapterType}" 가 조치를 보낸다고 선언했는데 그 구현이 없습니다 — ` +
|
|
133
|
+
'actuate 를 만들거나 선언을 지워야 합니다'
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
ok: false,
|
|
138
|
+
code: 'not-supported',
|
|
139
|
+
detail: `원본 "${source}" (${adapterType}) 는 조치를 받지 않습니다 — 이 커넥터는 관측만 냅니다`
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/*
|
|
144
|
+
* 사이트를 **지어내지 않는다.** 원본 하나가 여러 사이트를 낼 수 있다(`discoverSites`).
|
|
145
|
+
*
|
|
146
|
+
* 구동(§`control-routing.ts`)은 사이트를 몰라도 넘긴다 — 시늉을 멈추는 일에는 대상이 하나다.
|
|
147
|
+
* 조치는 안 된다. 어느 공장에 낼지 모르는 작업지시를 첫 번째 사이트로 보내면 **잘못된 공장이
|
|
148
|
+
* 실제로 만든다.**
|
|
149
|
+
*/
|
|
150
|
+
const siteId = String(input.siteId ?? '').trim()
|
|
151
|
+
if (!siteId) {
|
|
152
|
+
return {
|
|
153
|
+
ok: false,
|
|
154
|
+
code: 'no-site',
|
|
155
|
+
detail: `트윈 "${input.instanceId}" 이 어느 사이트에서 왔는지 알 수 없습니다 — 조치를 보낼 대상을 정할 수 없습니다`
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return { ok: true, siteId, adapterType: input.ref.adapterType, source: input.ref.source }
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** 승인은 남는다는 것을 함께 말한다 — 원인을 고친 뒤 다시 넘길 수 있다. */
|
|
163
|
+
export const APPROVAL_STANDS = '승인은 그대로 남아 있습니다 — 고친 뒤 다시 넘길 수 있습니다'
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* **연결을 배포 사이로 옮기는 문 셋** — 몸통은 `connection-portability.ts` 의 순수 함수들이다.
|
|
3
|
+
*
|
|
4
|
+
* ── 무엇을 옮기나 (사용자와 맞춘 개념, 2026-09-05) ─────────────────────────
|
|
5
|
+
* **옮기는 것은 「붙는 법」이지 「붙은 결과」가 아니다.**
|
|
6
|
+
*
|
|
7
|
+
* 옮긴다 연결 선언 — 주소 · 도메인 · 라인 크기 · 자재 이름공간
|
|
8
|
+
* 안 옮긴다 마스터 원본에서 다시 읽으면 된다
|
|
9
|
+
* 안 옮긴다 사실(저널) 원본이 다시 보내면 된다
|
|
10
|
+
* 안 옮긴다 잰 값 그 서버에서 다시 세면 된다
|
|
11
|
+
* 안 옮긴다 비밀값 그 환경의 것이지 셋업이 아니다
|
|
12
|
+
*
|
|
13
|
+
* **꾸러미는 정본이 아니라 운반 수단이다.** 정본은 언제나 그 배포의 DB 이고, 꾸러미는 쓰고 버린다.
|
|
14
|
+
*
|
|
15
|
+
* ── 주체는 받는 쪽이다 ─────────────────────────────────────────────────────
|
|
16
|
+
* 운영이 개발에서 **당겨온다.** 개발이 운영으로 밀면 개발 환경에 운영 자격이 살고, 그 통로는 실수로도
|
|
17
|
+
* 열린다. 그래서 `pullTwinConnectionsFrom` 이 본래 원하던 문이고, 나머지 둘은 저쪽에 못 닿을 때의
|
|
18
|
+
* 길이다.
|
|
19
|
+
*
|
|
20
|
+
* ── 쓰기 전에 계획을 보인다 ────────────────────────────────────────────────
|
|
21
|
+
* `planImport` 가 새로/갱신/건너뜀과 **사람이 채울 비밀값**을 먼저 낸다. 부딪힘(두 연결이 같은 공장에
|
|
22
|
+
* 조치를 내는 것)이 하나라도 있으면 **아무것도 쓰지 않는다** — 반만 적용된 연결 설정은 어느 쪽이
|
|
23
|
+
* 참인지 아무도 모른다.
|
|
24
|
+
*/
|
|
25
|
+
import { Arg, Ctx, Directive, Mutation, Query, Resolver } from 'type-graphql'
|
|
26
|
+
import { ScalarObject, getRepository } from '@things-factory/shell'
|
|
27
|
+
|
|
28
|
+
import { TwinReference } from './twin-reference.js'
|
|
29
|
+
import { exportConnections, intakeCollisions, planImport, sayPlan, secretEnvName } from './connection-portability.js'
|
|
30
|
+
|
|
31
|
+
/** 그 환경이 이 비밀값을 줄 수 있나 — **값은 읽지 않는다.** 이름의 존재만 본다. */
|
|
32
|
+
function hasSecret(source: string, key: string): boolean {
|
|
33
|
+
const name = secretEnvName(source, key)
|
|
34
|
+
const v = process.env[name]
|
|
35
|
+
return typeof v === 'string' && v.trim().length > 0
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
@Resolver()
|
|
39
|
+
export class ConnectionPortabilityResolver {
|
|
40
|
+
/**
|
|
41
|
+
* 지금 연결을 꾸러미로 낸다 — **비밀값은 안 나간다.**
|
|
42
|
+
*
|
|
43
|
+
* 이름 규칙으로 걸러서 **새 비밀 칸도 기본으로 안 나간다**(`SECRET_KEYS`). 나가지 않은 것은
|
|
44
|
+
* 꾸러미가 이름으로 말하고, 받는 쪽이 그 이름을 사람에게 보인다.
|
|
45
|
+
*/
|
|
46
|
+
/*
|
|
47
|
+
* **읽기인데 쓰기와 같은 문을 쓴다.** 이 꾸러미는 연결 주소·도메인과 **비밀 칸의 이름**까지 낸다 —
|
|
48
|
+
* 그 이름만으로도 그 배포가 무엇에 붙어 있는지 드러난다. 권한 가드가 「쓰기는 GATE 하나」로
|
|
49
|
+
* 통일해 두었고, 이 문은 읽기지만 그 무게가 쓰기 쪽이다(ADR-0027).
|
|
50
|
+
*/
|
|
51
|
+
@Directive('@privilege(category: "twin", privilege: "mutation", domainOwnerGranted: true, superUserGranted: true)')
|
|
52
|
+
@Query(returns => ScalarObject, {
|
|
53
|
+
description:
|
|
54
|
+
'Export this deployment’s twin connection declarations as a portable bundle. Secrets are never included — the bundle names them so the receiving side can ask a human.'
|
|
55
|
+
})
|
|
56
|
+
async twinConnectionBundle(
|
|
57
|
+
@Ctx() context: ResolverContext,
|
|
58
|
+
@Arg('note', { nullable: true, description: 'Free note carried in the bundle (why this export was made).' }) note?: string
|
|
59
|
+
): Promise<any> {
|
|
60
|
+
const domain = context.state.domain
|
|
61
|
+
const rows = await getRepository(TwinReference).find({ where: { domain: { id: domain.id } } })
|
|
62
|
+
return exportConnections(rows as any, { at: new Date().toISOString(), domain: domain.subdomain ?? domain.name, ...(note ? { note } : {}) })
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 꾸러미를 받아 적용한다 — **`dryRun` 이 기본이다.**
|
|
67
|
+
*
|
|
68
|
+
* 쓰기가 기본이면 「무엇이 바뀌나」를 보기 위해 쓰게 된다. 계획을 먼저 보고 사람이 정하는 것이
|
|
69
|
+
* 이 문의 요점이고, 그래서 적용은 명시해야 일어난다.
|
|
70
|
+
*/
|
|
71
|
+
@Directive('@privilege(category: "twin", privilege: "mutation", domainOwnerGranted: true, superUserGranted: true)')
|
|
72
|
+
@Mutation(returns => ScalarObject, {
|
|
73
|
+
description:
|
|
74
|
+
'Apply a connection bundle. Returns the plan (new / updated / skipped, secrets a human must supply, collisions). Defaults to dryRun — pass apply:true to write.'
|
|
75
|
+
})
|
|
76
|
+
async importTwinConnections(
|
|
77
|
+
@Arg('bundle', type => ScalarObject) bundle: any,
|
|
78
|
+
@Ctx() context: ResolverContext,
|
|
79
|
+
@Arg('apply', { nullable: true, description: 'Write the plan. Omit or false to only see what would happen.' }) apply?: boolean
|
|
80
|
+
): Promise<any> {
|
|
81
|
+
const domainId = context.state.domain.id
|
|
82
|
+
const repo = getRepository(TwinReference)
|
|
83
|
+
const existing = await repo.find({ where: { domain: { id: domainId } } })
|
|
84
|
+
|
|
85
|
+
const plan = planImport(bundle, existing as any, hasSecret)
|
|
86
|
+
const said = sayPlan(plan)
|
|
87
|
+
|
|
88
|
+
/*
|
|
89
|
+
* 부딪힘이 있으면 **아무것도 쓰지 않는다.** 두 연결이 같은 공장에 조치를 내면 어느 쪽이 그 라인의
|
|
90
|
+
* 주인인지 정할 수 없고, 반만 적용하면 그 물음이 조용히 남는다.
|
|
91
|
+
*/
|
|
92
|
+
if (plan.collisions.length || plan.errors.length) {
|
|
93
|
+
return { ok: false, applied: 0, plan, said, refused: plan.collisions.length ? 'intake-collision' : 'bundle-error' }
|
|
94
|
+
}
|
|
95
|
+
if (!apply) return { ok: true, applied: 0, plan, said, dryRun: true }
|
|
96
|
+
|
|
97
|
+
let applied = 0
|
|
98
|
+
for (const a of plan.actions) {
|
|
99
|
+
/* 건너뛴 것은 이유가 계획에 남아 있다 — 여기서 다시 판정하지 않는다. */
|
|
100
|
+
if (a.action === 'skip') continue
|
|
101
|
+
|
|
102
|
+
const c = a.connection
|
|
103
|
+
/*
|
|
104
|
+
* **비밀값을 빈 값으로 안 채운다.** 꾸러미에 비밀값이 없고, `planImport` 가 그 이름을
|
|
105
|
+
* `missingSecrets`·`needsHuman` 으로 이미 냈다. 여기서 그 칸을 `undefined` 로 쓰면 사람이
|
|
106
|
+
* 화면에서 넣어 둔 값을 파일 하나가 지운다.
|
|
107
|
+
*
|
|
108
|
+
* 그래서 갱신은 **선언 칸만** 건드린다. 이미 있는 연결의 `connectionConfig` 는 꾸러미가 든
|
|
109
|
+
* 것만 덮어쓰고 나머지 키는 그대로 남긴다.
|
|
110
|
+
*/
|
|
111
|
+
const found = await repo.findOne({ where: { domain: { id: domainId }, source: c.source } })
|
|
112
|
+
const declared = {
|
|
113
|
+
...(c.system ? { system: c.system } : {}),
|
|
114
|
+
...(c.siteName ? { siteName: c.siteName } : {}),
|
|
115
|
+
adapterType: c.adapterType,
|
|
116
|
+
...(c.scope ? { scopeSpec: c.scope } : {})
|
|
117
|
+
} as any
|
|
118
|
+
|
|
119
|
+
if (found) {
|
|
120
|
+
const merged = { ...((found as any).connectionConfig ?? {}), ...(c.config ?? {}) }
|
|
121
|
+
await repo.update({ id: (found as any).id }, { ...declared, connectionConfig: merged } as any)
|
|
122
|
+
} else {
|
|
123
|
+
await repo.save(repo.create({ source: c.source, ...declared, connectionConfig: c.config ?? {}, domain: { id: domainId } as any }))
|
|
124
|
+
}
|
|
125
|
+
applied++
|
|
126
|
+
}
|
|
127
|
+
return { ok: true, applied, plan, said }
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* 저쪽 배포에서 당겨와 적용한다 — **본래 원한 문.**
|
|
132
|
+
*
|
|
133
|
+
* 받는 쪽이 주체다. 저쪽의 `twinConnectionBundle` 을 불러 이 배포에 적용한다 — 위 두 문을 잇는
|
|
134
|
+
* 얇은 층이고, 판정과 쓰기는 `importTwinConnections` 와 같은 규칙을 쓴다.
|
|
135
|
+
*
|
|
136
|
+
* **자격은 부르는 사람이 준다.** 이 서버가 저쪽 자격을 들고 있으면 그 통로가 상시로 열려 있게 되고,
|
|
137
|
+
* 그것은 옮기는 일에 필요한 것보다 넓다.
|
|
138
|
+
*/
|
|
139
|
+
@Directive('@privilege(category: "twin", privilege: "mutation", domainOwnerGranted: true, superUserGranted: true)')
|
|
140
|
+
@Mutation(returns => ScalarObject, {
|
|
141
|
+
description:
|
|
142
|
+
'Pull connection declarations from another deployment and apply them here. The caller supplies that deployment’s URL and token — this server does not keep them.'
|
|
143
|
+
})
|
|
144
|
+
async pullTwinConnectionsFrom(
|
|
145
|
+
@Arg('endpoint', { description: 'That deployment’s GraphQL endpoint.' }) endpoint: string,
|
|
146
|
+
@Ctx() context: ResolverContext,
|
|
147
|
+
@Arg('token', { nullable: true, description: 'Bearer token for that deployment. Not stored.' }) token?: string,
|
|
148
|
+
@Arg('apply', { nullable: true }) apply?: boolean
|
|
149
|
+
): Promise<any> {
|
|
150
|
+
let bundle: any
|
|
151
|
+
try {
|
|
152
|
+
const res = await fetch(endpoint, {
|
|
153
|
+
method: 'POST',
|
|
154
|
+
headers: { 'Content-Type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}) },
|
|
155
|
+
body: JSON.stringify({ query: '{ twinConnectionBundle }' })
|
|
156
|
+
})
|
|
157
|
+
const json: any = await res.json()
|
|
158
|
+
/* 저쪽이 오류를 냈으면 그 말을 그대로 올린다 — 여기서 다시 지으면 원인이 흐려진다. */
|
|
159
|
+
if (json?.errors?.length) return { ok: false, applied: 0, refused: 'remote-error', remote: json.errors.map((e: any) => e?.message) }
|
|
160
|
+
bundle = json?.data?.twinConnectionBundle
|
|
161
|
+
} catch (e: any) {
|
|
162
|
+
return { ok: false, applied: 0, refused: 'unreachable', remote: [String(e?.message ?? e)] }
|
|
163
|
+
}
|
|
164
|
+
if (!bundle) return { ok: false, applied: 0, refused: 'empty-bundle' }
|
|
165
|
+
|
|
166
|
+
return this.importTwinConnections(bundle, context, apply)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** 이 리졸버가 다루는 부딪힘 판정 — 화면이 같은 함수를 쓸 수 있게 다시 내보낸다. */
|
|
171
|
+
export { intakeCollisions }
|