@things-factory/headless-twin 10.0.12 → 10.0.13
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 +57 -2
- package/dist-server/engine/canonical-ingest.js +43 -2
- package/dist-server/engine/canonical-ingest.js.map +1 -1
- package/dist-server/engine/command-routing.js +1 -1
- package/dist-server/engine/command-routing.js.map +1 -1
- package/dist-server/engine/index.d.ts +3 -0
- package/dist-server/engine/index.js +4 -0
- package/dist-server/engine/index.js.map +1 -1
- package/dist-server/engine/ingest-health.d.ts +335 -0
- package/dist-server/engine/ingest-health.js +434 -0
- package/dist-server/engine/ingest-health.js.map +1 -0
- package/dist-server/engine/integration-coverage.d.ts +76 -0
- package/dist-server/engine/integration-coverage.js +73 -0
- package/dist-server/engine/integration-coverage.js.map +1 -0
- package/dist-server/engine/integration-probes.d.ts +65 -0
- package/dist-server/engine/integration-probes.js +100 -0
- package/dist-server/engine/integration-probes.js.map +1 -0
- package/dist-server/engine/integration-runner.d.ts +45 -0
- package/dist-server/engine/integration-runner.js +59 -0
- package/dist-server/engine/integration-runner.js.map +1 -0
- package/dist-server/engine/integration-target-profile.d.ts +57 -0
- package/dist-server/engine/integration-target-profile.js +79 -0
- package/dist-server/engine/integration-target-profile.js.map +1 -0
- package/dist-server/engine/kpi-fold.d.ts +27 -0
- package/dist-server/engine/kpi-fold.js +47 -4
- package/dist-server/engine/kpi-fold.js.map +1 -1
- package/dist-server/engine/kpi-query.js +128 -14
- package/dist-server/engine/kpi-query.js.map +1 -1
- package/dist-server/engine/live-feed-registry.js +2 -16
- package/dist-server/engine/live-feed-registry.js.map +1 -1
- package/dist-server/engine/load-meter.d.ts +1 -1
- package/dist-server/engine/load-meter.js +12 -3
- package/dist-server/engine/load-meter.js.map +1 -1
- package/dist-server/engine/log.d.ts +18 -0
- package/dist-server/engine/log.js +80 -0
- package/dist-server/engine/log.js.map +1 -0
- package/dist-server/engine/loop-lag.d.ts +54 -0
- package/dist-server/engine/loop-lag.js +87 -0
- package/dist-server/engine/loop-lag.js.map +1 -0
- package/dist-server/engine/model-gap.d.ts +108 -0
- package/dist-server/engine/model-gap.js +95 -0
- package/dist-server/engine/model-gap.js.map +1 -0
- package/dist-server/engine/restart-policy.d.ts +3 -3
- package/dist-server/engine/restart-policy.js +4 -4
- package/dist-server/engine/restart-policy.js.map +1 -1
- package/dist-server/engine/runtime-key.js +1 -1
- package/dist-server/engine/runtime-key.js.map +1 -1
- package/dist-server/engine/stage-path.d.ts +83 -0
- package/dist-server/engine/stage-path.js +118 -0
- package/dist-server/engine/stage-path.js.map +1 -0
- package/dist-server/engine/twin-engine.d.ts +352 -16
- package/dist-server/engine/twin-engine.js +1037 -166
- package/dist-server/engine/twin-engine.js.map +1 -1
- package/dist-server/index.js +34 -2
- package/dist-server/index.js.map +1 -1
- package/dist-server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.d.ts +5 -0
- package/dist-server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.js +58 -0
- package/dist-server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.js.map +1 -0
- package/dist-server/migrations/1786200000000-CarryLiveFeedCursor.d.ts +5 -0
- package/dist-server/migrations/1786200000000-CarryLiveFeedCursor.js +55 -0
- package/dist-server/migrations/1786200000000-CarryLiveFeedCursor.js.map +1 -0
- package/dist-server/migrations/index.js +7 -1
- package/dist-server/migrations/index.js.map +1 -1
- package/dist-server/service/index.d.ts +5 -2
- package/dist-server/service/index.js +18 -7
- package/dist-server/service/index.js.map +1 -1
- package/dist-server/service/reference/discovery-result.d.ts +1 -1
- package/dist-server/service/reference/discovery-result.js +1 -1
- package/dist-server/service/reference/discovery-result.js.map +1 -1
- package/dist-server/service/reference/index.d.ts +4 -1
- package/dist-server/service/reference/index.js +6 -1
- package/dist-server/service/reference/index.js.map +1 -1
- package/dist-server/service/reference/reference-adapter.d.ts +184 -3
- package/dist-server/service/reference/reference-adapter.js +28 -4
- package/dist-server/service/reference/reference-adapter.js.map +1 -1
- package/dist-server/service/reference/reference-assessment.d.ts +68 -0
- package/dist-server/service/reference/reference-assessment.js +136 -0
- package/dist-server/service/reference/reference-assessment.js.map +1 -0
- package/dist-server/service/reference/reference-live.d.ts +12 -1
- package/dist-server/service/reference/reference-live.js +116 -11
- package/dist-server/service/reference/reference-live.js.map +1 -1
- package/dist-server/service/reference/reference-master.d.ts +47 -1
- package/dist-server/service/reference/reference-master.js +34 -4
- package/dist-server/service/reference/reference-master.js.map +1 -1
- package/dist-server/service/reference/reference-probe.d.ts +92 -0
- package/dist-server/service/reference/reference-probe.js +186 -0
- package/dist-server/service/reference/reference-probe.js.map +1 -0
- package/dist-server/service/reference/reference-progress-subscription.d.ts +17 -0
- package/dist-server/service/reference/reference-progress-subscription.js +94 -0
- package/dist-server/service/reference/reference-progress-subscription.js.map +1 -0
- package/dist-server/service/reference/reference-progress.d.ts +38 -0
- package/dist-server/service/reference/reference-progress.js +71 -0
- package/dist-server/service/reference/reference-progress.js.map +1 -0
- package/dist-server/service/reference/reference-resolver.js +210 -43
- package/dist-server/service/reference/reference-resolver.js.map +1 -1
- package/dist-server/service/reference/twin-reference.d.ts +32 -0
- package/dist-server/service/reference/twin-reference.js +10 -0
- package/dist-server/service/reference/twin-reference.js.map +1 -1
- package/dist-server/service/twin-control/twin-control-mutation.js +4 -3
- package/dist-server/service/twin-control/twin-control-mutation.js.map +1 -1
- package/dist-server/service/twin-event/journal-count.d.ts +21 -0
- package/dist-server/service/twin-event/journal-count.js +26 -0
- package/dist-server/service/twin-event/journal-count.js.map +1 -0
- package/dist-server/service/twin-event/twin-event-keys.d.ts +9 -0
- package/dist-server/service/twin-event/twin-event-keys.js +15 -18
- package/dist-server/service/twin-event/twin-event-keys.js.map +1 -1
- package/dist-server/service/twin-event/twin-event-type.d.ts +3 -1
- package/dist-server/service/twin-event/twin-event-type.js +18 -1
- package/dist-server/service/twin-event/twin-event-type.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 +9 -1
- package/dist-server/service/twin-event/twin-event.js.map +1 -1
- package/dist-server/service/twin-forecast/twin-forecast-query.js +2 -1
- package/dist-server/service/twin-forecast/twin-forecast-query.js.map +1 -1
- package/dist-server/service/twin-ingest-window/index.d.ts +5 -0
- package/dist-server/service/twin-ingest-window/index.js +10 -0
- package/dist-server/service/twin-ingest-window/index.js.map +1 -0
- package/dist-server/service/twin-ingest-window/twin-ingest-window-query.d.ts +12 -0
- package/dist-server/service/twin-ingest-window/twin-ingest-window-query.js +123 -0
- package/dist-server/service/twin-ingest-window/twin-ingest-window-query.js.map +1 -0
- package/dist-server/service/twin-ingest-window/twin-ingest-window-writer.d.ts +1 -0
- package/dist-server/service/twin-ingest-window/twin-ingest-window-writer.js +51 -0
- package/dist-server/service/twin-ingest-window/twin-ingest-window-writer.js.map +1 -0
- package/dist-server/service/twin-ingest-window/twin-ingest-window.d.ts +16 -0
- package/dist-server/service/twin-ingest-window/twin-ingest-window.js +108 -0
- package/dist-server/service/twin-ingest-window/twin-ingest-window.js.map +1 -0
- package/dist-server/service/twin-instance/twin-instance.js.map +1 -1
- package/dist-server/service/twin-journal/twin-journal-query.js +58 -5
- package/dist-server/service/twin-journal/twin-journal-query.js.map +1 -1
- package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.d.ts +14 -0
- package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js +72 -2
- package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js.map +1 -1
- package/dist-server/service/twin-metrics/twin-metrics-query.d.ts +1 -0
- package/dist-server/service/twin-metrics/twin-metrics-query.js +78 -3
- package/dist-server/service/twin-metrics/twin-metrics-query.js.map +1 -1
- package/dist-server/service/twin-model/axis-journal-evidence.d.ts +2 -0
- package/dist-server/service/twin-model/axis-journal-evidence.js +89 -0
- package/dist-server/service/twin-model/axis-journal-evidence.js.map +1 -0
- package/dist-server/service/twin-model/epcis-coverage.d.ts +1 -1
- package/dist-server/service/twin-model/epcis-coverage.js +19 -7
- package/dist-server/service/twin-model/epcis-coverage.js.map +1 -1
- package/dist-server/service/twin-model/iec61850-coverage.d.ts +1 -1
- package/dist-server/service/twin-model/iec61850-coverage.js +18 -7
- package/dist-server/service/twin-model/iec61850-coverage.js.map +1 -1
- package/dist-server/service/twin-model/isa95-coverage.d.ts +14 -0
- package/dist-server/service/twin-model/isa95-coverage.js +23 -9
- package/dist-server/service/twin-model/isa95-coverage.js.map +1 -1
- package/dist-server/service/twin-model/item-ref.d.ts +24 -0
- package/dist-server/service/twin-model/item-ref.js +88 -0
- package/dist-server/service/twin-model/item-ref.js.map +1 -0
- package/dist-server/service/twin-model/name-index.d.ts +36 -0
- package/dist-server/service/twin-model/name-index.js +116 -0
- package/dist-server/service/twin-model/name-index.js.map +1 -0
- package/dist-server/service/twin-model/project-structure.js +1 -1
- package/dist-server/service/twin-model/project-structure.js.map +1 -1
- package/dist-server/service/twin-model/status-tally.d.ts +9 -0
- package/dist-server/service/twin-model/status-tally.js +37 -0
- package/dist-server/service/twin-model/status-tally.js.map +1 -0
- package/dist-server/service/twin-model/twin-lineage-query.js +40 -12
- package/dist-server/service/twin-model/twin-lineage-query.js.map +1 -1
- package/dist-server/service/twin-model/twin-model-item-query.js +38 -39
- package/dist-server/service/twin-model/twin-model-item-query.js.map +1 -1
- package/dist-server/service/twin-model/twin-model-query.js +156 -4
- package/dist-server/service/twin-model/twin-model-query.js.map +1 -1
- package/dist-server/service/twin-model/twin-model-tree-query.js +7 -0
- package/dist-server/service/twin-model/twin-model-tree-query.js.map +1 -1
- package/dist-server/service/twin-readiness/index.d.ts +2 -0
- package/dist-server/service/twin-readiness/index.js +6 -0
- package/dist-server/service/twin-readiness/index.js.map +1 -0
- package/dist-server/service/twin-readiness/twin-readiness-query.d.ts +3 -0
- package/dist-server/service/twin-readiness/twin-readiness-query.js +103 -0
- package/dist-server/service/twin-readiness/twin-readiness-query.js.map +1 -0
- package/dist-server/service/twin-space/twin-space-resolver.d.ts +2 -2
- package/dist-server/service/twin-space/twin-space-resolver.js +5 -4
- package/dist-server/service/twin-space/twin-space-resolver.js.map +1 -1
- package/dist-shared/entity-delta.d.ts +23 -3
- package/dist-shared/entity-delta.js +12 -8
- package/dist-shared/entity-delta.js.map +1 -1
- package/dist-shared/kpi-broadcast.js +1 -1
- package/dist-shared/kpi-broadcast.js.map +1 -1
- package/dist-shared/touched-items.d.ts +7 -0
- package/dist-shared/touched-items.js +80 -0
- package/dist-shared/touched-items.js.map +1 -0
- package/package.json +7 -7
- package/server/engine/canonical-ingest.ts +92 -7
- package/server/engine/command-routing.ts +1 -1
- package/server/engine/index.ts +4 -0
- package/server/engine/ingest-health.ts +704 -0
- package/server/engine/integration-coverage.ts +147 -0
- package/server/engine/integration-probes.ts +144 -0
- package/server/engine/integration-runner.ts +95 -0
- package/server/engine/integration-target-profile.ts +103 -0
- package/server/engine/kpi-fold.ts +72 -4
- package/server/engine/kpi-query.ts +129 -15
- package/server/engine/live-feed-registry.ts +2 -1
- package/server/engine/load-meter.ts +12 -3
- package/server/engine/log.ts +72 -0
- package/server/engine/loop-lag.ts +120 -0
- package/server/engine/model-gap.ts +168 -0
- package/server/engine/restart-policy.ts +4 -4
- package/server/engine/runtime-key.ts +1 -1
- package/server/engine/stage-path.ts +172 -0
- package/server/engine/twin-engine.ts +1135 -162
- package/server/index.ts +35 -2
- package/server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.ts +54 -0
- package/server/migrations/1786200000000-CarryLiveFeedCursor.ts +53 -0
- package/server/migrations/index.ts +7 -1
- package/server/service/index.ts +11 -0
- package/server/service/reference/discovery-result.ts +1 -1
- package/server/service/reference/index.ts +6 -1
- package/server/service/reference/reference-adapter.ts +197 -5
- package/server/service/reference/reference-assessment.ts +215 -0
- package/server/service/reference/reference-live.ts +123 -11
- package/server/service/reference/reference-master.ts +64 -4
- package/server/service/reference/reference-probe.ts +264 -0
- package/server/service/reference/reference-progress-subscription.ts +73 -0
- package/server/service/reference/reference-progress.ts +95 -0
- package/server/service/reference/reference-resolver.ts +200 -12
- package/server/service/reference/twin-reference.ts +39 -1
- package/server/service/twin-control/twin-control-mutation.ts +4 -3
- package/server/service/twin-event/journal-count.ts +53 -0
- package/server/service/twin-event/twin-event-keys.ts +16 -1
- package/server/service/twin-event/twin-event-type.ts +26 -2
- package/server/service/twin-event/twin-event.ts +7 -0
- package/server/service/twin-forecast/twin-forecast-query.ts +2 -1
- package/server/service/twin-ingest-window/index.ts +7 -0
- package/server/service/twin-ingest-window/twin-ingest-window-query.ts +125 -0
- package/server/service/twin-ingest-window/twin-ingest-window-writer.ts +57 -0
- package/server/service/twin-ingest-window/twin-ingest-window.ts +113 -0
- package/server/service/twin-instance/twin-instance.ts +1 -1
- package/server/service/twin-journal/twin-journal-query.ts +59 -5
- package/server/service/twin-lifecycle/twin-lifecycle-mutation.ts +77 -3
- package/server/service/twin-metrics/twin-metrics-query.ts +81 -3
- package/server/service/twin-model/axis-journal-evidence.ts +57 -0
- package/server/service/twin-model/epcis-coverage.ts +2 -8
- package/server/service/twin-model/iec61850-coverage.ts +2 -8
- package/server/service/twin-model/isa95-coverage.ts +22 -9
- package/server/service/twin-model/item-ref.ts +80 -0
- package/server/service/twin-model/name-index.ts +94 -0
- package/server/service/twin-model/project-structure.ts +1 -1
- package/server/service/twin-model/status-tally.ts +37 -0
- package/server/service/twin-model/twin-lineage-query.ts +38 -9
- package/server/service/twin-model/twin-model-item-query.ts +26 -27
- package/server/service/twin-model/twin-model-query.ts +155 -4
- package/server/service/twin-model/twin-model-tree-query.ts +7 -0
- package/server/service/twin-readiness/index.ts +3 -0
- package/server/service/twin-readiness/twin-readiness-query.ts +96 -0
- package/server/service/twin-space/twin-space-resolver.ts +5 -4
- package/shared/entity-delta.ts +31 -7
- package/shared/kpi-broadcast.ts +1 -1
- package/shared/touched-items.ts +73 -0
- package/test/axis-journal-evidence.test.ts +71 -0
- package/test/axis-read.test.ts +101 -1
- package/test/boot-resume.test.ts +92 -26
- package/test/broadcast-cost-baseline.test.ts +200 -0
- package/test/broadcast-period.test.ts +58 -0
- package/test/canonical-ingest-vocabularies.test.ts +36 -1
- package/test/canonical-quantity-door.test.ts +61 -0
- package/test/command-routing.test.ts +1 -1
- package/test/declared-location-types.test.ts +89 -0
- package/test/discovery-result.test.ts +1 -1
- package/test/duration-estimators.test.ts +1 -1
- package/test/entity-delta.test.ts +2 -2
- package/test/ingest-bench.test.ts +3 -3
- package/test/ingest-health-engine.test.ts +248 -0
- package/test/ingest-health-wiring.test.ts +119 -0
- package/test/ingest-health.test.ts +306 -0
- package/test/ingest-history.test.ts +247 -0
- package/test/integration-probes.test.ts +103 -0
- package/test/integration-runner.test.ts +95 -0
- package/test/item-ref.test.ts +78 -0
- package/test/journal-read-discipline.test.ts +177 -0
- package/test/journal-retention.test.ts +133 -0
- package/test/journal-sort-axis.test.ts +142 -0
- package/test/journal-write-door.test.ts +110 -0
- package/test/journal-write-trend.test.ts +115 -0
- package/test/kernel-kind-guard.test.ts +4 -4
- package/test/kpi-fold.test.ts +90 -3
- package/test/kpi-query-bench.test.ts +2 -2
- package/test/lineage-survives-restart.test.ts +17 -1
- package/test/live-cursor-wiring.test.ts +87 -0
- package/test/live-feed-registry.test.ts +9 -1
- package/test/live-kernel-facts.test.ts +7 -7
- package/test/live-mirror-parity.test.ts +6 -0
- package/test/load-meter.test.ts +29 -15
- package/test/local-declarations.test.ts +1 -1
- package/test/log-stamp.test.ts +59 -0
- package/test/loop-lag.test.ts +82 -0
- package/test/model-gap.test.ts +153 -0
- package/test/oee-accumulator.test.ts +4 -0
- package/test/operational-vocabulary.test.ts +4 -4
- package/test/read-failure-visible.test.ts +145 -0
- package/test/reference-grounding.test.ts +70 -0
- package/test/resolve-ts-siblings.mjs +52 -0
- package/test/restart-policy.test.ts +7 -7
- package/test/revision-axis.test.ts +93 -0
- package/test/runtime-key.test.ts +2 -2
- package/test/scale-twin-bench.test.ts +2 -2
- package/test/spec-coverage.test.ts +1 -1
- package/test/stage-path.test.ts +95 -0
- package/test/standard-coverage.test.ts +15 -3
- package/test/status-tally.test.ts +55 -0
- package/test/structure-revision-db.test.ts +4 -0
- package/test/tenant-registry-db.test.ts +1 -1
- package/test/time-range.test.ts +152 -0
- package/test/touched-items.test.ts +61 -0
- package/test/twin-event-keys.test.ts +10 -2
- package/test/twin-model-item-db.test.ts +2 -0
- package/test/warm-start-seam.test.ts +4 -0
- package/test/yield-loop.test.ts +57 -2
- package/tsconfig.shared.tsbuildinfo +1 -1
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { pubsub } from '@things-factory/shell'
|
|
2
|
+
import { HOST_STEPS, type ReferenceProgress } from './reference-progress.js'
|
|
3
|
+
import { assessMaster } from './reference-assessment.js'
|
|
1
4
|
import { Arg, Ctx, Mutation, Query, Resolver, Directive} from 'type-graphql'
|
|
2
5
|
|
|
3
6
|
import { ScalarObject, getRepository } from '@things-factory/shell'
|
|
@@ -207,8 +210,37 @@ export class TwinReferenceResolver {
|
|
|
207
210
|
error: `twin "${instanceId}" does not remember where it came from — it cannot be re-read. Re-create it from its template or reference so the origin is recorded.`
|
|
208
211
|
}
|
|
209
212
|
|
|
213
|
+
/*
|
|
214
|
+
* 진행을 알리는 통로 — 만들 때와 **같은 것**이다. 레퍼런스에서 온 트윈만 알린다(템플릿에서 온
|
|
215
|
+
* 트윈은 원본을 읽지 않으므로 알릴 단계가 없다).
|
|
216
|
+
*
|
|
217
|
+
* 분모는 그 어댑터가 선언한 단계 수다(`masterSteps`) + 호스트 단계. 선언하지 않은 어댑터는
|
|
218
|
+
* `total` 이 없고, 그때 화면은 퍼센트를 만들지 않는다 — 모르는 진행률을 지어내지 않는다.
|
|
219
|
+
*/
|
|
220
|
+
const progressSource: string | undefined = origin.kind === 'reference' ? origin.source : undefined
|
|
221
|
+
const refRow = progressSource
|
|
222
|
+
? await getRepository(TwinReference).findOne({ where: { domain: { id: domainId }, source: progressSource } })
|
|
223
|
+
: null
|
|
224
|
+
const adapterForSteps = refRow ? getAdapter(refRow.adapterType) : undefined
|
|
225
|
+
const declaredSteps = adapterForSteps?.masterSteps?.length
|
|
226
|
+
const stepTotal = declaredSteps === undefined ? undefined : declaredSteps + HOST_STEPS.length
|
|
227
|
+
const siteId = origin.siteId ?? instanceId
|
|
228
|
+
let lastDone = 0
|
|
229
|
+
let lastStep: ReferenceProgress['step'] = (adapterForSteps?.masterSteps?.[0] ?? 'provision') as ReferenceProgress['step']
|
|
230
|
+
const emit = (p: Omit<ReferenceProgress, 'siteCount' | 'siteId' | 'siteIndex'>) => {
|
|
231
|
+
if (!progressSource) return
|
|
232
|
+
lastDone = p.done
|
|
233
|
+
lastStep = p.step
|
|
234
|
+
pubsub.publish('twin-reference-progress', {
|
|
235
|
+
twinReferenceProgress: { ...p, siteId, siteIndex: 0, siteCount: 1, total: p.total ?? stepTotal, source: progressSource }
|
|
236
|
+
})
|
|
237
|
+
}
|
|
238
|
+
|
|
210
239
|
try {
|
|
211
|
-
const master = await this.masterFromOrigin(domainId, origin, instanceId
|
|
240
|
+
const master = await this.masterFromOrigin(domainId, origin, instanceId, st =>
|
|
241
|
+
emit({ step: st.key, done: st.done, total: stepTotal, detail: st.detail })
|
|
242
|
+
)
|
|
243
|
+
emit({ step: 'provision', done: declaredSteps ?? 0, total: stepTotal })
|
|
212
244
|
/*
|
|
213
245
|
* **어느 현장의 트윈이었나를 지킨다** — 다시 읽기는 구조만 새로 그리는 일이고, 현장 소속은 그때
|
|
214
246
|
* 사람이 고른 것이다. 기억이 없는 옛 트윈은 **지금 있는 현장**을 그대로 쓴다(빈손으로 두면 템플릿
|
|
@@ -223,14 +255,51 @@ export class TwinReferenceResolver {
|
|
|
223
255
|
keepSpace ? { spaceId: keepSpace } : undefined,
|
|
224
256
|
context.state.user
|
|
225
257
|
)
|
|
258
|
+
emit({ step: 'start', done: (declaredSteps ?? 0) + 1, total: stepTotal })
|
|
259
|
+
/*
|
|
260
|
+
* **총평도 새로 남긴다.** 다시 읽었는데 총평이 옛것이면 화면이 지난 읽기를 현재로 보여 준다 —
|
|
261
|
+
* 그 표를 보고 사람이 원본을 고치러 간다.
|
|
262
|
+
*/
|
|
263
|
+
if (refRow && progressSource) {
|
|
264
|
+
const prevSpec = (refRow.scopeSpec as any) ?? {}
|
|
265
|
+
await getRepository(TwinReference).update(
|
|
266
|
+
{ id: refRow.id },
|
|
267
|
+
{
|
|
268
|
+
scopeSpec: { ...prevSpec, assessments: { ...(prevSpec.assessments ?? {}), [siteId]: assessMaster(master, siteId) } }
|
|
269
|
+
} as any
|
|
270
|
+
)
|
|
271
|
+
}
|
|
226
272
|
return { ok: true, instanceId: res.instanceId, spaceId: res.spaceId, warnings: res.warnings }
|
|
227
273
|
} catch (e: any) {
|
|
274
|
+
/* 막힌 자리를 그대로 알린다 — 「끝낸 수」의 다음 단계다(만들 때와 같은 규칙). */
|
|
275
|
+
emit({
|
|
276
|
+
step: (adapterForSteps?.masterSteps?.[lastDone] ?? lastStep) as ReferenceProgress['step'],
|
|
277
|
+
done: lastDone,
|
|
278
|
+
total: stepTotal,
|
|
279
|
+
failed: true,
|
|
280
|
+
error: e?.message ?? String(e)
|
|
281
|
+
})
|
|
228
282
|
return refusal(context, e, 'resync failed')
|
|
229
283
|
}
|
|
230
284
|
}
|
|
231
285
|
|
|
232
286
|
/** 기억해 둔 원천에서 마스터를 다시 얻는다 — 어댑터든 템플릿이든 이 한 곳에서 갈린다. */
|
|
233
|
-
private async masterFromOrigin(
|
|
287
|
+
private async masterFromOrigin(
|
|
288
|
+
domainId: string,
|
|
289
|
+
origin: any,
|
|
290
|
+
instanceId: string,
|
|
291
|
+
/*
|
|
292
|
+
* **다시 읽기도 진행을 보여야 한다** (2026-08-22).
|
|
293
|
+
*
|
|
294
|
+
* 만들 때는 8단계가 화면에 흐르는데 다시 읽을 때는 아무것도 없었다 — 사용자가 버튼을 누르고
|
|
295
|
+
* 「진행되고 있는지 확인할 수가 없다」고 했다. 실제로 chef 는 원본 읽기가 14~21초이고 그 사이
|
|
296
|
+
* 화면에 아무 변화가 없다.
|
|
297
|
+
*
|
|
298
|
+
* 통로는 만들 때와 **같은 것**이다(`twin-reference-progress`). 화면도 같은 표시를 쓴다 —
|
|
299
|
+
* 두 벌을 만들면 한쪽만 고쳐지고 사람은 같은 일이 다르게 보이는 것을 결함으로 읽는다.
|
|
300
|
+
*/
|
|
301
|
+
onStep?: (s: { key: any; done: number; total?: number; detail?: string }) => void
|
|
302
|
+
): Promise<any> {
|
|
234
303
|
if (origin.kind === 'template') {
|
|
235
304
|
const tpl = getTemplate(origin.templateId)
|
|
236
305
|
if (!tpl) throw new Error(`unknown template: ${origin.templateId} — this twin cannot be rebuilt`)
|
|
@@ -267,7 +336,7 @@ export class TwinReferenceResolver {
|
|
|
267
336
|
* 다른 현장의 구조를 이 트윈에 덮는 것은 오류가 아니라 조용히 틀린 공장으로 나타난다.
|
|
268
337
|
*/
|
|
269
338
|
const picked = pickOriginSite(sites, origin)
|
|
270
|
-
if (picked.site) return { ...(await adapter.fetchMaster(cfg, picked.site)), origin }
|
|
339
|
+
if (picked.site) return { ...(await adapter.fetchMaster(cfg, picked.site, onStep)), origin }
|
|
271
340
|
if (picked.reason === 'site-gone')
|
|
272
341
|
throw new Error(
|
|
273
342
|
`reference "${origin.source}" no longer reports site "${origin.siteId}" — refusing to overwrite this twin with a different site`
|
|
@@ -368,8 +437,19 @@ export class TwinReferenceResolver {
|
|
|
368
437
|
async twinReferences(@Ctx() context: ResolverContext): Promise<any[]> {
|
|
369
438
|
const domainId = context.state.domain.id
|
|
370
439
|
const rows = await getRepository(TwinReference).find({ where: { domain: { id: domainId } } })
|
|
371
|
-
|
|
372
|
-
|
|
440
|
+
/*
|
|
441
|
+
* provenance 라이브 대조 — 지워진 트윈은 producedCount 에서 자동 제외(상태 동기의 lazy 방식).
|
|
442
|
+
*
|
|
443
|
+
* **id 만 읽는다** (2026-08-21). 예전에는 인스턴스 행 전체를 읽었고, 그 행에는 **모델 JSON** 이
|
|
444
|
+
* 들어 있다(자리 수백 개 + 발자국 좌표). 그래서 이 목록을 여는 데 수 메가바이트를 읽고 파싱했고,
|
|
445
|
+
* 화면은 그동안 「아직 연결된 시스템이 없습니다」를 보이고 있었다 — 사용자에게는 「없다」는 답으로
|
|
446
|
+
* 읽힌다. 여기서 필요한 것은 살아 있는 id 집합 하나다.
|
|
447
|
+
*/
|
|
448
|
+
const liveIds = new Set(
|
|
449
|
+
(await getRepository(TwinInstance).find({ where: { domain: { id: domainId } }, select: ['instanceId'] })).map(
|
|
450
|
+
i => i.instanceId
|
|
451
|
+
)
|
|
452
|
+
)
|
|
373
453
|
return rows.map(r => {
|
|
374
454
|
const produced = (((r.scopeSpec as any)?.produced ?? []) as any[]).filter(p => liveIds.has(p.instanceId))
|
|
375
455
|
return {
|
|
@@ -445,15 +525,37 @@ export class TwinReferenceResolver {
|
|
|
445
525
|
const secretKeys = new Set((getAdapter(r.adapterType)?.meta?.configSchema ?? []).filter(f => f.secret).map(f => f.key))
|
|
446
526
|
const cfg: any = {}
|
|
447
527
|
for (const [k, v] of Object.entries((r.connectionConfig as any) ?? {})) cfg[k] = secretKeys.has(k) ? (v ? '••••••' : '') : v
|
|
528
|
+
/*
|
|
529
|
+
* **이 연결이 만든 트윈 id 를 함께 낸다.** 상세 화면이 「이 연결이 잘 동기화되고 있나」를 물으려면
|
|
530
|
+
* 어느 트윈을 물어야 하는지 알아야 한다. 예전에는 개수(`producedCount`)만 있어서 화면이 id 를
|
|
531
|
+
* 알 방법이 없었다.
|
|
532
|
+
*/
|
|
533
|
+
const producedInstanceIds = [
|
|
534
|
+
...new Set(
|
|
535
|
+
((r.scopeSpec as any)?.produced ?? [])
|
|
536
|
+
.map((p: any) => p?.instanceId)
|
|
537
|
+
.filter((x: any): x is string => typeof x === 'string' && !!x)
|
|
538
|
+
)
|
|
539
|
+
]
|
|
448
540
|
return {
|
|
449
541
|
source: r.source, system: r.system, siteName: r.siteName, description: r.description,
|
|
450
|
-
adapterType: r.adapterType, connectionConfig: cfg, status: r.status, lastSyncedAt: r.lastSyncedAt, lastError: r.lastError
|
|
542
|
+
adapterType: r.adapterType, connectionConfig: cfg, status: r.status, lastSyncedAt: r.lastSyncedAt, lastError: r.lastError,
|
|
543
|
+
producedInstanceIds
|
|
451
544
|
}
|
|
452
545
|
}
|
|
453
546
|
|
|
454
|
-
@Query(returns => [ScalarObject], { description: 'List reference connector metadata (label, domain, config schema, availability) for the connect UI.' })
|
|
547
|
+
@Query(returns => [ScalarObject], { description: 'List reference connector metadata (label, domain, config schema, availability, capabilities) for the connect UI.' })
|
|
455
548
|
referenceAdapters(): any[] {
|
|
456
|
-
|
|
549
|
+
/*
|
|
550
|
+
* **능력을 함께 낸다** (2026-08-21).
|
|
551
|
+
*
|
|
552
|
+
* `capabilities` 는 어댑터 뿌리에 있고 `meta` 에는 없었다. 그래서 연결 화면이 「이 원본이 라이브를
|
|
553
|
+
* 낼 수 있나」를 알 수 없었고, 트윈을 만들 때 `live` 를 말하지 못해 **재기동 정책이 없다고 거절**됐다
|
|
554
|
+
* (실 연동에서 그것에 막혔다 — 미러는 정의상 `resync` 이므로 `live` 하나로 정해진다).
|
|
555
|
+
*
|
|
556
|
+
* 화면이 그 사실을 알아야 정책을 사람에게 물을지 스스로 정할지 가를 수 있다.
|
|
557
|
+
*/
|
|
558
|
+
return listAdapters().map(a => ({ type: a.type, ...(a.meta ?? {}), capabilities: a.capabilities ?? [] }))
|
|
457
559
|
}
|
|
458
560
|
|
|
459
561
|
@Directive('@privilege(category: "twin", privilege: "mutation", domainOwnerGranted: true, superUserGranted: true)')
|
|
@@ -601,9 +703,60 @@ export class TwinReferenceResolver {
|
|
|
601
703
|
if (refusal) return { ok: false, error: refusal.message, errorCode: refusal.code, errorParams: refusal.params }
|
|
602
704
|
const instances: any[] = []
|
|
603
705
|
const failed: any[] = []
|
|
604
|
-
|
|
706
|
+
/*
|
|
707
|
+
* **진행을 구독으로 흘린다** (2026-08-21).
|
|
708
|
+
*
|
|
709
|
+
* 마스터 읽기는 원본에 여러 질의를 보내는 긴 작업이고, 화면은 그 사이 아무것도 알 수 없었다.
|
|
710
|
+
* 단계를 아는 것은 어댑터뿐이므로 어댑터가 말하고(표준 낱말), 호스트가 자기 두 단계(생성·기동)를
|
|
711
|
+
* 더해 흘린다.
|
|
712
|
+
*
|
|
713
|
+
* **분모는 어댑터가 선언한 것**이다(`masterSteps`). 선언하지 않은 어댑터는 `total` 이 없고, 그때
|
|
714
|
+
* 화면은 퍼센트를 만들지 않는다 — 모르는 진행률을 지어내지 않는다.
|
|
715
|
+
*/
|
|
716
|
+
const declared = adapter.masterSteps?.length
|
|
717
|
+
const stepTotal = declared === undefined ? undefined : declared + HOST_STEPS.length
|
|
718
|
+
const emit = (p: Omit<ReferenceProgress, 'siteCount'>) =>
|
|
719
|
+
pubsub.publish('twin-reference-progress', {
|
|
720
|
+
twinReferenceProgress: { ...p, siteCount: sites.length, total: p.total ?? stepTotal, source }
|
|
721
|
+
})
|
|
722
|
+
|
|
723
|
+
/*
|
|
724
|
+
* **읽은 뒤의 총평을 사이트마다 남긴다** (2026-08-22).
|
|
725
|
+
*
|
|
726
|
+
* 총평은 읽는 순간에만 있었다 — 대화상자를 닫으면 사라지고, 다시 보려면 같은 읽기를 또 해야 했다.
|
|
727
|
+
* 그런데 사람이 그것을 묻는 자리는 「모델 살펴보기」다(트윈을 보다가 「이건 왜 비었나」를 묻는다).
|
|
728
|
+
* 그래서 레퍼런스 행에 남겨 그 화면이 언제든 읽게 한다.
|
|
729
|
+
*
|
|
730
|
+
* 사실만 담는다(`assessMaster`) — 문장은 화면이 만들고, 더 알고 싶을 때 AI 가 이 사실만 근거로
|
|
731
|
+
* 서술한다. 다시 읽으면 그 사이트의 총평이 새것으로 바뀐다(지난 읽기의 총평을 현재로 보이게
|
|
732
|
+
* 두지 않는다).
|
|
733
|
+
*/
|
|
734
|
+
const assessments: Record<string, unknown> = {}
|
|
735
|
+
|
|
736
|
+
for (const [siteIndex, site] of sites.entries()) {
|
|
737
|
+
/*
|
|
738
|
+
* **실패를 실제로 막힌 단계에 적는다** (2026-08-22).
|
|
739
|
+
*
|
|
740
|
+
* 예전에는 실패를 무조건 `provision` 으로 적었다. 그래서 원본을 읽다 끊긴 것도 화면에는
|
|
741
|
+
* 「트윈 생성에서 실패」로 떴다 — 사람이 원인을 엉뚱한 자리에서 찾는다(실제로 그렇게 됐다).
|
|
742
|
+
*
|
|
743
|
+
* 규약이 답을 준다: `done` 은 **끝낸 단계 수**다(`ReferenceProgress.done`). 그러면 지금 막힌 것은
|
|
744
|
+
* 그 다음 단계, 즉 `masterSteps[done]` 이다. 그 표를 넘어서면(호스트 단계) 마지막으로 알린 것을 쓴다.
|
|
745
|
+
*
|
|
746
|
+
* 이 규칙은 두 가지 보고 방식에 함께 맞는다 — 단계 **시작**에 알리는 어댑터(`done` 이 아직 0)와
|
|
747
|
+
* 단계 **완료**에 알리는 어댑터(`done` 이 1 늘어난다) 모두 「끝낸 수」만 지키면 된다.
|
|
748
|
+
*/
|
|
749
|
+
let lastDone = 0
|
|
750
|
+
let lastStep: ReferenceProgress['step'] = (adapter.masterSteps?.[0] ?? 'provision') as ReferenceProgress['step']
|
|
751
|
+
const track = (p: Omit<ReferenceProgress, 'siteCount'>) => {
|
|
752
|
+
lastDone = p.done
|
|
753
|
+
lastStep = p.step
|
|
754
|
+
emit(p)
|
|
755
|
+
}
|
|
605
756
|
try {
|
|
606
|
-
const master = await adapter.fetchMaster(cfg, site
|
|
757
|
+
const master = await adapter.fetchMaster(cfg, site, st =>
|
|
758
|
+
track({ siteId: site.siteId, siteIndex, step: st.key, done: st.done, total: stepTotal, detail: st.detail })
|
|
759
|
+
)
|
|
607
760
|
/* 어느 레퍼런스의 **어느 사이트**에서 왔는지 남긴다 — 다시 읽기가 같은 어댑터·같은 사이트로 돌아오려면
|
|
608
761
|
사이트 정체가 있어야 한다(`source` 는 참조 이름이라 사이트 전부에 같다). */
|
|
609
762
|
/*
|
|
@@ -611,7 +764,9 @@ export class TwinReferenceResolver {
|
|
|
611
764
|
* 미러는 정의상 `resync` 다(외부가 진실이므로 우리 과거를 이어 붙이지 않는다). 시뮬로 띄우려면
|
|
612
765
|
* 스스로 선언해야 한다 — 그 자리에서 우리가 고르면 재기동마다 이력을 지우는 트윈이 조용히 생긴다.
|
|
613
766
|
*/
|
|
767
|
+
assessments[site.siteId] = assessMaster(master, site.siteId)
|
|
614
768
|
const policy = readRestartPolicy(restartPolicy ?? (live ? 'resync' : undefined), `importTwinReference("${source}")`)
|
|
769
|
+
track({ siteId: site.siteId, siteIndex, step: 'provision', done: declared ?? 0, total: stepTotal })
|
|
615
770
|
const res = await TwinEngine.ingestMaster(
|
|
616
771
|
domainId,
|
|
617
772
|
{ ...master, origin: { kind: 'reference', source, siteId: site.siteId, ...(spaceId ? { spaceId } : {}) } },
|
|
@@ -619,15 +774,27 @@ export class TwinReferenceResolver {
|
|
|
619
774
|
{ spaceId },
|
|
620
775
|
context.state.user
|
|
621
776
|
)
|
|
777
|
+
track({ siteId: site.siteId, siteIndex, step: 'start', done: (declared ?? 0) + 1, total: stepTotal })
|
|
622
778
|
if (start !== false) {
|
|
623
779
|
// live=true + 어댑터가 openLiveFeed 지원 → mirror(실 이벤트 미러). 아니면 sim 기동(fallback).
|
|
624
|
-
|
|
780
|
+
/* 읽기 커서를 저장할 자리를 함께 넘긴다 — 레퍼런스의 이름은 호스트가 붙이는 것이고
|
|
781
|
+
커넥터는 그것을 알 수 없다(§`startReferenceLiveFeed`). */
|
|
782
|
+
const wentLive = live
|
|
783
|
+
? await startReferenceLiveFeed(domainId, ref.adapterType, cfg, site, res.instanceId, ref.source)
|
|
784
|
+
: false
|
|
625
785
|
if (!wentLive) await TwinEngine.startFromRegistry(domainId, res.instanceId)
|
|
626
786
|
instances.push({ siteId: site.siteId, instanceId: res.instanceId, spaceId: res.spaceId, mode: wentLive ? 'live' : 'sim', warnings: res.warnings })
|
|
627
787
|
} else {
|
|
628
788
|
instances.push({ siteId: site.siteId, instanceId: res.instanceId, spaceId: res.spaceId, mode: 'provisioned', warnings: res.warnings })
|
|
629
789
|
}
|
|
630
790
|
} catch (e: any) {
|
|
791
|
+
/*
|
|
792
|
+
* **실패한 단계를 그대로 알린다.** 화면이 진행 표시를 지우지 않고 그 자리를 붉게 두어야
|
|
793
|
+
* 사람이 「어디까지 갔는지」를 안다. 막힌 자리는 「끝낸 수」의 다음 단계다(위 주석).
|
|
794
|
+
*/
|
|
795
|
+
const at = (adapter.masterSteps?.[lastDone] ?? lastStep) as ReferenceProgress['step']
|
|
796
|
+
emit({ siteId: site.siteId, siteIndex, step: at, done: lastDone, total: stepTotal, failed: true, error: e?.message ?? String(e) })
|
|
797
|
+
|
|
631
798
|
failed.push({ siteId: site.siteId, error: e?.message ?? 'failed' })
|
|
632
799
|
}
|
|
633
800
|
}
|
|
@@ -639,6 +806,10 @@ export class TwinReferenceResolver {
|
|
|
639
806
|
{
|
|
640
807
|
status: 'connected',
|
|
641
808
|
lastSyncedAt: new Date().toISOString(),
|
|
809
|
+
/* 못 옮긴 값을 **세어 남긴다**. 아무 인스턴스도 보고하지 않았으면 `null` — 「없었다」로 위장하지 않는다. */
|
|
810
|
+
lastWarningCount: instances.some((i: any) => Array.isArray(i.warnings))
|
|
811
|
+
? instances.reduce((n: number, i: any) => n + (Array.isArray(i.warnings) ? i.warnings.length : 0), 0)
|
|
812
|
+
: null,
|
|
642
813
|
/*
|
|
643
814
|
* **영어 canonical 로 남긴다.** 이 값은 DB 에 저장되고 연결 화면이 그대로 뿌린다 — 예전에는
|
|
644
815
|
* 한글 산문(`N개 사이트 실패`)이라 다섯 언어 화면에 한글이 새어 나갔다. 어댑터가 주는 사유도
|
|
@@ -646,7 +817,24 @@ export class TwinReferenceResolver {
|
|
|
646
817
|
* 어느 사이트가 왜 실패했는지는 `failed[]` 가 답한다 — 화면이 그것을 보여 준다.
|
|
647
818
|
*/
|
|
648
819
|
lastError: failed.length ? `${failed.length} of ${sites.length} site(s) failed: ${failed.map((f: any) => f.siteId).join(', ')}` : null,
|
|
649
|
-
|
|
820
|
+
/*
|
|
821
|
+
* ── 실패 사유를 **남긴다** (2026-08-21) ────────────────────────────────
|
|
822
|
+
*
|
|
823
|
+
* 사유는 대화상자에만 있었다. 닫으면 사라지고, 다시 보려면 같은 실패를 다시 만들어야 했다 —
|
|
824
|
+
* 실제로 「승화푸드」 연결이 그 상태였다(요약 한 줄만 남고 원인은 어디에도 없었다).
|
|
825
|
+
*
|
|
826
|
+
* `scopeSpec` 은 이미 이 레퍼런스의 부속 사실을 담는 자리이므로(`produced`) 스키마를 늘리지 않고
|
|
827
|
+
* 여기 적는다. 성공한 회차에는 **비운다** — 지나간 실패를 현재 상태처럼 보이게 두지 않는다.
|
|
828
|
+
*/
|
|
829
|
+
scopeSpec: {
|
|
830
|
+
...((ref.scopeSpec as any) ?? {}),
|
|
831
|
+
produced: [...prev, ...producedNew],
|
|
832
|
+
/* 이번에 읽은 사이트의 총평만 갈아 끼운다 — 읽지 않은 사이트의 것은 그대로 둔다. */
|
|
833
|
+
assessments: { ...(((ref.scopeSpec as any) ?? {}).assessments ?? {}), ...assessments },
|
|
834
|
+
...(failed.length
|
|
835
|
+
? { lastFailures: failed.map((f: any) => ({ siteId: f.siteId, error: f.error, at: new Date().toISOString() })) }
|
|
836
|
+
: { lastFailures: [] })
|
|
837
|
+
}
|
|
650
838
|
} as any
|
|
651
839
|
)
|
|
652
840
|
return { ok: instances.length > 0, instances, failed }
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { CreateDateColumn, Entity, Index, Column, RelationId, ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'
|
|
2
|
-
import { ObjectType, Field, ID } from 'type-graphql'
|
|
2
|
+
import { ObjectType, Field, ID, Int } from 'type-graphql'
|
|
3
3
|
|
|
4
4
|
import { Domain, ScalarObject } from '@things-factory/shell'
|
|
5
5
|
|
|
@@ -70,6 +70,44 @@ export class TwinReference {
|
|
|
70
70
|
@Field({ nullable: true, description: 'Last connection/sync error message, if any.' })
|
|
71
71
|
lastError?: string
|
|
72
72
|
|
|
73
|
+
/**
|
|
74
|
+
* 마지막 동기에서 **못 옮긴 값의 수**. `null` = 세지 않았다(0 과 다르다).
|
|
75
|
+
*
|
|
76
|
+
* 세지 않은 구조 인제스트는 도크를 랙으로 바꿔 놓고도 조용히 성공한다. 그 사실을 사후에 물을 수
|
|
77
|
+
* 있어야 단계 판정이 「구조는 됐다」를 말할 수 있다 — 없는 동안 판정은 늘 `unmeasured` 였다.
|
|
78
|
+
* 기본값을 두지 않는다: 0 을 기본으로 깔면 「경고 없었다」와 「세지 않았다」가 같아진다.
|
|
79
|
+
*/
|
|
80
|
+
@Column({ type: 'int', nullable: true })
|
|
81
|
+
@Field(type => Int, { nullable: true, description: 'Number of values the last sync could not carry over (mapping fallbacks). Null means they were not counted at all, which is different from zero.' })
|
|
82
|
+
lastWarningCount?: number
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* **어디까지 읽었나** — 라이브 피드의 읽기 커서(§`LiveFeedContinuity`).
|
|
86
|
+
*
|
|
87
|
+
* ── 왜 저장하나 (2026-08-23 실측) ──────────────────────────────────────────
|
|
88
|
+
* 어댑터가 붙을 때마다 `Date.now() − 되돌아볼 날수` 로 창을 새로 만들고 있었다. 그래서 **재기동마다
|
|
89
|
+
* 미러의 과거가 잘렸다** — 작업 2,855 → 2,820(8시간 흐른 만큼). 미러가 아는 것이 「원본의 사실」이
|
|
90
|
+
* 아니라 「창의 함수」였고, 그 사실을 아무도 말하지 않았다.
|
|
91
|
+
*
|
|
92
|
+
* 커널이 그 갈림을 이미 적어 두었다(§`hydrateContinuity`): 「원천이 애초에 다시 말해 주지 않는 축」은
|
|
93
|
+
* 재기동 연속성으로 이어받는다. 「우리가 어디까지 읽었나」가 정확히 그 성질이므로 **어댑터의 사물함이
|
|
94
|
+
* 아니라 이 층**에 있다 — 원본이 늘 때마다 저장 기제가 늘고 그중 하나가 조용히 다르게 동작하지 않게.
|
|
95
|
+
*
|
|
96
|
+
* ── 왜 캐시가 아니라 표인가 ────────────────────────────────────────────────
|
|
97
|
+
* 만료되면 창이 다시 미끄러지고, 그 손실은 오류 없이 조용하다. 스냅샷 체크포인트와 성질이 다르다
|
|
98
|
+
* (그쪽은 잃어도 원천이 정정해 준다 — 이 값은 **잃으면 원천에 묻지 않게 된다**).
|
|
99
|
+
*
|
|
100
|
+
* 모양은 `{ streams: { [흐름]: { since?, seen[] } }, firstAttachedAt? }` 다. **흐름 이름은 어댑터가
|
|
101
|
+
* 정한다** — 원본마다 흐름 수와 뜻이 다르므로 이 층은 열쇠로만 다룬다. `simple-json` 이라 드라이버
|
|
102
|
+
* 다섯을 그대로 지난다.
|
|
103
|
+
*
|
|
104
|
+
* `firstAttachedAt` 은 「언제부터 아는가」다. 커널은 상한만 안다(`nowTime` = 마지막으로 들은 시각).
|
|
105
|
+
* 둘이 함께 「이 트윈이 아는 구간」이고, 화면이 수를 보일 때 그 구간을 말해야 한다.
|
|
106
|
+
*/
|
|
107
|
+
@Column({ type: 'simple-json', nullable: true })
|
|
108
|
+
@Field(type => ScalarObject, { nullable: true, description: 'Live feed read cursor carried across restarts, keyed by adapter-defined stream. Null means the feed has never attached, and the next attach decides its first window.' })
|
|
109
|
+
liveCursor?: any
|
|
110
|
+
|
|
73
111
|
@CreateDateColumn()
|
|
74
112
|
@Field({ nullable: true, description: 'Timestamp when the reference was first created.' })
|
|
75
113
|
createdAt?: Date
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { twinError } from '../../engine/log.js'
|
|
1
2
|
import { Arg, Ctx, Mutation, Query, Resolver, Directive } from 'type-graphql'
|
|
2
3
|
|
|
3
4
|
import { validateScenario } from '@operato/twin-kernel'
|
|
@@ -42,7 +43,7 @@ export class TwinControlMutation {
|
|
|
42
43
|
const ack = await TwinEngine.dispatchCommand(domainId, instanceId, command)
|
|
43
44
|
/* 라이브 구조 변이(resource.add)는 저장 model 에도 반영 — 프로비저닝 편집기·재기동이 추가분을 잃지 않게. */
|
|
44
45
|
if (ack?.accepted && command?.type === 'resource.add') {
|
|
45
|
-
await TwinEngine.syncBoardEquipment(domainId, instanceId).catch(err =>
|
|
46
|
+
await TwinEngine.syncBoardEquipment(domainId, instanceId).catch(err => twinError('twin syncBoardEquipment fail', err))
|
|
46
47
|
}
|
|
47
48
|
/*
|
|
48
49
|
* 감사 기록 — 승인 게이트가 앞을 막고, 이 줄이 뒤를 설명한다. 둘 다 있어야 "사람 손을 거쳤다" 를
|
|
@@ -56,7 +57,7 @@ export class TwinControlMutation {
|
|
|
56
57
|
const repo = getRepository(TwinAuditEvent)
|
|
57
58
|
await repo.save(repo.create({ domain: context.state.domain, instanceId, ...audit }))
|
|
58
59
|
} catch (err) {
|
|
59
|
-
|
|
60
|
+
twinError(`[twin-control] command audit not recorded for "${instanceId}"`, err)
|
|
60
61
|
}
|
|
61
62
|
|
|
62
63
|
/* 거절 사유는 **언어 중립 코드**로 함께 돌려준다 — 화면이 `twin.cmderr.<code>` 로 사람 말을 만든다. */
|
|
@@ -132,7 +133,7 @@ export class TwinControlMutation {
|
|
|
132
133
|
await repo.save(repo.create({ domain: context.state.domain, instanceId, ...audit }))
|
|
133
134
|
} catch (err) {
|
|
134
135
|
/* 기록이 실패해도 결과는 돌려준다 — 다만 삼키지 않는다(「기록 없음」과 「일 없음」을 구별해야 한다). */
|
|
135
|
-
|
|
136
|
+
twinError(`[twin-control] source control audit not recorded for "${instanceId}"`, err)
|
|
136
137
|
}
|
|
137
138
|
return result
|
|
138
139
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 저널을 **상한까지만 센다** — 세는 것도 읽는 것이다 (2026-08-22).
|
|
3
|
+
*
|
|
4
|
+
* ── 왜 (실측) ───────────────────────────────────────────────────────────────
|
|
5
|
+
* 조건에 맞는 행을 전부 세는 질의는 행을 실어 오지 않아도 비싸다. 개발 저널에서 트윈 하나가 358만
|
|
6
|
+
* 행이었고 그 한 번이 **5.32초**였다. 색인만 상한까지 읽으면 같은 조건에서 **0.04초**다(130배).
|
|
7
|
+
*
|
|
8
|
+
* 그 5초 동안 이벤트 루프는 다른 요청을 처리하지 못한다 — 이력 화면 한 번, 준비도 화면 한 번이
|
|
9
|
+
* 사용자에게는 「눌렀는데 아무 반응이 없다」로 나타난다. 준비도 화면은 그런 세기를 **넷** 했다.
|
|
10
|
+
*
|
|
11
|
+
* ── 규율 ────────────────────────────────────────────────────────────────────
|
|
12
|
+
* 세는 목적이 둘 중 하나다.
|
|
13
|
+
* ① **판정** — 「한 건이라도 있나」. 그때는 하나만 찾으면 끝이다(`anyJournalRow`).
|
|
14
|
+
* ② **표시** — 사람에게 수를 보인다. 그때는 상한까지 세고, 넘으면 **모른다고 말한다**(`countUpTo`).
|
|
15
|
+
*
|
|
16
|
+
* 상한을 넘었을 때 상한값을 전체로 적으면 화면이 거짓을 말한다. 그래서 `capped` 를 함께 낸다 —
|
|
17
|
+
* 없는 것과 모르는 것을 가르는 이 프로젝트의 규율이 세기에도 같이 적용된다.
|
|
18
|
+
*/
|
|
19
|
+
import type { ObjectLiteral, Repository } from 'typeorm'
|
|
20
|
+
|
|
21
|
+
/** 표시용 세기의 기본 상한 — 화면이 한 번에 보여 줄 범위를 훨씬 넘는 값. */
|
|
22
|
+
export const JOURNAL_COUNT_CAP = 10_000
|
|
23
|
+
|
|
24
|
+
export interface CappedCount {
|
|
25
|
+
/** 센 수. `capped` 면 상한까지만 센 값이므로 **전체가 아니다**. */
|
|
26
|
+
count: number
|
|
27
|
+
/** 상한을 넘었나 — 그때 전체 수는 모르는 것이 사실이다. */
|
|
28
|
+
capped: boolean
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 상한까지만 세기 — 키 컬럼만 읽으므로 행 본문을 만들지 않는다.
|
|
33
|
+
*
|
|
34
|
+
* `count()` 를 쓰지 않는 이유: 그것은 상한을 받지 않는다(조건에 맞는 전부를 센다).
|
|
35
|
+
*/
|
|
36
|
+
export async function countUpTo<T extends ObjectLiteral>(
|
|
37
|
+
repo: Repository<T>,
|
|
38
|
+
where: any,
|
|
39
|
+
cap = JOURNAL_COUNT_CAP
|
|
40
|
+
): Promise<CappedCount> {
|
|
41
|
+
const rows = await repo.find({ where, select: ['id'] as any, take: cap + 1 })
|
|
42
|
+
return rows.length > cap ? { count: cap, capped: true } : { count: rows.length, capped: false }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 한 건이라도 있나 — **판정에는 수가 필요 없다.**
|
|
47
|
+
*
|
|
48
|
+
* 예전에는 전부 센 뒤 `> 0` 을 봤다. 그 답을 얻는 데 358만 행을 세고 있었다.
|
|
49
|
+
*/
|
|
50
|
+
export async function anyJournalRow<T extends ObjectLiteral>(repo: Repository<T>, where: any): Promise<boolean> {
|
|
51
|
+
const rows = await repo.find({ where, select: ['id'] as any, take: 1 })
|
|
52
|
+
return rows.length > 0
|
|
53
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { twinWarn } from '../../engine/log.js'
|
|
1
2
|
/*
|
|
2
3
|
* 저널 검색 키 추출 — **순수**. 인제스트가 기록할 때 한 번 뽑아 인덱스 가능한 실컬럼으로 승격한다.
|
|
3
4
|
*
|
|
@@ -18,6 +19,7 @@
|
|
|
18
19
|
|
|
19
20
|
/** 승격된 검색 키 — 전부 선택적. 뽑히지 않으면 **빈 문자열이 아니라 undefined**(결측≠빈값). */
|
|
20
21
|
export interface TwinEventKeys {
|
|
22
|
+
action?: string
|
|
21
23
|
bizStep?: string
|
|
22
24
|
epc?: string
|
|
23
25
|
orderId?: string
|
|
@@ -37,7 +39,7 @@ function clip(v: unknown, field: string): string | undefined {
|
|
|
37
39
|
const s = String(v)
|
|
38
40
|
if (!s) return undefined
|
|
39
41
|
if (s.length <= MAX_KEY) return s
|
|
40
|
-
|
|
42
|
+
twinWarn(
|
|
41
43
|
`[twin-event-keys] ${field} exceeds ${MAX_KEY} chars and was clipped for indexing — ` +
|
|
42
44
|
`search on this value may be incomplete. payload keeps the full value. (${s.slice(0, 60)}…)`
|
|
43
45
|
)
|
|
@@ -90,9 +92,22 @@ export function equipmentIdOf(envelope: any): string | undefined {
|
|
|
90
92
|
return d.moverId ?? undefined
|
|
91
93
|
}
|
|
92
94
|
|
|
95
|
+
/**
|
|
96
|
+
* EPCIS 행위 — `ADD`·`OBSERVE`·`DELETE`. 세 값이 아니면 `undefined`(원천의 잡값을 색인에 넣지 않는다).
|
|
97
|
+
*
|
|
98
|
+
* 이 키는 검색 축이 아니라 **판별식**이다: 되풀어 읽는 스냅샷은 원리적으로 `DELETE` 를 못 낸다.
|
|
99
|
+
* 그래서 「이 트윈이 반출을 한 번이라도 접었는가」가 진짜 delta 피드의 증거인데, payload 안에 있는
|
|
100
|
+
* 동안에는 5개 드라이버 어디에서도 그 질문을 할 수 없었다. 운영 델타에는 행위가 없다 — 비운다.
|
|
101
|
+
*/
|
|
102
|
+
export function actionOf(envelope: any): string | undefined {
|
|
103
|
+
const a = (envelope?.data ?? envelope ?? {}).action
|
|
104
|
+
return a === 'ADD' || a === 'OBSERVE' || a === 'DELETE' ? a : undefined
|
|
105
|
+
}
|
|
106
|
+
|
|
93
107
|
/** 한 이벤트에서 승격 키 전부 — 기록 경로가 이 함수 하나만 부른다. */
|
|
94
108
|
export function twinEventKeys(envelope: any): TwinEventKeys {
|
|
95
109
|
return {
|
|
110
|
+
action: actionOf(envelope),
|
|
96
111
|
bizStep: clip(bizStepOf(envelope), 'bizStep'),
|
|
97
112
|
epc: clip(epcOf(envelope), 'epc'),
|
|
98
113
|
orderId: clip(orderOf(envelope), 'orderId'),
|
|
@@ -14,8 +14,32 @@ export class TwinEventList {
|
|
|
14
14
|
@Field(type => [TwinEvent], { description: 'The events on this page, ordered by the requested sorting (revision descending by default).' })
|
|
15
15
|
items: TwinEvent[]
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
/*
|
|
18
|
+
* 전체 수 — **상한을 넘으면 비운다.**
|
|
19
|
+
*
|
|
20
|
+
* 세는 데도 값이 든다(2026-08-22 실측: 저널 358만 행인 트윈에서 전량 세기 5.32초 · 상한 1만까지
|
|
21
|
+
* 0.04초). 그래서 상한까지만 세고, 넘으면 「모른다」로 둔다. 그때 상한값을 전체로 적으면 화면이
|
|
22
|
+
* 거짓을 말한다 — 없는 것과 모르는 것을 가르는 것이 이 필드의 규율이다.
|
|
23
|
+
*/
|
|
24
|
+
@Field(type => Int, {
|
|
25
|
+
nullable: true,
|
|
26
|
+
description:
|
|
27
|
+
'Total number of events matching the filters, ignoring pagination. Lets the caller show an honest "shown of total" count instead of silently truncating. NULL when the server stopped counting at countCap: the exact total is then unknown, and capped says so — do not render the cap as if it were the total.'
|
|
28
|
+
})
|
|
29
|
+
total?: number
|
|
30
|
+
|
|
31
|
+
@Field({
|
|
32
|
+
nullable: true,
|
|
33
|
+
description:
|
|
34
|
+
'True when there are more matching events than countCap, so total could not be established. The caller should say "first N shown" and offer narrowing rather than implying it received everything.'
|
|
35
|
+
})
|
|
36
|
+
capped?: boolean
|
|
37
|
+
|
|
38
|
+
@Field(type => Int, {
|
|
39
|
+
nullable: true,
|
|
40
|
+
description: 'The counting limit the server applied. Reported so the screen can say how far the count is trustworthy instead of guessing.'
|
|
41
|
+
})
|
|
42
|
+
countCap?: number
|
|
19
43
|
|
|
20
44
|
/*
|
|
21
45
|
* 다음 페이지 커서. 저널은 **머리에 계속 쌓이는 목록**이라 offset 으로 뒤를 읽으면
|
|
@@ -34,6 +34,8 @@ import { Domain, ScalarObject } from '@things-factory/shell'
|
|
|
34
34
|
@Index('ix_twin_event_4', (e: TwinEvent) => [e.domain, e.instanceId, e.orderId], { unique: false })
|
|
35
35
|
/* ix_5 — "이 지시가 무엇을 일으켰나". 감사 기록의 commandId 로 저널을 다시 계산하는 축. */
|
|
36
36
|
@Index('ix_twin_event_5', (e: TwinEvent) => [e.domain, e.correlationId], { unique: false })
|
|
37
|
+
/* ix_6 — "반출을 한 번이라도 접었나". 없으면 그 질문이 인스턴스 저널 전량 주사가 된다. */
|
|
38
|
+
@Index('ix_twin_event_6', (e: TwinEvent) => [e.domain, e.instanceId, e.action], { unique: false })
|
|
37
39
|
@ObjectType({ description: 'Append-only twin event journal record (EPCIS event or operational delta).' })
|
|
38
40
|
export class TwinEvent {
|
|
39
41
|
@PrimaryGeneratedColumn('uuid')
|
|
@@ -132,6 +134,11 @@ export class TwinEvent {
|
|
|
132
134
|
@Field({ nullable: true, description: 'Id of the command that caused this fact (kernel correlation id; defaults to the commandId). Empty for facts that simulation time produced rather than a command.' })
|
|
133
135
|
correlationId?: string
|
|
134
136
|
|
|
137
|
+
/* 옛 행은 비어 있다 — 승격 전에 기록된 것이라 **모른다**(0 이나 OBSERVE 로 위장하지 않는다). */
|
|
138
|
+
@Column({ length: 16, nullable: true })
|
|
139
|
+
@Field({ nullable: true, description: 'EPCIS action (ADD / OBSERVE / DELETE), promoted from the payload. Empty for operational deltas, and for rows journaled before this column existed.' })
|
|
140
|
+
action?: string
|
|
141
|
+
|
|
135
142
|
@Column({ length: 255, nullable: true })
|
|
136
143
|
@Field({ nullable: true, description: 'Business step (CBV bizStep tail), promoted from the payload for indexed filtering.' })
|
|
137
144
|
bizStep?: string
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { twinLog } from '../../engine/log.js'
|
|
1
2
|
import { Arg, Ctx, Int, Mutation, Query, Resolver, Directive} from 'type-graphql'
|
|
2
3
|
|
|
3
4
|
import { getRepository, ScalarObject } from '@things-factory/shell'
|
|
@@ -193,7 +194,7 @@ export class TwinForecastQuery {
|
|
|
193
194
|
const basis = await TwinEngine.modelBasis(domainId, instanceId)
|
|
194
195
|
const stale = !!stored && modelBasisChanged(stored.basis, basis)
|
|
195
196
|
if (stale) {
|
|
196
|
-
|
|
197
|
+
twinLog(
|
|
197
198
|
`[twin-forecast] "${instanceId}": calibration not applied — model changed since it was learned.` +
|
|
198
199
|
` learned on: ${describeModelBasis(stored!.basis)} / now: ${describeModelBasis(basis)}`
|
|
199
200
|
)
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { TwinIngestWindow } from './twin-ingest-window.js'
|
|
2
|
+
import { TwinIngestWindowQuery } from './twin-ingest-window-query.js'
|
|
3
|
+
|
|
4
|
+
export const entities = [TwinIngestWindow]
|
|
5
|
+
export const resolvers = [TwinIngestWindowQuery]
|
|
6
|
+
|
|
7
|
+
export { installIngestWindowWriter } from './twin-ingest-window-writer.js'
|