@things-factory/headless-twin 10.0.19 → 10.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-server/engine/canonical-ingest.d.ts +10 -1
- package/dist-server/engine/canonical-ingest.js +110 -33
- package/dist-server/engine/canonical-ingest.js.map +1 -1
- package/dist-server/engine/event-time.d.ts +25 -0
- package/dist-server/engine/event-time.js +98 -0
- package/dist-server/engine/event-time.js.map +1 -0
- package/dist-server/engine/index.d.ts +2 -0
- package/dist-server/engine/index.js +23 -0
- package/dist-server/engine/index.js.map +1 -1
- package/dist-server/engine/ingest-dedupe.d.ts +18 -6
- package/dist-server/engine/ingest-dedupe.js +201 -15
- package/dist-server/engine/ingest-dedupe.js.map +1 -1
- package/dist-server/engine/ingest-health.d.ts +60 -1
- package/dist-server/engine/ingest-health.js +43 -8
- package/dist-server/engine/ingest-health.js.map +1 -1
- package/dist-server/engine/kpi-fold.d.ts +19 -0
- package/dist-server/engine/kpi-fold.js +4 -1
- package/dist-server/engine/kpi-fold.js.map +1 -1
- package/dist-server/engine/kpi-query.js +23 -3
- package/dist-server/engine/kpi-query.js.map +1 -1
- package/dist-server/engine/oee-accumulator.d.ts +35 -2
- package/dist-server/engine/oee-accumulator.js +56 -4
- package/dist-server/engine/oee-accumulator.js.map +1 -1
- package/dist-server/engine/twin-engine.d.ts +72 -3
- package/dist-server/engine/twin-engine.js +118 -5
- package/dist-server/engine/twin-engine.js.map +1 -1
- package/dist-server/index.js +21 -0
- package/dist-server/index.js.map +1 -1
- package/dist-server/migrations/1786600000000-RenameOperatoMesAdapterType.d.ts +5 -0
- package/dist-server/migrations/1786600000000-RenameOperatoMesAdapterType.js +66 -0
- package/dist-server/migrations/1786600000000-RenameOperatoMesAdapterType.js.map +1 -0
- package/dist-server/migrations/1786700000000-AddDispatchRetryColumns.d.ts +5 -0
- package/dist-server/migrations/1786700000000-AddDispatchRetryColumns.js +79 -0
- package/dist-server/migrations/1786700000000-AddDispatchRetryColumns.js.map +1 -0
- package/dist-server/migrations/index.js +5 -1
- package/dist-server/migrations/index.js.map +1 -1
- package/dist-server/routes.js +12 -20
- package/dist-server/routes.js.map +1 -1
- package/dist-server/service/actuation/actuation-advisor.d.ts +60 -0
- package/dist-server/service/actuation/actuation-advisor.js +53 -0
- package/dist-server/service/actuation/actuation-advisor.js.map +1 -0
- package/dist-server/service/actuation/actuation-outcome.d.ts +52 -0
- package/dist-server/service/actuation/actuation-outcome.js +34 -0
- package/dist-server/service/actuation/actuation-outcome.js.map +1 -0
- package/dist-server/service/actuation/actuation-rule-resolver.d.ts +19 -0
- package/dist-server/service/actuation/actuation-rule-resolver.js +161 -0
- package/dist-server/service/actuation/actuation-rule-resolver.js.map +1 -0
- package/dist-server/service/actuation/actuation-rule.d.ts +55 -0
- package/dist-server/service/actuation/actuation-rule.js +130 -0
- package/dist-server/service/actuation/actuation-rule.js.map +1 -0
- package/dist-server/service/actuation/approval-gateway.d.ts +20 -0
- package/dist-server/service/actuation/approval-gateway.js +14 -0
- package/dist-server/service/actuation/approval-gateway.js.map +1 -0
- package/dist-server/service/actuation/catch-up-effects.d.ts +16 -0
- package/dist-server/service/actuation/catch-up-effects.js +120 -0
- package/dist-server/service/actuation/catch-up-effects.js.map +1 -0
- package/dist-server/service/actuation/command-dispatcher.d.ts +206 -8
- package/dist-server/service/actuation/command-dispatcher.js +191 -14
- package/dist-server/service/actuation/command-dispatcher.js.map +1 -1
- package/dist-server/service/actuation/command-store.d.ts +10 -2
- package/dist-server/service/actuation/command-store.js +168 -5
- package/dist-server/service/actuation/command-store.js.map +1 -1
- package/dist-server/service/actuation/dispatch-after-approval.d.ts +11 -0
- package/dist-server/service/actuation/dispatch-after-approval.js +73 -0
- package/dist-server/service/actuation/dispatch-after-approval.js.map +1 -0
- package/dist-server/service/actuation/effect-match.d.ts +53 -0
- package/dist-server/service/actuation/effect-match.js +65 -0
- package/dist-server/service/actuation/effect-match.js.map +1 -0
- package/dist-server/service/actuation/index.d.ts +21 -2
- package/dist-server/service/actuation/index.js +21 -2
- package/dist-server/service/actuation/index.js.map +1 -1
- package/dist-server/service/actuation/retry-plan.d.ts +63 -0
- package/dist-server/service/actuation/retry-plan.js +99 -0
- package/dist-server/service/actuation/retry-plan.js.map +1 -0
- package/dist-server/service/actuation/rule-evaluate.d.ts +127 -0
- package/dist-server/service/actuation/rule-evaluate.js +205 -0
- package/dist-server/service/actuation/rule-evaluate.js.map +1 -0
- package/dist-server/service/actuation/rule-loop.d.ts +5 -0
- package/dist-server/service/actuation/rule-loop.js +156 -0
- package/dist-server/service/actuation/rule-loop.js.map +1 -0
- package/dist-server/service/actuation/rule-runner.d.ts +52 -0
- package/dist-server/service/actuation/rule-runner.js +135 -0
- package/dist-server/service/actuation/rule-runner.js.map +1 -0
- package/dist-server/service/actuation/rule-wiring.d.ts +24 -0
- package/dist-server/service/actuation/rule-wiring.js +80 -0
- package/dist-server/service/actuation/rule-wiring.js.map +1 -0
- package/dist-server/service/actuation/settle-effects.d.ts +21 -0
- package/dist-server/service/actuation/settle-effects.js +101 -0
- package/dist-server/service/actuation/settle-effects.js.map +1 -0
- package/dist-server/service/actuation/sweep-dispatch.d.ts +37 -0
- package/dist-server/service/actuation/sweep-dispatch.js +125 -0
- package/dist-server/service/actuation/sweep-dispatch.js.map +1 -0
- package/dist-server/service/actuation/twin-command-resolver.d.ts +120 -0
- package/dist-server/service/actuation/twin-command-resolver.js +406 -0
- package/dist-server/service/actuation/twin-command-resolver.js.map +1 -0
- package/dist-server/service/actuation/twin-command.d.ts +81 -0
- package/dist-server/service/actuation/twin-command.js +60 -0
- package/dist-server/service/actuation/twin-command.js.map +1 -1
- package/dist-server/service/actuation/twin-now.d.ts +41 -0
- package/dist-server/service/actuation/twin-now.js +65 -0
- package/dist-server/service/actuation/twin-now.js.map +1 -0
- package/dist-server/service/index.d.ts +5 -2
- package/dist-server/service/index.js +8 -0
- package/dist-server/service/index.js.map +1 -1
- package/dist-server/service/reference/actuation-routing.d.ts +60 -0
- package/dist-server/service/reference/actuation-routing.js +126 -0
- package/dist-server/service/reference/actuation-routing.js.map +1 -0
- package/dist-server/service/reference/actuation-target.d.ts +65 -0
- package/dist-server/service/reference/actuation-target.js +94 -0
- package/dist-server/service/reference/actuation-target.js.map +1 -0
- package/dist-server/service/reference/connection-portability-resolver.d.ts +29 -0
- package/dist-server/service/reference/connection-portability-resolver.js +181 -0
- package/dist-server/service/reference/connection-portability-resolver.js.map +1 -0
- package/dist-server/service/reference/connection-portability.d.ts +138 -0
- package/dist-server/service/reference/connection-portability.js +230 -0
- package/dist-server/service/reference/connection-portability.js.map +1 -0
- package/dist-server/service/reference/fill-cadence.d.ts +17 -0
- package/dist-server/service/reference/fill-cadence.js +31 -0
- package/dist-server/service/reference/fill-cadence.js.map +1 -0
- package/dist-server/service/reference/fill-dedupe.d.ts +16 -0
- package/dist-server/service/reference/fill-dedupe.js +63 -0
- package/dist-server/service/reference/fill-dedupe.js.map +1 -0
- package/dist-server/service/reference/fill-loop.d.ts +35 -0
- package/dist-server/service/reference/fill-loop.js +187 -0
- package/dist-server/service/reference/fill-loop.js.map +1 -0
- package/dist-server/service/reference/fill-span.d.ts +10 -0
- package/dist-server/service/reference/fill-span.js +37 -0
- package/dist-server/service/reference/fill-span.js.map +1 -0
- package/dist-server/service/reference/hook-contract.d.ts +88 -4
- package/dist-server/service/reference/hook-contract.js +105 -4
- package/dist-server/service/reference/hook-contract.js.map +1 -1
- package/dist-server/service/reference/hook-rejected.d.ts +25 -0
- package/dist-server/service/reference/hook-rejected.js +48 -0
- package/dist-server/service/reference/hook-rejected.js.map +1 -0
- package/dist-server/service/reference/index.d.ts +4 -1
- package/dist-server/service/reference/index.js +5 -1
- package/dist-server/service/reference/index.js.map +1 -1
- package/dist-server/service/reference/live-cadence.d.ts +68 -0
- package/dist-server/service/reference/live-cadence.js +44 -0
- package/dist-server/service/reference/live-cadence.js.map +1 -0
- package/dist-server/service/reference/live-feed-lease.d.ts +15 -0
- package/dist-server/service/reference/live-feed-lease.js +56 -0
- package/dist-server/service/reference/live-feed-lease.js.map +1 -0
- package/dist-server/service/reference/live-ingest.d.ts +21 -0
- package/dist-server/service/reference/live-ingest.js +66 -0
- package/dist-server/service/reference/live-ingest.js.map +1 -0
- package/dist-server/service/reference/reference-adapter.d.ts +291 -2
- package/dist-server/service/reference/reference-adapter.js +19 -1
- package/dist-server/service/reference/reference-adapter.js.map +1 -1
- package/dist-server/service/reference/reference-fill-run.d.ts +50 -0
- package/dist-server/service/reference/reference-fill-run.js +142 -0
- package/dist-server/service/reference/reference-fill-run.js.map +1 -0
- package/dist-server/service/reference/reference-fill.d.ts +90 -0
- package/dist-server/service/reference/reference-fill.js +141 -0
- package/dist-server/service/reference/reference-fill.js.map +1 -0
- package/dist-server/service/reference/reference-hook.d.ts +22 -0
- package/dist-server/service/reference/reference-hook.js +135 -55
- package/dist-server/service/reference/reference-hook.js.map +1 -1
- package/dist-server/service/reference/reference-link-groups.d.ts +10 -0
- package/dist-server/service/reference/reference-link-groups.js +15 -0
- package/dist-server/service/reference/reference-link-groups.js.map +1 -0
- package/dist-server/service/reference/reference-links.d.ts +14 -0
- package/dist-server/service/reference/reference-links.js +73 -0
- package/dist-server/service/reference/reference-links.js.map +1 -0
- package/dist-server/service/reference/reference-live.d.ts +1 -0
- package/dist-server/service/reference/reference-live.js +32 -6
- package/dist-server/service/reference/reference-live.js.map +1 -1
- package/dist-server/service/reference/reference-master.d.ts +56 -2
- package/dist-server/service/reference/reference-master.js +33 -3
- package/dist-server/service/reference/reference-master.js.map +1 -1
- package/dist-server/service/reference/reference-resolver.d.ts +27 -0
- package/dist-server/service/reference/reference-resolver.js +79 -1
- package/dist-server/service/reference/reference-resolver.js.map +1 -1
- package/dist-server/service/twin-event/twin-event-keys.d.ts +31 -3
- package/dist-server/service/twin-event/twin-event-keys.js +36 -4
- package/dist-server/service/twin-event/twin-event-keys.js.map +1 -1
- package/dist-server/service/twin-event/twin-event.d.ts +1 -0
- package/dist-server/service/twin-event/twin-event.js +11 -2
- package/dist-server/service/twin-event/twin-event.js.map +1 -1
- package/dist-server/service/twin-model/choose-recipe.d.ts +15 -0
- package/dist-server/service/twin-model/choose-recipe.js +33 -0
- package/dist-server/service/twin-model/choose-recipe.js.map +1 -0
- package/dist-server/service/twin-model/external-resolver.d.ts +76 -0
- package/dist-server/service/twin-model/external-resolver.js +109 -0
- package/dist-server/service/twin-model/external-resolver.js.map +1 -0
- package/dist-server/service/twin-model/project-structure.js +16 -3
- package/dist-server/service/twin-model/project-structure.js.map +1 -1
- package/dist-server/service/twin-model/twin-equipment.d.ts +17 -0
- package/dist-server/service/twin-model/twin-equipment.js +10 -0
- package/dist-server/service/twin-model/twin-equipment.js.map +1 -1
- package/dist-server/service/twin-model/twin-lineage-query.js +75 -1
- package/dist-server/service/twin-model/twin-lineage-query.js.map +1 -1
- package/dist-server/service/twin-model/twin-model-item-query.js +94 -5
- package/dist-server/service/twin-model/twin-model-item-query.js.map +1 -1
- package/dist-server/service/twin-model/twin-model-tree-query.js.map +1 -1
- package/dist-server/service/twin-space/twin-space.d.ts +29 -0
- package/dist-server/service/twin-space/twin-space.js +13 -0
- package/dist-server/service/twin-space/twin-space.js.map +1 -1
- package/dist-server/service/twin-subject/event-subjects.d.ts +3 -1
- package/dist-server/service/twin-subject/event-subjects.js +8 -1
- package/dist-server/service/twin-subject/event-subjects.js.map +1 -1
- package/dist-shared/entity-delta.js +1 -1
- package/dist-shared/entity-delta.js.map +1 -1
- package/package.json +7 -7
- package/server/engine/canonical-ingest.ts +119 -33
- package/server/engine/event-time.ts +96 -0
- package/server/engine/index.ts +20 -0
- package/server/engine/ingest-dedupe.ts +203 -27
- package/server/engine/ingest-health.ts +91 -6
- package/server/engine/kpi-fold.ts +24 -1
- package/server/engine/kpi-query.ts +23 -4
- package/server/engine/oee-accumulator.ts +80 -8
- package/server/engine/twin-engine.ts +119 -5
- package/server/index.ts +23 -0
- package/server/migrations/1786600000000-RenameOperatoMesAdapterType.ts +66 -0
- package/server/migrations/1786700000000-AddDispatchRetryColumns.ts +82 -0
- package/server/migrations/index.ts +5 -1
- package/server/routes.ts +12 -24
- package/server/service/actuation/actuation-advisor.ts +124 -0
- package/server/service/actuation/actuation-outcome.ts +97 -0
- package/server/service/actuation/actuation-rule-resolver.ts +147 -0
- package/server/service/actuation/actuation-rule.ts +139 -0
- package/server/service/actuation/approval-gateway.ts +44 -0
- package/server/service/actuation/catch-up-effects.ts +152 -0
- package/server/service/actuation/command-dispatcher.ts +401 -20
- package/server/service/actuation/command-store.ts +182 -8
- package/server/service/actuation/dispatch-after-approval.ts +71 -0
- package/server/service/actuation/effect-match.ts +124 -0
- package/server/service/actuation/index.ts +21 -2
- package/server/service/actuation/retry-plan.ts +145 -0
- package/server/service/actuation/rule-evaluate.ts +352 -0
- package/server/service/actuation/rule-loop.ts +160 -0
- package/server/service/actuation/rule-runner.ts +216 -0
- package/server/service/actuation/rule-wiring.ts +84 -0
- package/server/service/actuation/settle-effects.ts +115 -0
- package/server/service/actuation/sweep-dispatch.ts +179 -0
- package/server/service/actuation/twin-command-resolver.ts +387 -0
- package/server/service/actuation/twin-command.ts +128 -1
- package/server/service/actuation/twin-now.ts +97 -0
- package/server/service/index.ts +9 -1
- package/server/service/reference/actuation-routing.ts +165 -0
- package/server/service/reference/actuation-target.ts +163 -0
- package/server/service/reference/connection-portability-resolver.ts +171 -0
- package/server/service/reference/connection-portability.ts +285 -0
- package/server/service/reference/fill-cadence.ts +29 -0
- package/server/service/reference/fill-dedupe.ts +66 -0
- package/server/service/reference/fill-loop.ts +153 -0
- package/server/service/reference/fill-span.ts +31 -0
- package/server/service/reference/hook-contract.ts +185 -5
- package/server/service/reference/hook-rejected.ts +53 -0
- package/server/service/reference/index.ts +5 -1
- package/server/service/reference/live-cadence.ts +129 -0
- package/server/service/reference/live-feed-lease.ts +56 -0
- package/server/service/reference/live-ingest.ts +94 -0
- package/server/service/reference/reference-adapter.ts +327 -5
- package/server/service/reference/reference-fill-run.ts +165 -0
- package/server/service/reference/reference-fill.ts +223 -0
- package/server/service/reference/reference-hook.ts +162 -56
- package/server/service/reference/reference-link-groups.ts +21 -0
- package/server/service/reference/reference-links.ts +77 -0
- package/server/service/reference/reference-live.ts +30 -5
- package/server/service/reference/reference-master.ts +90 -5
- package/server/service/reference/reference-resolver.ts +77 -2
- package/server/service/twin-event/twin-event-keys.ts +37 -4
- package/server/service/twin-event/twin-event.ts +18 -1
- package/server/service/twin-model/choose-recipe.ts +31 -0
- package/server/service/twin-model/external-resolver.ts +161 -0
- package/server/service/twin-model/project-structure.ts +16 -3
- package/server/service/twin-model/twin-equipment.ts +22 -0
- package/server/service/twin-model/twin-lineage-query.ts +82 -1
- package/server/service/twin-model/twin-model-item-query.ts +95 -4
- package/server/service/twin-model/twin-model-tree-query.ts +1 -1
- package/server/service/twin-space/twin-space.ts +39 -0
- package/server/service/twin-subject/event-subjects.ts +8 -1
- package/shared/entity-delta.ts +1 -1
- package/test/ack-shape-alignment.test.ts +97 -0
- package/test/actuation-approval-door.test.ts +421 -0
- package/test/actuation-dispatch.test.ts +16 -3
- package/test/actuation-effect.test.ts +162 -0
- package/test/actuation-outcome.test.ts +152 -0
- package/test/actuation-rule-runner.test.ts +281 -0
- package/test/actuation-rule.test.ts +266 -0
- package/test/actuation-seam.test.ts +249 -0
- package/test/actuation-settle.test.ts +164 -0
- package/test/actuation-target.test.ts +145 -0
- package/test/canonical-ingest-vocabularies.test.ts +49 -2
- package/test/capability-mapping.test.ts +1 -1
- package/test/choose-recipe.test.ts +117 -0
- package/test/command-store-writes-columns.test.ts +126 -0
- package/test/connection-portability-doors.test.ts +87 -0
- package/test/connector-capability-declaration.test.ts +54 -4
- package/test/declaration-reaches-model.test.ts +43 -0
- package/test/dedupe-transformation.test.ts +112 -0
- package/test/entity-delta.test.ts +1 -1
- package/test/event-subjects.test.ts +2 -1
- package/test/external-resolver.test.ts +171 -0
- package/test/fill-dedupe.test.ts +59 -0
- package/test/fill-loop.test.ts +35 -0
- package/test/first-envelope.test.ts +153 -0
- package/test/fold-shift-dst.test.ts +110 -0
- package/test/home-in-area.test.ts +90 -0
- package/test/hook-rejected-shape.test.ts +86 -0
- package/test/hook-response-sum.test.ts +193 -0
- package/test/hook-sequence.test.ts +1 -1
- package/test/ingest-dedupe.test.ts +56 -0
- package/test/ingest-expected-quiet.test.ts +111 -0
- package/test/ingest-idempotent.test.ts +79 -0
- package/test/ingest-reconcile-four-ways.test.ts +97 -0
- package/test/ingest-rules-header.test.ts +100 -0
- package/test/ingest-window-rolls-unobserved.test.ts +72 -0
- package/test/intake-mapping-parity.test.ts +175 -0
- package/test/live-cadence.test.ts +170 -0
- package/test/live-feed-lease.test.ts +63 -0
- package/test/live-mirror-parity.test.ts +1 -1
- package/test/oee-accumulator.test.ts +3 -3
- package/test/oee-site-calendar.test.ts +146 -0
- package/test/order-and-transaction-split.test.ts +107 -0
- package/test/reference-fill.test.ts +235 -0
- package/test/reference-hook.test.ts +1 -1
- package/test/reference-links.test.ts +56 -0
- package/test/retry-plan.test.ts +139 -0
- package/test/sweep-dispatch.test.ts +191 -0
- package/test/twin-event-keys.test.ts +20 -6
- package/test/twin-model-item-db.test.ts +6 -6
- package/test/twin-model-tree-db.test.ts +6 -6
- package/tsconfig.shared.tsbuildinfo +1 -1
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
* 시간축 = Date.parse(eventTime)(ms) — 구간 차분은 sim/실시간 무관하게 정확(고정 오프셋 상쇄).
|
|
12
12
|
*/
|
|
13
13
|
/* OEE 공식은 계약에 있다 — 소비자가 셋이라(시뮬·미러·MES) 커널에 두면 밖에서 다시 쓰게 된다. */
|
|
14
|
-
import { computeOee } from '@operato/ops-contract'
|
|
15
|
-
import type { OeeCounters } from '@operato/ops-contract'
|
|
14
|
+
import { computeOee, nonWorkingMsIn } from '@operato/ops-contract'
|
|
15
|
+
import type { OeeCounters, WorkCalendarEntry } from '@operato/ops-contract'
|
|
16
16
|
import type { CanonicalEnvelope, OeeMetrics } from '@operato/ops-contract'
|
|
17
17
|
|
|
18
18
|
interface Acc {
|
|
@@ -22,7 +22,7 @@ interface Acc {
|
|
|
22
22
|
busyMs: number
|
|
23
23
|
downMs: number
|
|
24
24
|
goodCount: number
|
|
25
|
-
|
|
25
|
+
nonconformingCount: number
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
/** 누적기의 완전한 재개점 — 저장했다 되살릴 수 있어야 꼬리만 계산할 수 있다. */
|
|
@@ -35,10 +35,68 @@ export class OeeAccumulator {
|
|
|
35
35
|
private acc = new Map<string, Acc>()
|
|
36
36
|
private maxMs = 0 // 최신 이벤트 시각 — nowMs 기본값(현재 계측 시점)
|
|
37
37
|
|
|
38
|
+
/*
|
|
39
|
+
* ── 현장이 언제 도나 — **계획 조업 시간의 근거** (2026-09-07) ────────────────
|
|
40
|
+
* 이것이 없던 동안 계획 조업 시간이 「재기 시작한 뒤 흐른 시간 전부」였다. 주말도 야간도 휴일도
|
|
41
|
+
* 계획 조업으로 세었다.
|
|
42
|
+
*
|
|
43
|
+
* [산술] 8시간 × 주5일이면 168시간 중 40시간이 조업이다 — PBT 가 4.2배 크고 가동률이 그만큼 작다.
|
|
44
|
+
* 그리고 그 수가 1로 잘려 있어서(오늘 걷었다) 그럴싸하게 보였다. 두 오류가 서로를 가렸다.
|
|
45
|
+
*
|
|
46
|
+
* plant 레인이 같은 자리를 먼저 겪고 실측을 냈다 — 3교대를 물려받으면 `availability 0.4583`,
|
|
47
|
+
* 달력이 아예 없으면 **수를 안 낸다**. 트윈은 창 전체를 계획으로 쳐서 수를 냈다.
|
|
48
|
+
*
|
|
49
|
+
* ⚠ **선언이 없으면 채우지 않는다.** `nonWorkingMsIn` 이 그때 `undefined` 를 내고, 그 값을 0 으로
|
|
50
|
+
* 바꾸지 않는다 — 0 은 「달력을 봤고 쉬는 시간이 없었다」는 주장이다. 지금은 그러면 PBT 가 창 전체가
|
|
51
|
+
* 되어 예전과 같은 수가 나온다. **그것을 「안 낸다」로 바꿀지는 아직 결정되지 않았다** — 거동 변화가
|
|
52
|
+
* 크고 화면이 비는 자리라 사용자 판단을 기다린다(성숙도에 미달로 세우는 짝과 함께 가야 한다).
|
|
53
|
+
*/
|
|
54
|
+
private siteCalendar?: readonly WorkCalendarEntry[]
|
|
55
|
+
private calendarByResource = new Map<string, readonly WorkCalendarEntry[]>()
|
|
56
|
+
private utcOffsetMinutes?: number
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 모델에서 조업 달력을 읽어 얹는다 — 모델을 들고 있는 쪽(`TwinEngine`)이 부른다.
|
|
60
|
+
*
|
|
61
|
+
* ── 우선순위가 여기 있는 이유 ───────────────────────────────────────────────
|
|
62
|
+
* ```
|
|
63
|
+
* 설비의 달력 이 설비의 가동 계획 — 선언했으면 이것이 이긴다 (24시간 도는 로)
|
|
64
|
+
* 현장의 달력 이 현장의 조업일
|
|
65
|
+
* 없음 제약 없음 — 계획 조업 시간이 창 전체가 된다
|
|
66
|
+
* ```
|
|
67
|
+
* 계획 조업 시간을 내는 자리가 여기라 그 규칙도 여기 둔다. 부르는 쪽마다 고르게 하면 라이브와
|
|
68
|
+
* 히스토리가 다른 답을 낸다.
|
|
69
|
+
*
|
|
70
|
+
* ⚠ **설비의 달력은 사람의 근무조가 아니다.** 씨앗이 사람의 근무조를 설비 달력에 복사한 일이 있었고,
|
|
71
|
+
* 그때 아무도 근무하지 않는 시간이 근무 시간으로 기록됐다. 사람 쪽은 `persons[].workCalendar` 다.
|
|
72
|
+
*
|
|
73
|
+
* 세우는 자리에서 한 번만 주지 않고 따로 부르는 이유는 모델이 다시 실릴 수 있기 때문이다
|
|
74
|
+
* (재동기화 · 이관). 그때 달력만 갈아 끼우면 누적된 계측을 버리지 않는다.
|
|
75
|
+
*/
|
|
76
|
+
setWorkCalendar(model?: { workCalendar?: readonly WorkCalendarEntry[]; equipment?: readonly any[]; utcOffsetMinutes?: number }): void {
|
|
77
|
+
this.siteCalendar = model?.workCalendar?.length ? model.workCalendar : undefined
|
|
78
|
+
this.utcOffsetMinutes = model?.utcOffsetMinutes
|
|
79
|
+
this.calendarByResource.clear()
|
|
80
|
+
for (const e of model?.equipment ?? []) {
|
|
81
|
+
const cal = e?.workCalendar
|
|
82
|
+
if (e?.id && Array.isArray(cal) && cal.length) this.calendarByResource.set(String(e.id), cal)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 이 설비의 계획 가동 달력 — 자기 것이 있으면 그것, 없으면 현장의 것.
|
|
88
|
+
*
|
|
89
|
+
* 인자 이름이 `moverId` 인 것은 **봉투의 옛 세대 필드 이름**이라 그렇다(§`apply`). 새로 만드는
|
|
90
|
+
* 이름에는 그 낱말을 쓰지 않는다 — 어휘 가드가 그것을 잡았고, 맞게 잡았다.
|
|
91
|
+
*/
|
|
92
|
+
private calendarFor(moverId: string): readonly WorkCalendarEntry[] | undefined {
|
|
93
|
+
return this.calendarByResource.get(moverId) ?? this.siteCalendar
|
|
94
|
+
}
|
|
95
|
+
|
|
38
96
|
private ensure(moverId: string, t: number): Acc {
|
|
39
97
|
let a = this.acc.get(moverId)
|
|
40
98
|
if (!a) {
|
|
41
|
-
a = { lastMs: t, startMs: t, busyMs: 0, downMs: 0, goodCount: 0,
|
|
99
|
+
a = { lastMs: t, startMs: t, busyMs: 0, downMs: 0, goodCount: 0, nonconformingCount: 0 }
|
|
42
100
|
this.acc.set(moverId, a)
|
|
43
101
|
}
|
|
44
102
|
return a
|
|
@@ -70,11 +128,11 @@ export class OeeAccumulator {
|
|
|
70
128
|
const t = Date.parse(env.eventTime)
|
|
71
129
|
if (t > this.maxMs) this.maxMs = t
|
|
72
130
|
if (env.eventType === 'quality.output') {
|
|
73
|
-
const q = env.data as { moverId?: string; goodCount?: number;
|
|
131
|
+
const q = env.data as { moverId?: string; goodCount?: number; nonconformingCount?: number }
|
|
74
132
|
if (!q?.moverId) return
|
|
75
133
|
const a = this.ensure(q.moverId, t)
|
|
76
134
|
a.goodCount = q.goodCount ?? a.goodCount // 누적값 최신화(good/scrap 은 무버 누적)
|
|
77
|
-
a.
|
|
135
|
+
a.nonconformingCount = q.nonconformingCount ?? a.nonconformingCount
|
|
78
136
|
return
|
|
79
137
|
}
|
|
80
138
|
if (env.eventType !== 'equipment.status') return
|
|
@@ -97,13 +155,27 @@ export class OeeAccumulator {
|
|
|
97
155
|
* 화면에 `idleMs 1767226603000`(약 56,000년) 유휴가 섰다. 창을 **말하지 않는다** — 공식이
|
|
98
156
|
* 없음을 없음으로 다룬다(§computeOee).
|
|
99
157
|
*/
|
|
100
|
-
if (!a) return { runMs: 0, setupMs: 0, downMs: 0, goodCount: 0,
|
|
158
|
+
if (!a) return { runMs: 0, setupMs: 0, downMs: 0, goodCount: 0, nonconformingCount: 0 }
|
|
101
159
|
const dt = Math.max(0, nowMs - a.lastMs)
|
|
102
160
|
let busyMs = a.busyMs
|
|
103
161
|
let downMs = a.downMs
|
|
104
162
|
if (a.status === 'busy') busyMs += dt
|
|
105
163
|
else if (a.status === 'down') downMs += dt
|
|
106
|
-
|
|
164
|
+
/*
|
|
165
|
+
* 계획 정지 — 계약이 이 칸에 그 뜻을 적어 뒀다(점심 · 예방보전 · 교대). 휴일과 주말이 같은
|
|
166
|
+
* 갈래다. `metricsSinceMs` 를 당겨서 같은 효과를 내지 않는다: 그 칸은 「언제부터 쟀나」라는
|
|
167
|
+
* 사실이고, 파생값을 얹으면 실제보다 늦게 재기 시작했다고 말하는 것이 된다.
|
|
168
|
+
*/
|
|
169
|
+
const holdMs = nonWorkingMsIn(this.calendarFor(moverId), a.startMs, nowMs, this.utcOffsetMinutes)
|
|
170
|
+
return {
|
|
171
|
+
runMs: busyMs,
|
|
172
|
+
setupMs: 0,
|
|
173
|
+
downMs,
|
|
174
|
+
goodCount: a.goodCount,
|
|
175
|
+
nonconformingCount: a.nonconformingCount,
|
|
176
|
+
metricsSinceMs: a.startMs,
|
|
177
|
+
...(holdMs !== undefined ? { holdMs } : {})
|
|
178
|
+
}
|
|
107
179
|
}
|
|
108
180
|
|
|
109
181
|
/** 공유 공식으로 근사 OEE 산출 — 정확한 OEE 는 실 텔레메트리(option ①). nowMs 기본=최신 이벤트 시각. */
|
|
@@ -70,6 +70,7 @@ import {
|
|
|
70
70
|
recordIngestSource,
|
|
71
71
|
recordDuplicates,
|
|
72
72
|
rollIngestWindow,
|
|
73
|
+
recordVocabulary,
|
|
73
74
|
/* 별칭 — 같은 이름의 정적 메서드와 헷갈리지 않게(그 메서드가 이것을 부른다). */
|
|
74
75
|
recordJournalWrite as recordLedgerWrite,
|
|
75
76
|
type IngestHealthView,
|
|
@@ -1799,8 +1800,23 @@ export class TwinEngine {
|
|
|
1799
1800
|
* `timezone` 을 읽는다. 공간이 말하지 않으면 **테넌트로 내려가지 않는다** — 잘못된 입자로 답하는 것이
|
|
1800
1801
|
* 모르는 것보다 나쁘다(그때는 커널이 UTC 로 읽고, 그 기본값은 계약에 밝혀져 있다).
|
|
1801
1802
|
*
|
|
1802
|
-
* 커널은 zero-dep 이라 시간대 데이터베이스를 갖지 않으므로 **분 오프셋**으로 풀어 넘긴다.
|
|
1803
|
-
*
|
|
1803
|
+
* 커널은 zero-dep 이라 시간대 데이터베이스를 갖지 않으므로 **분 오프셋**으로 풀어 넘긴다.
|
|
1804
|
+
*
|
|
1805
|
+
* ── 이름도 함께 얹는다 (2026-09-07) ────────────────────────────────────────
|
|
1806
|
+
* 오프셋 하나만 얹으면 **그 값이 이 순간의 것으로 모델에 굳는다.** 트윈은 몇 주씩 돌고, 자료에
|
|
1807
|
+
* 이미 `America/Tijuana`(PST −480 / PDT −420)가 있다 — 여름에 뜬 트윈이 11월까지 돌면 그 뒤 모든
|
|
1808
|
+
* 시각이 한 시간 틀린다.
|
|
1809
|
+
*
|
|
1810
|
+
* 지난 구간을 접을 때가 더 나쁘다. 1년치를 교대별로 접으면 그 1년을 한 오프셋으로 읽는데 그 안에
|
|
1811
|
+
* 전환이 두 번 있다. 어긋난 시각이 오류를 내지 않고 **다른 교대로 세어진다.**
|
|
1812
|
+
*
|
|
1813
|
+
* 그래서 이름(`timezone`)을 함께 얹어, 순간마다 다시 풀 수 있게 한다. 커널은 이 값을 쓰지 않는다 —
|
|
1814
|
+
* 여전히 오프셋을 호출마다 받는다. 푸는 것은 호스트의 몫이고 그것이 계약이 적어 둔 분담이다.
|
|
1815
|
+
*
|
|
1816
|
+
* `utcOffsetMinutes` 도 계속 얹는다. 아직 이름을 안 보는 소비처가 그 값으로 돌고, 이름이 없는 공간에
|
|
1817
|
+
* 대해서는 그것이 유일한 답이다.
|
|
1818
|
+
*
|
|
1819
|
+
* ⚠ 이것으로도 완전하지 않다. 정확한 답은 호스트가 표준대로 **절대 구간**을 계산해 넣는 것이다.
|
|
1804
1820
|
*/
|
|
1805
1821
|
static async withSpaceTimeBase(model: TwinModelDef, domainId: string, spaceId?: string): Promise<TwinModelDef> {
|
|
1806
1822
|
const sid = spaceId ?? (model as any)?.spaceId
|
|
@@ -1811,7 +1827,8 @@ export class TwinEngine {
|
|
|
1811
1827
|
if (space?.timezone) twinWarn(`[twin-engine] space "${sid}" declares time zone "${space.timezone}" but it is not a known IANA zone — times will be read as UTC.`)
|
|
1812
1828
|
return model
|
|
1813
1829
|
}
|
|
1814
|
-
|
|
1830
|
+
/* 이름이 IANA 로 읽히는 것을 위에서 이미 확인했다 — 오프셋이 나왔다는 것이 그 증거다. */
|
|
1831
|
+
return { ...model, utcOffsetMinutes: offset, timezone: space!.timezone }
|
|
1815
1832
|
}
|
|
1816
1833
|
|
|
1817
1834
|
static startLive(id: string, domainId: string, kind: string, model: TwinModelDef): InstanceRuntime {
|
|
@@ -1829,7 +1846,13 @@ export class TwinEngine {
|
|
|
1829
1846
|
/* `projector` 필드는 옛 이름으로 남긴다 — 소비처가 `snapshot()` 을 부르므로 얇은 어댑터로 잇는다.
|
|
1830
1847
|
* (P3 에서 소비처를 커널 어휘로 바꾸면 사라진다.) */
|
|
1831
1848
|
const projector = { apply: (e: CanonicalEnvelope) => kernel.apply(e), snapshot: () => kernel.getSnapshot() }
|
|
1832
|
-
const
|
|
1849
|
+
const oeeAcc = new OeeAccumulator()
|
|
1850
|
+
/*
|
|
1851
|
+
* **현장의 조업 달력을 얹는다** — 이것을 안 부르면 계획 조업 시간이 창 전체가 된다(§`OeeAccumulator`).
|
|
1852
|
+
* 자리를 만들고 부르지 않는 것이 이 저장소에서 가장 자주 나는 결함이라, 세우는 자리에서 바로 붙인다.
|
|
1853
|
+
*/
|
|
1854
|
+
oeeAcc.setWorkCalendar(model as any)
|
|
1855
|
+
const inst: InstanceRuntime = { id, domainId, mode: 'live', restartPolicy: 'resync', kernel, projector, oee: oeeAcc, spaceId: (model as any)?.spaceId, model, unsub: () => {} }
|
|
1833
1856
|
/*
|
|
1834
1857
|
* ── 커널이 **판정으로 낸 사실**도 저널에 남는다 (2026-08-14 실측으로 잡음) ────
|
|
1835
1858
|
*
|
|
@@ -3923,6 +3946,12 @@ export class TwinEngine {
|
|
|
3923
3946
|
* 가동 이력이 나온다(그 뒤에 일어난 고장이 과거 화면에 섞이지 않는다).
|
|
3924
3947
|
*/
|
|
3925
3948
|
const oee = new OeeAccumulator()
|
|
3949
|
+
/*
|
|
3950
|
+
* 히스토리도 같은 달력으로 읽는다 — 라이브와 다른 기준으로 세면 두 화면이 다른 수를 보인다.
|
|
3951
|
+
* 여기서 쓸 모델은 `newest` 다(이 스코프의 마지막 구조). 구간마다의 `model` 은 위 루프 안에만
|
|
3952
|
+
* 있고, 달력은 구조 개정마다 갈리는 값이 아니다.
|
|
3953
|
+
*/
|
|
3954
|
+
oee.setWorkCalendar(newest as any)
|
|
3926
3955
|
/* 재개점이 있으면 그 위에 꼬리만 얹는다 — 없으면 꼬리분만 세어 가용률이 알리지 않고 작아진다. */
|
|
3927
3956
|
if (resume?.fold?.oee) oee.restore(resume.fold.oee)
|
|
3928
3957
|
for (const r of wanted) {
|
|
@@ -4306,6 +4335,35 @@ export class TwinEngine {
|
|
|
4306
4335
|
return !!this.instances[runtimeKey(domainId, id)]
|
|
4307
4336
|
}
|
|
4308
4337
|
|
|
4338
|
+
/**
|
|
4339
|
+
* 이 트윈의 **중복 판정 기억을 미리 채운다** — 저널에서 읽은 정체로.
|
|
4340
|
+
*
|
|
4341
|
+
* 기억은 프로세스 안에만 있어서 재기동하면 비워진다(§`FactDeduper`). 그 뒤 backfill 을 돌리면
|
|
4342
|
+
* 이미 저널에 있는 사실도 「처음 본 것」이 되어 다시 앉는다. 실측으로 39건이 통째로 다시 앉았다
|
|
4343
|
+
* (2026-09-06, `mes-line-a`).
|
|
4344
|
+
*
|
|
4345
|
+
* 안 도는 트윈에는 채울 곳이 없다 — 0을 낸다(거짓이 아니라 사실이다).
|
|
4346
|
+
*/
|
|
4347
|
+
static warmDeduper(domainId: string, id: string, identities: readonly string[]): number {
|
|
4348
|
+
const inst = this.instances[runtimeKey(domainId, id)]
|
|
4349
|
+
if (!inst || !identities.length) return 0
|
|
4350
|
+
return (inst.deduper ??= new FactDeduper()).remember(identities)
|
|
4351
|
+
}
|
|
4352
|
+
|
|
4353
|
+
/**
|
|
4354
|
+
* 이 프로세스가 지금 들고 있는 트윈들 — 테넌트로 좁혀서.
|
|
4355
|
+
*
|
|
4356
|
+
* 주기 작업(규칙 판정 등)이 「무엇을 돌아야 하나」를 물을 자리다. 등록부(DB)를 읽으면 **다른 프로세스가
|
|
4357
|
+
* 든 트윈까지 나온다** — 그것을 돌면 같은 조치가 두 벌 올라간다. 도는 것은 든 쪽이 돈다.
|
|
4358
|
+
*
|
|
4359
|
+
* `owns()` 와 같은 술어를 쓴다(`this.instances`). 두 벌이면 어긋난다.
|
|
4360
|
+
*/
|
|
4361
|
+
static ownedTwins(domainId?: string): { domainId: string; instanceId: string }[] {
|
|
4362
|
+
return Object.entries(this.instances)
|
|
4363
|
+
.map(([key, inst]) => ({ domainId: inst.domainId, instanceId: parseRuntimeKey(key).instanceId }))
|
|
4364
|
+
.filter(t => !domainId || t.domainId === domainId)
|
|
4365
|
+
}
|
|
4366
|
+
|
|
4309
4367
|
/**
|
|
4310
4368
|
* 이 트윈이 이 테넌트 것인가 — **떠 있든 아니든.**
|
|
4311
4369
|
*
|
|
@@ -4475,6 +4533,52 @@ export class TwinEngine {
|
|
|
4475
4533
|
this.onWindowClosed = fn
|
|
4476
4534
|
}
|
|
4477
4535
|
|
|
4536
|
+
/**
|
|
4537
|
+
* **모든 트윈의 창을 굴린다** — 보는 사람이 없어도 창이 닫히게.
|
|
4538
|
+
*
|
|
4539
|
+
* ── 무엇이 있었나 (2026-09-05 실측) ────────────────────────────────────────
|
|
4540
|
+
* 창은 「다음 사건이 왔을 때 이전 창을 닫는」 모양으로만 굴렀다. 그러면 사실이 드문 트윈은 창이
|
|
4541
|
+
* 계속 열려 있고, 표에 아무것도 안 남는다. 두 수가 그것을 정확히 말한다.
|
|
4542
|
+
*
|
|
4543
|
+
* mes-miratek 사실 46건 · 62분에 흩어짐 창 1개
|
|
4544
|
+
* mes-line-a 사실 11건 창 48개
|
|
4545
|
+
*
|
|
4546
|
+
* **사실 수와 창 수가 거꾸로다.** 창을 만든 것이 유입이 아니라 **누가 물어본 것**이다 —
|
|
4547
|
+
* `ingestHealthOf()` 가 굴리므로, 점검표·화면이 계속 묻는 트윈만 기록이 쌓였다.
|
|
4548
|
+
*
|
|
4549
|
+
* 그래서 **아무도 안 보는 트윈일수록 기록이 없다.** 사실이 조용히 사라지는 것을 잡으려고 만든
|
|
4550
|
+
* 장부가 정확히 그 반대로 돌고 있었다. 실제로 137봉투가 장부 없이 지나갔다(인티그레이션 레인 실측).
|
|
4551
|
+
*
|
|
4552
|
+
* ── 왜 주기인가 ────────────────────────────────────────────────────────────
|
|
4553
|
+
* 창이 닫히는 이유는 **시간이 지난 것**이다. 사건도 질의도 그것을 알려 주는 신호일 뿐이고, 둘 다
|
|
4554
|
+
* 안 올 수 있다. 시간은 늘 지나므로 주기가 그 사실에 맞는 신호다.
|
|
4555
|
+
*
|
|
4556
|
+
* 조용한 트윈이 자기 침묵을 기록하게 되는 것이 이 변경의 값이다 — `expected-quiet` 판정이 그
|
|
4557
|
+
* 기록 위에 선다.
|
|
4558
|
+
*/
|
|
4559
|
+
static rollAllIngestWindows(nowMs: number = Date.now()): number {
|
|
4560
|
+
let closed = 0
|
|
4561
|
+
for (const key of Object.keys(this.ingestLedgers)) {
|
|
4562
|
+
const ledger = this.ingestLedgers[key]
|
|
4563
|
+
if (!ledger) continue
|
|
4564
|
+
/*
|
|
4565
|
+
* 키가 깨져 있으면 그 트윈만 건너뛴다 — 하나 때문에 나머지 장부가 안 굴러서는 안 된다.
|
|
4566
|
+
* 훅의 실패도 `rollIngestWindow` 가 감싼다(영속을 지키려고 관측을 멈추지 않는다).
|
|
4567
|
+
*/
|
|
4568
|
+
let at: { domainId: string; instanceId: string }
|
|
4569
|
+
try {
|
|
4570
|
+
at = parseRuntimeKey(key)
|
|
4571
|
+
} catch {
|
|
4572
|
+
continue
|
|
4573
|
+
}
|
|
4574
|
+
rollIngestWindow(ledger, nowMs, undefined, w => {
|
|
4575
|
+
closed++
|
|
4576
|
+
this.onWindowClosed?.(at, w)
|
|
4577
|
+
})
|
|
4578
|
+
}
|
|
4579
|
+
return closed
|
|
4580
|
+
}
|
|
4581
|
+
|
|
4478
4582
|
/**
|
|
4479
4583
|
* 한 번의 인제스트 결과를 장부에 적는다.
|
|
4480
4584
|
*
|
|
@@ -4541,7 +4645,15 @@ export class TwinEngine {
|
|
|
4541
4645
|
*
|
|
4542
4646
|
* 「통과」로 세지 않는다 — 트윈이 멈춘 사이 사실이 사라지는데 화면이 100% 라고 말하게 된다.
|
|
4543
4647
|
*/
|
|
4544
|
-
undelivered = 0
|
|
4648
|
+
undelivered = 0,
|
|
4649
|
+
/**
|
|
4650
|
+
* **저널에 안 간 어휘 요소의 수** — 로트 번호·소비기한처럼 시각축이 없는 값.
|
|
4651
|
+
*
|
|
4652
|
+
* 사라진 것이 아니라 저널로 가지 않는 것이다. 그런데 그 수를 빼 주는 자리가 없으면
|
|
4653
|
+
* 「보낸 것 − 앉은 것」이 안 맞고 **그 차이를 사라진 것으로 읽게 된다**(2026-09-05 실측:
|
|
4654
|
+
* 커서 20 · 저널 17 · 어휘 1 을 사람이 손으로 세서 「둘이 빈다」를 찾았다).
|
|
4655
|
+
*/
|
|
4656
|
+
vocabulary = 0
|
|
4545
4657
|
): void {
|
|
4546
4658
|
const key = runtimeKey(domainId, instanceId)
|
|
4547
4659
|
const ledger = this.ingestLedgers[key] ?? (this.ingestLedgers[key] = newIngestLedger())
|
|
@@ -4553,6 +4665,8 @@ export class TwinEngine {
|
|
|
4553
4665
|
closed => this.onWindowClosed?.({ domainId, instanceId }, closed),
|
|
4554
4666
|
undelivered
|
|
4555
4667
|
)
|
|
4668
|
+
/* 창을 이미 굴린 뒤라 순서가 중요하다 — 먼저 적으면 한 유입이 두 창에 걸친다. */
|
|
4669
|
+
recordVocabulary(ledger, vocabulary)
|
|
4556
4670
|
}
|
|
4557
4671
|
|
|
4558
4672
|
/**
|
package/server/index.ts
CHANGED
|
@@ -9,6 +9,9 @@ import './routes.js'
|
|
|
9
9
|
import { TwinEngine } from './engine/index.js'
|
|
10
10
|
import { resumeReferenceLiveFeeds } from './service/reference/reference-live.js'
|
|
11
11
|
import { installIngestWindowWriter } from './service/twin-ingest-window/index.js'
|
|
12
|
+
import { installReferenceLinkResolver } from './service/reference/reference-links.js'
|
|
13
|
+
import { startActuationRuleLoop } from './service/actuation/rule-loop.js'
|
|
14
|
+
import { startFillLoop } from './service/reference/fill-loop.js'
|
|
12
15
|
|
|
13
16
|
/*
|
|
14
17
|
* 모듈 부팅 — **도는 트윈을 되살린다.**
|
|
@@ -26,6 +29,11 @@ process.on('bootstrap-module-start' as any, async ({ app, config, client }: any)
|
|
|
26
29
|
* 남는다. 커널 층은 저장 계층을 모르므로 이 등록이 그 둘을 잇는 유일한 자리다.
|
|
27
30
|
*/
|
|
28
31
|
installIngestWindowWriter()
|
|
32
|
+
/*
|
|
33
|
+
* 원본에만 있는 관계를 조회하는 문을 연다 — MES 의 생산 실적·정비 계획처럼 트윈이 담지 않는
|
|
34
|
+
* 것들. 조회용 참조는 트윈으로 복제하지 않고 그때 물어본다.
|
|
35
|
+
*/
|
|
36
|
+
installReferenceLinkResolver()
|
|
29
37
|
/*
|
|
30
38
|
* **저널 보존 기간은 선언에서 온다** — 코드에 박아 두지 않는다(2026-08-22).
|
|
31
39
|
*
|
|
@@ -54,6 +62,21 @@ process.on('bootstrap-module-start' as any, async ({ app, config, client }: any)
|
|
|
54
62
|
|
|
55
63
|
await TwinEngine.bootstrap()
|
|
56
64
|
await resumeReferenceLiveFeeds()
|
|
65
|
+
/*
|
|
66
|
+
* 빠진 구간을 다시 받는 주기 — 기동에서 한 번, 그 뒤 10분마다(§`fill-loop.ts`).
|
|
67
|
+
* 피드를 붙인 **뒤에** 세운다: 커널이 되살아나기 전에 부르면 도는 트윈이 0개다.
|
|
68
|
+
*/
|
|
69
|
+
startFillLoop()
|
|
70
|
+
|
|
71
|
+
/*
|
|
72
|
+
* 조치 규칙 주기 — **트윈이 다 선 뒤에** 세운다. 앞에 세우면 첫 바퀴가 아직 안 선 트윈들을 돌고
|
|
73
|
+
* 「상태를 읽을 수 없다」를 규칙 행에 적는다. 그것은 사실이지만 사람에게는 오해다.
|
|
74
|
+
*
|
|
75
|
+
* 커널 틱과 따로 돈다 — 틱은 예외가 나면 그 트윈을 멈추고, 규칙 하나가 잘못됐다고 공장이 멈추면
|
|
76
|
+
* 안 된다(§`rule-loop.ts`).
|
|
77
|
+
*/
|
|
78
|
+
startActuationRuleLoop()
|
|
79
|
+
|
|
57
80
|
twinLog('Headless Twin host has started.')
|
|
58
81
|
} catch (ex) {
|
|
59
82
|
twinError('Headless Twin host failed to start.', ex)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { MigrationInterface, QueryRunner } from 'typeorm'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `twin_references.adapter_type` — `operato-mes` → `operato-plant`.
|
|
5
|
+
*
|
|
6
|
+
* ── 왜 마이그레이션이 필요한가 ────────────────────────────────────────────────
|
|
7
|
+
* 커넥터의 `type` 은 **저장되는 식별자**다. 트윈을 원본에 붙일 때 그 문자열이 이 컬럼에 적히고,
|
|
8
|
+
* 넘길 때·라이브를 열 때 그 값으로 커넥터를 찾는다(§`getAdapter`).
|
|
9
|
+
*
|
|
10
|
+
* 그래서 제품 개명(`operato-mes` → `operato-plant`)이 코드만 바뀌면 **이미 붙어 있는 원본이 커넥터를
|
|
11
|
+
* 못 찾는다.** 그때 나오는 것은 이렇다.
|
|
12
|
+
*
|
|
13
|
+
* 조치 넘김 `no-adapter` 로 거절
|
|
14
|
+
* 라이브 커넥터가 없어 관측이 끊긴다
|
|
15
|
+
* 화면 「등록되지 않은 커넥터입니다 (operato-mes)」 한 줄
|
|
16
|
+
*
|
|
17
|
+
* **오류는 안 나고, 원인이 개명이라는 것이 그 문장에 없다.** 트윈은 그대로 서 있고 값만 안 온다.
|
|
18
|
+
* 이 저장소에서 개명이 저장된 식별자에 닿았을 때 늘 이 모양으로 났다(보드 키·씬 컴포넌트 타입).
|
|
19
|
+
*
|
|
20
|
+
* ── 왜 이 패키지인가 ─────────────────────────────────────────────────────────
|
|
21
|
+
* 표를 든 쪽이 이 패키지다(`twin_references`). 커넥터는 `operato-twin` 이 등록하지만, 그 값이 사는
|
|
22
|
+
* 자리는 여기다. 마이그레이션은 표를 든 패키지에 둔다.
|
|
23
|
+
*
|
|
24
|
+
* ── 되돌리기 ─────────────────────────────────────────────────────────────────
|
|
25
|
+
* `down` 이 값을 되돌린다. 옛 코드로 되돌아가는 경우가 실제로 있고(배포 롤백), 그때 값이 새 이름으로
|
|
26
|
+
* 남아 있으면 같은 결함이 반대 방향으로 난다.
|
|
27
|
+
*
|
|
28
|
+
* 멱등: 해당하는 행이 없으면 아무것도 하지 않는다. TypeORM 질의 빌더만 쓴다 — 드라이버마다 파라미터
|
|
29
|
+
* 표기가 다르므로 raw SQL 을 쓰지 않는다.
|
|
30
|
+
*/
|
|
31
|
+
const REFS = 'twin_references'
|
|
32
|
+
const COLUMN = 'adapter_type'
|
|
33
|
+
/**
|
|
34
|
+
* 질의 빌더에는 **속성 이름**을 준다.
|
|
35
|
+
*
|
|
36
|
+
* 처음에 컬럼 이름(`adapter_type`)으로 썼더니 TypeORM 이 표 이름을 엔티티(`TwinReference`)로 풀고
|
|
37
|
+
* 「그런 속성이 없다」로 거절했다 — 표 이름을 주더라도 매핑은 엔티티 기준이다. 컬럼 이름은 표가
|
|
38
|
+
* 있는지 볼 때만 쓴다.
|
|
39
|
+
*/
|
|
40
|
+
const PROPERTY = 'adapterType'
|
|
41
|
+
const OLD = 'operato-mes'
|
|
42
|
+
const NEW = 'operato-plant'
|
|
43
|
+
|
|
44
|
+
async function rename(queryRunner: QueryRunner, from: string, to: string): Promise<void> {
|
|
45
|
+
/* 표가 아직 없는 배포(첫 기동)에서는 할 일이 없다. */
|
|
46
|
+
const table = await queryRunner.getTable(REFS)
|
|
47
|
+
if (!table?.columns.some(c => c.name === COLUMN)) return
|
|
48
|
+
|
|
49
|
+
/* 객체 형태로 준다 — 문자열 조건은 드라이버마다 식별자 인용이 달라 그대로 나간다. */
|
|
50
|
+
await queryRunner.manager
|
|
51
|
+
.createQueryBuilder()
|
|
52
|
+
.update(REFS)
|
|
53
|
+
.set({ [PROPERTY]: to })
|
|
54
|
+
.where({ [PROPERTY]: from })
|
|
55
|
+
.execute()
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export class RenameOperatoMesAdapterType1786600000000 implements MigrationInterface {
|
|
59
|
+
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
60
|
+
await rename(queryRunner, OLD, NEW)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
public async down(queryRunner: QueryRunner): Promise<void> {
|
|
64
|
+
await rename(queryRunner, NEW, OLD)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 조치 표에 **일꾼이 쓸 네 칸**을 더한다.
|
|
3
|
+
*
|
|
4
|
+
* ── 왜 표에 두나 ────────────────────────────────────────────────────────────
|
|
5
|
+
* 물러섬을 메모리에 두면 재기동에 사라지고, 그러면 물러서던 것이 전부 즉시 재시도가 된다. 그리고
|
|
6
|
+
* 일꾼이 여러 대가 되면 같은 답을 봐야 한다.
|
|
7
|
+
*
|
|
8
|
+
* dispatch_note_next 저쪽 말 중 「다음에 할 일」 한 줄 — 원인은 dispatch_note
|
|
9
|
+
* retry 다시 해서 될 일인가 (again · after-fix · never)
|
|
10
|
+
* attempts 몇 번 넘겨 봤나 — 목록의 「몇 번 해 봤나」
|
|
11
|
+
* next_attempt_at 다음에 넘겨 볼 시각(ISO)
|
|
12
|
+
*
|
|
13
|
+
* ── 타입을 짐작하지 않는다 ──────────────────────────────────────────────────
|
|
14
|
+
* 엔티티가 `@Column({ nullable: true })` 로 두면 그것이 어느 컬럼 타입이 되는지는 **드라이버마다
|
|
15
|
+
* 다르다.** `'varchar'` 라고 적으면 sqlite·postgres·mysql 에서는 맞고 MSSQL·Oracle 에서 어긋난다.
|
|
16
|
+
*
|
|
17
|
+
* 그래서 같은 성질로 **이미 서 있는 형제 칸**의 타입을 읽어 그대로 쓴다 — 드라이버가 스스로 고른
|
|
18
|
+
* 답이라 다섯 곳에서 다 맞는다. 형제가 없으면 짐작하지 않고 멈춘다.
|
|
19
|
+
*
|
|
20
|
+
* 글자 셋 dispatch_note 에서
|
|
21
|
+
* 수 하나 effect_seen 에서
|
|
22
|
+
*
|
|
23
|
+
* plant 레인이 오늘 `equipmentClassIds` 마이그레이션에서 같은 방법을 썼다.
|
|
24
|
+
*
|
|
25
|
+
* ── 멱등 ────────────────────────────────────────────────────────────────────
|
|
26
|
+
* 칸이 이미 있으면 아무것도 하지 않는다 — 개발 sqlite 는 엔티티 선언만으로 이미 만들어 둔다.
|
|
27
|
+
* 표가 아직 없는 첫 기동에서도 할 일이 없다.
|
|
28
|
+
*/
|
|
29
|
+
import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm'
|
|
30
|
+
|
|
31
|
+
const TABLE = 'twin_command_records'
|
|
32
|
+
|
|
33
|
+
/** 글자 칸의 형제 — 같은 성질(선택·자유 문자열)로 이미 서 있다. */
|
|
34
|
+
const TEXT_SIBLING = 'dispatch_note'
|
|
35
|
+
/** 수 칸의 형제 — 선택·정수. */
|
|
36
|
+
const INT_SIBLING = 'effect_seen'
|
|
37
|
+
|
|
38
|
+
const TEXT_COLUMNS = ['dispatch_note_next', 'retry', 'next_attempt_at'] as const
|
|
39
|
+
const INT_COLUMNS = ['attempts'] as const
|
|
40
|
+
|
|
41
|
+
export class AddDispatchRetryColumns1786700000000 implements MigrationInterface {
|
|
42
|
+
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
43
|
+
const table = await queryRunner.getTable(TABLE)
|
|
44
|
+
if (!table) return /* 첫 기동 — 엔티티가 만든다 */
|
|
45
|
+
|
|
46
|
+
const typeOf = (sibling: string): string | undefined => table.columns.find(c => c.name === sibling)?.type
|
|
47
|
+
|
|
48
|
+
const textType = typeOf(TEXT_SIBLING)
|
|
49
|
+
const intType = typeOf(INT_SIBLING)
|
|
50
|
+
|
|
51
|
+
/*
|
|
52
|
+
* 형제가 없으면 **멈춘다.** 짐작한 타입으로 칸을 세우면 어느 드라이버에서 어긋나는지 개발에서
|
|
53
|
+
* 안 잡히고, 그 배포에서만 난다.
|
|
54
|
+
*/
|
|
55
|
+
if (!textType || !intType) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
`${TABLE} 에서 형제 칸을 못 찾았습니다 — ${TEXT_SIBLING}(${textType ?? '없음'}) · ` +
|
|
58
|
+
`${INT_SIBLING}(${intType ?? '없음'}). 타입을 짐작하지 않고 멈춥니다`
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
for (const name of TEXT_COLUMNS) {
|
|
63
|
+
if (table.columns.some(c => c.name === name)) continue
|
|
64
|
+
await queryRunner.addColumn(TABLE, new TableColumn({ name, type: textType, isNullable: true }))
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
for (const name of INT_COLUMNS) {
|
|
68
|
+
if (table.columns.some(c => c.name === name)) continue
|
|
69
|
+
await queryRunner.addColumn(TABLE, new TableColumn({ name, type: intType, isNullable: true }))
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
public async down(queryRunner: QueryRunner): Promise<void> {
|
|
74
|
+
const table = await queryRunner.getTable(TABLE)
|
|
75
|
+
if (!table) return
|
|
76
|
+
|
|
77
|
+
for (const name of [...TEXT_COLUMNS, ...INT_COLUMNS]) {
|
|
78
|
+
if (!table.columns.some(c => c.name === name)) continue
|
|
79
|
+
await queryRunner.dropColumn(TABLE, name)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -8,6 +8,8 @@ import { CarryLiveFeedCursor1786200000000 } from './1786200000000-CarryLiveFeedC
|
|
|
8
8
|
import { IndexEventTypeByTime1786300000000 } from './1786300000000-IndexEventTypeByTime.js'
|
|
9
9
|
import { IndexEventCreatedAt1786400000000 } from './1786400000000-IndexEventCreatedAt.js'
|
|
10
10
|
import { CreateTwinSubjectEvents1786500000000 } from './1786500000000-CreateTwinSubjectEvents.js'
|
|
11
|
+
import { AddDispatchRetryColumns1786700000000 } from './1786700000000-AddDispatchRetryColumns.js'
|
|
12
|
+
import { RenameOperatoMesAdapterType1786600000000 } from './1786600000000-RenameOperatoMesAdapterType.js'
|
|
11
13
|
|
|
12
14
|
export const migrations = [
|
|
13
15
|
RenameTwinInstanceBoardToModel1786000000000,
|
|
@@ -15,5 +17,7 @@ export const migrations = [
|
|
|
15
17
|
CarryLiveFeedCursor1786200000000,
|
|
16
18
|
IndexEventTypeByTime1786300000000,
|
|
17
19
|
IndexEventCreatedAt1786400000000,
|
|
18
|
-
CreateTwinSubjectEvents1786500000000
|
|
20
|
+
CreateTwinSubjectEvents1786500000000,
|
|
21
|
+
RenameOperatoMesAdapterType1786600000000,
|
|
22
|
+
AddDispatchRetryColumns1786700000000
|
|
19
23
|
]
|
package/server/routes.ts
CHANGED
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
* 대신 **연결마다 비밀값**을 두고 그것으로 확인한다. 비밀값을 선언하지 않은 연결은 훅을 받지 않는다 —
|
|
16
16
|
* 인증 없는 입구를 열어 두는 것보다 훅을 못 쓰는 것이 낫다.
|
|
17
17
|
*/
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
18
|
+
import { liveIngest } from './service/reference/live-ingest.js'
|
|
19
|
+
import { requestFill } from './service/reference/fill-loop.js'
|
|
20
20
|
import { handleHook } from './service/reference/reference-hook.js'
|
|
21
21
|
import { databaseStore } from './service/reference/hook-store.js'
|
|
22
22
|
|
|
@@ -50,28 +50,16 @@ process.on('bootstrap-module-domain-public-route' as any, (_app: any, router: an
|
|
|
50
50
|
* 폴링이 쓰는 길을 그대로 쓴다 — 검증도 유입도 같은 자리다. 그래서 트윈 쪽에서는 물어서 받은
|
|
51
51
|
* 것과 밀어 받은 것이 구별되지 않는다. 구별되는 것은 유입 장부의 `sources` 한 줄뿐이다.
|
|
52
52
|
*/
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
/*
|
|
64
|
-
* 커널이 받지 못한 수를 이름으로 남긴다 — 트윈이 멈춘 사이 버려진 레코드를 「통과」로 세면
|
|
65
|
-
* 화면이 100% 라고 말한다. 중복으로 걸러진 것은 버려진 것이 아니므로 빼고 센다.
|
|
66
|
-
*/
|
|
67
|
-
const undelivered = Math.max(0, accepted.length - applied - duplicates)
|
|
68
|
-
TwinEngine.recordIngestResult(domain.id, instanceId, records.length, rejected, Date.now(), undelivered)
|
|
69
|
-
/*
|
|
70
|
-
* 떨어진 것을 **수가 아니라 목록으로** 넘긴다 — 응답에 실어야 보내는 쪽이 그것만 따로 둘 수
|
|
71
|
-
* 있다. 수만 주면 배치 전체를 다시 보내거나 전부 버리는 두 갈래밖에 없다.
|
|
72
|
-
*/
|
|
73
|
-
return { applied, duplicates, rejected }
|
|
74
|
-
}
|
|
53
|
+
/*
|
|
54
|
+
* 유입 몸통은 `live-ingest.ts` 한 곳에 있다 — backfill 이 같은 것을 쓴다. 여기에 두면 두 길이
|
|
55
|
+
* 서로 다른 규칙으로 받게 되고, 한쪽만 수정되는 날이 온다.
|
|
56
|
+
*/
|
|
57
|
+
ingest: (instanceId, records) => liveIngest(domain.id, instanceId, records, 'hook'),
|
|
58
|
+
/*
|
|
59
|
+
* 구멍을 본 그 자리에서 바로 받는다 — 번호가 비었다는 것을 아는 가장 이른 순간이다.
|
|
60
|
+
* 기다리지 않는다(§`requestFill`): 훅의 응답이 늦으면 보내는 쪽이 되풀이한다.
|
|
61
|
+
*/
|
|
62
|
+
onGap: scope => requestFill(domain.id, String(ctx.params.instanceId ?? ''), scope)
|
|
75
63
|
})
|
|
76
64
|
ctx.status = outcome.status
|
|
77
65
|
ctx.body = outcome.body
|