@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,387 @@
|
|
|
1
|
+
import { Arg, Args, Ctx, Directive, Mutation, Query, Resolver } from 'type-graphql'
|
|
2
|
+
|
|
3
|
+
import { MES_COMMANDS, nextCommandState } from '@operato/ops-contract'
|
|
4
|
+
|
|
5
|
+
import { randomUUID } from 'node:crypto'
|
|
6
|
+
|
|
7
|
+
import { ListParam, ScalarObject, getQueryBuilderFromListParams, getRepository } from '@things-factory/shell'
|
|
8
|
+
|
|
9
|
+
import { askAdvisor } from './actuation-advisor.js'
|
|
10
|
+
import { approvalGateway } from './approval-gateway.js'
|
|
11
|
+
import { approve, dispatch, linkApproval, propose, reject } from './command-dispatcher.js'
|
|
12
|
+
import { actuationReadinessOf, actuationTargetOf, routeActuation } from '../reference/actuation-routing.js'
|
|
13
|
+
|
|
14
|
+
import { actuationOutcomeOf } from './actuation-outcome.js'
|
|
15
|
+
import { databaseCommandStore } from './command-store.js'
|
|
16
|
+
import { TwinCommandRecord } from './twin-command.js'
|
|
17
|
+
import { twinNow } from './twin-now.js'
|
|
18
|
+
|
|
19
|
+
/*
|
|
20
|
+
* 승인 대기 줄과 그 결정 — **사람이 쓰는 자리**.
|
|
21
|
+
*
|
|
22
|
+
* ── 왜 필요한가 (2026-08-31) ─────────────────────────────────────────────
|
|
23
|
+
* 게이트를 세우고 승인할 화면을 만들지 않았다. 그래서 되돌릴 수 없는 조치는 전부 대기 상태로 쌓이고
|
|
24
|
+
* **사람이 승인할 방법이 없었다.** 서버만 있고 사람이 쓸 자리가 없는 상태다.
|
|
25
|
+
*
|
|
26
|
+
* ── 왜 조회와 결정을 한 파일에 두나 ──────────────────────────────────────
|
|
27
|
+
* 대기 줄을 여는 것과 그 줄에서 결정하는 것은 한 화면의 일이다. 나누면 화면이 두 곳을 부르게 되고,
|
|
28
|
+
* 그중 하나만 권한이 바뀌는 날 목록은 보이는데 결정이 안 되거나 그 반대가 된다.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
@Resolver()
|
|
32
|
+
export class TwinCommandResolver {
|
|
33
|
+
/**
|
|
34
|
+
* 커맨드 목록 — 기본은 **결정을 기다리는 것부터**.
|
|
35
|
+
*
|
|
36
|
+
* 정렬을 최근순으로 두지 않는다. 승인 대기 줄에서 사람이 찾는 것은 「새로 온 것」이 아니라
|
|
37
|
+
* **「오래 기다린 것」**이다 — 오래 기다린 조치가 뒤로 밀리면 그것이 가장 늦게 처리된다.
|
|
38
|
+
*/
|
|
39
|
+
@Query(returns => ScalarObject, {
|
|
40
|
+
nullable: true,
|
|
41
|
+
description: 'List actuation commands with the standard list contract. Defaults to the oldest waiting first.'
|
|
42
|
+
})
|
|
43
|
+
async twinCommands(
|
|
44
|
+
/* 표준 목록 계약은 `@Args` 로 펼쳐 받는다 — `@Arg` 로 받으면 스키마 조립이 실패한다. */
|
|
45
|
+
@Args(type => ListParam) params: ListParam,
|
|
46
|
+
@Ctx() context: ResolverContext
|
|
47
|
+
): Promise<any> {
|
|
48
|
+
const domainId = context.state.domain?.id
|
|
49
|
+
if (!domainId) return { items: [], total: 0 }
|
|
50
|
+
|
|
51
|
+
const effective: ListParam = {
|
|
52
|
+
...(params ?? ({} as ListParam)),
|
|
53
|
+
sortings: params?.sortings?.length ? params.sortings : [{ name: 'createdAt', desc: false }]
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const qb = getQueryBuilderFromListParams({
|
|
57
|
+
repository: getRepository(TwinCommandRecord),
|
|
58
|
+
params: effective,
|
|
59
|
+
domain: context.state.domain,
|
|
60
|
+
alias: 'c',
|
|
61
|
+
/* 전부 실컬럼이다 — 질의 빌더는 없는 컬럼의 LIKE 를 경고 후 무시한다(인덱스 없는 전체 스캔 방지). */
|
|
62
|
+
searchables: ['instanceId', 'type', 'origin', 'state', 'approvalRef', 'decidedBy', 'error'],
|
|
63
|
+
sortables: ['createdAt', 'updatedAt', 'state', 'type', 'instanceId'],
|
|
64
|
+
/* 사람이 읽는 속도로 본다. */
|
|
65
|
+
defaultLimit: 50,
|
|
66
|
+
maxLimit: 200
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
const [items, total] = await qb.getManyAndCount()
|
|
70
|
+
return { items, total }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* **이 조치에 대해 트윈이 지금 보는 것과 AI 의견** — 승인 화면이 부른다.
|
|
75
|
+
*
|
|
76
|
+
* ── 왜 조회인가 (2026-09-01) ───────────────────────────────────────────
|
|
77
|
+
* 결재선에 올릴 때 묻지 않는다. 그 자리는 트랜잭션 안이고 거기서 LLM 을 기다리면 DB 연결 하나를 몇 초
|
|
78
|
+
* 붙잡는다. 그리고 **승인하는 사람은 지금 판단한다** — 올린 시각의 의견은 낡았다.
|
|
79
|
+
*
|
|
80
|
+
* AI 는 판정하지 않는다. 이 값을 읽어 승인·거절을 막는 코드가 없다. 반대해도 승인되고 동의해도
|
|
81
|
+
* 거절된다 — 판정에 쓰면 AI 가 사실상 승인자가 되고, 그러면 사고 뒤에 책임을 물을 자리가 사라진다.
|
|
82
|
+
*/
|
|
83
|
+
@Query(returns => ScalarObject, {
|
|
84
|
+
nullable: true,
|
|
85
|
+
description: 'What the twin sees now for one command, plus an AI opinion when asked. Advisory only — never gates the decision.'
|
|
86
|
+
})
|
|
87
|
+
async twinActuationOpinion(
|
|
88
|
+
@Arg('id') id: string,
|
|
89
|
+
/**
|
|
90
|
+
* AI 에게도 물을 것인가 — **누르면 묻는다.**
|
|
91
|
+
*
|
|
92
|
+
* 기본은 안 묻는다. 화면을 열 때마다 부르면 승인자 다섯 명이 두 번씩 열 때 한 건에 열 번이고,
|
|
93
|
+
* 대부분의 지시는 평범해서 그중 아홉 번은 같은 말을 다시 받는 것이다.
|
|
94
|
+
*/
|
|
95
|
+
@Arg('askAi', { nullable: true }) askAi: boolean,
|
|
96
|
+
@Arg('locale', { nullable: true }) locale: string,
|
|
97
|
+
@Ctx() context: ResolverContext
|
|
98
|
+
): Promise<any> {
|
|
99
|
+
const domainId = context.state.domain?.id
|
|
100
|
+
if (!domainId) throw new Error('테넌트를 알 수 없습니다')
|
|
101
|
+
|
|
102
|
+
const command = await databaseCommandStore().load(domainId, id)
|
|
103
|
+
if (!command) throw new Error(`모르는 조치입니다 (${id})`)
|
|
104
|
+
|
|
105
|
+
/* 트윈의 지금 상태는 늘 새로 읽는다 — 계산값이라 싸고, 승인자는 지금을 봐야 한다. */
|
|
106
|
+
const now = twinNow(domainId, command.instanceId, command.causeRef)
|
|
107
|
+
|
|
108
|
+
if (!askAi) return { command, twinNow: now }
|
|
109
|
+
|
|
110
|
+
/*
|
|
111
|
+
* **의견을 저장하지 않는다.**
|
|
112
|
+
*
|
|
113
|
+
* 의견은 「지금」에 대한 것이고 승인자마다 여는 시각이 다르다. 저장해서 같은 문장을 돌려주면 그것이
|
|
114
|
+
* 오히려 낡은 값이다. 그리고 이 값을 읽어 승인·거절을 막는 코드가 없다 — 판정에 쓰면 AI 가 사실상
|
|
115
|
+
* 승인자가 되고, 사고 뒤에 책임을 물을 자리가 사라진다.
|
|
116
|
+
*/
|
|
117
|
+
const advice = await askAdvisor({ command, twinNow: now, ...(locale ? { locale } : {}) })
|
|
118
|
+
|
|
119
|
+
return { command, twinNow: now, advice }
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* 조치를 낸다 — **대기 줄에 무언가가 들어오는 유일한 자리.**
|
|
124
|
+
*
|
|
125
|
+
* 결재선이 등록된 배포에서는 여기서 바로 결재선에 올라간다(§`ApprovalGateway`). 등록되지 않았으면
|
|
126
|
+
* 제안 상태로 남고, 이 리졸버의 직접 승인이 유일한 문이 된다. 어느 쪽인지는 답에 적어 준다 — 화면이
|
|
127
|
+
* 「어디서 승인하는가」를 사람에게 말해야 한다.
|
|
128
|
+
*
|
|
129
|
+
* 되돌릴 수 있는 조치는 결재선에 올리지 않는다. 계약이 그 판단을 든다(§`requiresApproval`).
|
|
130
|
+
*/
|
|
131
|
+
@Directive('@transaction')
|
|
132
|
+
@Directive('@privilege(category: "twin", privilege: "mutation", domainOwnerGranted: true, superUserGranted: true)')
|
|
133
|
+
@Mutation(returns => ScalarObject, { description: 'Propose one actuation command against a twin. Raises it for approval when an approval gateway is installed.' })
|
|
134
|
+
async proposeTwinCommand(
|
|
135
|
+
@Arg('instanceId') instanceId: string,
|
|
136
|
+
@Arg('type') type: string,
|
|
137
|
+
@Arg('payload', type => ScalarObject, { nullable: true }) payload: any,
|
|
138
|
+
@Ctx() context: ResolverContext
|
|
139
|
+
): Promise<any> {
|
|
140
|
+
const { domain, user } = context.state
|
|
141
|
+
const by = String(user?.email ?? user?.name ?? '').trim()
|
|
142
|
+
if (!by) throw new Error('누가 내는지 알 수 없습니다 — 로그인한 사용자가 없습니다')
|
|
143
|
+
|
|
144
|
+
const store = databaseCommandStore(context.state.tx)
|
|
145
|
+
const command = await propose(store, domain.id, MES_COMMANDS, {
|
|
146
|
+
id: randomUUID(),
|
|
147
|
+
instanceId,
|
|
148
|
+
type,
|
|
149
|
+
/*
|
|
150
|
+
* 사람이 낸 것이다. 규칙·AI 가 내는 길은 아직 없다 — 그 길은 그것을 내는 쪽이 붙인다.
|
|
151
|
+
* 여기서 인자로 받으면 사람이 「규칙이 냈다」로 적어 승인자를 오판하게 만들 수 있다.
|
|
152
|
+
*/
|
|
153
|
+
origin: 'operator',
|
|
154
|
+
/* 낸 사람을 적는다 — 이 값이 있어야 자기 것을 자기가 승인하는 것을 막을 수 있다. */
|
|
155
|
+
proposedBy: by,
|
|
156
|
+
proposedAt: new Date().toISOString(),
|
|
157
|
+
...(payload ? { payload } : {})
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
if (command.reversible === true) {
|
|
161
|
+
return { command, gateway: null, note: '되돌릴 수 있는 조치입니다 — 승인 없이 넘길 수 있습니다' }
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const gateway = approvalGateway()
|
|
165
|
+
if (!gateway) {
|
|
166
|
+
return {
|
|
167
|
+
command,
|
|
168
|
+
gateway: null,
|
|
169
|
+
note: '결재선이 설치되지 않은 배포입니다 — 이 목록에서 직접 승인합니다'
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/*
|
|
174
|
+
* 올리기와 적기가 한 트랜잭션에 있다(`@transaction`). 올렸는데 적지 못하면 결재선에는 올라가 있고
|
|
175
|
+
* 커맨드는 직접 결정도 되는 상태가 남는다 — 그때 승인이 두 곳에 갈린다.
|
|
176
|
+
*/
|
|
177
|
+
const approvalRef = await gateway.request(command, context)
|
|
178
|
+
const linked = await linkApproval(store, domain.id, command.id, approvalRef)
|
|
179
|
+
|
|
180
|
+
return { command: linked, gateway: gateway.label, note: `${gateway.label} 에 올렸습니다 — 승인은 그곳에서 합니다` }
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* **사람이 결재선에 올린다** — 규칙이 낸 조치가 승인 앞에 서는 유일한 길.
|
|
185
|
+
*
|
|
186
|
+
* ── 왜 규칙이 직접 올리지 않나 ─────────────────────────────────────────
|
|
187
|
+
* 업무 목록의 결재선은 **올린 사람 기준**으로 풀린다 — `Myself` · `MyDepartment` · `MySupervisor` 가
|
|
188
|
+
* 전부 로그인한 사람을 본다. 배경 루프에는 「나」가 없으므로, 시스템 사용자를 지어내 올리면 그 셋이
|
|
189
|
+
* 시스템 사용자를 가리키고 결재선이 뜻을 잃는다.
|
|
190
|
+
*
|
|
191
|
+
* 그래서 규칙은 내는 것까지 하고, 여기서 사람이 올린다. **올린 사람이 「이것을 승인자들 앞에 놓았다」의
|
|
192
|
+
* 주체**가 되고, 그 사람 기준으로 결재선이 풀린다.
|
|
193
|
+
*
|
|
194
|
+
* 올린 사람은 자기가 올린 것을 승인할 수 있다 — 규칙이 낸 조치에는 낸 사람이 없으므로 승인자가 곧
|
|
195
|
+
* 두 번째 사람이다. 올린 것과 낸 것은 다르다.
|
|
196
|
+
*/
|
|
197
|
+
@Directive('@transaction')
|
|
198
|
+
@Directive('@privilege(category: "twin", privilege: "mutation", domainOwnerGranted: true, superUserGranted: true)')
|
|
199
|
+
@Mutation(returns => ScalarObject, { description: 'Raise one proposed command to the approval line. The signed-in user is the submitter.' })
|
|
200
|
+
async raiseTwinCommand(@Arg('id') id: string, @Ctx() context: ResolverContext): Promise<any> {
|
|
201
|
+
const { domain, user } = context.state
|
|
202
|
+
if (!user?.id) throw new Error('누가 올리는지 알 수 없습니다 — 로그인한 사용자가 없습니다')
|
|
203
|
+
|
|
204
|
+
const store = databaseCommandStore(context.state.tx)
|
|
205
|
+
const command = await store.load(domain.id, id)
|
|
206
|
+
if (!command) throw new Error(`모르는 조치입니다 (${id})`)
|
|
207
|
+
|
|
208
|
+
if (command.approvalRef) {
|
|
209
|
+
throw new Error(`이미 결재선에 올라가 있습니다 (${command.approvalRef}) — 승인은 업무 목록에서 합니다`)
|
|
210
|
+
}
|
|
211
|
+
if (command.state !== 'proposed') {
|
|
212
|
+
throw new Error(`${command.state} 상태의 조치는 올릴 수 없습니다 — 이미 결정되었거나 나갔습니다`)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const gateway = approvalGateway()
|
|
216
|
+
if (!gateway) {
|
|
217
|
+
throw new Error('결재선이 설치되지 않은 배포입니다 — 이 목록에서 직접 승인합니다')
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const approvalRef = await gateway.request(command, context)
|
|
221
|
+
const linked = await linkApproval(store, domain.id, command.id, approvalRef)
|
|
222
|
+
|
|
223
|
+
return { command: linked, gateway: gateway.label, note: `${gateway.label} 에 올렸습니다 — 승인은 그곳에서 합니다` }
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* 승인한다 — **누가 승인했는지는 로그인한 사람에서 온다.**
|
|
228
|
+
*
|
|
229
|
+
* 인자로 받지 않는다. 받으면 다른 사람 이름으로 승인할 수 있고, 그러면 그 기록이 사고 뒤에 쓸 수
|
|
230
|
+
* 없어진다. 승인 기록의 값어치가 거기서 나온다.
|
|
231
|
+
*/
|
|
232
|
+
@Directive('@privilege(category: "twin", privilege: "mutation", domainOwnerGranted: true, superUserGranted: true)')
|
|
233
|
+
@Mutation(returns => ScalarObject, { description: 'Approve one actuation command. The approver is the signed-in user.' })
|
|
234
|
+
async approveTwinCommand(
|
|
235
|
+
@Arg('id') id: string,
|
|
236
|
+
@Arg('note', { nullable: true }) note: string,
|
|
237
|
+
@Ctx() context: ResolverContext
|
|
238
|
+
): Promise<any> {
|
|
239
|
+
const { domain, user } = context.state
|
|
240
|
+
const by = String(user?.email ?? user?.name ?? '').trim()
|
|
241
|
+
if (!by) throw new Error('누가 승인하는지 알 수 없습니다 — 로그인한 사용자가 없습니다')
|
|
242
|
+
|
|
243
|
+
return approve(databaseCommandStore(context.state.tx), domain.id, id, {
|
|
244
|
+
by,
|
|
245
|
+
at: new Date().toISOString(),
|
|
246
|
+
...(note?.trim() ? { note: note.trim() } : {})
|
|
247
|
+
})
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* 거절한다 — 여기서 끝난다.
|
|
252
|
+
*
|
|
253
|
+
* 사유를 요구한다. 거절은 현장이 기다리던 조치를 취소하는 것이고, 이유 없이 거절되면 낸 쪽이 다시
|
|
254
|
+
* 낸다. 그러면 같은 것이 대기 줄에 계속 돌아온다.
|
|
255
|
+
*/
|
|
256
|
+
@Directive('@privilege(category: "twin", privilege: "mutation", domainOwnerGranted: true, superUserGranted: true)')
|
|
257
|
+
@Mutation(returns => ScalarObject, { description: 'Reject one actuation command. A reason is required.' })
|
|
258
|
+
async rejectTwinCommand(
|
|
259
|
+
@Arg('id') id: string,
|
|
260
|
+
@Arg('reason') reason: string,
|
|
261
|
+
@Ctx() context: ResolverContext
|
|
262
|
+
): Promise<any> {
|
|
263
|
+
const { domain, user } = context.state
|
|
264
|
+
const by = String(user?.email ?? user?.name ?? '').trim()
|
|
265
|
+
if (!by) throw new Error('누가 거절하는지 알 수 없습니다 — 로그인한 사용자가 없습니다')
|
|
266
|
+
if (!reason?.trim()) throw new Error('거절 사유가 필요합니다 — 이유 없이 거절되면 같은 조치가 다시 올라옵니다')
|
|
267
|
+
|
|
268
|
+
return reject(databaseCommandStore(context.state.tx), domain.id, id, { by, at: new Date().toISOString(), note: reason.trim() })
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* 어댑터에 넘긴다 — **승인을 지나지 않은 것은 여기서 막힌다.**
|
|
273
|
+
*
|
|
274
|
+
* ── 이름이 왜 `dispatchTwinCommand` 가 아닌가 (2026-09-01) ─────────────
|
|
275
|
+
* 그 이름은 이미 **커널 자극**이 쓴다(`TwinControlMutation.dispatchTwinCommand` —
|
|
276
|
+
* `order.hold`·`resource.repair` 를 커널에 넣는다). 같은 이름을 두 번 선언하면 type-graphql 이
|
|
277
|
+
* 오류 없이 하나만 남기고, 실제로 이 뮤테이션이 통째로 가려져 있었다 — 걸어 보고 알았다.
|
|
278
|
+
*
|
|
279
|
+
* 자극과 조치가 다른 어휘라는 것을 계약에서 갈라 놓고(§`propose` 의 거절), 정작 API 이름에서 겹쳐
|
|
280
|
+
* 두었다. 여기서는 조치임을 이름에 적는다.
|
|
281
|
+
*
|
|
282
|
+
* 화면이 이것을 따로 부르는 이유는, 승인과 넘김이 다른 순간이기 때문이다. 승인한 사람과 넘긴 시각이
|
|
283
|
+
* 같아야 할 이유가 없고, 어댑터가 죽어 있으면 승인은 남아 있어야 한다.
|
|
284
|
+
*
|
|
285
|
+
* ── 여기가 끊겨 있었다 (2026-09-02) ──────────────────────────────────
|
|
286
|
+
* 이 자리가 **어댑터를 찾지도 않고** 「구현이 없습니다」를 답했다. MES 커넥터는 그때 이미 `actuate`
|
|
287
|
+
* 를 다 만들어 두었다. 그 답을 사실로 읽어서 설계 문서에도, 다른 레인에도 그렇게 알렸다 — 둘 다
|
|
288
|
+
* 틀렸다.
|
|
289
|
+
*
|
|
290
|
+
* 이제 원본을 찾아 그 원본이 선언한 능력으로 판단한다(§`routeActuation`). 보내지 못하면 **무엇이
|
|
291
|
+
* 없어서인지** 말한다. 조용히 성공을 답하면 화면은 나간 줄로 읽고, 현장은 오지 않는 지시를 기다린다.
|
|
292
|
+
*
|
|
293
|
+
* ── 여기에는 트랜잭션을 걸지 않는다 ──────────────────────────────────
|
|
294
|
+
* 넘기기 전에 「넘기는 중」을 먼저 적는 것이 이 함수의 요점이다. 한 트랜잭션에 묶으면 그 기록이
|
|
295
|
+
* 커밋 전까지 아무에게도 보이지 않고, 넘기는 사이에 프로세스가 죽으면 그대로 사라진다. 그러면 저쪽은
|
|
296
|
+
* 받았는데 우리는 안 보낸 줄 알고 다시 보낸다 — 현장이 두 번 움직인다.
|
|
297
|
+
*/
|
|
298
|
+
/**
|
|
299
|
+
* **이 조치를 넘길 수 있나** — 누르기 전에 답한다.
|
|
300
|
+
*
|
|
301
|
+
* ── 왜 이 자리가 필요한가 (2026-09-03) ────────────────────────────────────
|
|
302
|
+
* 판정 함수 둘을 만들어 두고 **부르는 자리를 안 만들었다**(`actuationTargetOf`·
|
|
303
|
+
* `actuationReadinessOf`). 그래서 화면이 물을 방법이 없었고, 못 넘기는 조치도 눌러 보고 알게 됐다.
|
|
304
|
+
* 인티그레이션 레인이 그 사실을 두 번 짚어 줬다 — 자리는 있고 길이 없는 그 부류다.
|
|
305
|
+
*
|
|
306
|
+
* 그동안 값이 실제로 없어졌다. 저쪽에 비밀값이 없던 날, 사람이 승인 화면까지 가서 누르고 실패를
|
|
307
|
+
* 보고 다시 로그인했다. **승인은 살아 있지만 사람의 시간은 돌아오지 않는다.**
|
|
308
|
+
*
|
|
309
|
+
* ── 둘을 함께 답한다 ──────────────────────────────────────────────────────
|
|
310
|
+
* 화면이 묻는 것은 하나다 — 「지금 넘겨도 되나」. 그 답이 두 겹이라 둘을 함께 싣는다.
|
|
311
|
+
*
|
|
312
|
+
* target 보낼 대상이 정해지나 — 망을 타지 않는다(원본·능력·사이트)
|
|
313
|
+
* readiness 저쪽이 받을 준비가 됐나 — 커넥터가 망을 탈 수 있다
|
|
314
|
+
*
|
|
315
|
+
* **문이 아니다.** 이 답으로 넘김을 막지 않는다(§`actuationReadinessOf`) — 화면이 미리 말하는
|
|
316
|
+
* 데까지다. 그래서 조회이고 뮤테이션이 아니다.
|
|
317
|
+
*
|
|
318
|
+
* 화면이 **목록을 그릴 때 부르지 말 것.** 커넥터가 저쪽에 물을 수 있어서 목록 한 줄마다 망을 탄다.
|
|
319
|
+
* 사람이 「넘기기」를 열 때 한 번이다.
|
|
320
|
+
*/
|
|
321
|
+
/*
|
|
322
|
+
* 권한 지시자를 달지 않는다 — 이 파일의 다른 조회 둘과 같다. **쓰기만 게이트를 지난다**(ADR-0027),
|
|
323
|
+
* 그리고 조회는 테넌트로 좁혀진다. 처음에 `privilege: "query"` 를 달았더니 권한 문장 검사가
|
|
324
|
+
* 「정본과 다르다」로 잡았다.
|
|
325
|
+
*/
|
|
326
|
+
@Query(returns => ScalarObject, {
|
|
327
|
+
nullable: true,
|
|
328
|
+
description: 'Whether one approved actuation can be dispatched now — target and far-side readiness. Advisory, not a gate.'
|
|
329
|
+
})
|
|
330
|
+
async twinActuationReadiness(@Arg('id') id: string, @Ctx() context: ResolverContext): Promise<any> {
|
|
331
|
+
const domainId = context.state.domain?.id
|
|
332
|
+
if (!domainId) throw new Error('테넌트를 알 수 없습니다')
|
|
333
|
+
|
|
334
|
+
const command = await databaseCommandStore().load(domainId, id)
|
|
335
|
+
if (!command) throw new Error(`모르는 조치입니다 (${id})`)
|
|
336
|
+
|
|
337
|
+
/*
|
|
338
|
+
* 상태를 함께 낸다 — **이 조치가 넘길 수 있는 상태인가**는 저쪽 사정과 다른 물음이다. 이미
|
|
339
|
+
* `acked` 인 것은 저쪽이 아무리 준비돼 있어도 다시 넘기지 않는다(종착).
|
|
340
|
+
*/
|
|
341
|
+
const dispatchable = nextCommandState(command.state, 'dispatch') !== undefined
|
|
342
|
+
|
|
343
|
+
const [target, readiness] = await Promise.all([
|
|
344
|
+
actuationTargetOf(domainId, command.instanceId),
|
|
345
|
+
actuationReadinessOf(domainId, command.instanceId)
|
|
346
|
+
])
|
|
347
|
+
|
|
348
|
+
return { id, state: command.state, dispatchable, target, readiness }
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/*
|
|
352
|
+
* 권한 지시자를 달지 않는다 — 이 파일의 다른 조회들과 같다(ADR-0027: 쓰기만 게이트를 지난다).
|
|
353
|
+
*/
|
|
354
|
+
@Query(returns => ScalarObject, {
|
|
355
|
+
nullable: true,
|
|
356
|
+
description: 'What happened to one actuation — for the person who just approved it. Includes whether work remains at the source.'
|
|
357
|
+
})
|
|
358
|
+
async twinActuationOutcome(@Arg('id') id: string, @Ctx() context: ResolverContext): Promise<any> {
|
|
359
|
+
const domainId = context.state.domain?.id
|
|
360
|
+
if (!domainId) throw new Error('테넌트를 알 수 없습니다')
|
|
361
|
+
|
|
362
|
+
const command = await databaseCommandStore().load(domainId, id)
|
|
363
|
+
if (!command) throw new Error(`모르는 조치입니다 (${id})`)
|
|
364
|
+
|
|
365
|
+
/*
|
|
366
|
+
* **승인 직후에 부르는 자리다.** 발신은 승인 응답이 나간 다음 틱에 일어나므로(§
|
|
367
|
+
* `dispatchAfterApproval`), 그 순간에는 아직 `approved` 일 수 있다. 그것을 감추지 않는다 —
|
|
368
|
+
* 화면이 「보내는 중」을 그리고 짧게 다시 묻는다(UX 레인 규칙: 대략 0.5초·2초 두 번, 그래도
|
|
369
|
+
* 안 끝나면 조치 목록으로 안내).
|
|
370
|
+
*
|
|
371
|
+
* 여기서 기다리지 않는 이유: 기다리면 이 조회가 저쪽 왕복만큼 멈추고, 사람이 화면을 떠난
|
|
372
|
+
* 뒤에도 붙잡는다.
|
|
373
|
+
*/
|
|
374
|
+
return actuationOutcomeOf(command, nextCommandState(command.state, 'dispatch') !== undefined)
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
@Directive('@privilege(category: "twin", privilege: "mutation", domainOwnerGranted: true, superUserGranted: true)')
|
|
378
|
+
@Mutation(returns => ScalarObject, { description: 'Send one approved actuation command to its connector. Not a kernel stimulus.' })
|
|
379
|
+
async dispatchTwinActuation(@Arg('id') id: string, @Ctx() context: ResolverContext): Promise<any> {
|
|
380
|
+
const { domain } = context.state
|
|
381
|
+
|
|
382
|
+
/* 트랜잭션을 넘기지 않는다 — 위의 이유다. 지시자가 뒤에 붙어도 이 자리는 바뀌지 않아야 한다. */
|
|
383
|
+
return dispatch(databaseCommandStore(), domain.id, id, command =>
|
|
384
|
+
routeActuation(domain.id, command.instanceId, command)
|
|
385
|
+
)
|
|
386
|
+
}
|
|
387
|
+
}
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* 이 파일은 그 값을 담기만 한다 — 판정이 두 벌이 되면 화면과 서버가 다른 답을 낸다.
|
|
13
13
|
*/
|
|
14
14
|
import { Column, CreateDateColumn, Entity, Index, ManyToOne, PrimaryGeneratedColumn, RelationId, UpdateDateColumn } from 'typeorm'
|
|
15
|
-
import { Field, ID, ObjectType } from 'type-graphql'
|
|
15
|
+
import { Field, ID, Int, ObjectType } from 'type-graphql'
|
|
16
16
|
|
|
17
17
|
import { Domain, ScalarObject } from '@things-factory/shell'
|
|
18
18
|
|
|
@@ -48,6 +48,19 @@ export class TwinCommandRecord {
|
|
|
48
48
|
@Field({ description: 'Who proposed it: operator, rule, ai or schedule. A person weighs this when approving.' })
|
|
49
49
|
origin: string
|
|
50
50
|
|
|
51
|
+
/**
|
|
52
|
+
* **낸 사람** — 종류가 아니라 사람이다.
|
|
53
|
+
*
|
|
54
|
+
* `origin` 은 「사람이 냈나 · 규칙이 냈나」만 말한다. 그것만으로는 **낸 사람이 자기 것을 승인하는 것**을
|
|
55
|
+
* 막을 수 없고, 그러면 게이트가 시늉이 된다. 되돌릴 수 없는 조치에 두 사람이 필요하다는 것이 이 표의
|
|
56
|
+
* 존재 이유다.
|
|
57
|
+
*
|
|
58
|
+
* 규칙·AI 가 낸 것은 비어 있다 — 사람이 아니다.
|
|
59
|
+
*/
|
|
60
|
+
@Column({ nullable: true })
|
|
61
|
+
@Field({ nullable: true, description: 'The person who proposed it. Absent when a rule or model proposed it.' })
|
|
62
|
+
proposedBy?: string
|
|
63
|
+
|
|
51
64
|
/**
|
|
52
65
|
* **되돌릴 수 있는가** — 게이트가 걸리는 기준이다.
|
|
53
66
|
*
|
|
@@ -62,8 +75,45 @@ export class TwinCommandRecord {
|
|
|
62
75
|
@Field({ description: 'proposed · approved · rejected · dispatched · acked · failed.' })
|
|
63
76
|
state: string
|
|
64
77
|
|
|
78
|
+
/**
|
|
79
|
+
* **무엇 때문에 냈나** — 규칙이 낸 조치가 가리키는 관측의 식별자(`Attention.id`).
|
|
80
|
+
*
|
|
81
|
+
* 두 가지를 한다.
|
|
82
|
+
*
|
|
83
|
+
* 같은 조건을 두 번 올리지 않는다 결정을 기다리는 커맨드의 이 값을 모아 판정에 넘긴다
|
|
84
|
+
* 승인하는 사람이 근거를 본다 「무슨 일이 있어서 이것이 올라왔나」
|
|
85
|
+
*
|
|
86
|
+
* 앞엣것이 이 칸의 존재 이유다. 프로세스 기억으로 중복을 막으면 **재기동이 곧 중복**이 된다 — 다시 선
|
|
87
|
+
* 트윈이 같은 조건을 처음 보는 것으로 읽고 대기 줄에 같은 지시를 또 올린다.
|
|
88
|
+
*
|
|
89
|
+
* 사람이 낸 것은 비어 있다. 사람은 자기가 왜 내는지 알고 있고, 그 이유를 값으로 요구하면 아무 글자나 적힌다.
|
|
90
|
+
*/
|
|
91
|
+
@Column({ nullable: true })
|
|
92
|
+
@Field({ nullable: true, description: 'The observation that caused a rule to propose this. Empty when a person proposed it.' })
|
|
93
|
+
causeRef?: string
|
|
94
|
+
|
|
95
|
+
/** 어느 규칙이 냈나 — 규칙을 끄거나 고칠 때 그 규칙이 무엇을 냈는지 되짚을 자리. */
|
|
96
|
+
@Column({ nullable: true })
|
|
97
|
+
@Field({ nullable: true, description: 'The rule that proposed this, when a rule did.' })
|
|
98
|
+
ruleRef?: string
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* **결재선에 올라간 자리** — 업무 목록(`@things-factory/worklist`)의 액티비티 인스턴스 식별자.
|
|
102
|
+
*
|
|
103
|
+
* 이 값이 있으면 승인·거절은 **그 목록에서만** 한다. 여기 있는 직접 결정 뮤테이션은 막힌다 — 문이 두
|
|
104
|
+
* 벌이면 결재선에는 대기로 남아 있는데 조치는 이미 나간 상태가 만들어진다.
|
|
105
|
+
*
|
|
106
|
+
* 비어 있으면 결재선을 쓰지 않는 배포다. 그때는 직접 결정이 유일한 문이다.
|
|
107
|
+
*/
|
|
108
|
+
@Column({ nullable: true })
|
|
109
|
+
@Field({ nullable: true, description: 'Approval record this command waits on. While set, decisions come only from there.' })
|
|
110
|
+
approvalRef?: string
|
|
111
|
+
|
|
65
112
|
/* ── 승인의 기록 ─────────────────────────────────────────────────────────
|
|
66
113
|
* 한 칸에 JSON 으로 담지 않고 세 칸으로 나눈다 — 「누가 승인했나」로 묻는 질의가 실제로 온다.
|
|
114
|
+
*
|
|
115
|
+
* 결재선을 지난 경우에도 여기에 적는다. 판정의 온전한 기록(위임·상신·회차)은 결재선이 들고, 이 세 칸은
|
|
116
|
+
* **이 커맨드에 대한 마지막 판정**이다 — 커맨드 목록이 결재선을 조회하지 않고도 「누가 언제」를 보여야 한다.
|
|
67
117
|
*/
|
|
68
118
|
@Column({ nullable: true })
|
|
69
119
|
@Field({ nullable: true, description: 'Who approved or rejected it.' })
|
|
@@ -91,6 +141,83 @@ export class TwinCommandRecord {
|
|
|
91
141
|
@Field({ nullable: true, description: 'Reference the adapter returned. Without it a reversal cannot name what to undo.' })
|
|
92
142
|
dispatchRef?: string
|
|
93
143
|
|
|
144
|
+
/*
|
|
145
|
+
* ── 저쪽에서 실제로 무엇이 되었나 ─────────────────────────────────────────
|
|
146
|
+
* 상태(`state`)와 다른 축이다. `acked` 는 「저쪽이 답했다」로 끝나고, 이 셋은 그 뒤에 관측으로
|
|
147
|
+
* 돌아온 사실이다. 되돌아오는 관측이 없는 커넥터의 조치는 이 셋이 영원히 빈다 — 그것이 정상이고,
|
|
148
|
+
* 빈 것은 「효과가 없었다」가 아니라 **「모른다」**다.
|
|
149
|
+
*/
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* 받아들였으나 저쪽이 덧붙인 말 — **실패가 아니고 「끝났다」도 아니다.**
|
|
153
|
+
*
|
|
154
|
+
* MES 가 지시를 접수해 놓고 지시서를 못 붙이면 그 사실이 여기 온다. `error` 와 다른 칸이어야 한다 —
|
|
155
|
+
* `error` 에 적으면 실패로 읽혀 커맨드가 `failed` 로 앉는다.
|
|
156
|
+
*/
|
|
157
|
+
@Column({ nullable: true })
|
|
158
|
+
@Field({ nullable: true, description: 'What the source added while accepting — not a failure, not a completion. Kept verbatim.' })
|
|
159
|
+
dispatchNote?: string
|
|
160
|
+
|
|
161
|
+
/** 저쪽이 보고한 상태 — **해석하지 않고 그대로.** 어느 값이 「나갔다」인지는 현장이 안다. */
|
|
162
|
+
@Column({ nullable: true })
|
|
163
|
+
@Field({ nullable: true, description: 'Status the source reported for this instruction, kept verbatim. The kernel does not interpret it.' })
|
|
164
|
+
effectStatus?: string
|
|
165
|
+
|
|
166
|
+
/** 그 보고를 관측한 시각. 컬럼 타입을 적지 않는다 — 드라이버 방언이라 개발에서 안 잡힌다. */
|
|
167
|
+
@Column({ nullable: true })
|
|
168
|
+
@Field({ nullable: true, description: 'When that report was observed. Empty means no report has been correlated yet.' })
|
|
169
|
+
effectAt?: string
|
|
170
|
+
|
|
171
|
+
/** 몇 번 보고됐나 — 한 번도 안 바뀐 것과 다섯 번 바뀐 것은 다른 일이다. */
|
|
172
|
+
@Column({ type: 'int', nullable: true })
|
|
173
|
+
@Field(type => Int, { nullable: true, description: 'How many reports have been correlated to this instruction.' })
|
|
174
|
+
effectSeen?: number
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* 저쪽이 받으면서 덧붙인 말 중 **다음에 할 일** 한 줄 — 원인은 `dispatchNote` 다.
|
|
178
|
+
*
|
|
179
|
+
* 왜 둘로 나누나: 읽는 사람은 「그래서 내가 뭘 해야 하나」를 먼저 찾는다(UX 레인). 한 문장으로
|
|
180
|
+
* 주면 화면이 자르게 되고, 자르는 규칙이 화면마다 생긴다. 어댑터가 이미 둘로 알고 있으니
|
|
181
|
+
* 붙이지 않고 그대로 받는다.
|
|
182
|
+
*/
|
|
183
|
+
@Column({ nullable: true })
|
|
184
|
+
@Field({ nullable: true, description: 'The next action the source expects, as one line. The cause is dispatchNote.' })
|
|
185
|
+
dispatchNoteNext?: string
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* **다시 해서 될 일인가** — 어댑터가 실패마다 답한다(§`CommandAck.retry`).
|
|
189
|
+
*
|
|
190
|
+
* again 그대로 다시 해 볼 만하다 — 못 닿았거나 저쪽이 잠깐 흔들렸다
|
|
191
|
+
* after-fix 사람이 고친 뒤 그대로 나간다 — 설정 문제
|
|
192
|
+
* never 이 조치로는 영원히 안 된다 — 지시 내용이 틀렸다
|
|
193
|
+
*
|
|
194
|
+
* 가르는 자리는 「조치를 다시 낼 필요가 있나」다. 설정이 틀린 것은 조치가 멀쩡하므로 `after-fix`,
|
|
195
|
+
* 지시 내용이 틀린 것은 그 조치가 영원히 틀렸으므로 `never` 다(인티그레이션 레인 판단).
|
|
196
|
+
*
|
|
197
|
+
* **비어 있으면 `never` 로 읽지 않는다.** 모르는 것과 영원히 안 되는 것은 다르다 — 선언하지 않은
|
|
198
|
+
* 커넥터의 실패를 `never` 로 접으면 고칠 수 있는 것을 사람이 포기한다.
|
|
199
|
+
*/
|
|
200
|
+
@Column({ nullable: true })
|
|
201
|
+
@Field({ nullable: true, description: 'Whether retrying can work: again · after-fix · never. Empty means the adapter did not say.' })
|
|
202
|
+
retry?: string
|
|
203
|
+
|
|
204
|
+
/** 몇 번 넘겨 봤나 — 목록의 「몇 번 해 봤나」가 이 값이다. 첫 시도가 1이다. */
|
|
205
|
+
@Column({ type: 'int', nullable: true })
|
|
206
|
+
@Field(type => Int, { nullable: true, description: 'How many dispatch attempts have been made. The first attempt is 1.' })
|
|
207
|
+
attempts?: number
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* **다음에 넘겨 볼 시각**(ISO) — 물러섬을 표로 든다.
|
|
211
|
+
*
|
|
212
|
+
* 메모리에 두지 않는 이유: 재기동하면 사라지고, 그러면 물러서던 것이 전부 즉시 재시도가 된다.
|
|
213
|
+
* 표에 두면 일꾼이 여러 대여도 같은 답을 본다.
|
|
214
|
+
*
|
|
215
|
+
* 비어 있으면 **다음 주기에 바로** 본다(한 번도 실패하지 않은 것).
|
|
216
|
+
*/
|
|
217
|
+
@Column({ nullable: true })
|
|
218
|
+
@Field({ nullable: true, description: 'When to try dispatching again (ISO 8601). Empty means the next cycle.' })
|
|
219
|
+
nextAttemptAt?: string
|
|
220
|
+
|
|
94
221
|
@Column({ nullable: true })
|
|
95
222
|
@Field({ nullable: true, description: 'Why the dispatch failed. The server sentence, kept as it came.' })
|
|
96
223
|
error?: string
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* **승인하는 사람에게 트윈이 지금 보고 있는 것을 함께 준다.**
|
|
3
|
+
*
|
|
4
|
+
* ── 왜 필요한가 (2026-09-01, 사용자 지적) ────────────────────────────────
|
|
5
|
+
* 사용자가 물었다 — 「보통 AI 는 분명하지 않으면 되묻고, 잘못된 명령이면 확인이나 역제안을 하는데
|
|
6
|
+
* 왜 이 기능에는 그것이 없나.」
|
|
7
|
+
*
|
|
8
|
+
* 확인해 보니 대화하는 AI 는 되묻는다(`twin-ai` §assistant 프롬프트). 그런데 **현장에 지시를 내는
|
|
9
|
+
* 경로는 아무것도 묻지 않는다.** 사람이 「볼트 30개 추가」를 올리면 그대로 결재선에 올라간다. 트윈이
|
|
10
|
+
* 도금 대기가 여섯 배인 것을 알고 있어도 그 말을 할 자리가 없다.
|
|
11
|
+
*
|
|
12
|
+
* 말하는 쪽은 안전한 경로이고, 말 못 하는 쪽이 공장을 움직인다.
|
|
13
|
+
*
|
|
14
|
+
* ── 판정하지 않는다 ──────────────────────────────────────────────────────
|
|
15
|
+
* 트윈이 「이 지시는 틀렸다」고 말하지 않는다. 무엇이 무엇과 어긋나는지 판정하려면 그 규칙을 선언해야
|
|
16
|
+
* 하고, 선언 없이 판정하면 지어낸 규칙이 사람의 지시를 막는다.
|
|
17
|
+
*
|
|
18
|
+
* **트윈은 지금 보이는 것을 내놓고, 승인하는 사람이 판단한다.**
|
|
19
|
+
*
|
|
20
|
+
* ── 이름을 정확히 쓴다 ───────────────────────────────────────────────────
|
|
21
|
+
* 이 값의 이름은 `twinNow` 다. `related` 나 `relevant` 가 아니다 — **이 지시와 관련 있다고 판정한 것이
|
|
22
|
+
* 아니고**, 이 트윈에서 지금 서 있는 것 전부 중 심각한 순서로 몇 개다. 이름이 관련성을 주장하면 승인자가
|
|
23
|
+
* 「트윈이 이 지시 때문에 이것을 골랐다」로 읽는다.
|
|
24
|
+
*/
|
|
25
|
+
import { digestAttentions } from '../../engine/attention-digest.js'
|
|
26
|
+
import { TwinEngine } from '../../engine/twin-engine.js'
|
|
27
|
+
|
|
28
|
+
/** 승인 화면이 읽는 한 줄 — 커널 낱말 그대로. 사람의 말은 표현계층이 만든다. */
|
|
29
|
+
export interface TwinNowAttention {
|
|
30
|
+
id: string
|
|
31
|
+
kind: string
|
|
32
|
+
severity: string
|
|
33
|
+
anchor?: Record<string, string | undefined>
|
|
34
|
+
params?: Record<string, string | number>
|
|
35
|
+
timeStamp?: string
|
|
36
|
+
/** 커널이 권하는 방향 — 실행 가능한 것과 권고가 섞여 있다(`command` 가 있으면 자극이다). */
|
|
37
|
+
recommendedActions?: { code: string; command?: string; args?: unknown }[]
|
|
38
|
+
/** 이 조치가 이것 때문에 올라왔나 — 규칙이 낸 것만 표시된다. */
|
|
39
|
+
isCause?: boolean
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface TwinNow {
|
|
43
|
+
/** 심각한 순서로 몇 개. 전부가 아니다. */
|
|
44
|
+
attentions: TwinNowAttention[]
|
|
45
|
+
/** 지금 서 있는 것의 전체 수 — **잘랐으면 몇을 잘랐는지 알 수 있게.** */
|
|
46
|
+
total: number
|
|
47
|
+
/** 이 조치를 낸 근거가 아직 서 있나 — 규칙이 낸 것에만 값이 있다. */
|
|
48
|
+
causeStanding?: boolean
|
|
49
|
+
/**
|
|
50
|
+
* 트윈의 상태를 못 읽었으면 그 이유.
|
|
51
|
+
*
|
|
52
|
+
* 빈 목록으로 답하지 않는다 — 「지금 아무 문제 없다」와 「트윈이 멈춰 있다」는 다른 사실이고,
|
|
53
|
+
* 승인하는 사람에게 그 차이가 크다.
|
|
54
|
+
*/
|
|
55
|
+
unavailable?: string
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** 승인 화면에 몇 개까지 싣나 — 사람이 읽는 수. 스물여섯 줄은 아무도 안 읽는다. */
|
|
59
|
+
export const TWIN_NOW_LIMIT = 5
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 이 트윈이 지금 무엇을 보고 있나 — 결재선 입력에 실어 보낼 모양으로.
|
|
63
|
+
*
|
|
64
|
+
* `causeRef` 를 주면 그것이 아직 서 있는지도 함께 답한다. 승인하는 사람이 물어야 하는 것 중 가장 중요한
|
|
65
|
+
* 것이 「지금도 그런가」이고, 조건이 사라진 뒤 승인하면 필요 없는 것을 만든다.
|
|
66
|
+
*/
|
|
67
|
+
export function twinNow(domainId: string, instanceId: string, causeRef?: string, limit = TWIN_NOW_LIMIT): TwinNow {
|
|
68
|
+
if (!TwinEngine.owns(domainId, instanceId)) {
|
|
69
|
+
return { attentions: [], total: 0, unavailable: '이 서버가 이 트윈을 들고 있지 않다 — 멈춰 있거나 다른 서버가 돌고 있다' }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const kernel = TwinEngine.kernel(domainId, instanceId)
|
|
73
|
+
const snap = kernel?.getSnapshot?.() ?? TwinEngine.snapshot(domainId, instanceId)
|
|
74
|
+
if (!snap) {
|
|
75
|
+
return { attentions: [], total: 0, unavailable: '이 트윈의 상태를 읽을 수 없다 — 아직 서지 않았거나 멈췄다' }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const all = (snap.attentions ?? []) as TwinNowAttention[]
|
|
79
|
+
|
|
80
|
+
/* 자리 색은 쓰지 않는다 — 승인 화면은 지도가 아니다. 자른 뒤의 수는 반드시 함께 낸다. */
|
|
81
|
+
const digest = digestAttentions(all as any, limit)
|
|
82
|
+
|
|
83
|
+
const attentions = (digest.attentions as TwinNowAttention[]).map(a => ({
|
|
84
|
+
...a,
|
|
85
|
+
...(causeRef && a.id === causeRef ? { isCause: true } : {})
|
|
86
|
+
}))
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
attentions,
|
|
90
|
+
total: digest.attentionTotal,
|
|
91
|
+
/*
|
|
92
|
+
* 근거가 서 있나 — **자른 목록이 아니라 전량에서 찾는다.** 상위 다섯에 안 들었다고 사라진 것이
|
|
93
|
+
* 아니다. 잘린 목록에서 찾으면 심각한 것이 여럿일 때 「사라졌다」로 답한다.
|
|
94
|
+
*/
|
|
95
|
+
...(causeRef ? { causeStanding: all.some(a => a.id === causeRef) } : {})
|
|
96
|
+
}
|
|
97
|
+
}
|