@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,87 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* ★ 읽기 커서는 **부르는 쪽이 자리를 가리켜야** 이어진다 — 커넥터는 그 이름을 알 수 없다.
|
|
3
|
+
*
|
|
4
|
+
* ── 무엇이 조용히 끊겨 있었나 (2026-08-23) ──────────────────────────────────
|
|
5
|
+
* 커서를 저장할 컬럼과 계약을 만들고 배선까지 했는데, **한 번도 저장되지 않았다.** 저장할 자리를
|
|
6
|
+
* `cfg`·`site` 에서 찾으려 했기 때문이다.
|
|
7
|
+
*
|
|
8
|
+
* cfg 사람이 화면에서 입력한 접속 설정 — endpoint · token · subdomain · liveLookbackDays
|
|
9
|
+
* site 커넥터가 원본에서 발견한 것 — { siteId, name }
|
|
10
|
+
*
|
|
11
|
+
* **둘 다 그 값을 가질 수 없다.** 레퍼런스의 이름(`TwinReference.source`)은 **호스트가 붙이는 것**이고
|
|
12
|
+
* 원본은 그것을 모른다. 커넥터가 채울 수 없는 값을 커넥터에서 읽으려 한 것이 잘못이었다.
|
|
13
|
+
*
|
|
14
|
+
* 그리고 그 실패는 **로그로는 말했다** — 「this feed does not say which reference it reads from」.
|
|
15
|
+
* 그 한 줄로 다른 레인이 바로 짚었다(조용히 넘기지 않은 것이 값을 했다). 그러나 로그는 사람이 볼 때만
|
|
16
|
+
* 값이 있다. 배선이 다시 끊기면 이 시험이 먼저 말한다.
|
|
17
|
+
*
|
|
18
|
+
* ── 왜 「늘었나」로 확인하면 안 되나 ────────────────────────────────────────
|
|
19
|
+
* 재기동 뒤 수가 늘어도 그것은 커서가 이어진 증거가 아니다 — 실제로 같은 판에 다른 변경(공정별 전이)이
|
|
20
|
+
* 들어가 작업 수가 배로 늘었고, 그 사이 커서는 여전히 매번 새로 만들어졌다. 확인할 것은 **배선**이다.
|
|
21
|
+
*/
|
|
22
|
+
import { test } from 'node:test'
|
|
23
|
+
import assert from 'node:assert/strict'
|
|
24
|
+
import { readFileSync } from 'node:fs'
|
|
25
|
+
import { join } from 'node:path'
|
|
26
|
+
|
|
27
|
+
const DIR = join(import.meta.dirname, '..', 'server', 'service', 'reference')
|
|
28
|
+
const LIVE = readFileSync(join(DIR, 'reference-live.ts'), 'utf8')
|
|
29
|
+
const RESOLVER = readFileSync(join(DIR, 'reference-resolver.ts'), 'utf8')
|
|
30
|
+
|
|
31
|
+
/** 주석을 걷어낸 코드만 — 「쓰지 않는다」를 적어 둔 근거가 가드를 뒤집지 않게. */
|
|
32
|
+
function codeOf(src: string): string {
|
|
33
|
+
return src.replace(/\/\*[\s\S]*?\*\//g, ' ').replace(/(^|[^:])\/\/.*$/gm, '$1')
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
test('커서를 저장할 자리는 부르는 쪽이 가리킨다 — cfg·site 에서 짐작하지 않는다', () => {
|
|
37
|
+
const code = codeOf(LIVE)
|
|
38
|
+
/* 커넥터가 채울 수 없는 값을 커넥터의 인자에서 읽지 않는다. */
|
|
39
|
+
assert.doesNotMatch(code, /cfg as any\)\?\.source/, 'cfg 에서 레퍼런스 이름을 찾지 않는다')
|
|
40
|
+
assert.doesNotMatch(code, /site as any\)\?\.source/, 'site 에서 레퍼런스 이름을 찾지 않는다')
|
|
41
|
+
/* 대신 인자로 받는다. */
|
|
42
|
+
assert.match(code, /loadLiveCursor\(domainId, instanceId, source\)/, '받은 이름으로 커서를 찾는다')
|
|
43
|
+
/* 가드가 스스로를 증명한다 — 주석을 걷어낸 코드가 비어 버리지 않았다. */
|
|
44
|
+
assert.match(code, /startReferenceLiveFeed/, '대상 함수가 남아 있다')
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test('모든 호출처가 그 이름을 넘긴다 — 한 곳만 빠져도 그 경로는 조용히 창이 미끄러진다', () => {
|
|
48
|
+
/*
|
|
49
|
+
* 호출처가 둘이고 **둘 다** `TwinReference` 를 들고 있다(부팅 재개 · 인제스트 직후 기동).
|
|
50
|
+
* 하나만 넘기면 그 경로로 세워진 트윈만 커서를 잃고, 그 차이는 오류 없이 조용하다.
|
|
51
|
+
*/
|
|
52
|
+
const calls = [...codeOf(LIVE).matchAll(/startReferenceLiveFeed\(([^)]*)\)/g)].map(m => m[1])
|
|
53
|
+
const resolverCalls = [...codeOf(RESOLVER).matchAll(/startReferenceLiveFeed\([\s\S]{0,200}?\)/g)].map(m => m[0])
|
|
54
|
+
const all = [...calls.filter(a => !a.includes('domainId: string')), ...resolverCalls]
|
|
55
|
+
assert.ok(all.length >= 2, `호출처를 찾지 못했다 (${all.length}) — 이 가드가 아무것도 지키지 않는다`)
|
|
56
|
+
for (const call of all) {
|
|
57
|
+
assert.match(call, /ref\.source/, `이 호출처가 레퍼런스 이름을 넘기지 않는다: ${call.replace(/\s+/g, ' ')}`)
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test('이름이 없으면 조용히 돌지 않는다 — 말한다', () => {
|
|
62
|
+
/*
|
|
63
|
+
* 커서 없이 도는 것은 「창이 매번 미끄러진다」는 뜻이고, 그 손실은 오류 없이 사라진다. 그래서 그
|
|
64
|
+
* 사실을 로그로 말한다 — 실제로 그 한 줄이 이 결함을 찾아냈다.
|
|
65
|
+
*/
|
|
66
|
+
const code = codeOf(LIVE)
|
|
67
|
+
assert.match(code, /if \(!source\)[\s\S]{0,200}twinWarn/, '이름이 없으면 경고한다')
|
|
68
|
+
assert.match(code, /did not say which reference/, '무엇이 없어서 못 이었는지 말한다')
|
|
69
|
+
/* 행을 못 찾은 경우도 따로 말한다 — 이름은 왔는데 그 레퍼런스가 없는 것은 다른 사실이다. */
|
|
70
|
+
assert.match(code, /no reference row for source/, '레퍼런스 행이 없는 경우를 가려 말한다')
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
test('커서는 레퍼런스의 것이다 — 인스턴스마다 두지 않는다', () => {
|
|
74
|
+
/*
|
|
75
|
+
* 한 레퍼런스가 여러 인스턴스를 낳고(`scopeSpec.produced`) 원본을 읽는 것은 한 번이다.
|
|
76
|
+
* 인스턴스마다 커서를 두면 같은 표를 여러 번 읽는다.
|
|
77
|
+
*/
|
|
78
|
+
const code = codeOf(LIVE)
|
|
79
|
+
assert.match(code, /getRepository\(TwinReference\)/, '레퍼런스 행에 저장한다')
|
|
80
|
+
assert.match(code, /liveCursor: next/, '어댑터가 준 모양을 그대로 적는다')
|
|
81
|
+
assert.doesNotMatch(code, /liveCursor[\s\S]{0,40}instanceId/, '인스턴스로 커서를 가르지 않는다')
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
test('저장 실패를 삼키지 않는다', () => {
|
|
85
|
+
/* 조용히 실패하면 다음 재기동에서 창이 다시 미끄러지고, 이 컬럼을 만든 이유가 사라진다. */
|
|
86
|
+
assert.match(codeOf(LIVE), /live cursor not saved/, '저장 실패를 말한다')
|
|
87
|
+
})
|
|
@@ -11,7 +11,15 @@
|
|
|
11
11
|
import { test } from 'node:test'
|
|
12
12
|
import assert from 'node:assert/strict'
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
/*
|
|
15
|
+
* **빌드 산출물을 부른다** — 이 모듈은 로그 문(`engine/log.js`)을 값으로 참조하고, 타입만 지우고 소스를
|
|
16
|
+
* 실행하는 방식은 그 `.js` 경로를 `.ts` 로 바꿔 주지 않는다. 소스를 부르면 「모듈을 찾을 수 없다」로 선다.
|
|
17
|
+
* **소스를 고친 뒤에는 `npm run build:server` 를 먼저 돌린다.**
|
|
18
|
+
*/
|
|
19
|
+
import { createRequire } from 'node:module'
|
|
20
|
+
|
|
21
|
+
const req = createRequire(import.meta.url)
|
|
22
|
+
const { registerLiveFeedProbe, liveFeedAttached, liveFeedStateOf } = req('../dist-server/engine/live-feed-registry.js')
|
|
15
23
|
|
|
16
24
|
/* 두 계층의 장부를 흉내낸다 — 커넥터 기반과 프리셋 기반. */
|
|
17
25
|
const connectorFeeds = new Set<string>()
|
|
@@ -41,15 +49,15 @@ test('답하지 못하는 장부가 다른 계층의 사실을 죽이지 않는
|
|
|
41
49
|
|
|
42
50
|
test('「없음」과 「해당 없음」을 섞지 않는다', () => {
|
|
43
51
|
/* 도는 미러 — 이 둘이 우리가 보이려던 구별이다. */
|
|
44
|
-
assert.equal(liveFeedStateOf({
|
|
45
|
-
assert.equal(liveFeedStateOf({
|
|
52
|
+
assert.equal(liveFeedStateOf({ restartPolicy: 'resync', running: true, instanceId: 'ems-plant-1' }), 'attached')
|
|
53
|
+
assert.equal(liveFeedStateOf({ restartPolicy: 'resync', running: true, instanceId: 'ems-plant-2' }), 'detached')
|
|
46
54
|
|
|
47
55
|
/* 시뮬 트윈에는 나를 피드가 **없어야 한다** — 결함이 아니라 그 트윈의 성질이다. */
|
|
48
|
-
assert.equal(liveFeedStateOf({
|
|
49
|
-
assert.equal(liveFeedStateOf({
|
|
56
|
+
assert.equal(liveFeedStateOf({ restartPolicy: 'resume', running: true, instanceId: 'hatio-us' }), 'not-applicable')
|
|
57
|
+
assert.equal(liveFeedStateOf({ restartPolicy: 'reset', running: true, instanceId: 'trailer-new' }), 'not-applicable')
|
|
50
58
|
|
|
51
59
|
/* 사람이 멈춰 둔 트윈에게 피드를 묻는 것은 뜻이 없다. */
|
|
52
|
-
assert.equal(liveFeedStateOf({
|
|
60
|
+
assert.equal(liveFeedStateOf({ restartPolicy: 'resync', running: false, instanceId: 'ems-plant-1' }), 'not-applicable')
|
|
53
61
|
|
|
54
62
|
/* 선언이 없으면 미러라고 짐작하지 않는다(짐작으로 경보를 울리지 않는다). */
|
|
55
63
|
assert.equal(liveFeedStateOf({ running: true, instanceId: 'ems-plant-1' }), 'not-applicable')
|
|
@@ -63,5 +71,5 @@ test('목록이 그 사실을 함께 낸다 — 화면이 따로 물어야 하
|
|
|
63
71
|
assert.ok(at >= 0, 'list() 가 없다 — 이름이 바뀌었으면 이 검사도 함께 고친다')
|
|
64
72
|
const body = src.slice(at, src.indexOf('\n /** 운영 공간 목록', at))
|
|
65
73
|
assert.match(body, /liveFeed: liveFeedStateOf\(/, '관리 목록 행에 피드 사실이 실려야 한다')
|
|
66
|
-
assert.match(body, /
|
|
74
|
+
assert.match(body, /restartPolicy: r\.restartPolicy/, '선언된 모드를 넘겨야 한다(짐작하지 않는다)')
|
|
67
75
|
})
|
|
@@ -57,26 +57,26 @@ test('식별자가 아니라 **객체 동일성**으로 가른다 — 어댑터
|
|
|
57
57
|
assert.equal(/applying\.has\(e\.eventId\)/.test(start), false, 'id 비교로 되돌아갔다')
|
|
58
58
|
})
|
|
59
59
|
|
|
60
|
-
test('커널이 낸 사실도
|
|
60
|
+
test('커널이 낸 사실도 브로드캐스팅을 예약한다 — 피크·마감은 상태를 바꾼다', () => {
|
|
61
61
|
const body = methodOf('startLive')
|
|
62
|
-
assert.match(body, /inst\.dirty = true/, '상태가 바뀌었으므로 다음 주기에
|
|
63
|
-
assert.match(body, /ensureBroadcastCoalescer\(\)/, '
|
|
62
|
+
assert.match(body, /inst\.dirty = true/, '상태가 바뀌었으므로 다음 주기에 브로드캐스팅해야 한다')
|
|
63
|
+
assert.match(body, /ensureBroadcastCoalescer\(\)/, '브로드캐스팅은 주기로 합친다(사건마다 브로드캐스팅하면 폭발한다)')
|
|
64
64
|
})
|
|
65
65
|
|
|
66
66
|
/*
|
|
67
|
-
* ── 구조 전환도
|
|
67
|
+
* ── 구조 전환도 브로드캐스팅한다 (2026-08-18) ───────────────────────────────────────
|
|
68
68
|
* 화면이 보는 것 상당수가 구조에서 파생된다(계약 대비 판정·주목 신호·자리 색). 구조 전환이 조용히
|
|
69
69
|
* 끝나면 지도 레일은 다음 계측 표본이 올 때까지 옛 화면을 들고 있고, 헤더 배지는 폴링으로 먼저 알아
|
|
70
70
|
* 「배지엔 있고 목록엔 없는」 어긋난 화면이 된다(실화면에서 봤다).
|
|
71
71
|
*/
|
|
72
|
-
test('구조 전환은 상태
|
|
72
|
+
test('구조 전환은 상태 브로드캐스팅을 예약한다 — 다음 표본까지 화면이 옛것을 들고 있지 않게', () => {
|
|
73
73
|
const body = methodOf('adoptStructure')
|
|
74
74
|
assert.match(body, /inst\.dirty = true/, '구조가 바뀐 순간이 상태가 바뀐 순간이다')
|
|
75
75
|
assert.match(body, /ensureBroadcastCoalescer\(\)/, '새 경로가 아니라 있는 병합 규율에 얹는다')
|
|
76
|
-
/* 커널 전환보다 먼저
|
|
76
|
+
/* 커널 전환보다 먼저 브로드캐스팅하면 구독자가 옛 스냅샷을 되물어 간다 — 순서가 규율이다. */
|
|
77
77
|
assert.ok(
|
|
78
78
|
body.indexOf('inst.kernel.adoptStructure(model)') < body.indexOf('inst.dirty = true'),
|
|
79
|
-
'커널을 갈고 나서
|
|
79
|
+
'커널을 갈고 나서 브로드캐스팅한다'
|
|
80
80
|
)
|
|
81
81
|
})
|
|
82
82
|
|
|
@@ -22,6 +22,10 @@ const master = () => ({
|
|
|
22
22
|
source: 'mirror',
|
|
23
23
|
system: 'wms' as const,
|
|
24
24
|
siteName: 'M',
|
|
25
|
+
/* 이 픽스처가 식별자를 만드는 이름공간 — **픽스처마다 다르게** 둔다(같은 접두 아래에서 서로 다른
|
|
26
|
+
시험의 번호가 부딪치면 표준의 유일성 요구를 깬다, CBV 2.0 §8.2.4). 커널은 선언 없이 팔레트·문서를
|
|
27
|
+
만들지 않는다(0.7.45 에서 날조 프리픽스를 지웠다). */
|
|
28
|
+
identity: { namespaces: ['https://twin.test/live-mirror-parity'] },
|
|
25
29
|
space: { width: 90, depth: 40, unit: 'm' },
|
|
26
30
|
locations: [
|
|
27
31
|
{ id: 'a-dc', type: 'warehouse', role: 'area' as const, name: '창고' },
|
|
@@ -99,6 +103,8 @@ test('★ 라이브 미러: 핵심 관측 상태(점유·용량·위치·아이
|
|
|
99
103
|
function runSimRich() {
|
|
100
104
|
const m = {
|
|
101
105
|
source: 'rich', system: 'wms' as const, siteName: 'R',
|
|
106
|
+
/* 같은 파일이라도 픽스처가 다르면 이름공간도 다르다 — 두 픽스처의 번호가 한 공간에서 겹치지 않게. */
|
|
107
|
+
identity: { namespaces: ['https://twin.test/live-mirror-parity/rich'] },
|
|
102
108
|
space: { width: 90, depth: 40, unit: 'm' },
|
|
103
109
|
locations: [
|
|
104
110
|
{ id: 'a', type: 'warehouse', role: 'area' as const, name: '창고' },
|
package/test/load-meter.test.ts
CHANGED
|
@@ -11,21 +11,7 @@
|
|
|
11
11
|
import { test } from 'node:test'
|
|
12
12
|
import assert from 'node:assert/strict'
|
|
13
13
|
|
|
14
|
-
import {
|
|
15
|
-
fleetLoad,
|
|
16
|
-
newCpuSampler,
|
|
17
|
-
sampleCpu,
|
|
18
|
-
judgeCycle,
|
|
19
|
-
loadSummary,
|
|
20
|
-
newLoadMeter,
|
|
21
|
-
recordFork,
|
|
22
|
-
recordPhase,
|
|
23
|
-
rollWindow,
|
|
24
|
-
slowTickMessage,
|
|
25
|
-
TICK_BUDGET_RATIO,
|
|
26
|
-
WARN_COOLDOWN_MS,
|
|
27
|
-
WINDOW_MS
|
|
28
|
-
} from '../server/engine/load-meter.ts'
|
|
14
|
+
import { LOAD_PHASES, fleetLoad, newCpuSampler, sampleCpu, judgeCycle, loadSummary, newLoadMeter, recordFork, recordPhase, rollWindow, slowTickMessage, TICK_BUDGET_RATIO, WARN_COOLDOWN_MS, WINDOW_MS } from '../server/engine/load-meter.ts'
|
|
29
15
|
|
|
30
16
|
const TICK = 1000
|
|
31
17
|
|
|
@@ -312,3 +298,31 @@ test('시계가 뒤로 가거나 구간이 0 이면 비율을 지어내지 않
|
|
|
312
298
|
assert.equal(sampleCpu(s, 1_200_000, 1000), null, '구간 0')
|
|
313
299
|
assert.equal(sampleCpu(newCpuSampler(), 5, 5), null)
|
|
314
300
|
})
|
|
301
|
+
|
|
302
|
+
/*
|
|
303
|
+
* ── 부팅 구간도 재는 이름을 갖는다 (2026-08-22) ─────────────────────────────
|
|
304
|
+
* 실측으로 부팅이 620~717초였고 트윈 하나를 세우는 데 15~93초였는데, **어느 작업이 그 시간을 쓰는지
|
|
305
|
+
* 답할 이름이 없었다** — 재는 이름이 도는 중의 작업뿐이었다. 그래서 짐작으로 원인을 적을 수밖에
|
|
306
|
+
* 없었고, 그것은 이 프로젝트가 금하는 일이다.
|
|
307
|
+
*/
|
|
308
|
+
test('부팅 구간을 재는 이름이 있다 — 웜스타트와 자극 시작', () => {
|
|
309
|
+
assert.ok(LOAD_PHASES.includes('warmStart' as any), '상태 심기를 재는 이름이 있어야 한다')
|
|
310
|
+
assert.ok(LOAD_PHASES.includes('stimulus' as any), '자극 시작을 재는 이름이 있어야 한다')
|
|
311
|
+
|
|
312
|
+
/* 계기가 그 값을 실제로 들고 답한다 — 이름만 있고 담기지 않으면 화면이 빈칸을 보인다. */
|
|
313
|
+
const m = newLoadMeter()
|
|
314
|
+
recordPhase(m, 'warmStart', 93_000, 1_000)
|
|
315
|
+
recordPhase(m, 'stimulus', 72_000, 1_000)
|
|
316
|
+
assert.equal(m.phases.warmStart?.maxMs, 93_000)
|
|
317
|
+
assert.equal(m.phases.stimulus?.maxMs, 72_000)
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
test('부팅 구간은 틱 예산으로 판정하지 않는다 — 한 번뿐인 일이다', () => {
|
|
321
|
+
/*
|
|
322
|
+
* 웜스타트는 트윈을 세울 때 한 번이고 몇십 초가 정상일 수 있다. 그것을 틱 예산(간격의 절반)으로
|
|
323
|
+
* 재면 부팅마다 「느린 주기」가 쏟아진다 — 거짓 경보는 참 경보를 죽인다. 판정은 틱만 받는다.
|
|
324
|
+
*/
|
|
325
|
+
const m = newLoadMeter()
|
|
326
|
+
recordPhase(m, 'warmStart', 93_000, 1_000)
|
|
327
|
+
assert.equal(m.overBudget, 0, '기록만으로는 판정하지 않는다')
|
|
328
|
+
})
|
|
@@ -69,7 +69,15 @@ test('EMS 밖에도 공용 어휘가 있다 — 속도는 어느 트윈에서나
|
|
|
69
69
|
/* 읽는 코드가 있는 것만 — 판정 기준 셋(주의)과 속도. 새 효과를 붙이면 이 목록도 함께 자란다. */
|
|
70
70
|
assert.deepEqual(
|
|
71
71
|
specs.map(s => s.id).sort(),
|
|
72
|
-
[
|
|
72
|
+
[
|
|
73
|
+
'attention.congestionRatio',
|
|
74
|
+
'attention.scrapMinSamples',
|
|
75
|
+
'attention.scrapRate',
|
|
76
|
+
'operation.duration',
|
|
77
|
+
'operation.setupDuration',
|
|
78
|
+
'operation.yield',
|
|
79
|
+
'speed'
|
|
80
|
+
],
|
|
73
81
|
'읽는 코드가 있는 것만'
|
|
74
82
|
)
|
|
75
83
|
/* 모르는 속성은 이제 「어휘가 없다」가 아니라 **「그 속성을 읽지 않는다」**로 거절된다(더 정확하다). */
|
|
@@ -418,7 +426,7 @@ test('현장 겹이 원천 값을 덮으면 그 원천 값을 기억한다 —
|
|
|
418
426
|
* 모델에 설비 `load-1` 이 두 번 있던 것이다. 배선이 사라지지 않게 소스 모양으로 지킨다(이 함수는 DB 를
|
|
419
427
|
* 물고 있어 단위 시험이 불러올 수 없다).
|
|
420
428
|
*/
|
|
421
|
-
test('구조 투영이 중복 id 를 이름으로 말한다 — 제약 위반 문구를 사용자에게
|
|
429
|
+
test('구조 투영이 중복 id 를 이름으로 말한다 — 제약 위반 문구를 사용자에게 보이지 않는다', () => {
|
|
422
430
|
const src = readFileSync(fileURLToPath(new URL('../server/service/twin-model/project-structure.ts', import.meta.url)), 'utf-8')
|
|
423
431
|
assert.match(src, /has two \$\{what\} with id/, '무엇이 겹쳤는지 이름으로 말한다')
|
|
424
432
|
assert.match(src, /ids must be unique within a twin/, '규칙도 함께 말한다')
|
|
@@ -552,3 +560,102 @@ test('두 겹이 함께 얹힌다 — 트윈 값이 현장 값을 이긴다(시
|
|
|
552
560
|
assert.deepEqual((out.model as any).localDurations, { putaway: 'PT120S' }, '좁은 겹이 이긴다')
|
|
553
561
|
assert.deepEqual(out.unreadable, [])
|
|
554
562
|
})
|
|
563
|
+
|
|
564
|
+
/*
|
|
565
|
+
* ── 공정 모수: 수율·셋업 (2026-08-19) ───────────────────────────────────────
|
|
566
|
+
* 양품률 기본값은 커널 소스의 상수(0.8)였고 **그 상수가 불량 과다 주목 신호까지 만들었다.** 현장이 자기
|
|
567
|
+
* 수를 말할 자리를 준다. 사람은 퍼센트로 말하고(80=80%), 커널은 비율(0..1)을 읽는다 — 환산은 한 곳에서.
|
|
568
|
+
*/
|
|
569
|
+
test('수율은 퍼센트로 받아 비율로 얹는다 — 80 이 0.8 이다(한 곳에서만 나눈다)', () => {
|
|
570
|
+
const { accepted, rejected } = validateLocalDeclarations('mes', [
|
|
571
|
+
{ target: 'operation', id: 'weld', properties: [{ id: 'operation.yield', value: '95' }] }
|
|
572
|
+
])
|
|
573
|
+
assert.deepEqual(rejected, [])
|
|
574
|
+
const { model, unreadable } = applyLocalDeclarations({ locations: [], equipment: [] }, accepted)
|
|
575
|
+
assert.deepEqual(unreadable, [])
|
|
576
|
+
assert.deepEqual((model as any).localParams, { weld: { yield: '0.95' } })
|
|
577
|
+
assert.equal((model as any).localDurations, undefined, '모수는 시간 표를 건드리지 않는다')
|
|
578
|
+
})
|
|
579
|
+
|
|
580
|
+
test('수율은 퍼센트 범위를 지킨다 — 0.8 을 넣으면 걸린다(SOC 에서 겪은 함정)', () => {
|
|
581
|
+
const low = validateLocalDeclarations('mes', [
|
|
582
|
+
{ target: 'operation', id: 'weld', properties: [{ id: 'operation.yield', value: '0.8' }] }
|
|
583
|
+
])
|
|
584
|
+
assert.equal(low.rejected[0]?.reason, 'out-of-range')
|
|
585
|
+
const high = validateLocalDeclarations('mes', [
|
|
586
|
+
{ target: 'operation', id: 'weld', properties: [{ id: 'operation.yield', value: '150' }] }
|
|
587
|
+
])
|
|
588
|
+
assert.equal(high.rejected[0]?.reason, 'out-of-range')
|
|
589
|
+
})
|
|
590
|
+
|
|
591
|
+
test('셋업은 단위와 함께 받아 ISO 8601 로 얹는다 — 시간 표가 아니라 모수 표에', () => {
|
|
592
|
+
const { accepted } = validateLocalDeclarations('mes', [
|
|
593
|
+
{ target: 'operation', id: 'paint', properties: [{ id: 'operation.setupDuration', value: '7.5', uom: 'MIN' }] }
|
|
594
|
+
])
|
|
595
|
+
const { model } = applyLocalDeclarations({ locations: [], equipment: [] }, accepted)
|
|
596
|
+
assert.deepEqual((model as any).localParams, { paint: { setupDuration: 'PT450S' } })
|
|
597
|
+
})
|
|
598
|
+
|
|
599
|
+
test('셋업도 단위 없이 받지 않는다 — 7 초와 7 분은 60배 다르다', () => {
|
|
600
|
+
const { rejected } = validateLocalDeclarations('mes', [
|
|
601
|
+
{ target: 'operation', id: 'paint', properties: [{ id: 'operation.setupDuration', value: '7' }] }
|
|
602
|
+
])
|
|
603
|
+
assert.equal(rejected[0]?.reason, 'uom-required')
|
|
604
|
+
})
|
|
605
|
+
|
|
606
|
+
test('한 공정에 시간·수율·셋업을 함께 얹는다 — 서로 덮지 않는다', () => {
|
|
607
|
+
const { accepted } = validateLocalDeclarations('mes', [
|
|
608
|
+
{
|
|
609
|
+
target: 'operation',
|
|
610
|
+
id: 'weld',
|
|
611
|
+
properties: [
|
|
612
|
+
{ id: 'operation.duration', value: '3', uom: 'MIN' },
|
|
613
|
+
{ id: 'operation.yield', value: '92' },
|
|
614
|
+
{ id: 'operation.setupDuration', value: '30', uom: 'SEC' }
|
|
615
|
+
]
|
|
616
|
+
}
|
|
617
|
+
])
|
|
618
|
+
const { model } = applyLocalDeclarations({ locations: [], equipment: [] }, accepted)
|
|
619
|
+
assert.deepEqual((model as any).localDurations, { weld: 'PT180S' })
|
|
620
|
+
assert.deepEqual((model as any).localParams, { weld: { yield: '0.92', setupDuration: 'PT30S' } })
|
|
621
|
+
})
|
|
622
|
+
|
|
623
|
+
test('모수 철회는 모수 표에서 뺀다 — 시간 선언은 그대로 남는다', () => {
|
|
624
|
+
const declared = [
|
|
625
|
+
{
|
|
626
|
+
target: 'operation' as const,
|
|
627
|
+
id: 'weld',
|
|
628
|
+
properties: [
|
|
629
|
+
{ id: 'operation.duration', value: '3', uom: 'MIN' },
|
|
630
|
+
{ id: 'operation.yield', value: '92' }
|
|
631
|
+
]
|
|
632
|
+
}
|
|
633
|
+
]
|
|
634
|
+
const applied = applyLocalDeclarations({ locations: [], equipment: [] }, declared)
|
|
635
|
+
const { layer, revoked, notFound } = revokeLocalDeclarations(applied.layer, [
|
|
636
|
+
{ target: 'operation', id: 'weld', propertyId: 'operation.yield' }
|
|
637
|
+
])
|
|
638
|
+
assert.deepEqual(notFound, [])
|
|
639
|
+
assert.deepEqual(layer[0].properties.map(p => p.id), ['operation.duration'], '시간 선언은 겹에 남는다')
|
|
640
|
+
const back = restoreRevoked(applied.model, revoked)
|
|
641
|
+
assert.equal((back.model as any).localParams, undefined, '빈 껍데기를 남기지 않는다')
|
|
642
|
+
assert.deepEqual((back.model as any).localDurations, { weld: 'PT180S' }, '시간은 그대로')
|
|
643
|
+
})
|
|
644
|
+
|
|
645
|
+
test('없는 모수를 철회했다고 말하지 않는다 — 조용한 성공이 가장 비싸다', () => {
|
|
646
|
+
const back = restoreRevoked({ locations: [], equipment: [], localParams: { weld: { yield: '0.9' } } }, [
|
|
647
|
+
{ target: 'operation', id: 'weld', propertyId: 'operation.setupDuration', replaced: null }
|
|
648
|
+
])
|
|
649
|
+
assert.deepEqual(back.restored, [])
|
|
650
|
+
assert.deepEqual(back.missingTargets, [{ target: 'operation', id: 'weld' }])
|
|
651
|
+
assert.deepEqual((back.model as any).localParams, { weld: { yield: '0.9' } }, '남의 값을 건드리지 않는다')
|
|
652
|
+
})
|
|
653
|
+
|
|
654
|
+
test('두 겹이 함께 얹혀도 모수가 섞이지 않는다 — 좁은 겹이 이긴다', () => {
|
|
655
|
+
const out = applyDeclarationLayers(
|
|
656
|
+
{ locations: [], equipment: [] },
|
|
657
|
+
[{ target: 'operation', id: 'weld', properties: [{ id: 'operation.yield', value: '80' }] }],
|
|
658
|
+
[{ target: 'operation', id: 'weld', properties: [{ id: 'operation.yield', value: '95' }] }]
|
|
659
|
+
)
|
|
660
|
+
assert.deepEqual((out.model as any).localParams, { weld: { yield: '0.95' } })
|
|
661
|
+
})
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 로그에 **시각이 있다** — 그리고 어떤 인자도 버리지 않는다 (2026-08-22).
|
|
3
|
+
*
|
|
4
|
+
* 루프가 55초 막힌 것을 계기로 알아냈는데 그 시각에 무슨 일이 있었는지 로그에서 찾을 수 없었다.
|
|
5
|
+
* 트윈의 줄이 `console.log` 로 나가 시각이 없었기 때문이다. 이 시험이 지키는 것:
|
|
6
|
+
* ① 형식 — 프레임워크 줄과 같은 모양, **밀리초까지**(계기의 값이 epoch ms 다)
|
|
7
|
+
* ② 지역 시각 — 프레임워크의 다른 줄과 같은 기준(UTC 로 찍으면 나란히 읽을 수 없다)
|
|
8
|
+
* ③ 인자를 버리지 않는다 — 오류는 스택까지(`console.error(msg, err)` 로 쓰던 자리가 많다)
|
|
9
|
+
* ④ 트윈 소스에 `console.` 직접 호출이 남지 않는다 — 하나라도 남으면 그 줄만 시각이 없다
|
|
10
|
+
*/
|
|
11
|
+
import { test } from 'node:test'
|
|
12
|
+
import assert from 'node:assert/strict'
|
|
13
|
+
import { readFileSync, readdirSync } from 'node:fs'
|
|
14
|
+
import { fileURLToPath } from 'node:url'
|
|
15
|
+
|
|
16
|
+
import { line, stamp } from '../server/engine/log.ts'
|
|
17
|
+
|
|
18
|
+
test('시각은 밀리초와 오프셋까지 적는다 — 같은 초 안의 순서를 가릴 수 있게', () => {
|
|
19
|
+
const s = stamp(new Date(2026, 7, 22, 5, 54, 12, 345))
|
|
20
|
+
assert.match(s, /^2026-08-22T05:54:12\.345[+-]\d\d:\d\d$/, `형식이 다르다: ${s}`)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
test('지역 시각으로 적는다 — 프레임워크의 다른 줄과 같은 기준', () => {
|
|
24
|
+
const at = new Date(2026, 0, 2, 3, 4, 5, 6)
|
|
25
|
+
const s = stamp(at)
|
|
26
|
+
assert.ok(s.startsWith('2026-01-02T03:04:05.006'), `지역 시각이 아니다: ${s}`)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
test('오류는 스택까지 남는다 — 「어디서 났나」를 잃지 않게', () => {
|
|
30
|
+
const err = new Error('불이 났다')
|
|
31
|
+
const out = line(['[twin-engine] 저장 실패', err])
|
|
32
|
+
assert.ok(out.includes('[twin-engine] 저장 실패'))
|
|
33
|
+
assert.ok(out.includes('불이 났다'))
|
|
34
|
+
assert.ok(out.includes('log-stamp.test'), '스택이 있어야 어느 자리에서 났는지 알 수 있다')
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test('값이 없어도 있었다는 사실은 남긴다 — 조용히 지우지 않는다', () => {
|
|
38
|
+
assert.equal(line(['a', undefined, null, 0]), 'a undefined null 0')
|
|
39
|
+
const circular: any = { name: 'ㄱ' }
|
|
40
|
+
circular.self = circular
|
|
41
|
+
assert.ok(line(['보고', circular]).startsWith('보고 '), '순환 참조에도 줄이 남아야 한다')
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
test('트윈 소스에 console 직접 호출이 남아 있지 않다 — 한 줄이라도 남으면 그 줄만 시각이 없다', () => {
|
|
45
|
+
const root = fileURLToPath(new URL('../server/', import.meta.url))
|
|
46
|
+
const offenders: string[] = []
|
|
47
|
+
const walk = (dir: string) => {
|
|
48
|
+
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
49
|
+
const p = dir + e.name
|
|
50
|
+
if (e.isDirectory()) walk(p + '/')
|
|
51
|
+
else if (e.name.endsWith('.ts') && e.name !== 'log.ts') {
|
|
52
|
+
const src = readFileSync(p, 'utf-8')
|
|
53
|
+
for (const m of src.matchAll(/console\.(log|warn|error)\(/g)) offenders.push(`${p.slice(root.length)} console.${m[1]}`)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
walk(root)
|
|
58
|
+
assert.deepEqual(offenders, [], `시각 없는 로그가 남아 있다 — twinLog·twinWarn·twinError 를 쓴다:\n ${offenders.join('\n ')}`)
|
|
59
|
+
})
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 루프 지체 계기 — **원인을 지어내지 않기 위한 값**이 정확한지 본다 (2026-08-21).
|
|
3
|
+
*
|
|
4
|
+
* 지키는 것:
|
|
5
|
+
* ① 늦게 깨어난 만큼만 지체다 — 제때 깨어난 표본은 0 이고, 0 을 정체로 세지 않는다.
|
|
6
|
+
* ② 창이 닫히기 전에는 `recent` 가 **null** 이다(채우는 중인 창을 「지금」으로 읽으면 과소 집계다).
|
|
7
|
+
* ③ 가장 길게 막힌 **시각**을 든다 — 언제였는지가 원인을 가리는 근거다.
|
|
8
|
+
* ④ 문턱 이하의 흔들림은 정체로 세지 않는다.
|
|
9
|
+
*/
|
|
10
|
+
import { test } from 'node:test'
|
|
11
|
+
import assert from 'node:assert/strict'
|
|
12
|
+
|
|
13
|
+
import { LoopLagSampler } from '../server/engine/loop-lag.ts'
|
|
14
|
+
|
|
15
|
+
/** 시계를 손으로 밀어 넣는다 — 실제 타이머로 재면 기계 상태가 결과를 정한다. */
|
|
16
|
+
function sampler(sampleMs = 500, stallMs = 200, windowMs = 10_000) {
|
|
17
|
+
let now = 1_000_000
|
|
18
|
+
const s = new LoopLagSampler(sampleMs, stallMs, windowMs, () => now)
|
|
19
|
+
return { s, advance: (ms: number) => (now += ms), at: () => now }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
test('제때 깨어난 표본은 지체가 0 이고 정체로 세지 않는다', () => {
|
|
23
|
+
const { s, advance, at } = sampler()
|
|
24
|
+
for (let i = 0; i < 4; i++) {
|
|
25
|
+
advance(500)
|
|
26
|
+
assert.equal(s.sample(at()), 0)
|
|
27
|
+
}
|
|
28
|
+
assert.equal(s.view().total.stalls, 0)
|
|
29
|
+
assert.equal(s.view().total.maxMs, 0)
|
|
30
|
+
assert.equal(s.view().total.samples, 4)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
test('늦게 깨어난 만큼이 지체다 — 그 초과분만', () => {
|
|
34
|
+
const { s, advance, at } = sampler()
|
|
35
|
+
advance(500)
|
|
36
|
+
s.sample(at())
|
|
37
|
+
advance(4_600) // 예정보다 4,100ms 늦게
|
|
38
|
+
assert.equal(s.sample(at()), 4_100)
|
|
39
|
+
assert.equal(s.view().total.maxMs, 4_100)
|
|
40
|
+
assert.equal(s.view().total.stalls, 1)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
test('문턱 이하의 흔들림은 정체가 아니다 — 값은 남기고 세지는 않는다', () => {
|
|
44
|
+
const { s, advance, at } = sampler(500, 200)
|
|
45
|
+
advance(650) // 150ms 지체 — 문턱 200 미만
|
|
46
|
+
s.sample(at())
|
|
47
|
+
const v = s.view()
|
|
48
|
+
assert.equal(v.total.maxMs, 150)
|
|
49
|
+
assert.equal(v.total.stalls, 0, '문턱 이하를 세면 정상 상태가 정체로 보인다')
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('창이 닫히기 전에는 recent 가 null 이다 — 채우는 중인 창을 「지금」으로 읽지 않는다', () => {
|
|
53
|
+
const { s, advance, at } = sampler(500, 200, 2_000)
|
|
54
|
+
advance(500)
|
|
55
|
+
s.sample(at())
|
|
56
|
+
assert.equal(s.view().recent, null)
|
|
57
|
+
|
|
58
|
+
/* 창 길이를 넘기면 그 창이 닫혀 `recent` 가 된다. */
|
|
59
|
+
advance(2_000)
|
|
60
|
+
s.sample(at())
|
|
61
|
+
const v = s.view()
|
|
62
|
+
assert.ok(v.recent, '창 길이를 넘겼으면 닫힌 창이 있어야 한다')
|
|
63
|
+
assert.equal(v.recent!.stalls, 1, '1,500ms 지체는 정체 한 번이다')
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test('가장 길게 막힌 시각을 든다 — 언제였는지가 원인을 가린다', () => {
|
|
67
|
+
const { s, advance, at } = sampler()
|
|
68
|
+
advance(500)
|
|
69
|
+
s.sample(at())
|
|
70
|
+
advance(3_000)
|
|
71
|
+
const worst = at()
|
|
72
|
+
s.sample(worst)
|
|
73
|
+
advance(500)
|
|
74
|
+
s.sample(at())
|
|
75
|
+
assert.equal(s.view().worstAtMs, worst)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('한 번도 재지 않았으면 정체 시각은 null 이다 — 0 으로 적지 않는다', () => {
|
|
79
|
+
const { s } = sampler()
|
|
80
|
+
assert.equal(s.view().worstAtMs, null)
|
|
81
|
+
assert.equal(s.view().recent, null)
|
|
82
|
+
})
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 실측 양품률 — **이력에서 배운 수율이 선언값·상수를 이긴다.**
|
|
3
|
+
*
|
|
4
|
+
* ── 왜 (2026-08-19) ─────────────────────────────────────────────────────────
|
|
5
|
+
* 소요시간에는 이력 층이 있었는데 수율에는 없었다. 양품률 기본값은 커널 소스의 상수(0.8)였고 **그 상수가
|
|
6
|
+
* 불량 과다 주목 신호까지 만들었다.** 이제 커널이 판정을 작업 완료에 적고(`outcome`), 폴드가 축으로 들고,
|
|
7
|
+
* KPI 가 종류별로 세고(`quality`), 이 모듈이 비율로 바꿔 커널 시임에 넣는다.
|
|
8
|
+
*
|
|
9
|
+
* 이 시험이 지키는 것:
|
|
10
|
+
* ① 분모는 **판정된 것만** — 이동·체류를 섞으면 양품률이 낮게 나온다.
|
|
11
|
+
* ② 표본이 모자라면 **배우지 않는다** — 비율은 드문 사건을 재는 값이라 작은 표본이 잡음을 만든다.
|
|
12
|
+
* ③ 0 도 사실이다 — 전량 불량이면 0 을 배운다(「값이 없다」와 다르다).
|
|
13
|
+
* ④ 모르는 종류에는 `undefined` — 그러면 커널이 선언값·상수로 내려간다.
|
|
14
|
+
* ⑤ 배우지 못한 것을 **밝힌다** — 조용히 일부만 반영하지 않는다.
|
|
15
|
+
*/
|
|
16
|
+
import { test } from 'node:test'
|
|
17
|
+
import assert from 'node:assert/strict'
|
|
18
|
+
import { readFileSync } from 'node:fs'
|
|
19
|
+
import { fileURLToPath } from 'node:url'
|
|
20
|
+
|
|
21
|
+
import { buildYieldEstimator, DEFAULT_YIELD_MIN_SAMPLES } from '../server/engine/measured-yield.ts'
|
|
22
|
+
|
|
23
|
+
const group = (key: string, good: number, scrap: number) => ({ key, quality: { good, scrap } })
|
|
24
|
+
|
|
25
|
+
test('판정된 것만 분모다 — 이동·체류가 섞이지 않는다', () => {
|
|
26
|
+
/* tasks 는 1000 건이어도 판정은 40 건뿐인 종류가 정상이다(중간 스테이션은 품질을 가리지 않는다). */
|
|
27
|
+
const r = buildYieldEstimator([{ key: 'assembly', tasks: 1000, quality: { good: 36, scrap: 4 } } as any])
|
|
28
|
+
assert.equal(r.learned.assembly, 0.9, '36/40 = 0.9')
|
|
29
|
+
assert.deepEqual(r.samples.assembly, { good: 36, scrap: 4 })
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test('표본이 모자라면 배우지 않고 그 사실을 낸다 — 세 건 중 한 건 불량은 33% 가 아니다', () => {
|
|
33
|
+
const r = buildYieldEstimator([group('weld', 2, 1)])
|
|
34
|
+
assert.deepEqual(r.learned, {})
|
|
35
|
+
assert.deepEqual(r.skipped, [{ kind: 'weld', judged: 3 }])
|
|
36
|
+
assert.equal(r.estimator, undefined, '배운 것이 없으면 시임을 만들지 않는다')
|
|
37
|
+
assert.equal(r.minSamples, DEFAULT_YIELD_MIN_SAMPLES)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test('하한은 소요보다 높다 — 비율은 드문 사건을 재는 값이다', () => {
|
|
41
|
+
/* 불량률 5% 인 공정은 20건에서 불량이 한 건이다 — 그 아래로 내려가면 이력이 잡음이 된다. */
|
|
42
|
+
assert.ok(DEFAULT_YIELD_MIN_SAMPLES >= 20, `하한이 ${DEFAULT_YIELD_MIN_SAMPLES} 이면 비율에 너무 낮다`)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test('전량 불량은 0 을 배운다 — 「값이 없다」와 구별한다', () => {
|
|
46
|
+
const r = buildYieldEstimator([group('paint', 0, 25)])
|
|
47
|
+
assert.equal(r.learned.paint, 0)
|
|
48
|
+
assert.equal(r.estimator!.estimate({ kind: 'paint' }), 0, '0 을 답한다(undefined 로 뭉개지 않는다)')
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test('판정이 아예 없는 종류는 건너뛴 것이 아니다 — 품질을 가리지 않는 공정이 정상이다', () => {
|
|
52
|
+
const r = buildYieldEstimator([{ key: 'putaway', tasks: 500 } as any])
|
|
53
|
+
assert.deepEqual(r.learned, {})
|
|
54
|
+
assert.deepEqual(r.skipped, [], '표본 부족으로 세지 않는다')
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('모르는 종류에는 undefined — 커널이 선언값·상수로 내려갈 수 있게', () => {
|
|
58
|
+
const r = buildYieldEstimator([group('weld', 30, 2)])
|
|
59
|
+
assert.equal(r.estimator!.estimate({ kind: 'weld' }), 30 / 32)
|
|
60
|
+
assert.equal(r.estimator!.estimate({ kind: 'paint' }), undefined)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
test("'unknown' 축은 쓰지 않는다 — 종류를 모르는 기록을 특정 종류의 근거로 삼지 않는다", () => {
|
|
64
|
+
const r = buildYieldEstimator([group('unknown', 100, 5)])
|
|
65
|
+
assert.deepEqual(r.learned, {})
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test('빈 입력에도 답을 만든다 — 부르는 쪽이 조건문을 두 벌 갖지 않게', () => {
|
|
69
|
+
for (const input of [undefined, []]) {
|
|
70
|
+
const r = buildYieldEstimator(input as any)
|
|
71
|
+
assert.deepEqual(r.learned, {})
|
|
72
|
+
assert.equal(r.estimator, undefined)
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
/*
|
|
77
|
+
* ── 사슬이 이어져 있나 (소스로 지킨다) ──────────────────────────────────────
|
|
78
|
+
* 이 층은 넷이 이어져야 뜻이 있다: 커널이 판정을 적고 → 폴드가 축으로 들고 → KPI 가 세고 → 여기서 비율.
|
|
79
|
+
* 한 칸만 빠져도 화면은 「이력에서 배운 값」이라고 말하면서 상수로 돈다. 실 앱에서 관측하려면 라인이
|
|
80
|
+
* 한 바퀴 돌아야 하므로(판정은 완료에서 일어난다), 경로는 여기서 지킨다.
|
|
81
|
+
*/
|
|
82
|
+
test('사슬 넷이 이어져 있다 — 판정→축→집계→비율', () => {
|
|
83
|
+
const hostSrc = (f: string) => readFileSync(fileURLToPath(new URL(`../server/${f}`, import.meta.url)), 'utf-8')
|
|
84
|
+
assert.match(hostSrc('engine/kpi-fold.ts'), /facets\.outcome/, 'KPI 가 판정 축을 읽는다')
|
|
85
|
+
assert.match(hostSrc('engine/kpi-fold.ts'), /quality\?: \{ good: number; scrap: number \}/, '종류별로 센다')
|
|
86
|
+
const engine = hostSrc('engine/twin-engine.ts')
|
|
87
|
+
assert.match(engine, /buildYieldEstimator\(kpi\?\.groups\?\.items/, '같은 폴드에서 배운다(두 번 접지 않는다)')
|
|
88
|
+
assert.match(engine, /kernel\.yieldEstimator = yields\.estimator/, '커널 시임에 넣는다')
|
|
89
|
+
assert.match(engine, /yieldOf missing/, '옛 커널이면 그 사실을 말한다')
|
|
90
|
+
})
|