@things-factory/headless-twin 10.0.11 → 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 +81 -3
- package/dist-server/engine/canonical-ingest.js +89 -10
- 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/declared-stimulus.d.ts +43 -0
- package/dist-server/engine/declared-stimulus.js +57 -0
- package/dist-server/engine/declared-stimulus.js.map +1 -0
- package/dist-server/engine/index.d.ts +5 -0
- package/dist-server/engine/index.js +6 -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 +39 -0
- package/dist-server/engine/kpi-fold.js +68 -5
- package/dist-server/engine/kpi-fold.js.map +1 -1
- package/dist-server/engine/kpi-query.d.ts +3 -3
- package/dist-server/engine/kpi-query.js +132 -18
- package/dist-server/engine/kpi-query.js.map +1 -1
- package/dist-server/engine/live-feed-registry.d.ts +1 -1
- package/dist-server/engine/live-feed-registry.js +3 -17
- 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/local-declarations.d.ts +3 -6
- package/dist-server/engine/local-declarations.js +97 -16
- package/dist-server/engine/local-declarations.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/measured-yield.d.ts +42 -0
- package/dist-server/engine/measured-yield.js +75 -0
- package/dist-server/engine/measured-yield.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/operation-basis.d.ts +16 -0
- package/dist-server/engine/operation-basis.js +20 -2
- package/dist-server/engine/operation-basis.js.map +1 -1
- package/dist-server/engine/property-effects.js +17 -0
- package/dist-server/engine/property-effects.js.map +1 -1
- package/dist-server/engine/restart-policy.d.ts +13 -0
- package/dist-server/engine/restart-policy.js +52 -0
- package/dist-server/engine/restart-policy.js.map +1 -0
- package/dist-server/engine/runtime-key.js +1 -1
- package/dist-server/engine/runtime-key.js.map +1 -1
- package/dist-server/engine/spec-coverage.d.ts +8 -0
- package/dist-server/engine/spec-coverage.js +3 -1
- package/dist-server/engine/spec-coverage.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 +400 -31
- package/dist-server/engine/twin-engine.js +1198 -187
- package/dist-server/engine/twin-engine.js.map +1 -1
- package/dist-server/index.js +35 -3
- 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/control-routing.d.ts +14 -0
- package/dist-server/service/reference/control-routing.js +64 -0
- package/dist-server/service/reference/control-routing.js.map +1 -0
- 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 +5 -1
- package/dist-server/service/reference/index.js +8 -1
- package/dist-server/service/reference/index.js.map +1 -1
- package/dist-server/service/reference/reference-adapter.d.ts +251 -3
- package/dist-server/service/reference/reference-adapter.js +43 -1
- 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 +119 -14
- 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.d.ts +3 -3
- package/dist-server/service/reference/reference-resolver.js +242 -58
- 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-audit/command-audit.d.ts +34 -0
- package/dist-server/service/twin-audit/command-audit.js +15 -1
- package/dist-server/service/twin-audit/command-audit.js.map +1 -1
- package/dist-server/service/twin-audit/twin-audit-event.d.ts +3 -0
- package/dist-server/service/twin-audit/twin-audit-event.js +10 -0
- package/dist-server/service/twin-audit/twin-audit-event.js.map +1 -1
- package/dist-server/service/twin-control/twin-control-mutation.d.ts +15 -2
- package/dist-server/service/twin-control/twin-control-mutation.js +88 -36
- 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.d.ts +1 -1
- package/dist-server/service/twin-instance/twin-instance.js +2 -2
- 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 +74 -3
- 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 +157 -5
- 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 +164 -15
- package/server/engine/command-routing.ts +1 -1
- package/server/engine/declared-stimulus.ts +66 -0
- package/server/engine/index.ts +6 -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 +101 -5
- package/server/engine/kpi-query.ts +137 -23
- package/server/engine/live-feed-registry.ts +4 -3
- package/server/engine/load-meter.ts +12 -3
- package/server/engine/local-declarations.ts +97 -19
- package/server/engine/log.ts +72 -0
- package/server/engine/loop-lag.ts +120 -0
- package/server/engine/measured-yield.ts +89 -0
- package/server/engine/model-gap.ts +168 -0
- package/server/engine/operation-basis.ts +33 -2
- package/server/engine/property-effects.ts +17 -0
- package/server/engine/restart-policy.ts +55 -0
- package/server/engine/runtime-key.ts +1 -1
- package/server/engine/spec-coverage.ts +23 -3
- package/server/engine/stage-path.ts +172 -0
- package/server/engine/twin-engine.ts +1318 -186
- package/server/index.ts +36 -3
- 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/control-routing.ts +62 -0
- package/server/service/reference/discovery-result.ts +1 -1
- package/server/service/reference/index.ts +7 -1
- package/server/service/reference/reference-adapter.ts +275 -5
- package/server/service/reference/reference-assessment.ts +215 -0
- package/server/service/reference/reference-live.ts +126 -14
- 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 +246 -19
- package/server/service/reference/twin-reference.ts +39 -1
- package/server/service/twin-audit/command-audit.ts +34 -1
- package/server/service/twin-audit/twin-audit-event.ts +20 -0
- package/server/service/twin-control/twin-control-mutation.ts +78 -30
- 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 +16 -10
- package/server/service/twin-journal/twin-journal-query.ts +59 -5
- package/server/service/twin-lifecycle/twin-lifecycle-mutation.ts +79 -4
- 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 +156 -5
- 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/adopt-structure-live.test.ts +7 -7
- package/test/axis-journal-evidence.test.ts +71 -0
- package/test/axis-read.test.ts +101 -1
- package/test/boot-resume.test.ts +93 -27
- 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/control-capability.test.ts +103 -0
- package/test/declared-location-types.test.ts +89 -0
- package/test/declared-stimulus.test.ts +88 -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/ingest-running-guard.test.ts +5 -5
- package/test/instance-cache-lifecycle.test.ts +1 -1
- 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-baseline-db.test.ts +1 -1
- package/test/kpi-fold.test.ts +90 -3
- package/test/kpi-query-bench.test.ts +3 -3
- package/test/lineage-survives-restart.test.ts +20 -4
- package/test/live-cursor-wiring.test.ts +87 -0
- package/test/live-feed-registry.test.ts +15 -7
- 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 +109 -2
- package/test/log-stamp.test.ts +59 -0
- package/test/loop-lag.test.ts +82 -0
- package/test/measured-yield.test.ts +90 -0
- package/test/model-gap.test.ts +153 -0
- package/test/oee-accumulator.test.ts +4 -0
- package/test/operation-basis.test.ts +28 -1
- package/test/operational-vocabulary.test.ts +108 -0
- package/test/operations-capability-db.test.ts +4 -4
- package/test/project-structure-db.test.ts +1 -1
- package/test/projection-reaches-screen.test.ts +1 -1
- package/test/property-effects.test.ts +28 -0
- 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 +111 -0
- package/test/resync-origin-site.test.ts +7 -1
- 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/source-outcome-audit.test.ts +104 -0
- 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 +31 -27
- package/test/tenant-registry-db.test.ts +3 -3
- 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 +5 -3
- package/test/twin-model-tree-db.test.ts +7 -7
- package/test/twin-origin-resync.test.ts +4 -4
- package/test/warm-start-seam.test.ts +4 -0
- package/test/yield-loop.test.ts +199 -0
- package/tsconfig.shared.tsbuildinfo +1 -1
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 항목의 **이름**을 붙이는 자리 — 화면이 id 만 보이지 않게. (2026-08-22 공용으로 옮김)
|
|
3
|
+
*
|
|
4
|
+
* 이 둘은 항목 질의 안에 사적으로 있었다. 그런데 **전개(BOM 나무) 화면도 같은 이름이 필요하다** —
|
|
5
|
+
* 사용자가 「각 노드의 식별자가 반복되어서 이해가 어렵다. 식별자와 이름이 보이면」이라고 했다.
|
|
6
|
+
* 두 벌을 만들면 한쪽만 고쳐지므로 한 곳에 둔다(재발명 금지).
|
|
7
|
+
*/
|
|
8
|
+
/* 커널 런타임 로드 — 어느 축이 있는지는 카탈로그가 선언한다(이 파일은 도메인 모양을 알지 않는다). */
|
|
9
|
+
const { TWIN_AXES, axisSource } = require('@operato/twin-kernel')
|
|
10
|
+
|
|
11
|
+
/** `productionSpec.definition.recipes` 같은 경로를 문서에서 따라간다. */
|
|
12
|
+
export const at = (o: any, path: string) => path.split('.').reduce((a: any, k: string) => (a == null ? undefined : a[k]), o)
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 문서 항목의 **식별자** — 축마다 이름이 다르다(자리는 `id`, 공정은 `key`, 레시피도 `key`).
|
|
16
|
+
*
|
|
17
|
+
* 도메인 모양을 코드가 아는 것이 아니라 **식별자 관례**를 아는 것이다. 후보를 순서대로 보고 첫
|
|
18
|
+
* 값을 쓴다. 하나도 없으면 그 항목은 가리킬 수 없으므로 `null` — 목록에서 빼지 않고 **id 없음**으로
|
|
19
|
+
* 낸다(조용히 빠지면 건수가 안 맞는다).
|
|
20
|
+
*/
|
|
21
|
+
export const idOf = (o: any, info?: { idField?: string | string[] }): string | null => {
|
|
22
|
+
if (typeof o === 'string') return o
|
|
23
|
+
/*
|
|
24
|
+
* **무엇이 이 항목을 가리키는지는 선언이 말한다**(§`TwinAxisInfo.idField`).
|
|
25
|
+
*
|
|
26
|
+
* 예전에는 `id`·`key`·`gtin` 을 짐작했다. 그래서 `id` 가 없는 축(수요 구간 — 정체성이 구간 시작
|
|
27
|
+
* 시각이다)의 항목이 전부 「식별자 없음 — 참조할 수 없는 항목」으로 보였다. 값은 실재하는데
|
|
28
|
+
* 가리킬 수 없다고 말한 것이다.
|
|
29
|
+
*/
|
|
30
|
+
/*
|
|
31
|
+
* 선언이 **차례**를 말할 수 있다(2026-08-22) — 정체성이 한 칸으로 정해지지 않는 축이 있다. 물품이
|
|
32
|
+
* 그렇다: 직렬 물품은 `epc` 가 유일하지만 비직렬 로트가 자리마다 나뉘면 `subLotId` 가 개체를
|
|
33
|
+
* 구별한다(같은 로트의 두 부분은 같은 `epc` 를 갖는다). 앞에서 값이 있는 첫 칸이 이긴다.
|
|
34
|
+
*/
|
|
35
|
+
for (const f of info?.idField ? (Array.isArray(info.idField) ? info.idField : [info.idField]) : []) {
|
|
36
|
+
const v = o?.[f]
|
|
37
|
+
if (v !== undefined && v !== null) return String(v)
|
|
38
|
+
}
|
|
39
|
+
return o?.id ?? o?.key ?? o?.gtin ?? null
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* **id → 이름**, 모든 축을 한 표로.
|
|
45
|
+
*
|
|
46
|
+
* ── 왜 이 표가 필요한가 ──────────────────────────────────────────────────────
|
|
47
|
+
* 행은 다른 것을 **id 로** 가리킨다(사람의 `personnelClassIds`, 설비의 `homeLocationId`, 공정의
|
|
48
|
+
* `locationType`). 그 id 의 이름은 **가리켜진 축의 행**에 있으므로, 화면이 그것을 알려면 축을
|
|
49
|
+
* 하나씩 더 읽어야 한다 — 그래서 지금까지 표면마다 그냥 id 를 찍었다(`cutter-op`, `cut-station`).
|
|
50
|
+
* 표면을 하나씩 고치는 것은 끝이 없다: 새 화면은 처음부터 다시 id 를 찍는다.
|
|
51
|
+
*
|
|
52
|
+
* 그래서 **서버가 붙인다.** 서버는 이미 모델을 손에 들고 있고, 어느 축이 있는지는 커널 카탈로그가
|
|
53
|
+
* 선언한다(`TWIN_AXES`) — 도메인이 늘어도 이 함수는 그대로다.
|
|
54
|
+
*
|
|
55
|
+
* 값은 그 항목이 **자기 이름이라고 말한 것**이다(`name`·`label`·`description`). 그중 어떤 것은
|
|
56
|
+
* i18n 키이므로(등급의 `description`) 번역은 화면의 일로 남긴다. 이름이 없으면 표에 넣지 않는다 —
|
|
57
|
+
* 없는 것을 지어내지 않고, 그 경우 화면은 id 를 그대로 보인다.
|
|
58
|
+
*/
|
|
59
|
+
export function nameIndex(model: any): Record<string, string> {
|
|
60
|
+
const out: Record<string, string> = {}
|
|
61
|
+
for (const info of TWIN_AXES as any[]) {
|
|
62
|
+
/* 관측 축(상태·저널)은 문서에 없다 — 경로가 없는 축을 읽으면 모델 루트를 훑는다. */
|
|
63
|
+
if (axisSource(info.axis) !== 'document') continue
|
|
64
|
+
const raw = at(model ?? {}, info.path)
|
|
65
|
+
const list = Array.isArray(raw) ? raw : raw ? [raw] : []
|
|
66
|
+
for (const o of list) {
|
|
67
|
+
const id = idOf(o)
|
|
68
|
+
if (!id || typeof o !== 'object') continue
|
|
69
|
+
const label = o.name ?? o.label ?? o.description
|
|
70
|
+
/* 이름이 id 와 같으면 표에 넣지 않는다 — 같은 값을 두 번 보이는 셈이다. */
|
|
71
|
+
if (typeof label === 'string' && label && label !== id && !out[id]) out[id] = label
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return out
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 이 응답이 **실제로 가리킨 것만** 골라 낸다.
|
|
80
|
+
*
|
|
81
|
+
* 모델 전체 표를 그대로 실으면 자리가 10만인 현장에서 목록 한 페이지에 10만 줄짜리 이름표가
|
|
82
|
+
* 따라온다. 나온 값에 걸리는 것만 싣는다 — 규모에서 성립하는 유일한 방식이다.
|
|
83
|
+
*/
|
|
84
|
+
export function namesFor(rows: any[], index: Record<string, string>): Record<string, string> {
|
|
85
|
+
const out: Record<string, string> = {}
|
|
86
|
+
const take = (v: any, depth: number): void => {
|
|
87
|
+
if (v == null || depth > 4) return
|
|
88
|
+
if (typeof v === 'string') { if (index[v]) out[v] = index[v]; return }
|
|
89
|
+
if (Array.isArray(v)) { for (const x of v) take(x, depth + 1); return }
|
|
90
|
+
if (typeof v === 'object') for (const x of Object.values(v)) take(x, depth + 1)
|
|
91
|
+
}
|
|
92
|
+
for (const r of rows) { take(r?.fields ?? r, 0); if (typeof r?.id === 'string' && index[r.id]) out[r.id] = index[r.id] }
|
|
93
|
+
return out
|
|
94
|
+
}
|
|
@@ -43,7 +43,7 @@ export async function projectStructure(domainId: string, instanceId: string, mod
|
|
|
43
43
|
if (!instance) throw new Error(`cannot project structure: twin instance "${instanceId}" is not registered`)
|
|
44
44
|
|
|
45
45
|
/*
|
|
46
|
-
* **같은 id 가 둘이면 그 사실을 말한다** — 제약 위반 문구를 사용자에게
|
|
46
|
+
* **같은 id 가 둘이면 그 사실을 말한다** — 제약 위반 문구를 사용자에게 보이지 않는다.
|
|
47
47
|
*
|
|
48
48
|
* 실측(2026-08-18): 현장 값을 그 현장의 트윈 전부에 얹다가 한 트윈에서 투영이 터졌다. 올라온 것은
|
|
49
49
|
* `SQLITE_CONSTRAINT: UNIQUE constraint failed: twin_equipment...` 뿐이라 **무엇이 잘못됐는지 아무도
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* **수 하나가 뜻을 바꾸는 자리** — 상태별로 갈라 센다 (2026-08-23).
|
|
3
|
+
*
|
|
4
|
+
* ── 무엇이 있었나 ───────────────────────────────────────────────────────────
|
|
5
|
+
* 미러가 완료된 작업까지 상태에 들고 있게 되면서(관측 흡수에서 `completed` 를 떨어뜨리지 않게 커널을
|
|
6
|
+
* 고친 뒤) 개념 지도의 작업 카드가 「0」에서 「2,820」이 되었다. 이 카드는 **상태의 인구조사**이므로
|
|
7
|
+
* 그 수가 맞다 — 그런데 사람은 그것을 「지금 할 일 2,820건」으로 읽는다.
|
|
8
|
+
*
|
|
9
|
+
* 수를 줄여 맞는 척하게 만들지 않는다(그러면 인구조사가 거짓이 된다). 대신 **무엇으로 이루어진
|
|
10
|
+
* 수인지** 함께 낸다: 「진행 중 28 · 완료 2,792」.
|
|
11
|
+
*
|
|
12
|
+
* ── 지키는 것 ───────────────────────────────────────────────────────────────
|
|
13
|
+
* · 낱말은 **커널이 실어 보낸 그대로**다. 여기서 묶거나 고쳐 부르지 않는다(화면이 라벨을 입히고,
|
|
14
|
+
* 라벨이 없으면 낱말이 그대로 보여 누락이 드러난다).
|
|
15
|
+
* · 상태를 말하지 않는 축은 **아무것도 내지 않는다.** 빈 표를 내면 화면이 「상태가 없는 축」과
|
|
16
|
+
* 「상태를 못 읽은 축」을 구별할 수 없다.
|
|
17
|
+
* · 한 번만 훑는다. 카드가 이미 같은 배열의 길이를 세므로 셈이 한 벌 늘지 않는다.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/** 상태 한 갈래와 그 건수. */
|
|
21
|
+
export interface StatusCount {
|
|
22
|
+
status: string
|
|
23
|
+
count: number
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** 이 축의 개체를 상태별로 센다. 상태가 없으면 빈 객체(필드를 만들지 않는다). */
|
|
27
|
+
export function statusTally(bucket: unknown): { statusCounts?: StatusCount[] } {
|
|
28
|
+
if (!Array.isArray(bucket) || !bucket.length) return {}
|
|
29
|
+
const tally = new Map<string, number>()
|
|
30
|
+
for (const e of bucket) {
|
|
31
|
+
const s = (e as any)?.status
|
|
32
|
+
if (typeof s !== 'string' || !s) continue
|
|
33
|
+
tally.set(s, (tally.get(s) ?? 0) + 1)
|
|
34
|
+
}
|
|
35
|
+
if (!tally.size) return {}
|
|
36
|
+
return { statusCounts: [...tally].map(([status, count]) => ({ status, count })).sort((x, y) => y.count - x.count) }
|
|
37
|
+
}
|
|
@@ -2,6 +2,8 @@ import { Arg, Ctx, Int, Query, Resolver } from 'type-graphql'
|
|
|
2
2
|
|
|
3
3
|
import { ScalarObject, getRepository } from '@things-factory/shell'
|
|
4
4
|
|
|
5
|
+
import { itemRefOf, resolveByDeclaration } from './item-ref.js'
|
|
6
|
+
|
|
5
7
|
import { TwinInstance } from '../twin-instance/twin-instance.js'
|
|
6
8
|
import { TwinEvent } from '../twin-event/twin-event.js'
|
|
7
9
|
|
|
@@ -24,11 +26,6 @@ import { TwinEvent } from '../twin-event/twin-event.js'
|
|
|
24
26
|
* 읽히고, 계보에서 그 오해는 리콜 범위를 좁게 잡는 결과로 이어진다.
|
|
25
27
|
*/
|
|
26
28
|
|
|
27
|
-
/** GS1 SGTIN 의 **품목 참조** — 개체(serial)를 떼고 품목만 남긴다(`…sgtin:0614141.077101.42` → `077101`). */
|
|
28
|
-
function itemRefOf(epc: string): string | null {
|
|
29
|
-
const m = /sgtin:[^.]+\.([^.]+)\./.exec(epc)
|
|
30
|
-
return m ? m[1] : null
|
|
31
|
-
}
|
|
32
29
|
|
|
33
30
|
@Resolver()
|
|
34
31
|
export class TwinLineageQuery {
|
|
@@ -110,18 +107,50 @@ export class TwinLineageQuery {
|
|
|
110
107
|
const spec: any = (inst.model as any)?.productionSpec
|
|
111
108
|
const binding: Record<string, string> = spec?.binding ?? {}
|
|
112
109
|
const refToMaterial = new Map(Object.entries(binding).map(([mat, ref]) => [String(ref), mat]))
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
110
|
+
/*
|
|
111
|
+
* ── **화면은 끝마디로 묻는다** (2026-08-23) ─────────────────────────────────
|
|
112
|
+
*
|
|
113
|
+
* 서랍의 포커스 식별자는 클래스의 끝마디다(`net/class/product-M0000438`). 대응 표의 값은 전체
|
|
114
|
+
* 식별자(`https://…/class/product-M0000438`)이므로 곧바로는 맞지 않고, 그래서 「선언된 자재가
|
|
115
|
+
* 아닙니다」가 떴다 — 그 품목은 대응 표에 있었다.
|
|
116
|
+
*
|
|
117
|
+
* 끝마디로도 찾되 **하나뿐일 때만** 인정한다. 둘 이상이 같은 끝마디를 가지면 어느 것인지 모르므로
|
|
118
|
+
* 아무 말도 하지 않는다 — 짐작으로 이으면 남의 레시피와 견주게 되고, 그것이 「설계와 다르다」는
|
|
119
|
+
* 거짓 진단이 된다.
|
|
120
|
+
*/
|
|
121
|
+
const materialOf = (e: string) => resolveByDeclaration(itemRefOf(e), refToMaterial) ?? null
|
|
117
122
|
|
|
118
123
|
const myMaterial = materialOf(epc)
|
|
119
124
|
const recipes: any[] = spec?.definition?.recipes ?? []
|
|
120
125
|
const recipe = myMaterial ? recipes.find((r: any) => (r.outputs ?? []).some((o: any) => o.material === myMaterial)) : undefined
|
|
121
126
|
|
|
127
|
+
/*
|
|
128
|
+
* ── 「모르는 개체」와 「대응이 없는 개체」는 다른 사실이다 (2026-08-23) ──────
|
|
129
|
+
*
|
|
130
|
+
* 화면이 「이 개체는 선언된 자재가 아닙니다(라인 내부의 중간 산물일 수 있습니다)」라고 말했는데,
|
|
131
|
+
* 그 품목은 원본의 품목 선언에 **이름까지 있었다**(「냉동그린빈스」). 바인딩 표가 그 품목을 담지
|
|
132
|
+
* 못한 것이고, 그것은 「중간 산물」과 전혀 다른 이야기다 — 사용자가 할 일도 다르다(원본을 볼 일이
|
|
133
|
+
* 아니라 대응을 고칠 일이다).
|
|
134
|
+
*
|
|
135
|
+
* 그래서 개체가 가리키는 품목이 **모델의 품목 선언에 있는지** 함께 본다. 이름은 그 자리에 있으니
|
|
136
|
+
* 함께 낸다(화면이 사람에게 무엇을 말하는지 알 수 있게).
|
|
137
|
+
*/
|
|
138
|
+
const definitions: any[] = (inst.model as any)?.materialDefinitions ?? []
|
|
139
|
+
const defByRef = new Map(definitions.filter(d => d?.id).map(d => [String(d.id), d]))
|
|
140
|
+
/* 정의도 같은 문으로 되찾는다 — 규칙 하나, 후보가 둘 이상이면 답하지 않는다. */
|
|
141
|
+
const myRef = itemRefOf(epc)
|
|
142
|
+
const myDef = resolveByDeclaration(myRef, defByRef)
|
|
143
|
+
|
|
122
144
|
let conformance: any = null
|
|
123
145
|
if (!Object.keys(binding).length) {
|
|
124
146
|
conformance = { comparable: false, reason: 'no-binding' }
|
|
147
|
+
} else if (!myMaterial && myDef) {
|
|
148
|
+
/* 원본은 이 품목을 안다 — 개체를 자재에 잇는 대응이 없어서 비교하지 못한다. */
|
|
149
|
+
conformance = {
|
|
150
|
+
comparable: false,
|
|
151
|
+
reason: 'item-known-not-bound',
|
|
152
|
+
...(typeof myDef.name === 'string' && myDef.name ? { itemName: myDef.name } : {})
|
|
153
|
+
}
|
|
125
154
|
} else if (!myMaterial) {
|
|
126
155
|
conformance = { comparable: false, reason: 'item-not-bound' }
|
|
127
156
|
} else if (!recipe) {
|
|
@@ -2,14 +2,19 @@ import { Arg, Ctx, Int, Query, Resolver } from 'type-graphql'
|
|
|
2
2
|
|
|
3
3
|
import { ScalarObject, getRepository } from '@things-factory/shell'
|
|
4
4
|
|
|
5
|
+
import { itemRefOf } from './item-ref.js'
|
|
6
|
+
|
|
5
7
|
import { TwinEngine } from '../../engine/index.js'
|
|
6
8
|
import { unwrapState } from '../../engine/warm-start.js'
|
|
7
9
|
import { propertyEffectsOf } from '../../engine/property-effects.js'
|
|
10
|
+
/* 축이 사는 자리를 읽는 **한 곳** — 카드(`twin-model-query`)와 같은 규칙을 쓴다. */
|
|
11
|
+
import { axisValueOf } from '@things-factory/headless-twin/dist-shared/axis-read.js'
|
|
8
12
|
import { TwinInstance } from '../twin-instance/twin-instance.js'
|
|
9
13
|
import { TwinLocation } from './twin-location.js'
|
|
10
14
|
import { TwinEquipment } from './twin-equipment.js'
|
|
11
15
|
import { TwinOperation } from './twin-operation.js'
|
|
12
16
|
import { TwinArea } from '../twin-space/twin-area.js'
|
|
17
|
+
import { at, idOf, nameIndex, namesFor } from './name-index.js'
|
|
13
18
|
|
|
14
19
|
/* 커널 런타임 로드 — 축·관계·속성 선언의 SSOT. 이 파일은 도메인 모양을 알지 않는다. */
|
|
15
20
|
const { TWIN_AXES, TWIN_RELATIONS, axisInfo, axisSource, documentPath, relationsFrom, relationsTo, propertiesOf } = require('@operato/twin-kernel')
|
|
@@ -26,17 +31,6 @@ const ROW_BACKED: Record<string, { entity: any; idColumn: string; order: any }>
|
|
|
26
31
|
operations: { entity: TwinOperation, idColumn: 'operationKey', order: { seq: 'ASC' } }
|
|
27
32
|
}
|
|
28
33
|
|
|
29
|
-
/** `productionSpec.definition.recipes` 같은 경로를 문서에서 따라간다. */
|
|
30
|
-
const at = (o: any, path: string) => path.split('.').reduce((a: any, k: string) => (a == null ? undefined : a[k]), o)
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* 문서 항목의 **식별자** — 축마다 이름이 다르다(자리는 `id`, 공정은 `key`, 레시피도 `key`).
|
|
34
|
-
*
|
|
35
|
-
* 도메인 모양을 코드가 아는 것이 아니라 **식별자 관례**를 아는 것이다. 후보를 순서대로 보고 첫
|
|
36
|
-
* 값을 쓴다. 하나도 없으면 그 항목은 가리킬 수 없으므로 `null` — 목록에서 빼지 않고 **id 없음**으로
|
|
37
|
-
* 낸다(조용히 빠지면 건수가 안 맞는다).
|
|
38
|
-
*/
|
|
39
|
-
const idOf = (o: any): string | null => (typeof o === 'string' ? o : o?.id ?? o?.key ?? o?.gtin ?? null)
|
|
40
34
|
|
|
41
35
|
/** 검색 — 항목의 값 어디든 걸리면 맞는 것으로 본다. 축마다 검색 컬럼을 정해 두면 축이 늘 때마다 여기를 고쳐야 한다. */
|
|
42
36
|
function matches(o: any, q: string): boolean {
|
|
@@ -142,7 +136,7 @@ export class TwinModelItemQuery {
|
|
|
142
136
|
truncated: rows.length >= SCAN_MAX,
|
|
143
137
|
...(() => {
|
|
144
138
|
const items = hit.slice(skip, skip + take).map((r: any) => this.rowItem(axis, r))
|
|
145
|
-
return { items, names:
|
|
139
|
+
return { items, names: namesFor(items, nameIndex(inst.model ?? {})) }
|
|
146
140
|
})()
|
|
147
141
|
}
|
|
148
142
|
}
|
|
@@ -188,12 +182,15 @@ export class TwinModelItemQuery {
|
|
|
188
182
|
if (!snap) {
|
|
189
183
|
return { axis, label: info.label, basis: 'state', total: null, observed: false, offset: skip, items: [] }
|
|
190
184
|
}
|
|
191
|
-
|
|
185
|
+
/* 축이 사는 자리는 **커널 선언이 말한다**(`path`) — 축 이름으로만 읽으면 중첩된 축이 언제나
|
|
186
|
+
비어 보인다. 카드는 `axisValueOf` 로 읽는데 여기가 그 규칙을 지나지 않아, 「42」를 센 개념의
|
|
187
|
+
목록이 「0건」으로 나왔다(에너지의 수요 구간). 규칙의 집은 `axisValueOf` 하나다. */
|
|
188
|
+
const bucket = axisValueOf(snap, info)
|
|
192
189
|
const list: any[] = Array.isArray(bucket) ? bucket : []
|
|
193
190
|
const hitState = q ? list.filter(o => matches(o, q)) : list
|
|
194
191
|
const items = hitState
|
|
195
192
|
.slice(skip, skip + take)
|
|
196
|
-
.map(o => ({ id: idOf(o), fields: this.withOrderMaterial(axis, inst.model ?? {}, o) }))
|
|
193
|
+
.map(o => ({ id: idOf(o, info), fields: this.withOrderMaterial(axis, inst.model ?? {}, o) }))
|
|
197
194
|
return {
|
|
198
195
|
axis,
|
|
199
196
|
label: info.label,
|
|
@@ -206,7 +203,7 @@ export class TwinModelItemQuery {
|
|
|
206
203
|
offset: skip,
|
|
207
204
|
resourceClassLabels: this.classLabels(inst.model ?? {}),
|
|
208
205
|
items,
|
|
209
|
-
names:
|
|
206
|
+
names: namesFor(items, nameIndex(inst.model ?? {}))
|
|
210
207
|
}
|
|
211
208
|
}
|
|
212
209
|
|
|
@@ -233,7 +230,7 @@ export class TwinModelItemQuery {
|
|
|
233
230
|
const items = hit
|
|
234
231
|
.slice(skip, skip + take)
|
|
235
232
|
.map(o => ({ id: idOf(o) ?? (singular ? axis : null), fields: this.withBinding(axis, inst.model ?? {}, o) }))
|
|
236
|
-
return { items, names:
|
|
233
|
+
return { items, names: namesFor(items, nameIndex(inst.model ?? {})), singular }
|
|
237
234
|
})()
|
|
238
235
|
}
|
|
239
236
|
}
|
|
@@ -298,10 +295,9 @@ export class TwinModelItemQuery {
|
|
|
298
295
|
const binding: Record<string, string> = model?.productionSpec?.binding ?? {}
|
|
299
296
|
const refToMaterial = new Map(Object.entries(binding).map(([mat, r]) => [String(r), mat]))
|
|
300
297
|
/* 단일 품목 작업지시는 `gtin`, 여러 품목이면 라인마다 말한다 — 첫 라인으로 대표하지 않는다. */
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
}
|
|
298
|
+
/* 정체성 모양은 하나가 아니다 — 규칙의 집은 `item-ref` 하나다(웹 URI 원본에서 이 자리가 언제나
|
|
299
|
+
빈칸이었다). */
|
|
300
|
+
const refOf = itemRefOf
|
|
305
301
|
const mat = refToMaterial.get(String(refOf(fields.gtin) ?? ''))
|
|
306
302
|
if (mat) return { ...fields, material: mat }
|
|
307
303
|
const lineMats = (fields.lines ?? [])
|
|
@@ -359,8 +355,9 @@ export class TwinModelItemQuery {
|
|
|
359
355
|
/* 다시 계산할 것이 없다 — 아래에서 "모른다" 를 낸다(0 으로 메우지 않는다). */
|
|
360
356
|
}
|
|
361
357
|
}
|
|
362
|
-
|
|
363
|
-
const
|
|
358
|
+
/* 같은 규칙 — 축 이름이 아니라 선언된 자리에서 읽는다(§axisValueOf). */
|
|
359
|
+
const bucket = snap ? axisValueOf(snap, info) : null
|
|
360
|
+
const found = Array.isArray(bucket) ? bucket.find((o: any) => idOf(o, info) === itemId) : undefined
|
|
364
361
|
if (!snap || !found) {
|
|
365
362
|
return {
|
|
366
363
|
instanceId, axis, label: info.label, itemId,
|
|
@@ -373,7 +370,7 @@ export class TwinModelItemQuery {
|
|
|
373
370
|
names: {}, provenance: { basis: 'state' }
|
|
374
371
|
}
|
|
375
372
|
}
|
|
376
|
-
const names =
|
|
373
|
+
const names = namesFor([{ id: itemId, fields: found }], nameIndex(inst.model ?? {}))
|
|
377
374
|
return {
|
|
378
375
|
instanceId, axis, label: info.label, itemId,
|
|
379
376
|
identity: { standardClass: info.standardClass ?? null, typeRole: info.typeRole ?? null, kind: info.kind, gs1Scheme: info.gs1Scheme ?? null },
|
|
@@ -509,9 +506,9 @@ export class TwinModelItemQuery {
|
|
|
509
506
|
* **이웃의 이름도 함께 싣는다.** 관계 그림의 이웃은 대개 *들어오는* 링크(이 자리를 기준 자리로
|
|
510
507
|
* 삼는 설비·사람)라, 이 항목 자신의 필드에는 그 id 가 없다 — 필드만 훑으면 그림이 id 로 채워진다.
|
|
511
508
|
*/
|
|
512
|
-
names:
|
|
509
|
+
names: namesFor(
|
|
513
510
|
[{ id: itemId, fields: declared }, { fields: observed }, { fields: links }],
|
|
514
|
-
{ ...
|
|
511
|
+
{ ...nameIndex(model), ...areaNames }
|
|
515
512
|
),
|
|
516
513
|
provenance
|
|
517
514
|
}
|
|
@@ -665,14 +662,16 @@ export class TwinModelItemQuery {
|
|
|
665
662
|
return { absence: 'not-running' as const }
|
|
666
663
|
}
|
|
667
664
|
}
|
|
668
|
-
|
|
665
|
+
/* 같은 규칙 — 선언된 자리에서 읽는다. 축 이름으로 읽으면 중첩된 축이 언제나 「관측 없음」이 된다. */
|
|
666
|
+
const bucket = state ? axisValueOf(state, axisInfo(axis) ?? { axis }) : null
|
|
669
667
|
/*
|
|
670
668
|
* **왜 관측이 없는지 말한다.** 예전에는 그냥 `null` 이었고 화면은 그것을 "트윈이 실행 중이지 않아"
|
|
671
669
|
* 로 읽었다 — 실행 중인데도 그렇게 말했다(공정 상세에서 관측됨). 공정·레시피처럼 **관측 축이
|
|
672
670
|
* 아닌 것**은 트윈이 돌든 말든 관측이 없다. 이유가 다르면 화면이 다르게 말해야 한다.
|
|
673
671
|
*/
|
|
674
672
|
if (!Array.isArray(bucket)) return { absence: 'not-observed' as const }
|
|
675
|
-
|
|
673
|
+
/* 같은 규칙 — 선언이 말하는 자리로 찾는다. `id` 로만 찾으면 수요 구간을 열 수 없다. */
|
|
674
|
+
const value = bucket.find((x: any) => idOf(x, axisInfo(axis)) === itemId)
|
|
676
675
|
if (!value) return { absence: 'not-observed' as const }
|
|
677
676
|
return { value, basis: live ? 'state' : 'journal', ...(folded != null ? { foldedRevision: folded } : {}) }
|
|
678
677
|
}
|
|
@@ -1,8 +1,12 @@
|
|
|
1
|
+
import { axisSeenInJournal } from './axis-journal-evidence.js'
|
|
2
|
+
import { statusTally } from './status-tally.js'
|
|
3
|
+
import { TwinEvent } from '../twin-event/twin-event.js'
|
|
1
4
|
import { Arg, Ctx, Query, Resolver } from 'type-graphql'
|
|
2
5
|
|
|
3
6
|
import { ScalarObject, getRepository } from '@things-factory/shell'
|
|
4
7
|
|
|
5
8
|
import { TwinEngine } from '../../engine/index.js'
|
|
9
|
+
import { modelGapOf } from '../../engine/model-gap.js'
|
|
6
10
|
import { readBoardEquipment } from '@operato/twin-kernel'
|
|
7
11
|
import { TwinInstance } from '../twin-instance/twin-instance.js'
|
|
8
12
|
import { TwinSpace } from '../twin-space/twin-space.js'
|
|
@@ -98,6 +102,27 @@ function valuesAt(obj: any, field: string): any[] {
|
|
|
98
102
|
|
|
99
103
|
const idOf = (o: any): string | null => (typeof o === 'string' ? o : o?.id ?? o?.key ?? o?.gtin ?? null)
|
|
100
104
|
|
|
105
|
+
/** 상태가 빈 축의 이유 — 저널에 그 사실이 있으면 그렇게 말한다. 판단할 수 없으면 예전 그대로. */
|
|
106
|
+
async function emptyStateReason(domainId: string, instanceId: string, axis: string): Promise<string> {
|
|
107
|
+
try {
|
|
108
|
+
const seen = await axisSeenInJournal(getRepository(TwinEvent), { domain: { id: domainId }, instanceId }, axis)
|
|
109
|
+
/* 판단할 수 없는 축(`null`)은 예전 답을 그대로 쓴다 — 모르는 것을 새 사실로 만들지 않는다. */
|
|
110
|
+
return seen ? 'observed-not-in-state' : 'none-yet'
|
|
111
|
+
} catch {
|
|
112
|
+
/* 저널을 못 읽었다고 카드가 없는 말을 하지는 않는다. */
|
|
113
|
+
return 'none-yet'
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* 되세운 상태를 담아 둔다 — **열쇠에 저널 끝 리비전이 들어 있어** 새 사건이 오면 저절로 낡는다.
|
|
119
|
+
*
|
|
120
|
+
* 접기는 저널 전체를 훑는 일이라(리비전 12만에서 2~3초) 모델 화면을 열 때마다 그 값을 낼 수 없다.
|
|
121
|
+
* 프로세스 안에만 두고 몇 개만 남긴다 — 다시 뜨면 다시 접는다(정확함이 캐시보다 앞선다).
|
|
122
|
+
*/
|
|
123
|
+
const RECALL_CACHE = new Map<string, { snap: any; heardAt: string | null }>()
|
|
124
|
+
const RECALL_CACHE_MAX = 8
|
|
125
|
+
|
|
101
126
|
@Resolver()
|
|
102
127
|
export class TwinModelQuery {
|
|
103
128
|
/**
|
|
@@ -188,6 +213,60 @@ export class TwinModelQuery {
|
|
|
188
213
|
}
|
|
189
214
|
return { snap, folded, observing }
|
|
190
215
|
})())
|
|
216
|
+
|
|
217
|
+
/*
|
|
218
|
+
* ── **라이브가 잊은 것을 저널이 안다** (2026-08-23) ─────────────────────────
|
|
219
|
+
*
|
|
220
|
+
* 미러의 관측은 메모리에만 산다. 재기동하면 빈 채로 시작하고, 원본이 다시 말해 줄 때 채워진다 —
|
|
221
|
+
* 그런데 읽기 커서가 정직해진 뒤로는 원본이 **지난 것을 다시 말하지 않는다.** 그래서 재고 741건이
|
|
222
|
+
* 저널에 있는데 화면은 「0」을 보였다. 사용자가 「재고현황이 없다」고 물은 자리가 이것이다.
|
|
223
|
+
*
|
|
224
|
+
* 그 0 은 사실이 아니라 **우리가 잊은 것**이다. 저널을 접으면 그대로 되살아난다(실측: 리비전
|
|
225
|
+
* 97,689 을 2.4초에 접어 물품 741 · 작업 9,145 · 지시 2,856).
|
|
226
|
+
*
|
|
227
|
+
* ── 그래서 접는다. 다만 **언제 들은 값인지 함께 낸다** ──────────────────────
|
|
228
|
+
* 저널을 접는 것은 **원천이 우리에게 말한 것을 다시 세우는 일**이지, 없는 값을 심는 일이 아니다.
|
|
229
|
+
* 그러나 물품·재고처럼 **절대값인 축**은 우리가 못 듣는 사이 떠났을 수 있다 — 그래서 값과 함께
|
|
230
|
+
* `heardAt`(그 접기의 시계)을 낸다. 사용자가 신선도를 스스로 판정할 수 있어야 하고, 화면은 그것을
|
|
231
|
+
* 값 옆에 적는다. 시각 없이 수만 내면 낡은 값을 지금으로 읽는다.
|
|
232
|
+
*
|
|
233
|
+
* 접는 조건을 좁게 둔다: **관측 축이 비어 있고, 저널에 그 축의 사실이 있을 때만**(그 판정은 색인
|
|
234
|
+
* 한 줄이다). 그리고 한 요청에 **한 번만** 접는다 — 위 `stateOnce` 와 같은 규율이다.
|
|
235
|
+
*/
|
|
236
|
+
let journalPromise: Promise<{ snap: any; heardAt: string | null } | null> | null = null
|
|
237
|
+
const journalOnce = () =>
|
|
238
|
+
(journalPromise ??= (async () => {
|
|
239
|
+
try {
|
|
240
|
+
const tip = await getRepository(TwinEvent).findOne({
|
|
241
|
+
where: { domain: { id: domainId } as any, instanceId } as any,
|
|
242
|
+
order: { revision: 'DESC' } as any
|
|
243
|
+
})
|
|
244
|
+
if (!tip?.revision) return null
|
|
245
|
+
/*
|
|
246
|
+
* **한 번 접은 것을 다시 접지 않는다.** 접기는 저널 전체를 훑는 일이다(리비전 12만에서 2~3초).
|
|
247
|
+
* 이 화면은 사람이 여러 번 들어오는 자리이므로 그대로 두면 매번 그 값을 낸다 — 그리고 저널이
|
|
248
|
+
* 수백만 행이 되면 그 비용이 화면을 못 쓰게 만든다.
|
|
249
|
+
*
|
|
250
|
+
* 열쇠에 **저널 끝의 리비전**을 넣는다: 새 사건이 오면 열쇠가 달라지므로 캐시가 저절로 낡는다
|
|
251
|
+
* (캐시가 사실을 이기지 않는다). 프로세스 안에만 두고 몇 개만 남긴다.
|
|
252
|
+
*/
|
|
253
|
+
const key = `${domainId}:${instanceId}:${tip.revision}`
|
|
254
|
+
const hit = RECALL_CACHE.get(key)
|
|
255
|
+
if (hit) return hit
|
|
256
|
+
const snap = unwrapState(await TwinEngine.recover(domainId, instanceId, tip.revision))
|
|
257
|
+
if (!snap) return null
|
|
258
|
+
/* 그 접기의 시계가 「언제 들은 값인가」다. 없으면 저널 끝의 사건 시각으로 답한다. */
|
|
259
|
+
const out = { snap, heardAt: (snap as any).nowTime ?? tip.eventTime ?? null }
|
|
260
|
+
RECALL_CACHE.set(key, out)
|
|
261
|
+
/* 오래된 열쇠부터 버린다 — 트윈 몇 개를 번갈아 봐도 메모리가 자라지 않게. */
|
|
262
|
+
while (RECALL_CACHE.size > RECALL_CACHE_MAX) RECALL_CACHE.delete(RECALL_CACHE.keys().next().value as string)
|
|
263
|
+
return out
|
|
264
|
+
} catch {
|
|
265
|
+
/* 접지 못했으면 아무 말도 만들지 않는다 — 라이브가 말한 것을 그대로 둔다. */
|
|
266
|
+
return null
|
|
267
|
+
}
|
|
268
|
+
})())
|
|
269
|
+
|
|
191
270
|
const concepts = await Promise.all(
|
|
192
271
|
TWIN_AXES.map(async (a: any) => {
|
|
193
272
|
/*
|
|
@@ -210,6 +289,17 @@ export class TwinModelQuery {
|
|
|
210
289
|
count: null,
|
|
211
290
|
notApplicable: true,
|
|
212
291
|
emptyBecause: 'not-applicable',
|
|
292
|
+
/*
|
|
293
|
+
* **그 개념의 성질** — 선언인가 관측인가 (2026-08-22).
|
|
294
|
+
*
|
|
295
|
+
* `basis` 와 다르다: `basis` 는 「이 값이 어디서 왔나」이고 트윈마다·시점마다 달라진다(같은
|
|
296
|
+
* 축이 한 트윈에서 `journal`, 다른 트윈에서 `state` 로 온다). 그것으로 묶으면 같은 축이 화면
|
|
297
|
+
* 마다 다른 묶음에 들어간다. `source` 는 트윈과 무관하게 고정이므로 묶는 축이 된다.
|
|
298
|
+
*
|
|
299
|
+
* 화면이 축 이름을 열거하지 않아도 되고, 축이 늘면 자동으로 맞는 묶음에 들어간다 —
|
|
300
|
+
* 카탈로그가 정본이다.
|
|
301
|
+
*/
|
|
302
|
+
source: axisSource(a.axis),
|
|
213
303
|
basis: 'declaration'
|
|
214
304
|
}
|
|
215
305
|
}
|
|
@@ -243,6 +333,15 @@ export class TwinModelQuery {
|
|
|
243
333
|
* 0 으로 내면 "작업이 없는 공장" 으로 읽히고, 그건 거짓이다(관측이 없는 것과 0 건은 다르다).
|
|
244
334
|
* 이 화면이 관측 열에서 내내 지켜 온 구분을 카드에서도 지킨다.
|
|
245
335
|
*/
|
|
336
|
+
/*
|
|
337
|
+
* **「아직 없다」와 「왔는데 상태에 없다」는 다른 사실이다** (2026-08-22).
|
|
338
|
+
*
|
|
339
|
+
* 화면이 「작업 — 아직 하나도 없습니다」라고 말했는데 저널에는 `task.status` 5,786건이 있었고
|
|
340
|
+
* 마지막 상태가 `in-progress` 였다(원인은 커널의 내보내기였고 그 레인에 넘겼다). 카드가 단정한
|
|
341
|
+
* 탓에 사용자가 그 차이를 직접 조사해야 했다 — 화면이 아는 것을 말하지 않은 자리다.
|
|
342
|
+
*
|
|
343
|
+
* 그래서 상태가 빈 축은 저널을 **한 건만** 물어본다(색인 한 줄, `journal-count` 의 문).
|
|
344
|
+
*/
|
|
246
345
|
if (axisSource(a.axis) === 'state') {
|
|
247
346
|
/* 스냅샷은 `{revision, state}` 봉투다 — 벗기지 않으면 축이 한 겹 안에 있어 언제나 `null` 이
|
|
248
347
|
된다(커널은 도는데 카드가 "모름" 으로 보였다). 벗기는 규칙의 집은 `warm-start` 하나다. */
|
|
@@ -255,7 +354,25 @@ export class TwinModelQuery {
|
|
|
255
354
|
const { snap, folded, observing } = await stateOnce()
|
|
256
355
|
/* 축이 사는 자리는 **커널 선언이 말한다**(`path`) — 축 이름으로만 읽으면 중첩된 축이
|
|
257
356
|
언제나 「관측 없음」이 된다(에너지의 수요 구간이 그렇다). 규칙은 `axisValueOf` 한 곳. */
|
|
258
|
-
|
|
357
|
+
let bucket = snap ? axisValueOf(snap, a) ?? null : null
|
|
358
|
+
let heardAt: string | null = null
|
|
359
|
+
let fromJournal = false
|
|
360
|
+
/*
|
|
361
|
+
* 라이브가 잊은 축은 **저널에서 되세운다**(§`journalOnce`). 그 축의 사실이 저널에 있을 때만
|
|
362
|
+
* 접고, 값과 함께 「언제 들은 값인가」를 낸다 — 시각 없이 수만 내면 낡은 값을 지금으로 읽는다.
|
|
363
|
+
*/
|
|
364
|
+
if (!folded && Array.isArray(bucket) && bucket.length === 0) {
|
|
365
|
+
const seen = await axisSeenInJournal(getRepository(TwinEvent), { domain: { id: domainId }, instanceId }, a.axis).catch(() => null)
|
|
366
|
+
if (seen) {
|
|
367
|
+
const j = await journalOnce()
|
|
368
|
+
const jb = j?.snap ? axisValueOf(j.snap, a) ?? null : null
|
|
369
|
+
if (Array.isArray(jb) && jb.length) {
|
|
370
|
+
bucket = jb
|
|
371
|
+
heardAt = j?.heardAt ?? null
|
|
372
|
+
fromJournal = true
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
259
376
|
return {
|
|
260
377
|
axis: a.axis,
|
|
261
378
|
label: a.label,
|
|
@@ -264,13 +381,34 @@ export class TwinModelQuery {
|
|
|
264
381
|
standardClass: a.standardClass,
|
|
265
382
|
typeRole: a.typeRole ?? null,
|
|
266
383
|
count: Array.isArray(bucket) ? bucket.length : null,
|
|
384
|
+
/*
|
|
385
|
+
* **수 하나가 뜻을 바꾸는 자리** (2026-08-23).
|
|
386
|
+
*
|
|
387
|
+
* 미러가 완료된 작업까지 상태에 들고 있게 되면서 「작업 2,820」이 되었다. 이 카드는 상태의
|
|
388
|
+
* 인구조사이므로 그 수가 맞다 — 그런데 사람은 그것을 「지금 할 일 2,820건」으로 읽는다.
|
|
389
|
+
* 수를 줄여 거짓으로 만드는 대신 **무엇으로 이루어진 수인지** 함께 낸다.
|
|
390
|
+
*
|
|
391
|
+
* 낱말은 커널의 것을 그대로 쓴다(`created`·`assigned`·`in-progress`·`completed`). 화면이
|
|
392
|
+
* 라벨을 입히고, 없으면 낱말이 그대로 보여 누락이 드러난다.
|
|
393
|
+
*/
|
|
394
|
+
...statusTally(bucket),
|
|
267
395
|
/*
|
|
268
396
|
* **비었으면 왜 비었는지 말한다.** 0 과 `null` 만 내면 화면은 "없다" 까지만 말하고, 사용자는
|
|
269
397
|
* "안 적어 놨나 · 아직 안 일어났나 · 못 세었나" 를 구별할 수 없다. 이 화면이 관측 열에서
|
|
270
398
|
* 내내 지켜 온 구분(§observedAbsence)을 카드에서도 지킨다.
|
|
271
399
|
*/
|
|
272
|
-
...(Array.isArray(bucket)
|
|
273
|
-
|
|
400
|
+
...(Array.isArray(bucket)
|
|
401
|
+
? bucket.length
|
|
402
|
+
? {}
|
|
403
|
+
: { emptyBecause: await emptyStateReason(domainId, instanceId, a.axis) }
|
|
404
|
+
: { emptyBecause: 'not-observed' }),
|
|
405
|
+
source: axisSource(a.axis),
|
|
406
|
+
basis: folded || fromJournal ? 'journal' : 'state',
|
|
407
|
+
/*
|
|
408
|
+
* **언제 들은 값인가** — 저널에서 되세운 축만 갖는다. 라이브가 답한 축은 지금이므로 없다.
|
|
409
|
+
* 이 값이 없으면 화면이 낡은 수를 지금으로 읽는다(그 구별이 이 카드의 전부다).
|
|
410
|
+
*/
|
|
411
|
+
...(fromJournal && heardAt ? { heardAt, recalled: true } : {}),
|
|
274
412
|
observing,
|
|
275
413
|
/* 선언이 아니라 관측인 축 — 화면이 "원본이 침묵했다" 로 읽지 않게 사실로 말한다. */
|
|
276
414
|
observedOnly: true,
|
|
@@ -301,6 +439,7 @@ export class TwinModelQuery {
|
|
|
301
439
|
entity && inDocument > 0 ? 'not-projected' : raw == null ? 'not-declared' : 'declared-empty'
|
|
302
440
|
}),
|
|
303
441
|
/* 'rows' = DB 가 셌다(질의 가능) · 'document' = 문서를 세었다(아직 이주 전). */
|
|
442
|
+
source: axisSource(a.axis),
|
|
304
443
|
basis: entity ? 'rows' : 'document',
|
|
305
444
|
...(entity ? { documentCount: inDocument, stale } : {}),
|
|
306
445
|
/* 표준 투영이 없는 축은 **숨기지 않고** 그 사실을 낸다(ADR-0031). */
|
|
@@ -321,12 +460,24 @@ export class TwinModelQuery {
|
|
|
321
460
|
* 트윈이 안 실행 중이면 비교할 상대가 없다 — 그때는 `null` 이다(0 이 아니다. 없는 것과
|
|
322
461
|
* 세어 보지 못한 것은 다르다).
|
|
323
462
|
*/
|
|
463
|
+
/*
|
|
464
|
+
* 도출은 **한 벌**이다(`engine/model-gap.ts`). 예전에는 이 자리에 인라인으로 있었는데, 같은 사실을
|
|
465
|
+
* 「현장과 맞춰지고 있나」 화면도 물어야 해서 두 곳이 다른 수를 말할 위험이 생겼다.
|
|
466
|
+
*
|
|
467
|
+
* 이 화면은 **전체 목록**이 필요하다(인스펙션이니까). 그래서 상한 없는 원본을 여기서 만들고,
|
|
468
|
+
* 상한이 걸린 요약은 `modelGapOf` 가 답한다 — 같은 판정(`origin === 'observed'`)을 쓴다.
|
|
469
|
+
*/
|
|
324
470
|
const snap = TwinEngine.snapshot(domainId, instanceId)
|
|
471
|
+
const gap = modelGapOf(snap)
|
|
325
472
|
const observedOnly = snap
|
|
326
473
|
? {
|
|
327
474
|
equipment: (snap.equipment ?? []).filter((e: any) => e?.origin === 'observed').map((e: any) => e.id),
|
|
328
475
|
locations: (snap.locations ?? []).filter((l: any) => l?.origin === 'observed').map((l: any) => l.id),
|
|
329
|
-
persons: (snap.persons ?? []).filter((p: any) => p?.origin === 'observed').map((p: any) => p.id)
|
|
476
|
+
persons: (snap.persons ?? []).filter((p: any) => p?.origin === 'observed').map((p: any) => p.id),
|
|
477
|
+
/* 커널이 담을 줄 몰라 세어 둔 사건·모르는 설비 보고 — 인스펙션이 이미 물을 자리다. */
|
|
478
|
+
unhandled: gap?.unhandled ?? [],
|
|
479
|
+
unknownEquipmentReports: gap?.unknownEquipmentReports ?? null,
|
|
480
|
+
notCovered: gap?.notCovered ?? []
|
|
330
481
|
}
|
|
331
482
|
: null
|
|
332
483
|
|
|
@@ -374,7 +525,7 @@ export class TwinModelQuery {
|
|
|
374
525
|
})(),
|
|
375
526
|
description: inst.description ?? null,
|
|
376
527
|
kind: inst.kind,
|
|
377
|
-
|
|
528
|
+
restartPolicy: inst.restartPolicy,
|
|
378
529
|
source: ref
|
|
379
530
|
? { referenceId: ref.source, adapterType: ref.adapterType, status: ref.status, lastSyncedAt: ref.lastSyncedAt }
|
|
380
531
|
: null,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { nameIndex, namesFor } from './name-index.js'
|
|
1
2
|
import { Arg, Ctx, Int, Query, Resolver } from 'type-graphql'
|
|
2
3
|
|
|
3
4
|
import { ScalarObject, getRepository } from '@things-factory/shell'
|
|
@@ -171,6 +172,12 @@ export class TwinModelTreeQuery {
|
|
|
171
172
|
items,
|
|
172
173
|
edges,
|
|
173
174
|
/* 수량을 하나라도 모르면 아래 합계가 성립하지 않는다는 사실을 그대로 낸다. */
|
|
175
|
+
/*
|
|
176
|
+
* **이름을 함께 낸다** (2026-08-22). 나무의 줄이 id 만 보이면 사용자는 그 노드가 무엇인지 알 수
|
|
177
|
+
* 없다 — 사용자의 말: 「각 노드의 식별자가 반복되어서 이해가 어렵다. 식별자와 이름이 보이면.」
|
|
178
|
+
* 목록·항목 화면이 쓰는 것과 **같은 표**를 쓴다(`name-index`) — 두 벌이면 한쪽만 고쳐진다.
|
|
179
|
+
*/
|
|
180
|
+
names: namesFor(items, nameIndex(inst.model ?? {})),
|
|
174
181
|
qtyIncomplete: edges.some(e => e.step == null),
|
|
175
182
|
cycles: [...new Set(cycles)],
|
|
176
183
|
truncated: { depth: depthTruncated, items: itemTruncated, depthCap, itemCap },
|