@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,147 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* **원본의 표면 중 얼마를 분류했나** — 순수(import 없음).
|
|
3
|
+
*
|
|
4
|
+
* ── 왜 이 축이 생겼나 (2026-08-20) ──────────────────────────────────────────
|
|
5
|
+
* 탐침 열한 개는 「이 단계가 되나」를 묻는다. `live-ingest` 는 **레코드 한 건이 통과하면 초록**이다.
|
|
6
|
+
* 그래서 재고만 읽는 커넥터에도 「라이브 통과」가 나왔고, 그 사이 원본의 `production_input`(무엇으로
|
|
7
|
+
* 무엇을 만들었나 · 실제 투입량 · 선언된 필요량)과 오더 네 종류와 팔레트가 읽히지 않고 있었다.
|
|
8
|
+
*
|
|
9
|
+
* 그것들은 대화에서 하나씩 「아차」로 드러났다. 그 방식은 반복된다 — 빠진 것을 기억으로 찾기 때문이다.
|
|
10
|
+
* 그래서 축을 하나 더 둔다: **원본이 내놓는 표면을 세고, 그 각각을 커넥터가 무엇으로 처리하는지 적는다.**
|
|
11
|
+
*
|
|
12
|
+
* ── 규율 ────────────────────────────────────────────────────────────────────
|
|
13
|
+
* 표면 목록은 **원본에서 얻는다**(GraphQL 인트로스펙션 등). 커넥터가 손으로 적은 목록으로 세면 같은
|
|
14
|
+
* 눈먼 자리를 그대로 물려받는다 — 내가 잊은 표는 목록에도 없다.
|
|
15
|
+
*
|
|
16
|
+
* 「읽지 않는다」는 결론은 **정당할 수 있다.** 파생 표(집계·추적)나 우리 어휘에 자리가 없는 것을 읽지
|
|
17
|
+
* 않는 것은 결함이 아니다. 결함은 **분류하지 않은 것**이다 — 그것은 생각한 적이 없다는 뜻이다.
|
|
18
|
+
*
|
|
19
|
+
* 그래서 판정은 「전부 읽었나」가 아니라 **「전부 판단했나」** 다.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 표면 하나를 커넥터가 무엇으로 처리하는가 — **각 낱말에 적용 가능한 기준과 조치할 사람이 있다.**
|
|
24
|
+
* 기준이 없으면 분류가 느낌이 되고, 조치자가 없으면 표를 읽어도 누가 움직일지 알 수 없다.
|
|
25
|
+
*
|
|
26
|
+
* · `read` — **지금 커넥터가 질의한다.** 기준: 커넥터 코드에 그 질의 이름이 있다(확인 가능).
|
|
27
|
+
* 조치자: 없음(완료분).
|
|
28
|
+
*
|
|
29
|
+
* · `todo` — **실려야 하는 사실인데 아직 안 읽는다.** 기준 셋을 다 만족: ① 원본에 읽을 길이
|
|
30
|
+
* 있다 ② 우리 어휘에 담을 자리가 있다 ③ 아직 안 읽는다. 조치자: **커넥터**.
|
|
31
|
+
*
|
|
32
|
+
* · `blocked` — **실려야 하고 담을 자리도 있는데 원본이 읽을 길을 주지 않는다.** 기준: 목록 질의가
|
|
33
|
+
* 없거나, 있어도 필수 인자 때문에 전역으로 읽을 수 없다. 조치자: **원본 쪽**.
|
|
34
|
+
* `todo` 와 한 칸에 두지 않는 이유가 이것이다 — 움직여야 하는 사람이 다르다.
|
|
35
|
+
*
|
|
36
|
+
* · `no-home` — **원본에 길은 있는데 그 사실이 상태로 남을 자리가 없다.** 기준은 어휘가 아니라
|
|
37
|
+
* **상태에 반영하는 것**이다 — 이벤트 어휘에 통과할 자리가 있어도 상태에 반영되지
|
|
38
|
+
* 않으면 사실은 남지
|
|
39
|
+
* 않는다(오히려 더 나쁘다: 검증을 통과하므로 실은 줄 알고 잃는다).
|
|
40
|
+
* 조치자: **커널·아키텍처** — 반영할 것인지 판단해야 한다.
|
|
41
|
+
* 냉장 온도가 그 예다. EPCIS `SensorElement`·`sensorReport` 는 이미 있고 검증기도
|
|
42
|
+
* 본다. **소비하는 곳이 0 이다.** 그러므로 「어휘에 자리가 없다」가 아니라 「상태에
|
|
43
|
+
* 반영하는 곳이 없다」가 정확하고, 할 일도 자리 만들기가 아니라 그 반영이다.
|
|
44
|
+
* **「중요하지 않다」가 아니다.** 오히려 중요한데 못 담는다는 뜻이고, 그래서 판단이
|
|
45
|
+
* 필요하다. 냉장 온도가 그 예다 — 식품에서 일급 사실인데 계량 어휘가 에너지 전용이다.
|
|
46
|
+
*
|
|
47
|
+
* · `derived` — **원본이 다른 표에서 계산해 내놓는 값.** 기준: 그 질의의 결과가 우리가 읽는(또는
|
|
48
|
+
* 읽을) 사실로부터 계산된다. 읽으면 **같은 수를 두 곳에서 만들게 되고**, 두 수가
|
|
49
|
+
* 갈리면 어느 것을 믿을지 알 수 없다. 조치자: 없음 — 읽지 않는 것이 정답.
|
|
50
|
+
*
|
|
51
|
+
* · `duplicate` — **같은 사실을 다른 문으로 내놓는 것.** 기준: 목록 질의로 이미 읽는(또는 읽을) 그
|
|
52
|
+
* 사실의 단건 드릴다운이다. 계산된 값이 아니므로 `derived` 와 다르고, 관심 밖이
|
|
53
|
+
* 아니므로 `out-of-scope` 와도 다르다. 조치자: 없음.
|
|
54
|
+
*
|
|
55
|
+
* · `out-of-scope` — **공장의 상태나 활동이 아니다.** 기준: 그 사실이 자리·물건·설비·사람·작업·
|
|
56
|
+
* 오더 어디에도 해당하지 않는다(사용자 계정·세무·거래처 마스터). 조치자: 없음.
|
|
57
|
+
*
|
|
58
|
+
* 「읽지 않는다」는 결론은 정당할 수 있다(`derived`·`duplicate`·`out-of-scope`). 결함은 **분류하지
|
|
59
|
+
* 않은 것**이다 — 그것은 생각한 적이 없다는 뜻이다. 그래서 판정은 「전부 읽었나」가 아니라
|
|
60
|
+
* **「전부 판단했나」** 다.
|
|
61
|
+
*/
|
|
62
|
+
export type Disposition = 'read' | 'derived' | 'duplicate' | 'no-home' | 'out-of-scope' | 'blocked' | 'todo'
|
|
63
|
+
|
|
64
|
+
/** 표면 하나에 대한 판단 — **이유가 필수다.** 이유 없는 분류는 분류한 척이다. */
|
|
65
|
+
export interface SurfaceRuling {
|
|
66
|
+
disposition: Disposition
|
|
67
|
+
/** 왜 그렇게 정했나. 사람이 읽는 한 문장. */
|
|
68
|
+
why: string
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface CoverageInput {
|
|
72
|
+
/** 원본이 실제로 내놓는 표면 — **원본에서 얻은 것.** 커넥터가 적은 목록이 아니다. */
|
|
73
|
+
sourceSurface: readonly string[]
|
|
74
|
+
/** 커넥터의 판단표. */
|
|
75
|
+
rulings: Readonly<Record<string, SurfaceRuling>>
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface CoverageReport {
|
|
79
|
+
/** 원본 표면 수. */
|
|
80
|
+
total: number
|
|
81
|
+
/** 분류된 수. */
|
|
82
|
+
classified: number
|
|
83
|
+
/** **분류하지 않은 표면** — 생각한 적 없는 것. 이 목록이 비어야 한다. */
|
|
84
|
+
unclassified: string[]
|
|
85
|
+
/** 판단표에는 있는데 원본에 없는 것 — 원본이 바뀌었거나 이름을 잘못 적었다. */
|
|
86
|
+
stale: string[]
|
|
87
|
+
byDisposition: Record<Disposition, string[]>
|
|
88
|
+
/** 실어야 하는데 아직 아닌 것. 초록을 막지는 않지만 **세어 보여야** 한다. */
|
|
89
|
+
todo: string[]
|
|
90
|
+
/** 원본이 길을 주지 않아 막힌 것 — 이 목록이 곧 **원본 쪽 선결 과제**다. */
|
|
91
|
+
blocked: string[]
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const DISPOSITIONS: Disposition[] = ['read', 'derived', 'duplicate', 'no-home', 'out-of-scope', 'blocked', 'todo']
|
|
95
|
+
|
|
96
|
+
export function assessCoverage(input: CoverageInput): CoverageReport {
|
|
97
|
+
const surface = [...new Set(input.sourceSurface)].sort()
|
|
98
|
+
const byDisposition = DISPOSITIONS.reduce(
|
|
99
|
+
(acc, d) => ((acc[d] = []), acc),
|
|
100
|
+
{} as Record<Disposition, string[]>
|
|
101
|
+
)
|
|
102
|
+
const unclassified: string[] = []
|
|
103
|
+
|
|
104
|
+
for (const name of surface) {
|
|
105
|
+
const r = input.rulings[name]
|
|
106
|
+
/* 이유가 없으면 분류로 세지 않는다 — 낱말만 채운 것을 판단으로 세면 이 축이 무의미해진다. */
|
|
107
|
+
if (!r || !DISPOSITIONS.includes(r.disposition) || !r.why?.trim()) {
|
|
108
|
+
unclassified.push(name)
|
|
109
|
+
continue
|
|
110
|
+
}
|
|
111
|
+
byDisposition[r.disposition].push(name)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const known = new Set(surface)
|
|
115
|
+
const stale = Object.keys(input.rulings)
|
|
116
|
+
.filter(k => !known.has(k))
|
|
117
|
+
.sort()
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
total: surface.length,
|
|
121
|
+
classified: surface.length - unclassified.length,
|
|
122
|
+
unclassified,
|
|
123
|
+
stale,
|
|
124
|
+
byDisposition,
|
|
125
|
+
todo: byDisposition.todo,
|
|
126
|
+
blocked: byDisposition.blocked
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* 한 줄 요약 — 화면과 로그가 같은 문장을 쓰도록.
|
|
132
|
+
*
|
|
133
|
+
* **「전부 읽었다」고 말하지 않는다.** 말하는 것은 「전부 판단했다」다. 그리고 분류하지 않은 것이 남아
|
|
134
|
+
* 있으면 그 수를 앞세운다 — 그것이 이 축의 유일한 목적이다.
|
|
135
|
+
*/
|
|
136
|
+
export function coverageLine(r: CoverageReport): string {
|
|
137
|
+
if (r.unclassified.length) {
|
|
138
|
+
const head = r.unclassified.slice(0, 5).join(', ')
|
|
139
|
+
const more = r.unclassified.length > 5 ? ` +${r.unclassified.length - 5}` : ''
|
|
140
|
+
return `${r.unclassified.length}/${r.total} surface(s) not yet judged — ${head}${more}`
|
|
141
|
+
}
|
|
142
|
+
const read = r.byDisposition.read.length
|
|
143
|
+
const parts = [`${read} read`]
|
|
144
|
+
if (r.todo.length) parts.push(`${r.todo.length} still to carry`)
|
|
145
|
+
if (r.blocked.length) parts.push(`${r.blocked.length} blocked at the source`)
|
|
146
|
+
return `all ${r.total} surface(s) judged; ${parts.join(', ')}`
|
|
147
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 연결 적합성 하네스 — **통합을 시험 스위트처럼 돌린다.**
|
|
3
|
+
*
|
|
4
|
+
* 선언 적합성 하네스 세 벌과 같은 규율을 **연결**에 적용한다: 사실이 조용히 사라지는 것을 막는다.
|
|
5
|
+
* 다른 점은 대상이 우리 선언이 아니라 **남의 시스템**이라는 것이고, 그래서 두 규칙이 더 붙는다.
|
|
6
|
+
*
|
|
7
|
+
* ① **돌리지 않은 프로브는 「실패」가 아니라 「모른다」다.** 실패로 접으면 사람이 그 빨간 칸을
|
|
8
|
+
* 없애려고 무엇이든 하게 되고, 모른다로 접으면 **무엇을 아직 모르는지**가 남는다. 판정
|
|
9
|
+
* (`assessStagePath`)이 「모른다」를 달성으로 바꾸지 않으므로 이 구분이 끝까지 살아 있다.
|
|
10
|
+
* ② **쓰기 프로브는 명시 동의 없이 돌지 않는다.** 이 스위트는 고객의 운영 시스템을 두드린다.
|
|
11
|
+
* 읽기는 기본으로 돌리고, 남의 공장에 명령을 넣는 프로브는 사람이 켜야 돈다 — 켜지 않으면
|
|
12
|
+
* 폐루프 칸은 「검증되지 않음」으로 남는다(그게 사실이다).
|
|
13
|
+
*
|
|
14
|
+
* 순수 모듈이다(전송·DB 무의존). 실행기는 이 선언을 읽어 프로브를 돌리고 `ProbeResult[]` 를 낸다.
|
|
15
|
+
* 화면은 그 결과와 `assessStagePath` 의 판정을 함께 보인다 — 초록/빨강과 **그 초록이 무엇을 증명하나**.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { StageEvidence, StageId } from './stage-path.ts'
|
|
19
|
+
|
|
20
|
+
export type ProbeId =
|
|
21
|
+
| 'connect' // 붙는다
|
|
22
|
+
| 'discover' // 사이트가 열거된다
|
|
23
|
+
| 'master' // 구조가 들어온다
|
|
24
|
+
| 'master-warnings' // 못 옮긴 값을 세었다
|
|
25
|
+
| 'items' // 자리에 물품이 놓인다(비직렬 수량 포함)
|
|
26
|
+
| 'live-ingest' // 이벤트가 들어온다
|
|
27
|
+
| 'live-removal' // **반출이 관측된다** — 진짜 delta 의 증거
|
|
28
|
+
| 'declarations' // 공정 선언이 실려 있다
|
|
29
|
+
| 'forecast' // 예측이 돈다
|
|
30
|
+
| 'actuate-declared' // 액추에이션 능력이 선언∩구현이다
|
|
31
|
+
| 'actuate-effect' // 명령의 효과가 상관되어 돌아온다 (쓰기)
|
|
32
|
+
|
|
33
|
+
export interface ProbeSpec {
|
|
34
|
+
id: ProbeId
|
|
35
|
+
/** 이 프로브가 증거를 대는 단계. */
|
|
36
|
+
stage: StageId
|
|
37
|
+
/** **무엇을 증명하나** — 언어중립 키(사람 말은 표현계층이 렌더). 「무엇을 호출하나」가 아니다. */
|
|
38
|
+
proves: string
|
|
39
|
+
/** 대상 시스템을 바꾸는가. 참이면 사람이 켜야 돈다. */
|
|
40
|
+
writes: boolean
|
|
41
|
+
/** 이 프로브가 실패하면 뒤 프로브를 돌릴 의미가 없다. */
|
|
42
|
+
gating: boolean
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 시퀀스 — 순서가 곧 방법론이다. 앞이 막히면 뒤는 돌리지 않는다(돌려도 답이 무의미하다).
|
|
47
|
+
*
|
|
48
|
+
* `master-warnings` 를 **별개 프로브**로 둔 이유: 구조가 들어오는 것과 못 옮긴 값을 세는 것은 다른
|
|
49
|
+
* 사실이고, 세지 않은 인제스트는 도크를 랙으로 바꿔 놓고도 성공으로 보인다.
|
|
50
|
+
*/
|
|
51
|
+
export const PROBE_SEQUENCE: readonly ProbeSpec[] = [
|
|
52
|
+
{ id: 'connect', stage: 'structure', proves: 'probe.connect', writes: false, gating: true },
|
|
53
|
+
{ id: 'discover', stage: 'structure', proves: 'probe.discover', writes: false, gating: true },
|
|
54
|
+
{ id: 'master', stage: 'structure', proves: 'probe.master', writes: false, gating: true },
|
|
55
|
+
{ id: 'master-warnings', stage: 'structure', proves: 'probe.masterWarnings', writes: false, gating: false },
|
|
56
|
+
{ id: 'items', stage: 'structure', proves: 'probe.items', writes: false, gating: false },
|
|
57
|
+
{ id: 'live-ingest', stage: 'live', proves: 'probe.liveIngest', writes: false, gating: true },
|
|
58
|
+
{ id: 'live-removal', stage: 'live', proves: 'probe.liveRemoval', writes: false, gating: false },
|
|
59
|
+
{ id: 'declarations', stage: 'forecast', proves: 'probe.declarations', writes: false, gating: false },
|
|
60
|
+
{ id: 'forecast', stage: 'forecast', proves: 'probe.forecast', writes: false, gating: false },
|
|
61
|
+
{ id: 'actuate-declared', stage: 'closed-loop', proves: 'probe.actuateDeclared', writes: false, gating: true },
|
|
62
|
+
{ id: 'actuate-effect', stage: 'closed-loop', proves: 'probe.actuateEffect', writes: true, gating: false }
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
/** 프로브 한 건의 결과. `ran: false` = 돌리지 않았다(실패가 아니다). */
|
|
66
|
+
export interface ProbeResult {
|
|
67
|
+
id: ProbeId
|
|
68
|
+
ran: boolean
|
|
69
|
+
/**
|
|
70
|
+
* 이 대상에 **해당하지 않는다**. 통과도 실패도 미측정도 아닌 네 번째 값이다.
|
|
71
|
+
*
|
|
72
|
+
* 시뮬레이션 트윈에 라이브 프로브를 들이대면 빨간 칸이 생기는데 그건 결함이 아니다 — 시뮬은 원천에서
|
|
73
|
+
* 받는 것이 없다. 레퍼런스 없이 템플릿으로 세운 트윈에는 옮겨 온 마스터가 없으므로 「못 옮긴 값」도 없다.
|
|
74
|
+
* 이걸 통과로 접으면 없는 능력을 주장하고, 미측정으로 접으면 영원히 배선을 기다린다.
|
|
75
|
+
*/
|
|
76
|
+
applicable?: boolean
|
|
77
|
+
/** 돌렸을 때의 판정. 돌리지 않았으면 무의미하다. */
|
|
78
|
+
passed?: boolean
|
|
79
|
+
/** 센 값(자리 수·반출 건수·선언 수 등). 세지 않았으면 없다. */
|
|
80
|
+
count?: number
|
|
81
|
+
/** 선언 적재량 프로브만 — `declared`/`total`. */
|
|
82
|
+
coverage?: { declared: number; total: number }
|
|
83
|
+
/** 왜 실패했나·왜 건너뛰었나. 언어중립 코드 또는 원문 오류. */
|
|
84
|
+
detail?: string
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const by = (rs: readonly ProbeResult[], id: ProbeId) => rs.find(r => r.id === id)
|
|
88
|
+
/** 돌렸고 셌으면 그 수, 아니면 `null`(모른다). 실패한 프로브의 수를 0 으로 읽지 않는다. */
|
|
89
|
+
/**
|
|
90
|
+
* 센 값은 **판정과 무관한 사실**이다 — `passed` 를 요구하면 「세어서 0 을 얻었다」가 「모른다」로 접힌다.
|
|
91
|
+
* 그 구분이 이 하네스의 전부이므로 여기서 잃으면 나머지가 무의미하다. 해당 없음만 `null` 이다.
|
|
92
|
+
*/
|
|
93
|
+
const counted = (r: ProbeResult | undefined): number | null =>
|
|
94
|
+
r?.applicable === false ? null : r?.ran && r.count !== undefined ? r.count : null
|
|
95
|
+
/** 돌렸고 셌으면 그 수, 아니면 0. **존재 여부**를 묻는 자리에만 쓴다(모름과 0 을 구별할 필요가 없는 축). */
|
|
96
|
+
const countedOrZero = (r: ProbeResult | undefined): number => counted(r) ?? 0
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 프로브 결과를 **증거**로 접는다.
|
|
100
|
+
*
|
|
101
|
+
* 접는 규칙의 정수: **돌리지 않은 프로브는 `null`** 로 간다. 그러면 판정이 그 단계를 달성으로
|
|
102
|
+
* 만들지 않고 `unmeasured` 로 말한다 — 스위트를 절반만 돌리고 초록을 주장할 길이 없다.
|
|
103
|
+
*/
|
|
104
|
+
export function foldProbeResults(results: readonly ProbeResult[]): StageEvidence {
|
|
105
|
+
return {
|
|
106
|
+
locationCount: countedOrZero(by(results, 'master')),
|
|
107
|
+
itemCount: countedOrZero(by(results, 'items')),
|
|
108
|
+
masterWarnings: counted(by(results, 'master-warnings')),
|
|
109
|
+
/* 해당 없음은 **미측정이 아니다** — 물을 대상이 아닌 축을 판정에서 뺀다. */
|
|
110
|
+
masterWarningsApplicable: by(results, 'master-warnings')?.applicable !== false,
|
|
111
|
+
liveIngestTotal: countedOrZero(by(results, 'live-ingest')),
|
|
112
|
+
removalsObserved: counted(by(results, 'live-removal')),
|
|
113
|
+
forecastRan: by(results, 'forecast')?.ran === true && by(results, 'forecast')?.passed === true,
|
|
114
|
+
specCoverage: (() => {
|
|
115
|
+
const r = by(results, 'declarations')
|
|
116
|
+
return r?.ran && r.passed && r.coverage ? r.coverage : null
|
|
117
|
+
})(),
|
|
118
|
+
actuateCapability: by(results, 'actuate-declared')?.passed === true,
|
|
119
|
+
correlatedEffectObserved: counted(by(results, 'actuate-effect'))
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* 지금 돌릴 프로브 — 게이팅과 쓰기 동의를 지킨다.
|
|
125
|
+
*
|
|
126
|
+
* 앞의 게이팅 프로브가 실패했으면 **거기서 멈춘다.** 뒤를 돌려 빨간 칸을 늘리는 것은 정보를 주지
|
|
127
|
+
* 않는다(원인은 이미 앞에 있다). 쓰기 프로브는 `allowWrites` 없이는 목록에 들어오지 않는다.
|
|
128
|
+
*/
|
|
129
|
+
export function plannedProbes(results: readonly ProbeResult[], opts: { allowWrites?: boolean } = {}): ProbeId[] {
|
|
130
|
+
const out: ProbeId[] = []
|
|
131
|
+
for (const spec of PROBE_SEQUENCE) {
|
|
132
|
+
if (spec.writes && !opts.allowWrites) continue
|
|
133
|
+
const prior = by(results, spec.id)
|
|
134
|
+
if (prior?.applicable === false) continue // 해당 없는 프로브는 계획하지 않는다
|
|
135
|
+
if (!prior?.ran) out.push(spec.id)
|
|
136
|
+
if (spec.gating && prior?.ran && prior.passed === false) break
|
|
137
|
+
}
|
|
138
|
+
return out
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** 해당 없음으로 판정된 프로브 — 화면이 「왜 회색인가」를 말할 수 있게 사유와 함께 낸다. */
|
|
142
|
+
export function notApplicable(results: readonly ProbeResult[]): { id: ProbeId; detail?: string }[] {
|
|
143
|
+
return results.filter(r => r.applicable === false).map(r => ({ id: r.id, ...(r.detail ? { detail: r.detail } : {}) }))
|
|
144
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 실행기 — 모은 사실을 프로브 결과로 옮기고, 단계 판정과 프로파일까지 한 번에 낸다.
|
|
3
|
+
*
|
|
4
|
+
* I/O 는 여기 없다(호출자가 모은다). 그래서 판정 전체가 테스트된다.
|
|
5
|
+
*/
|
|
6
|
+
import { PROBE_SEQUENCE, foldProbeResults, notApplicable, plannedProbes, type ProbeId, type ProbeResult } from './integration-probes.ts'
|
|
7
|
+
import { assessStagePath, forbiddenClaims, type StagePath } from './stage-path.ts'
|
|
8
|
+
import { narrowProfile, unknownProfile, type FeedKind, type TargetProfile } from './integration-target-profile.ts'
|
|
9
|
+
|
|
10
|
+
/** 호출자가 모아 오는 사실. 전부 `null` 이 「모른다」다 — 0 이나 false 로 채우지 않는다. */
|
|
11
|
+
export interface SuiteFacts {
|
|
12
|
+
/** 이 트윈이 무엇인가. 시뮬은 원천에서 받는 것이 없다(라이브 프로브는 해당 없음). */
|
|
13
|
+
subject: 'live' | 'sim' | 'stopped' | 'unknown'
|
|
14
|
+
/** 레퍼런스(원천 어댑터)에서 왔나. 템플릿으로 세운 트윈은 옮겨 온 마스터가 없다. */
|
|
15
|
+
fromReference: boolean
|
|
16
|
+
/** `capabilitiesOf(adapter)` — 선언∩구현. */
|
|
17
|
+
adapterCapabilities: readonly string[]
|
|
18
|
+
connectOk: boolean | null
|
|
19
|
+
siteCount: number | null
|
|
20
|
+
locationCount: number | null
|
|
21
|
+
itemCount: number | null
|
|
22
|
+
/** 마지막 동기의 못 옮긴 값 수(`TwinReference.lastWarningCount`). */
|
|
23
|
+
lastWarningCount: number | null
|
|
24
|
+
/** 저널 누적 — 재시작에도 남는다(인메모리 계기와 다르다). */
|
|
25
|
+
journalTotal: number | null
|
|
26
|
+
/** 저널의 `action='DELETE'` 수 — **반출 관측**. 승격 전 행은 세어지지 않으므로 `null` 이 정직하다. */
|
|
27
|
+
removalsJournaled: number | null
|
|
28
|
+
specCoverage: { declared: number; total: number } | null
|
|
29
|
+
forecastRan: boolean | null
|
|
30
|
+
feed: FeedKind
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface SuiteAssessment {
|
|
34
|
+
results: ProbeResult[]
|
|
35
|
+
profile: TargetProfile
|
|
36
|
+
path: StagePath
|
|
37
|
+
notApplicable: { id: ProbeId; detail?: string }[]
|
|
38
|
+
forbiddenClaims: string[]
|
|
39
|
+
/** 다음에 돌릴 프로브 — 쓰기 동의 없이는 쓰기 프로브가 빠진다. */
|
|
40
|
+
planned: ProbeId[]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const r = (id: ProbeId, o: Partial<ProbeResult>): ProbeResult => ({ id, ran: false, ...o })
|
|
44
|
+
|
|
45
|
+
/** 값이 있으면 돌린 것으로 보고 판정한다. `null` = 안 돌렸다(실패 아님). */
|
|
46
|
+
const fromNumber = (id: ProbeId, v: number | null, min = 1): ProbeResult =>
|
|
47
|
+
v === null ? r(id, { ran: false }) : r(id, { ran: true, passed: v >= min, count: v })
|
|
48
|
+
|
|
49
|
+
export function assessSuite(f: SuiteFacts, opts: { allowWrites?: boolean } = {}): SuiteAssessment {
|
|
50
|
+
const liveApplies = f.subject !== 'sim'
|
|
51
|
+
const results: ProbeResult[] = [
|
|
52
|
+
f.connectOk === null ? r('connect', { ran: false }) : r('connect', { ran: true, passed: f.connectOk }),
|
|
53
|
+
fromNumber('discover', f.siteCount),
|
|
54
|
+
fromNumber('master', f.locationCount),
|
|
55
|
+
/* 레퍼런스 없이 세운 트윈에는 옮겨 온 마스터가 없다 — 「못 옮긴 값」을 물을 대상이 아니다. */
|
|
56
|
+
f.fromReference
|
|
57
|
+
? fromNumber('master-warnings', f.lastWarningCount, 0)
|
|
58
|
+
: r('master-warnings', { ran: false, applicable: false, detail: 'not-from-reference' }),
|
|
59
|
+
fromNumber('items', f.itemCount),
|
|
60
|
+
liveApplies ? fromNumber('live-ingest', f.journalTotal) : r('live-ingest', { ran: false, applicable: false, detail: 'subject-is-simulation' }),
|
|
61
|
+
liveApplies ? fromNumber('live-removal', f.removalsJournaled, 1) : r('live-removal', { ran: false, applicable: false, detail: 'subject-is-simulation' }),
|
|
62
|
+
f.specCoverage === null
|
|
63
|
+
? r('declarations', { ran: false })
|
|
64
|
+
: r('declarations', { ran: true, passed: f.specCoverage.declared > 0, coverage: f.specCoverage }),
|
|
65
|
+
f.forecastRan === null ? r('forecast', { ran: false }) : r('forecast', { ran: true, passed: f.forecastRan }),
|
|
66
|
+
r('actuate-declared', { ran: true, passed: f.adapterCapabilities.includes('actuate') }),
|
|
67
|
+
/* 액추에이션은 계약에 아직 없다(ADR-0036). 측정을 만들면 없는 것을 있는 척한다. */
|
|
68
|
+
r('actuate-effect', { ran: false, detail: 'actuation-not-wired' })
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
/* 반출을 셌는데 0 이면 「세지 않음」과 구별해 프로브를 실패로 남긴다 — count 0 은 사실이다. */
|
|
72
|
+
const removal = results.find(x => x.id === 'live-removal')!
|
|
73
|
+
if (removal.ran && f.removalsJournaled === 0) removal.count = 0
|
|
74
|
+
|
|
75
|
+
const profile = narrowProfile(unknownProfile(), {
|
|
76
|
+
feed: f.feed,
|
|
77
|
+
multiSite: f.siteCount === null ? null : f.siteCount > 1,
|
|
78
|
+
write: f.adapterCapabilities.includes('actuate') ? 'command' : 'unknown'
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
const evidence = { ...foldProbeResults(results), subject: liveApplies ? ('live' as const) : ('sim' as const), feed: f.feed }
|
|
82
|
+
const path = assessStagePath(evidence)
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
results,
|
|
86
|
+
profile,
|
|
87
|
+
path,
|
|
88
|
+
notApplicable: notApplicable(results),
|
|
89
|
+
forbiddenClaims: forbiddenClaims(path),
|
|
90
|
+
planned: plannedProbes(results, opts)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** 시퀀스 명세 — 화면이 회색 칸의 이유(해당 없음·미실행)를 나란히 보이도록 함께 낸다. */
|
|
95
|
+
export const SUITE_SPEC = PROBE_SEQUENCE
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 대상 프로파일 — **스위트가 갈리는 근거.**
|
|
3
|
+
*
|
|
4
|
+
* ── 왜 프로파일이 먼저인가 ──────────────────────────────────────────────────
|
|
5
|
+
* 처음 만든 시퀀스는 직선이었다. 모든 대상에게 같은 순서를 강요했고, 그래서 두 방향으로 거짓말을
|
|
6
|
+
* 했다: push 소스에 커서 프로브를 들이대 빨간 칸을 만들고(결함이 아닌데), 반대로 스냅샷 소스에는
|
|
7
|
+
* 반출을 못 본 이유를 「아직 안 돌렸다」로 적었다(구조적으로 못 내는 것인데).
|
|
8
|
+
*
|
|
9
|
+
* 갈림의 근거는 **대상의 성격**이고, 그 성격은 **우리가 정하는 것이 아니라 발견하는 것**이다.
|
|
10
|
+
* 그래서 프로파일의 모든 축은 `'unknown'` 에서 시작해 프로브가 좁힌다. 모르는 축으로는 라우팅하지
|
|
11
|
+
* 않는다 — 모르면 그 축에 걸린 프로브는 「아직 판단할 수 없음」이고, 짐작으로 건너뛰지 않는다.
|
|
12
|
+
*
|
|
13
|
+
* ── 축을 고른 기준 ─────────────────────────────────────────────────────────
|
|
14
|
+
* 실제로 **다른 프로브를 요구하는 것만** 축이 된다. 「벤더 이름」은 축이 아니다(같은 벤더가 REST 도
|
|
15
|
+
* 파일도 준다). 아래 일곱은 각각 적어도 한 프로브의 적용 여부를 바꾼다.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** 무엇으로 붙나 — 페이지·상한·질의 형태가 여기서 갈린다. */
|
|
19
|
+
export type Transport = 'rest' | 'graphql' | 'db' | 'file' | 'mqtt' | 'opc' | 'unknown'
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 변화를 어떻게 아나 — **라이브 단계의 성격이 통째로 이 축에 달려 있다.**
|
|
23
|
+
*
|
|
24
|
+
* · `push` 원천이 밀어 준다(구독·웹훅·큐·CDC). 커서가 필요 없다.
|
|
25
|
+
* · `delta-poll` 거래 이력을 커서로 읽는다. **커서 정합이 급소다.**
|
|
26
|
+
* · `snapshot-poll` 현재 상태를 되풀어 읽는다. **반출을 원리적으로 낼 수 없다** — 라이브 단계는
|
|
27
|
+
* 「아직」이 아니라 **구조적으로 불가**이고, 그 사실을 그대로 말한다.
|
|
28
|
+
* · `none` 변화를 알 길이 없다(구조 전용 소스).
|
|
29
|
+
*/
|
|
30
|
+
export type FeedKind = 'push' | 'delta-poll' | 'snapshot-poll' | 'none' | 'unknown'
|
|
31
|
+
|
|
32
|
+
/** 물건을 무엇이라 부르나 — 발명해야 하는지가 여기서 갈린다. */
|
|
33
|
+
export type IdentityKind = 'gs1' | 'local' | 'none' | 'unknown'
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 양을 어떻게 세나 — **개수냐 연속량이냐.**
|
|
37
|
+
*
|
|
38
|
+
* 식품·화학·제약은 KG·L 로 오고, 조립 제조는 대수로 온다. 커널·미러는 둘 다 다루지만(비직렬
|
|
39
|
+
* `MaterialSubLot`), **요구하는 프로브가 다르다**: 연속량이면 단위 어휘와 환산을 반드시 묻고,
|
|
40
|
+
* 직렬이면 그 프로브는 해당 없음이다.
|
|
41
|
+
*/
|
|
42
|
+
export type Quantization = 'serial' | 'continuous' | 'mixed' | 'unknown'
|
|
43
|
+
|
|
44
|
+
/** 이 원천이 겸하는 버티컬 — 둘 이상이면 트윈이 여럿이고 **핸드오버 경계**를 물어야 한다. */
|
|
45
|
+
export type Vertical = 'wms' | 'mes' | 'yms' | 'ems'
|
|
46
|
+
|
|
47
|
+
/** 밖으로 나가는 문 — 폐루프 프로브의 적용 여부. */
|
|
48
|
+
export type WriteSurface = 'none' | 'command' | 'unknown'
|
|
49
|
+
|
|
50
|
+
export interface TargetProfile {
|
|
51
|
+
transport: Transport
|
|
52
|
+
feed: FeedKind
|
|
53
|
+
multiSite: boolean | null
|
|
54
|
+
identity: IdentityKind
|
|
55
|
+
quantization: Quantization
|
|
56
|
+
/** 빈 배열 = 아직 모른다(없다는 뜻이 아니다). */
|
|
57
|
+
verticals: readonly Vertical[]
|
|
58
|
+
write: WriteSurface
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** 아무것도 모르는 상태 — 스위트는 여기서 시작한다. */
|
|
62
|
+
export function unknownProfile(): TargetProfile {
|
|
63
|
+
return { transport: 'unknown', feed: 'unknown', multiSite: null, identity: 'unknown', quantization: 'unknown', verticals: [], write: 'unknown' }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** 이 축을 아직 모르나 — 모르는 축으로는 라우팅하지 않는다. */
|
|
67
|
+
export function isUnknown(p: TargetProfile, axis: keyof TargetProfile): boolean {
|
|
68
|
+
const v = p[axis]
|
|
69
|
+
if (axis === 'multiSite') return v === null
|
|
70
|
+
if (axis === 'verticals') return (v as readonly Vertical[]).length === 0
|
|
71
|
+
return v === 'unknown'
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 프로브가 알아낸 것을 프로파일에 반영한다 — **좁히기만 한다.**
|
|
76
|
+
*
|
|
77
|
+
* 이미 알아낸 축을 나중 프로브가 `'unknown'` 으로 덮지 못하게 한다. 스위트는 여러 번 돌고, 뒤에
|
|
78
|
+
* 돌린 프로브가 그 축을 다루지 않는다는 이유로 앞의 발견을 지우면 라우팅이 왕복한다.
|
|
79
|
+
*/
|
|
80
|
+
export function narrowProfile(base: TargetProfile, found: Partial<TargetProfile>): TargetProfile {
|
|
81
|
+
const out: TargetProfile = { ...base }
|
|
82
|
+
for (const k of Object.keys(found) as (keyof TargetProfile)[]) {
|
|
83
|
+
const v = found[k]
|
|
84
|
+
if (v === undefined) continue
|
|
85
|
+
if (k === 'multiSite') { if (v !== null) out.multiSite = v as boolean; continue }
|
|
86
|
+
if (k === 'verticals') { const list = v as readonly Vertical[]; if (list.length) out.verticals = [...new Set([...out.verticals, ...list])]; continue }
|
|
87
|
+
if (v !== 'unknown') (out as any)[k] = v
|
|
88
|
+
}
|
|
89
|
+
return out
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* 이 프로파일에서 **라이브가 구조적으로 가능한가.**
|
|
94
|
+
*
|
|
95
|
+
* `snapshot-poll` 은 불가다 — 사라진 것은 그냥 없어지고 `OBSERVE` 만 남는다. 이걸 「아직 반출을 못
|
|
96
|
+
* 봤다」로 적으면 사람이 더 기다리게 되는데, 더 기다려도 오지 않는다. 방식을 바꿔야 한다는 것이 답이다.
|
|
97
|
+
*/
|
|
98
|
+
export function liveFeasible(p: TargetProfile): { ok: boolean; reason?: string } {
|
|
99
|
+
if (p.feed === 'unknown') return { ok: false, reason: 'feed-unknown' }
|
|
100
|
+
if (p.feed === 'none') return { ok: false, reason: 'no-feed' }
|
|
101
|
+
if (p.feed === 'snapshot-poll') return { ok: false, reason: 'snapshot-only-feed' }
|
|
102
|
+
return { ok: true }
|
|
103
|
+
}
|
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
energyIntensity,
|
|
33
33
|
energyOfWindows,
|
|
34
34
|
foldTaskRecords,
|
|
35
|
+
isOrderTerminal,
|
|
35
36
|
activeShiftAt,
|
|
36
37
|
type ElectricityCost,
|
|
37
38
|
type TariffDeclaration,
|
|
@@ -89,6 +90,15 @@ export interface KpiGroup {
|
|
|
89
90
|
waitTime: DurationStats
|
|
90
91
|
/** 이 축에 붙어 있던 시간의 합(자원 축에서 특히 의미 있다). */
|
|
91
92
|
busyMs: number
|
|
93
|
+
/**
|
|
94
|
+
* **품질 판정이 있었던 건수만** 센다 (2026-08-19).
|
|
95
|
+
*
|
|
96
|
+
* 판정이 없는 작업(이동·체류·중간 스테이션)은 어느 쪽에도 세지 않는다 — 모름을 양품으로 세면 양품률이
|
|
97
|
+
* 조용히 100% 로 올라간다. 그래서 `good + scrap` 은 `tasks` 보다 작을 수 있고, 그것이 정상이다.
|
|
98
|
+
*
|
|
99
|
+
* 판정이 하나도 없으면 **필드를 만들지 않는다**(0/0 을 「양품률 0%」로 읽히게 두지 않는다).
|
|
100
|
+
*/
|
|
101
|
+
quality?: { good: number; scrap: number }
|
|
92
102
|
}
|
|
93
103
|
|
|
94
104
|
export interface KpiResult {
|
|
@@ -215,6 +225,33 @@ export interface KpiResult {
|
|
|
215
225
|
* 조회 구간이 짧아 시작이 잘렸거나 저널이 유실된 경우다. 이 값을 숨기면 평균이 조용히 거짓이 된다.
|
|
216
226
|
*/
|
|
217
227
|
unpaired: { leadTime: number; workTime: number }
|
|
228
|
+
/**
|
|
229
|
+
* **완료로 세지 않은 오더 상태 낱말의 인구조사** — 낱말 → 창 안에서 그 낱말로 본 건수.
|
|
230
|
+
*
|
|
231
|
+
* 「못 읽었다」가 **아니다.** `picking` 처럼 완료가 아닌 낱말이 여기 오는 것은 정상이다. 이 값은
|
|
232
|
+
* 판정이 아니라 **재료**다: 완료가 0 인데 여기 `{ finished: 2780 }` 이 있으면 원인이 곧 보인다.
|
|
233
|
+
*
|
|
234
|
+
* ── 왜 필요한가, 그리고 무엇의 대체가 아닌가 ────────────────────────────────
|
|
235
|
+
* 이 폴드는 완료를 **동의어 목록**으로 판정한다(`completed`·`fulfilled`·`done`·`finished`). 그 방식은
|
|
236
|
+
* 원본의 낱말에서 뜻을 **추론**하는 것이고, 이 저장소가 다른 자리에서 거절하는 그것이다
|
|
237
|
+
* (「선언이 있으면 변환이지만 없으면 발명이다」).
|
|
238
|
+
*
|
|
239
|
+
* 실제로 그 대가를 치렀다: `finished` 가 목록에 없어 첫 실 시스템의 완료 오더가 전부 0 이었다.
|
|
240
|
+
* 그런데 **빈 자리는 이 목록이 아니라 커넥터였다** — 그 커넥터에 상태 표(`WORK_ORDER_STATE`)가
|
|
241
|
+
* 정의돼 있으면서 오더 경로에서 쓰이지 않았고(로트 경로는 썼다), 원본 낱말이 그대로 흘렀다.
|
|
242
|
+
* 목록에 `finished` 를 더한 것은 그 사실을 가린 땜빵이었다.
|
|
243
|
+
*
|
|
244
|
+
* **정석은 커넥터가 자기 표로 옮기고, 옮길 수 없는 낱말은 세고 넣지 않는 것이다**(재고 처분이 이미
|
|
245
|
+
* 그 규율로 선다: 「표현할 길이 없는 것은 넣지 않는다」). 그러면 이 폴드는 추론하지 않아도 된다.
|
|
246
|
+
*
|
|
247
|
+
* 그러므로 이 값은 **동의어 목록을 늘리는 근거가 아니다.** 목록은 커넥터들이 옮기기 시작하면
|
|
248
|
+
* 닫힌 어휘로 좁혀야 하고, 그때까지 이 값이 어긋남을 말해 준다. 순서가 중요하다 — 목록을 먼저
|
|
249
|
+
* 좁히면 아직 옮기지 않는 원본의 완료가 그 순간 0 이 된다.
|
|
250
|
+
*
|
|
251
|
+
* 상태를 말하지 않은 사건은 여기 오지 않는다. 그것은 「모르는 낱말」이 아니라 다른 결함이고
|
|
252
|
+
* (유입 문이 상태를 필수로 받는다) 그 진단은 그 문의 몫이다.
|
|
253
|
+
*/
|
|
254
|
+
orderStatusNotCounted: Record<string, number>
|
|
218
255
|
/**
|
|
219
256
|
* 소비한 이벤트 수 — **넘겨받은 배열 전체**(창 앞 lookback 포함). 진단용이다.
|
|
220
257
|
*
|
|
@@ -517,6 +554,24 @@ function facetKey(rec: CompletionRecord, options: KpiFoldOptions): string {
|
|
|
517
554
|
}
|
|
518
555
|
}
|
|
519
556
|
|
|
557
|
+
/**
|
|
558
|
+
* 이 축의 품질 — **판정이 있었던 것만** 센다(없으면 필드를 만들지 않는다).
|
|
559
|
+
*
|
|
560
|
+
* 양품률의 이력이 여기서 나온다: 커널이 작업 완료에 결과를 적고(`TaskStatusDelta.outcome`), 폴드가 축으로
|
|
561
|
+
* 들고(`TaskFacets.outcome`), 여기서 종류별로 센다. 그 사슬이 없으면 「이 공정의 양품률」은 저널에
|
|
562
|
+
* 남지 않는다(설비 누적 카운터는 종류를 모른다).
|
|
563
|
+
*/
|
|
564
|
+
function qualityOf(rows: CompletionRecord[]): { quality?: { good: number; scrap: number } } {
|
|
565
|
+
let good = 0
|
|
566
|
+
let scrap = 0
|
|
567
|
+
for (const r of rows) {
|
|
568
|
+
const o = r.facets.outcome
|
|
569
|
+
if (o === 'good') good++
|
|
570
|
+
else if (o === 'scrap') scrap++
|
|
571
|
+
}
|
|
572
|
+
return good + scrap > 0 ? { quality: { good, scrap } } : {}
|
|
573
|
+
}
|
|
574
|
+
|
|
520
575
|
/**
|
|
521
576
|
* 기록을 축으로 묶어 축별 통계를 낸다 — **전체와 같은 규칙**으로(같은 stats 함수, 같은 창).
|
|
522
577
|
*
|
|
@@ -543,7 +598,8 @@ function groupRecords(
|
|
|
543
598
|
leadTime: stats(rows.map(r => r.leadMs).filter((v): v is number => v !== undefined)),
|
|
544
599
|
workTime: stats(rows.map(r => r.workMs).filter((v): v is number => v !== undefined)),
|
|
545
600
|
waitTime: stats(rows.map(r => r.waitMs).filter((v): v is number => v !== undefined)),
|
|
546
|
-
busyMs: rows.reduce((a, r) => a + (r.workMs ?? 0), 0)
|
|
601
|
+
busyMs: rows.reduce((a, r) => a + (r.workMs ?? 0), 0),
|
|
602
|
+
...qualityOf(rows)
|
|
547
603
|
}))
|
|
548
604
|
.sort((a, b) => b.tasks - a.tasks || (a.key < b.key ? -1 : 1))
|
|
549
605
|
|
|
@@ -601,6 +657,10 @@ export function foldKpi(events: KpiEvent[], window: KpiWindow, options: KpiFoldO
|
|
|
601
657
|
/* 작업 짝맞춤은 **커널 규칙**으로(중복 구현 금지 — 두 곳에 적으면 같은 저널로 다른 답이 나온다). */
|
|
602
658
|
const { records: tasks } = foldTaskRecords(events.filter(e => e.eventType === 'task.status'))
|
|
603
659
|
let completedOrders = 0
|
|
660
|
+
/* 완료로 읽지 못한 오더 상태 낱말 → 건수(§`orderStatusNotCounted`). */
|
|
661
|
+
const orderStatusNotCounted = new Map<string, number>()
|
|
662
|
+
/* 이미 완료로 센 오더 — 폴링 원본이 같은 종결을 다시 보내도 한 번만 센다. */
|
|
663
|
+
const completedOrderIds = new Set<string>()
|
|
604
664
|
let seen = 0
|
|
605
665
|
let inWindow = 0
|
|
606
666
|
|
|
@@ -612,11 +672,45 @@ export function foldKpi(events: KpiEvent[], window: KpiWindow, options: KpiFoldO
|
|
|
612
672
|
const d = e.payload?.data ?? e.payload ?? {}
|
|
613
673
|
|
|
614
674
|
if (e.eventType === 'order.status') {
|
|
615
|
-
/*
|
|
616
|
-
*
|
|
675
|
+
/*
|
|
676
|
+
* **끝났나는 커널이 답한다** (2026-08-23) — `isOrderTerminal`.
|
|
677
|
+
*
|
|
678
|
+
* 예전에는 여기서 **동의어 목록**으로 추론했다(`completed`·`fulfilled`·`done`·`finished`).
|
|
679
|
+
* 그 방식이 낸 대가가 둘이었고 둘 다 조용했다.
|
|
680
|
+
*
|
|
681
|
+
* ① 원본의 낱말이 목록에 없으면 완료가 0 이 된다. 실제로 그랬다 — 첫 실 시스템의 `FINISHED`
|
|
682
|
+
* 가 빠져 있었고, 그때 목록에 낱말을 더한 것은 진짜 빈 자리(그 커넥터가 자기 상태 표를
|
|
683
|
+
* 안 쓰고 있었다)를 가린 땜빵이었다.
|
|
684
|
+
* ② **시뮬 트윈에서는 언제나 0 이었다.** 시뮬은 오더를 이행하며 `fulfilled` 만 올리고
|
|
685
|
+
* `status` 는 바꾸지 않는다 — 낱말로 묻는 판정은 시뮬의 완료를 한 건도 세지 못했다.
|
|
686
|
+
*
|
|
687
|
+
* 표준이 그 축을 갈라 두었다: `orders.status` ↔ `RequestState`(표준도 열거하지 않는다)이고,
|
|
688
|
+
* 「됐나」는 **실적**에서 읽는다(`JobResponse`, 요구는 `SegmentRequirement.Quantity`). 그래서
|
|
689
|
+
* 판정의 1차 근거는 **양**이고 낱말은 양으로 말할 수 없는 종결(취소·중도 종료)에만 쓴다.
|
|
690
|
+
*/
|
|
617
691
|
const status = String(d.status ?? '').toLowerCase()
|
|
618
|
-
|
|
619
|
-
|
|
692
|
+
const done = isOrderTerminal({ status: d.status, requested: d.requested, fulfilled: d.fulfilled })
|
|
693
|
+
if (at >= window.fromMs && at <= window.toMs) {
|
|
694
|
+
/*
|
|
695
|
+
* **오더당 한 번만 센다.** 폴링하는 원본은 같은 오더의 종결을 여러 주기에 걸쳐 다시 보낼 수
|
|
696
|
+
* 있고(그것은 결함이 아니라 폴링의 성질이다), 사건마다 세면 처리량이 조용히 부풀려진다.
|
|
697
|
+
* 작업 쪽은 커널의 짝맞춤이 이미 그 규율을 갖고 있다(「완료는 작업당 하나」) — 오더만 없었다.
|
|
698
|
+
*/
|
|
699
|
+
if (done) {
|
|
700
|
+
const id = String(d.orderId ?? '')
|
|
701
|
+
if (id && !completedOrderIds.has(id)) {
|
|
702
|
+
completedOrderIds.add(id)
|
|
703
|
+
completedOrders++
|
|
704
|
+
} else if (!id) completedOrders++ // 정체를 모르면 셈에서 빼지 않는다(조용히 줄이지 않는다)
|
|
705
|
+
}
|
|
706
|
+
/*
|
|
707
|
+
* **완료로 세지 않은 낱말의 인구조사.** 「못 읽었다」가 아니다 — `picking` 처럼 완료가 아닌
|
|
708
|
+
* 낱말이 여기 오는 것은 정상이다. 완료가 0 인데 여기 낱말이 쌓여 있으면 원인이 곧 보인다.
|
|
709
|
+
*
|
|
710
|
+
* 빈 낱말은 세지 않는다: 상태를 말하지 않은 사건은 다른 결함이고(유입 문이 상태를 필수로
|
|
711
|
+
* 받는다) 여기서 섞으면 이 값의 뜻이 흐려진다.
|
|
712
|
+
*/
|
|
713
|
+
else if (status) orderStatusNotCounted.set(status, (orderStatusNotCounted.get(status) ?? 0) + 1)
|
|
620
714
|
}
|
|
621
715
|
}
|
|
622
716
|
}
|
|
@@ -729,6 +823,8 @@ export function foldKpi(events: KpiEvent[], window: KpiWindow, options: KpiFoldO
|
|
|
729
823
|
waitTime: stats(waits),
|
|
730
824
|
utilization,
|
|
731
825
|
unpaired: { leadTime: unpairedLead, workTime: unpairedWork },
|
|
826
|
+
/* 못 읽은 오더 상태 낱말 — 비면 빈 객체다(§`orderStatusNotCounted`). */
|
|
827
|
+
orderStatusNotCounted: Object.fromEntries(orderStatusNotCounted),
|
|
732
828
|
eventsSeen: seen,
|
|
733
829
|
eventsInWindow: inWindow,
|
|
734
830
|
...(options.groupBy ? { groups: groupRecords(records, options) } : {})
|