@things-factory/headless-twin 10.0.18 → 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 +19 -16
- 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 +230 -0
- package/dist-server/service/actuation/command-dispatcher.js +269 -0
- package/dist-server/service/actuation/command-dispatcher.js.map +1 -0
- package/dist-server/service/actuation/command-store.d.ts +11 -0
- package/dist-server/service/actuation/command-store.js +219 -0
- package/dist-server/service/actuation/command-store.js.map +1 -0
- 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 +25 -0
- package/dist-server/service/actuation/index.js +30 -0
- package/dist-server/service/actuation/index.js.map +1 -0
- 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 +111 -0
- package/dist-server/service/actuation/twin-command.js +170 -0
- package/dist-server/service/actuation/twin-command.js.map +1 -0
- 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 +28 -17
- 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 +149 -22
- package/dist-server/service/reference/hook-contract.js +236 -23
- 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/hook-store.d.ts +3 -0
- package/dist-server/service/reference/hook-store.js +28 -0
- package/dist-server/service/reference/hook-store.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 +355 -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 +64 -6
- package/dist-server/service/reference/reference-hook.js +265 -27
- 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 +19 -20
- 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 +511 -0
- package/server/service/actuation/command-store.ts +236 -0
- 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 +27 -0
- 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 +232 -0
- package/server/service/actuation/twin-now.ts +97 -0
- package/server/service/index.ts +11 -0
- 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 +280 -22
- package/server/service/reference/hook-rejected.ts +53 -0
- package/server/service/reference/hook-store.ts +28 -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 +392 -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 +355 -32
- 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 +209 -0
- 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 +333 -0
- 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 +114 -23
- 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
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* **레코드가 말하는 「언제 일어났나」를 읽는다** — fact 종류마다 칸 이름이 다르다.
|
|
3
|
+
*
|
|
4
|
+
* ── 같은 defect 가 세 번째다 ────────────────────────────────────────────────
|
|
5
|
+
* `canonical-ingest.ts` 가 `eventTime ?? at` **두 이름만** 보고 있었다. 그 주석에 이미 두 번의 사고가
|
|
6
|
+
* 적혀 있다 — 처음엔 `eventTime` 하나만 봐서 energy connector 의 사실 18,150건이 polling 시각으로
|
|
7
|
+
* 찍혔고, 그 다음에 `at` 을 더했다.
|
|
8
|
+
*
|
|
9
|
+
* 세 번째는 `nonconformance.disposition` 이었다.
|
|
10
|
+
*
|
|
11
|
+
* ```
|
|
12
|
+
* [측정함 2026-09-06, mes-line-a backfill 뒤]
|
|
13
|
+
* event_time 2026-09-05 23:50:51 payload decidedAt 2026-09-01T02:50:51.832Z 5일 차이
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* ── root cause: 선언과 봉투가 다른 이름을 쓴다 ──────────────────────────────
|
|
17
|
+
* 운영 레코드의 봉투 시각은 `at` 이다(`OperationalRecordEnvelopeFields`). 그런데 처분 fact 는 자기
|
|
18
|
+
* 시각을 **payload 의 `decidedAt`** 에 싣는다 — contract 가 그 자리에 required 로 선언했고 plant 도
|
|
19
|
+
* 맞게 보낸다. 읽는 쪽에 그 이름이 없어서 `defaultEventTime`(= ingest 시각)으로 떨어졌다.
|
|
20
|
+
*
|
|
21
|
+
* ── push 경로에서는 안 보인다 ───────────────────────────────────────────────
|
|
22
|
+
* 사실이 나자마자 보내면 ingest 시각과 event 시각이 몇 초 차이다. backfill 이 그 간격을 벌려서
|
|
23
|
+
* 드러냈다 — **backfill 이 만든 defect 가 아니라 원래 있던 것이 보인 것**이다.
|
|
24
|
+
*
|
|
25
|
+
* ── 이름을 늘리는 것으로는 안 끝난다 ────────────────────────────────────────
|
|
26
|
+
* 다음 fact 종류가 또 다른 이름을 쓰면 같은 자리를 밟는다. 그래서 두 가지를 함께 한다.
|
|
27
|
+
*
|
|
28
|
+
* ```
|
|
29
|
+
* 1 종류마다의 칸 이름을 한 표에 둔다 새 종류를 더할 때 여기가 눈에 띈다
|
|
30
|
+
* 2 못 읽으면 그 수를 낸다 조용히 ingest 시각으로 떨어지지 않는다
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* 종류 판정은 **contract 의 것을 부른다**(`operationalKindOf`). 여기서 다시 짐작하면 두 곳이 다른
|
|
34
|
+
* 답을 내고, 그때 어느 쪽이 맞는지 아무도 모른다.
|
|
35
|
+
*/
|
|
36
|
+
import { operationalKindOf } from '@operato/ops-contract'
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 운영 fact 종류가 자기 시각을 싣는 칸 — 봉투의 `at` 이 없을 때 읽는다.
|
|
40
|
+
*
|
|
41
|
+
* 새 종류가 자기 시각을 payload 에 실으면 여기 한 줄을 더한다.
|
|
42
|
+
*/
|
|
43
|
+
export const KIND_TIME_FIELD: Record<string, string> = {
|
|
44
|
+
/* `DispositionFact.decidedAt` — contract 가 required 로 선언했다. */
|
|
45
|
+
disposition: 'decidedAt'
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/*
|
|
49
|
+
* `equipment-period` 는 일부러 안 넣는다. 그 fact 의 봉투 시각은 **구간의 끝**이고(그때 성립한다),
|
|
50
|
+
* `recordTime` 은 「그 구간을 언제 적었나」다. 둘이 갈려 있어야 늦게 적은 구간이 늦게 일어난 것으로
|
|
51
|
+
* 읽히지 않는다 — contract 가 그 판단을 이미 적어 두었다.
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
function iso(v: unknown): string | undefined {
|
|
55
|
+
return typeof v === 'string' && v.trim() ? v : undefined
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 이 레코드가 말하는 시각. **못 읽으면 `undefined`** — 여기서 지금 시각으로 채우지 않는다.
|
|
60
|
+
*
|
|
61
|
+
* 보는 순서는 공통 이름이 먼저다. connector 가 봉투 층에서 시각을 실어 주면 그것이 그 레코드에 대한
|
|
62
|
+
* 가장 바깥의 판단이고, 종류마다의 칸은 그것이 없을 때 읽는다.
|
|
63
|
+
*/
|
|
64
|
+
export function factEventTime(record: unknown): string | undefined {
|
|
65
|
+
const r = record as any
|
|
66
|
+
const common = iso(r?.eventTime) ?? iso(r?.at)
|
|
67
|
+
if (common) return common
|
|
68
|
+
|
|
69
|
+
const kind = operationalKindOf(record)
|
|
70
|
+
const named = kind ? KIND_TIME_FIELD[kind] : undefined
|
|
71
|
+
return named ? iso(r?.[named]) : undefined
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 시각을 못 읽은 레코드를 종류별로 센다 — **조용히 ingest 시각으로 떨어진 것들.**
|
|
76
|
+
*
|
|
77
|
+
* 오류가 아니다. 절대값 snapshot 관측은 원본의 갱신 시각이 아니라 polling 순간이 맞는 시각이라
|
|
78
|
+
* (「어제 값을 오늘 봤다」는 「어제 일어났다」가 아니다) 여기 세어지는 것이 정상이다.
|
|
79
|
+
*
|
|
80
|
+
* 다만 그 수가 어디에도 안 남으면 `event_time == created_at` 이 유일한 단서가 되고, 그것은 우연히
|
|
81
|
+
* 같은 것과 구별되지 않는다.
|
|
82
|
+
*/
|
|
83
|
+
export function undatedByKind(records: readonly unknown[]): Record<string, number> {
|
|
84
|
+
const out: Record<string, number> = {}
|
|
85
|
+
for (const r of records) {
|
|
86
|
+
if (factEventTime(r) !== undefined) continue
|
|
87
|
+
const kind = operationalKindOf(r) ?? ((r as any)?.type as string) ?? '(종류 모름)'
|
|
88
|
+
out[kind] = (out[kind] ?? 0) + 1
|
|
89
|
+
}
|
|
90
|
+
return out
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** 표에 있는 종류인데 시각을 못 읽었다 — 그것은 실제로 결함이다. */
|
|
94
|
+
export function shouldHaveTime(kind: string): boolean {
|
|
95
|
+
return kind in KIND_TIME_FIELD
|
|
96
|
+
}
|
package/server/engine/index.ts
CHANGED
|
@@ -29,3 +29,23 @@ export * from './operation-basis.js'
|
|
|
29
29
|
export * from './measured-yield.js'
|
|
30
30
|
export * from './declared-stimulus.js'
|
|
31
31
|
export * from './integration-coverage.js'
|
|
32
|
+
|
|
33
|
+
/*
|
|
34
|
+
* 사실의 정체(identity)와 시각을 판단하는 함수들.
|
|
35
|
+
*
|
|
36
|
+
* 모듈 전체를 export 하지 않고 이 둘만 내보낸다. `FactDeduper` 같은 상태를 가진 클래스는 커널
|
|
37
|
+
* 안에서만 쓰여야 하고, 밖에서 인스턴스를 따로 만들면 중복 판단 기억이 두 벌이 된다.
|
|
38
|
+
*
|
|
39
|
+
* 밖에서 이 둘이 필요한 이유는 하나다 — 저널에 앉은 사실이 제대로 된 정체와 시각을 가졌는지
|
|
40
|
+
* 확인할 때, 유입할 때와 **똑같은 함수로** 판단해야 하기 때문이다. 확인하는 쪽이 규칙을 다시
|
|
41
|
+
* 구현하면 두 규칙이 어긋나는 날 확인이 조용히 통과한다.
|
|
42
|
+
*/
|
|
43
|
+
export { factIdentity } from './ingest-dedupe.js'
|
|
44
|
+
|
|
45
|
+
/*
|
|
46
|
+
* `shouldHaveTime` is the part that matters outside: it separates "this record legitimately has no
|
|
47
|
+
* time of its own" (a snapshot observation — the polling moment IS the right time) from "the kind
|
|
48
|
+
* declares a time field and we could not read it", which is a real defect. Anything checking the
|
|
49
|
+
* journal must use this split rather than inventing its own, or it reports healthy rows as broken.
|
|
50
|
+
*/
|
|
51
|
+
export { factEventTime, shouldHaveTime, undatedByKind } from './event-time.js'
|
|
@@ -45,34 +45,162 @@ function timeOf(envelope: any): number | undefined {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
/**
|
|
48
|
-
* 이 봉투가 말하는
|
|
48
|
+
* 이 봉투가 말하는 것을 **가르는 값들** — 하나라도 다르면 다른 사실이다.
|
|
49
49
|
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
50
|
+
* ── 예전에는 하나만 골랐다 (2026-09-05 실측) ───────────────────────────────
|
|
51
|
+
* 아는 이름을 순서대로 보고 **먼저 걸리는 하나**를 대상으로 삼았다. 「주체를 먼저 본다」는 판단
|
|
52
|
+
* 자체는 옳다 — 설비 사실은 「지금 붙어 있는 작업」을 참조로 싣는다. 그런데 **고르고 나머지를
|
|
53
|
+
* 버린 것**이 결함이었다.
|
|
54
|
+
*
|
|
55
|
+
* 실제로 난 일: plant 이 실적 둘을 보냈는데 한 건이 저널에 안 남았다.
|
|
56
|
+
*
|
|
57
|
+
* seq 36 quality.output moverId EQ-CT-MWELD-01 · taskId PR-CT-WIRE-01 · goodCount 12
|
|
58
|
+
* seq 41 quality.output moverId EQ-CT-MWELD-01 · taskId PR-CT-WIRE-02 · goodCount 9
|
|
59
|
+
*
|
|
60
|
+
* 두 실적의 시각이 같았고(원본이 끝시각을 복사) `moverId` 가 `taskId` 보다 먼저라 **둘이 한 사실이
|
|
61
|
+
* 됐다.** 거부가 아니라 되풀이로 세어졌다 — `offered 5 · rejected 0 · journaled 3`.
|
|
62
|
+
*
|
|
63
|
+
* **구별할 값이 이미 봉투 안에 있었는데 판정이 그것을 안 봤다.**
|
|
64
|
+
*
|
|
65
|
+
* ── 그리고 EPCIS 도 같았다 ─────────────────────────────────────────────────
|
|
66
|
+
* 저널 246만 건에 현재 규칙을 다시 걸어 보니 겹치는 묶음이 여섯이었고, 전부 이 모양이다.
|
|
67
|
+
*
|
|
68
|
+
* 같은 EPC · 같은 밀리초
|
|
69
|
+
* ① bizStep receiving action ADD disposition sellable_accessible
|
|
70
|
+
* ② bizStep commissioning action OBSERVE disposition reserved
|
|
71
|
+
*
|
|
72
|
+
* **한 개체의 서로 다른 관측**이다. EPCIS 에서 이상한 일이 아닌데 규칙이 그 셋을 안 봤다.
|
|
73
|
+
*
|
|
74
|
+
* ── 왜 넓히는 쪽이 맞나 ────────────────────────────────────────────────────
|
|
75
|
+
* 이 파일이 스스로 적어 둔 값이 그것이다 — 「알리지 않고 잃는 것보다 두 번 세는 것이 낫다」.
|
|
76
|
+
* 지금 거동은 **알리지 않고 잃는 쪽**이었다.
|
|
77
|
+
*
|
|
78
|
+
* 넓혀도 되풀이는 그대로 걸린다: 같은 봉투를 다시 보내면 **이 값들이 전부 같다.** 넓히는 것은
|
|
79
|
+
* 「다른 사실을 합치지 않는다」만 강해지고 「되풀이를 잡는다」는 안 약해진다.
|
|
52
80
|
*/
|
|
53
|
-
function
|
|
81
|
+
function discriminatorsOf(envelope: any): string[] {
|
|
54
82
|
const d = envelope?.data ?? {}
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
60
|
-
return undefined
|
|
83
|
+
const out: string[] = []
|
|
84
|
+
const put = (name: string, v: unknown) => {
|
|
85
|
+
const t = String(v ?? '').trim()
|
|
86
|
+
if (t) out.push(`${name}=${t}`)
|
|
61
87
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
88
|
+
|
|
89
|
+
/*
|
|
90
|
+
* 주체·참조를 **다 적는다.** 순서는 값이 아니라 이름으로 고정되므로 봉투마다 흔들리지 않는다.
|
|
91
|
+
*
|
|
92
|
+
* ⚠ **이 목록은 계약의 `identity` 선언을 손으로 옮긴 것이다.** 계약이 kind 마다 무엇이 그 사실을
|
|
93
|
+
* 가리키는지 이미 선언하는데(`operational-ingest.ts` 의 `SPECS[kind].identity`), 그것을 내보내지
|
|
94
|
+
* 않아서 여기가 따로 들고 있다.
|
|
95
|
+
*
|
|
96
|
+
* 그래서 새 kind 가 생기면 여기가 조용히 뒤처진다. 실제로 두 번 그랬다 — `subjectId`(처분)와
|
|
97
|
+
* `testableObjectId`(시험)가 빠져 있었고, 그동안 그 두 종류는 정체를 못 만들거나 서로 다른 사실이
|
|
98
|
+
* 뭉칠 수 있었다.
|
|
99
|
+
*
|
|
100
|
+
* 계약이 `operationalIdentityField(kind)` 를 내보내면 이 목록을 지운다. 그 문을 열어 두었고
|
|
101
|
+
* 배포를 기다린다.
|
|
102
|
+
*/
|
|
103
|
+
put('meter', d.meterId)
|
|
104
|
+
put('equipment', d.equipmentId)
|
|
105
|
+
put('mover', d.moverId) // vocabulary-guard: allow — 봉투의 옛 세대 필드 이름(저장된 계약)
|
|
106
|
+
put('task', d.taskId)
|
|
107
|
+
put('order', d.orderId)
|
|
108
|
+
put('person', d.personId)
|
|
109
|
+
put('asset', d.assetId)
|
|
110
|
+
put('location', d.locationId) // vocabulary-guard: allow — 같음
|
|
111
|
+
put('epc', d.epcList?.[0])
|
|
112
|
+
put('parent', d.parentID)
|
|
113
|
+
put('class', d.quantityList?.[0]?.epcClass)
|
|
114
|
+
put('transformation', d.transformationID)
|
|
115
|
+
/*
|
|
116
|
+
* ── 주체가 `subjectId` 인 사실이 있다 (2026-09-06 측정) ──────────────────────
|
|
117
|
+
* 부적합 처분과 시험 결과는 **표준 `TestableObjectID` 와 같은 자리**를 쓴다(로트 · 설비 · 자리에
|
|
118
|
+
* 두루 쓰는 하나의 이름). 그 이름이 여기 없어서 두 종류가 **정체를 못 만들었다** —
|
|
119
|
+
* `parts` 가 비면 `factIdentity` 가 `undefined` 를 내고, 그러면 dedupe 밖이다.
|
|
120
|
+
*
|
|
121
|
+
* 실제로 그랬다. `fillTwinScope` 를 두 번 돌릴 때마다 그 둘이 7건씩 다시 앉았다(처분 4 · 시험 3).
|
|
122
|
+
* 나머지 34건은 걸러졌는데 이 둘만 계속 늘었다.
|
|
123
|
+
*/
|
|
124
|
+
put('subject', d.subjectId)
|
|
125
|
+
put('spec', d.specId)
|
|
126
|
+
/*
|
|
127
|
+
* 시험 결과의 주체는 `testableObjectId` 다 — 표준 `TestableObjectID` 의 이름을 그대로 쓴다.
|
|
128
|
+
*
|
|
129
|
+
* 이 이름이 없어서 **같은 명세로 같은 시각에 잰 서로 다른 로트가 한 사실로 뭉쳤다.** 중복이 생기는
|
|
130
|
+
* 것보다 나쁘다 — 두 로트의 시험 결과 중 하나가 사라진다.
|
|
131
|
+
*
|
|
132
|
+
* 실제로 `mes-miratek` 에 같은 시험 결과가 10건 있었다(2026-09-06 확인). 그것은 뭉친 것이 아니라
|
|
133
|
+
* 반대로 dedupe 를 못 지난 것인데, 같은 자리에서 뭉치는 쪽도 열려 있었다.
|
|
134
|
+
*/
|
|
135
|
+
put('object', d.testableObjectId)
|
|
136
|
+
/* 완료 선언의 주체 — 「이 축은 여기까지가 전부다」를 말하는 축 이름. */
|
|
137
|
+
put('axis', d.completeAxis)
|
|
138
|
+
|
|
139
|
+
/*
|
|
140
|
+
* EPCIS 는 「무엇을 말하는 관측인가」가 이 셋으로 갈린다. 한 개체가 한 순간에 두 관측을 갖는
|
|
141
|
+
* 것은 표준에서 정상이고, 이것을 안 보면 뒤엣것이 사라진다.
|
|
142
|
+
*/
|
|
143
|
+
put('bizStep', d.bizStep)
|
|
144
|
+
put('action', d.action)
|
|
145
|
+
put('disposition', d.disposition)
|
|
146
|
+
|
|
147
|
+
/*
|
|
148
|
+
* ── 변환은 **무엇이 무엇으로 되었나**로 갈린다 (2026-09-07 측정) ─────────────
|
|
149
|
+
* 이 자리가 없어서 **사실 하나가 없어졌습니다.** 거절이 아니라 되풀이로 세어져서 경고도 없었습니다.
|
|
150
|
+
*
|
|
151
|
+
* ```
|
|
152
|
+
* plant outbox seq 37 in LOT-CT-SIDE-001 24개 → out LOT-CT-MAIN-003 12개
|
|
153
|
+
* plant outbox seq 42 in LOT-CT-SIDE-001 18개 → out LOT-CT-MAIN-004 9개
|
|
154
|
+
* transformationID · bizStep · 초까지 전부 같다
|
|
155
|
+
* 저널에 남은 것 LOT-CT-MAIN-003 하나
|
|
156
|
+
* ```
|
|
157
|
+
*
|
|
158
|
+
* `LOT-CT-MAIN-004` 를 어느 패널로 만들었는지 트윈이 답하지 못하는 상태였습니다.
|
|
159
|
+
*
|
|
160
|
+
* 그리고 **구별할 값이 봉투 안에 이미 있었습니다** — `outputQuantityList[0].epcClass` 가 서로
|
|
161
|
+
* 다릅니다. 이 파일 머리말이 적어 둔 그 모양 그대로입니다: 판정이 그것을 안 봤습니다.
|
|
162
|
+
*
|
|
163
|
+
* 산출만 보지 않고 **입력도 봅니다.** 같은 로트를 같은 초에 두 번 소비하면서 산출 이름이 같은
|
|
164
|
+
* 경우(수량만 다른 분할)가 남고, 그때 입력 수량이 유일한 차이입니다. 수량까지 넣는 이유가 그것입니다.
|
|
165
|
+
*/
|
|
166
|
+
put('inClass', classOf(d.inputQuantityList))
|
|
167
|
+
put('inEpc', d.inputEPCList?.[0])
|
|
168
|
+
put('inQty', qtyOf(d.inputQuantityList))
|
|
169
|
+
put('outClass', classOf(d.outputQuantityList))
|
|
170
|
+
put('outEpc', d.outputEPCList?.[0])
|
|
171
|
+
put('outQty', qtyOf(d.outputQuantityList))
|
|
172
|
+
/*
|
|
173
|
+
* 담김의 자식도 같은 이유로 본다 — 한 팔레트에 서로 다른 묶음을 같은 초에 두 번 실으면 뒤엣것이
|
|
174
|
+
* 사라진다. 위 변환에서 실제로 그렇게 잃었으니 여기도 함께 막는다(한 종류만 고치면 나머지에 남는다).
|
|
175
|
+
*/
|
|
176
|
+
put('childEpc', d.childEPCs?.[0])
|
|
177
|
+
put('childClass', classOf(d.childQuantityList))
|
|
178
|
+
put('childQty', qtyOf(d.childQuantityList))
|
|
179
|
+
/*
|
|
180
|
+
* 어느 오더의 사실인가도 정체의 일부다. 같은 공정을 같은 초에 두 오더로 돌리는 것은 정상이고,
|
|
181
|
+
* 이것을 안 보면 뒤엣것이 사라진다.
|
|
182
|
+
*/
|
|
183
|
+
put('tx', d.bizTransactionList?.[0]?.bizTransaction)
|
|
184
|
+
|
|
185
|
+
return out
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** 수량 목록의 첫 종류 — 목록이 비면 `undefined`(빈 문자열을 넣지 않는다). */
|
|
189
|
+
function classOf(list: unknown): string | undefined {
|
|
190
|
+
const first = Array.isArray(list) ? (list[0] as any) : undefined
|
|
191
|
+
const v = first?.epcClass
|
|
192
|
+
return typeof v === 'string' && v.trim() ? v : undefined
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* 수량 목록을 수까지 적는다 — 종류가 같고 수량만 다른 사실을 가른다.
|
|
197
|
+
*
|
|
198
|
+
* 목록 전체를 적는다. 첫 원소만 보면 「같은 것 둘 + 다른 것 하나」가 첫 원소로는 구별되지 않는다.
|
|
199
|
+
*/
|
|
200
|
+
function qtyOf(list: unknown): string | undefined {
|
|
201
|
+
if (!Array.isArray(list) || !list.length) return undefined
|
|
202
|
+
const parts = list.map((x: any) => `${x?.epcClass ?? ''}:${x?.quantity ?? ''}${x?.uom ? '/' + x.uom : ''}`)
|
|
203
|
+
return parts.join('|')
|
|
76
204
|
}
|
|
77
205
|
|
|
78
206
|
/**
|
|
@@ -81,14 +209,41 @@ function targetOf(envelope: any): string | undefined {
|
|
|
81
209
|
* 대상이나 시각을 알 수 없으면 **정체를 만들지 않는다**(`undefined`). 그때는 거르지 않고 통과시킨다 —
|
|
82
210
|
* 모르는 것을 같다고 판단하면 서로 다른 사실이 하나로 합쳐진다. 알리지 않고 잃는 것보다 두 번 세는 것이 낫다.
|
|
83
211
|
*/
|
|
212
|
+
/**
|
|
213
|
+
* 정체를 못 만든 사실을 **종류별로** 센다 — dedupe 밖으로 나간 것들.
|
|
214
|
+
*
|
|
215
|
+
* 이 수가 0이 아니면 그 종류는 다시 받을 때마다 저널에 다시 앉는다. 그 종류의 주체 이름을
|
|
216
|
+
* `discriminatorsOf` 에 더해야 한다.
|
|
217
|
+
*/
|
|
218
|
+
const unidentified: Record<string, number> = {}
|
|
219
|
+
|
|
220
|
+
/** 지금까지 정체를 못 만든 것 — 종류별. 읽고 비운다. */
|
|
221
|
+
export function takeUnidentified(): Record<string, number> {
|
|
222
|
+
const out = { ...unidentified }
|
|
223
|
+
for (const k of Object.keys(unidentified)) delete unidentified[k]
|
|
224
|
+
return out
|
|
225
|
+
}
|
|
226
|
+
|
|
84
227
|
export function factIdentity(envelope: any): string | undefined {
|
|
85
228
|
const type = String(envelope?.eventType ?? '').trim()
|
|
86
229
|
if (!type) return undefined
|
|
87
|
-
const
|
|
88
|
-
|
|
230
|
+
const parts = discriminatorsOf(envelope)
|
|
231
|
+
/*
|
|
232
|
+
* **정체를 못 만들면 dedupe 밖이다** — 그 사실은 다시 받을 때마다 새로 앉는다.
|
|
233
|
+
*
|
|
234
|
+
* 오늘 그것으로 저널이 부풀었다(처분·시험이 `subjectId` 를 주체로 쓰는데 그 이름이 위 목록에
|
|
235
|
+
* 없었다). 조용히 `undefined` 를 내면 다음 종류에서 또 나고 그때도 아무 흔적이 없다.
|
|
236
|
+
*
|
|
237
|
+
* 그래서 무엇이 정체를 못 만들었는지 센다(`unidentified`). 세는 것이지 막는 것이 아니다 —
|
|
238
|
+
* 정체를 못 만든다고 사실을 버리면 그것이 더 나쁘다.
|
|
239
|
+
*/
|
|
240
|
+
if (!parts.length) {
|
|
241
|
+
unidentified[type] = (unidentified[type] ?? 0) + 1
|
|
242
|
+
return undefined
|
|
243
|
+
}
|
|
89
244
|
const at = timeOf(envelope)
|
|
90
245
|
if (at === undefined) return undefined
|
|
91
|
-
return `${type}|${
|
|
246
|
+
return `${type}|${parts.join('&')}|${at}`
|
|
92
247
|
}
|
|
93
248
|
|
|
94
249
|
/**
|
|
@@ -109,6 +264,27 @@ export class FactDeduper {
|
|
|
109
264
|
private seen = new Set<string>()
|
|
110
265
|
private order: string[] = []
|
|
111
266
|
|
|
267
|
+
/**
|
|
268
|
+
* 이미 아는 것으로 **미리 기억한다** — 저널에서 읽어 채울 때.
|
|
269
|
+
*
|
|
270
|
+
* ── 왜 필요한가 (2026-09-06 측정) ─────────────────────────────────────────
|
|
271
|
+
* 이 기억은 프로세스 안에만 있다. 재기동하면 비워지고, 그 뒤 backfill 을 돌리면 **이미 저널에
|
|
272
|
+
* 있는 사실도 「처음 본 것」**이 된다.
|
|
273
|
+
*
|
|
274
|
+
* 실제로 그랬다. `mes-line-a` 를 재기동 뒤 backfill 하니 39건이 전부 다시 앉아 저널이
|
|
275
|
+
* 44 → 83건이 되고 44가지가 중복이 됐다.
|
|
276
|
+
*
|
|
277
|
+
* push 경로에서는 안 보인다. 재전송이 몇 분 안에 오므로 창 안에서 걸린다. **backfill 이 그 전제를
|
|
278
|
+
* 깬다** — 몇 달 전 사실을 지금 받으니 재기동 한 번이면 그 구간이 통째로 다시 앉는다.
|
|
279
|
+
*
|
|
280
|
+
* 그래서 backfill 이 부르기 전에 그 구간의 정체를 저널에서 읽어 여기 넣는다.
|
|
281
|
+
*/
|
|
282
|
+
remember(identities: readonly string[]): number {
|
|
283
|
+
let added = 0
|
|
284
|
+
for (const id of identities) if (id && !this.seen.has(id)) { this.known(id); added++ }
|
|
285
|
+
return added
|
|
286
|
+
}
|
|
287
|
+
|
|
112
288
|
/** 이 사실을 본 적이 있나 — 없으면 기억하고 `false`. */
|
|
113
289
|
private known(identity: string): boolean {
|
|
114
290
|
if (this.seen.has(identity)) return true
|
|
@@ -63,6 +63,8 @@ export interface IngestWindow {
|
|
|
63
63
|
* 세면 그 개선도, 되돌아가는 것도 보이지 않는다.
|
|
64
64
|
*/
|
|
65
65
|
flushes: number
|
|
66
|
+
/** 이 창에서 저널에 안 간 어휘 요소의 수 — 사라진 것이 아니라 저널로 가지 않는 것이다. */
|
|
67
|
+
vocabulary: number
|
|
66
68
|
}
|
|
67
69
|
|
|
68
70
|
/** 거부 표본 하나 — 매핑을 고치려면 실제 모양을 봐야 한다. */
|
|
@@ -85,6 +87,17 @@ export interface IngestLedger {
|
|
|
85
87
|
/** 누적 제시·거부·미전달. */
|
|
86
88
|
offered: number
|
|
87
89
|
rejected: number
|
|
90
|
+
/**
|
|
91
|
+
* **저널에 안 가는 어휘 요소의 수** — 로트 번호·소비기한처럼 시각축이 없는 값.
|
|
92
|
+
*
|
|
93
|
+
* ── 왜 세나 (2026-09-05, 인티그레이션 레인이 셈으로 찾음) ─────────────────
|
|
94
|
+
* 어휘는 일부러 저널로 안 간다(§`canonical-ingest`) — 그것이 옳다. 그런데 **그 수를 빼 주는
|
|
95
|
+
* 자리가 없어서** 「보낸 것 − 앉은 것」이 안 맞고, 그 차이를 「사라졌다」로 읽게 된다.
|
|
96
|
+
*
|
|
97
|
+
* 실제로 그랬다: 커서 20 · 저널 17 · 어휘 1 → 사람이 손으로 세서 「둘이 빈다」를 찾았다. 그
|
|
98
|
+
* 뺄셈을 코드가 하려면 이 항이 있어야 한다.
|
|
99
|
+
*/
|
|
100
|
+
vocabulary: number
|
|
88
101
|
undelivered: number
|
|
89
102
|
/** 누적 — 적힌 행과 두드린 횟수. */
|
|
90
103
|
rows: number
|
|
@@ -323,6 +336,21 @@ export function recordIngestSource(ledger: IngestLedger, source: string, count:
|
|
|
323
336
|
}
|
|
324
337
|
|
|
325
338
|
/** 같은 사실이 두 번 와서 버린 수 — 알리지 않고 버리지 않는다. */
|
|
339
|
+
/**
|
|
340
|
+
* **저널에 안 간 어휘 요소를 센다** — 없어진 것이 아니라 저널로 가지 않는 것이다.
|
|
341
|
+
*
|
|
342
|
+
* 어휘(로트 번호·소비기한 등)는 시각축이 없어 일부러 저널로 안 간다. 그런데 그 수를 빼 주는 자리가
|
|
343
|
+
* 없으면 「보낸 것 − 앉은 것」이 안 맞고, 그 차이를 **사라진 것으로 읽게 된다.**
|
|
344
|
+
*
|
|
345
|
+
* 창을 굴리지 않는다 — 부르는 쪽(`recordIngest`)이 이미 굴린 뒤에 부른다. 여기서 또 굴리면 한
|
|
346
|
+
* 유입이 두 창에 걸친다.
|
|
347
|
+
*/
|
|
348
|
+
export function recordVocabulary(ledger: IngestLedger, count: number): void {
|
|
349
|
+
if (!Number.isFinite(count) || count <= 0) return
|
|
350
|
+
ledger.vocabulary += count
|
|
351
|
+
ledger.window.vocabulary += count
|
|
352
|
+
}
|
|
353
|
+
|
|
326
354
|
export function recordDuplicates(ledger: IngestLedger, count: number, nowMs: number): void {
|
|
327
355
|
if (count <= 0) return
|
|
328
356
|
ledger.duplicates += count
|
|
@@ -398,7 +426,7 @@ export const HEALTHY_ACCEPTED_RATIO = 0.95
|
|
|
398
426
|
|
|
399
427
|
export function newIngestLedger(): IngestLedger {
|
|
400
428
|
return {
|
|
401
|
-
window: { startMs: -1, endMs: -1, offered: 0, rejected: 0, undelivered: 0, rows: 0, flushes: 0 },
|
|
429
|
+
window: { startMs: -1, endMs: -1, offered: 0, rejected: 0, vocabulary: 0, undelivered: 0, rows: 0, flushes: 0 },
|
|
402
430
|
trend: [],
|
|
403
431
|
trendDropped: 0,
|
|
404
432
|
offered: 0,
|
|
@@ -410,6 +438,7 @@ export function newIngestLedger(): IngestLedger {
|
|
|
410
438
|
lastUndeliveredAtMs: -1,
|
|
411
439
|
bySource: {},
|
|
412
440
|
duplicates: 0,
|
|
441
|
+
vocabulary: 0,
|
|
413
442
|
lastDuplicateAtMs: -1,
|
|
414
443
|
byReason: {},
|
|
415
444
|
reasonsCapped: false,
|
|
@@ -447,7 +476,7 @@ export function rollIngestWindow(
|
|
|
447
476
|
|
|
448
477
|
const closed: IngestWindow = { ...ledger.window, endMs: nowMs }
|
|
449
478
|
ledger.trend.push(closed)
|
|
450
|
-
ledger.window = { startMs: nowMs, endMs: -1, offered: 0, rejected: 0, undelivered: 0, rows: 0, flushes: 0 }
|
|
479
|
+
ledger.window = { startMs: nowMs, endMs: -1, offered: 0, rejected: 0, vocabulary: 0, undelivered: 0, rows: 0, flushes: 0 }
|
|
451
480
|
|
|
452
481
|
if (ledger.trend.length > TREND_LIMIT) {
|
|
453
482
|
const drop = ledger.trend.length - TREND_LIMIT
|
|
@@ -614,6 +643,21 @@ export type SyncVerdict =
|
|
|
614
643
|
| 'no-traffic' /** 피드는 붙었는데 제시가 0 — 원천·망을 본다 */
|
|
615
644
|
| 'not-connected' /** 피드 자체가 없다 — 결선한다 */
|
|
616
645
|
| 'not-applicable' /** 원본이 먹이지 않는 트윈(시뮬)·멈춘 트윈 — 물을 대상이 아니다 */
|
|
646
|
+
/**
|
|
647
|
+
* **지금은 조용한 것이 정상이다** — 원천이 지금 시각에 내놓지 않는다.
|
|
648
|
+
*
|
|
649
|
+
* ── 왜 초록도 빨강도 아닌가 (2026-09-04) ─────────────────────────────────
|
|
650
|
+
* 태양광은 밤에 발전하지 않고, 업무시간만 도는 원천은 저녁에 아무것도 내지 않는다. 그 침묵을
|
|
651
|
+
* `no-traffic` 으로 부르면 **정상인 밤마다 절반이 빨갛게 난다.** 거짓 빨강이 쌓이면 진짜 빨강도
|
|
652
|
+
* 같이 안 읽힌다 — 오늘 UX 레인이 「늘 참인 경고는 벽지가 된다」고 한 것과 같은 부류다.
|
|
653
|
+
*
|
|
654
|
+
* 그렇다고 `syncing` 도 아니다. 지금 받고 있는 것이 아니고, 초록으로 칠하면 **밤에 정말 끊긴
|
|
655
|
+
* 원천**을 아침까지 아무도 모른다.
|
|
656
|
+
*
|
|
657
|
+
* 그래서 셋째 값이다: 「지금은 안 오는 것이 맞다」. 화면은 이것을 회색으로 그리고, 다음에 올
|
|
658
|
+
* 시각을 함께 말할 수 있다.
|
|
659
|
+
*/
|
|
660
|
+
| 'expected-quiet'
|
|
617
661
|
| 'unknown' /** 한 번도 받지 않았거나 창이 아직 닫히지 않았다 — 기다린다 */
|
|
618
662
|
|
|
619
663
|
export function syncVerdictOf(args: {
|
|
@@ -627,6 +671,27 @@ export function syncVerdictOf(args: {
|
|
|
627
671
|
nowMs?: number
|
|
628
672
|
/** 마지막으로 **버려진** 시각. 한 번도 없으면 `-1`. */
|
|
629
673
|
lastUndeliveredAtMs?: number
|
|
674
|
+
/**
|
|
675
|
+
* **이 원천은 이만큼 조용해도 정상이다** — 긷는 주기를 원천 쪽이 선언한 값.
|
|
676
|
+
*
|
|
677
|
+
* 없으면 창 하나(`STALE_AFTER_MS`)로 견준다(지금까지의 거동). 15분마다 긷는 원천은 그 값을
|
|
678
|
+
* 선언해야 하고, 안 하면 긷는 사이마다 「유입 없음」으로 보인다 — 이 파일이 아래에서 그 한계를
|
|
679
|
+
* 스스로 적어 두고 있었다.
|
|
680
|
+
*
|
|
681
|
+
* 장부는 원천의 주기를 알 수 없다. **선언만이 그것을 아는 유일한 길이다.**
|
|
682
|
+
*/
|
|
683
|
+
expectedSilenceMs?: number
|
|
684
|
+
/**
|
|
685
|
+
* **지금이 그 원천의 조용한 시간인가** — 근무 달력 판정의 결과만 받는다.
|
|
686
|
+
*
|
|
687
|
+
* 달력 자체를 여기서 보지 않는다: 이 함수는 순수하고, 달력·시간대·시각 해석은 계약이 이미
|
|
688
|
+
* 한 자리에서 한다(`inWorkCalendar`·`offCalendarAt`). 부르는 쪽이 그것을 불러 참·거짓만 준다 —
|
|
689
|
+
* `capabilityOf` 가 `requiredTests` 를 받는 것과 같은 규율이다.
|
|
690
|
+
*
|
|
691
|
+
* **모르면 주지 않는다.** `false` 로 채우면 「지금은 활동 시간이다」를 단언하는 것이 되고, 달력을
|
|
692
|
+
* 선언하지 않은 원천이 전부 그렇게 읽힌다.
|
|
693
|
+
*/
|
|
694
|
+
quietNow?: boolean
|
|
630
695
|
/**
|
|
631
696
|
* 지금까지 제시된 누적 수 — **「받고 있는 중」과 「오는 것이 없다」를 가르는 사실**이다.
|
|
632
697
|
*
|
|
@@ -672,9 +737,28 @@ export function syncVerdictOf(args: {
|
|
|
672
737
|
* 한계 — 한 창(10분)보다 **느리게 긷는 원천**은 긷는 사이에 「유입 없음」으로 보인다. 장부는 원천의
|
|
673
738
|
* 주기를 모르므로 이보다 정확히 답할 수 없다. 주기를 알게 되면 그 값으로 견주는 것이 맞다.
|
|
674
739
|
*/
|
|
740
|
+
/*
|
|
741
|
+
* 견줄 길이는 **원천이 선언한 값이 있으면 그것**이다(§`expectedSilenceMs`). 없으면 창 하나 —
|
|
742
|
+
* 위 한계 문단이 말하는 그대로다.
|
|
743
|
+
*/
|
|
744
|
+
const silenceLimitMs = typeof args.expectedSilenceMs === 'number' && args.expectedSilenceMs > 0
|
|
745
|
+
? args.expectedSilenceMs
|
|
746
|
+
: STALE_AFTER_MS
|
|
747
|
+
|
|
675
748
|
if (typeof args.lastAtMs === 'number' && typeof args.nowMs === 'number') {
|
|
676
|
-
if (args.lastAtMs < 0)
|
|
677
|
-
|
|
749
|
+
if (args.lastAtMs < 0) {
|
|
750
|
+
/* 한 번도 받지 않았는데 지금이 조용한 시간이면 「기다린다」가 아니라 「지금은 맞다」다. */
|
|
751
|
+
return args.quietNow === true ? 'expected-quiet' : 'unknown'
|
|
752
|
+
}
|
|
753
|
+
if (args.nowMs - args.lastAtMs > silenceLimitMs) {
|
|
754
|
+
/*
|
|
755
|
+
* 오래 조용하다. **지금이 그 원천의 조용한 시간이면 그것이 정상이다.**
|
|
756
|
+
*
|
|
757
|
+
* 이 갈래를 위쪽(버려지는 중·피드 없음)보다 뒤에 두는 이유: 사실이 사라지고 있거나 결선이
|
|
758
|
+
* 없는 것은 **몇 시든 문제**다. 밤이라고 덮으면 밤새 새는 것을 아침에 안다.
|
|
759
|
+
*/
|
|
760
|
+
return args.quietNow === true ? 'expected-quiet' : 'no-traffic'
|
|
761
|
+
}
|
|
678
762
|
}
|
|
679
763
|
|
|
680
764
|
/*
|
|
@@ -698,9 +782,10 @@ export function syncVerdictOf(args: {
|
|
|
698
782
|
if (!args.recent) {
|
|
699
783
|
const touched = typeof args.lastAtMs === 'number' && args.lastAtMs >= 0
|
|
700
784
|
const nothingOffered = (args.offeredTotal ?? 0) <= 0
|
|
701
|
-
|
|
785
|
+
if (touched && nothingOffered) return args.quietNow === true ? 'expected-quiet' : 'no-traffic'
|
|
786
|
+
return 'unknown'
|
|
702
787
|
}
|
|
703
|
-
if (args.recent.offered <= 0) return 'no-traffic'
|
|
788
|
+
if (args.recent.offered <= 0) return args.quietNow === true ? 'expected-quiet' : 'no-traffic'
|
|
704
789
|
|
|
705
790
|
const ratio = (args.recent.offered - args.recent.rejected) / args.recent.offered
|
|
706
791
|
return ratio >= (args.threshold ?? HEALTHY_ACCEPTED_RATIO) ? 'syncing' : 'mapping-blocked'
|
|
@@ -915,8 +915,27 @@ export interface KpiFoldOptions {
|
|
|
915
915
|
/**
|
|
916
916
|
* 현장의 시각 기준(분) — 교대 축이 `HH:MM` 을 어느 기준으로 읽을지. **공간이 갖는 값**이다
|
|
917
917
|
* (테넌트가 아니다: 한 테넌트가 다른 시간대의 공장을 함께 가질 수 있다). 없으면 UTC.
|
|
918
|
+
*
|
|
919
|
+
* ⚠ 한 수다. 접기는 지난 구간을 훑으므로 **일광절약시간 전환을 넘을 수 없다** — 그때는
|
|
920
|
+
* `offsetAt` 을 넘긴다.
|
|
918
921
|
*/
|
|
919
922
|
utcOffsetMinutes?: number
|
|
923
|
+
/**
|
|
924
|
+
* 그 순간의 시각 기준을 푸는 함수 — **`utcOffsetMinutes` 보다 먼저 본다**.
|
|
925
|
+
*
|
|
926
|
+
* ── 왜 수가 아니라 함수인가 (2026-09-07) ─────────────────────────────────
|
|
927
|
+
* 접기는 지난 구간을 훑는다. 1년치를 교대별로 접으면 그 1년 안에 일광절약시간 전환이 두 번 들어
|
|
928
|
+
* 있는데, 고정 오프셋 하나로 읽으면 전환 뒤의 사실이 **한 시간 밀려 다른 교대로 세어진다.**
|
|
929
|
+
* 오류가 나지 않으므로 교대별 성과가 조용히 거짓이 된다. `America/Tijuana`(PST −480 / PDT −420)가
|
|
930
|
+
* 이미 자료에 있다.
|
|
931
|
+
*
|
|
932
|
+
* 이 파일은 시간대 데이터를 들지 않는다(커널과 같은 자세다) — 부르는 쪽이 푸는 법을 준다. 그래서
|
|
933
|
+
* 이 인자가 함수이고, 시험은 여전히 평범한 값으로 돈다.
|
|
934
|
+
*
|
|
935
|
+
* `undefined` 를 돌려주면 그 순간은 기준을 모르는 것이다 — 0(UTC)으로 채우지 않고
|
|
936
|
+
* `utcOffsetMinutes` 로 떨어진다.
|
|
937
|
+
*/
|
|
938
|
+
offsetAt?: (atMs: number) => number | undefined
|
|
920
939
|
/**
|
|
921
940
|
* 축의 상한(기본 20). 넘치면 잘라내고 **잘린 축의 수와 그 안의 건수를 함께 알린다**.
|
|
922
941
|
*
|
|
@@ -970,7 +989,11 @@ function facetKey(rec: CompletionRecord, options: KpiFoldOptions): string {
|
|
|
970
989
|
if (!options.shifts?.length) return 'unknown'
|
|
971
990
|
/* 시각까지 넘긴다 — **휴일에 일어난 일은 어느 교대에도 속하지 않는다.** 되풀이만 보면 그날 서지도
|
|
972
991
|
않은 교대에 집계돼, 교대별 성과가 알리지 않고 거짓이 된다. */
|
|
973
|
-
|
|
992
|
+
/* 그 순간의 기준을 먼저 묻는다 — 없으면 한 수로 떨어진다(§`offsetAt`). */
|
|
993
|
+
const atMs = typeof rec.at === 'number' ? rec.at : Date.parse(String(rec.at))
|
|
994
|
+
const offset =
|
|
995
|
+
(Number.isFinite(atMs) ? options.offsetAt?.(atMs) : undefined) ?? options.utcOffsetMinutes
|
|
996
|
+
return activeShiftAt(options.shifts, rec.at, offset) ?? 'unknown'
|
|
974
997
|
default:
|
|
975
998
|
return 'unknown'
|
|
976
999
|
}
|
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
import { twinWarn } from './log.js'
|
|
18
18
|
import { mergeStreams } from '../service/twin-journal/event-type-split-shape.js'
|
|
19
19
|
import { hydratedDate } from './read-time.js'
|
|
20
|
+
/* 시간대 이름을 순간마다 푸는 함수 — engine 이 service 를 들이는 것은 이 파일들의 관행이다(§`twin-engine.ts`). */
|
|
21
|
+
import { utcOffsetOf } from '../service/reference/reference-master.js'
|
|
20
22
|
import { Between, In } from 'typeorm'
|
|
21
23
|
|
|
22
24
|
import { type TariffDeclaration } from '@operato/twin-kernel'
|
|
@@ -257,8 +259,12 @@ type KpiGroupOut = NonNullable<KpiResult['groups']>['items'][number] &
|
|
|
257
259
|
* **가장 많이 쓰이는 선언 한 벌**을 쓴다 — 3교대 공장은 전 설비가 같은 교대이므로 이것이 곧 정답이고,
|
|
258
260
|
* 자원마다 교대가 다른 현장에서는 이 축이 거칠다는 뜻이다(그 사실을 여기 적어 둔다).
|
|
259
261
|
*
|
|
260
|
-
* 시각 기준은 모델에 실려 있다(
|
|
261
|
-
*
|
|
262
|
+
* 시각 기준은 모델에 실려 있다(공간이 갖는 값을 기동 시 호스트가 풀어 넣는다). 없으면 UTC 로 읽고,
|
|
263
|
+
* 그 기본값은 커널 계약에 밝혀져 있다.
|
|
264
|
+
*
|
|
265
|
+
* **이름이 있으면 이름을 쓴다**(`timezone`). 접기는 지난 구간을 훑으므로 고정 오프셋으로는 일광절약시간
|
|
266
|
+
* 전환을 넘을 수 없다 — 1년치를 한 오프셋으로 읽으면 그 안의 전환 두 번이 사실을 다른 교대로 세게 하고,
|
|
267
|
+
* 그 어긋남은 오류를 내지 않는다(2026-09-07).
|
|
262
268
|
*/
|
|
263
269
|
/**
|
|
264
270
|
* 이 범위에 걸린 운영 목표 — **좁은 선언이 넓은 선언을 덮는다.**
|
|
@@ -281,14 +287,16 @@ async function loadTargets(
|
|
|
281
287
|
async function shiftContext(
|
|
282
288
|
domainId: string,
|
|
283
289
|
instanceIds: string[]
|
|
284
|
-
): Promise<{ shifts?: any[]; utcOffsetMinutes?: number }> {
|
|
290
|
+
): Promise<{ shifts?: any[]; utcOffsetMinutes?: number; offsetAt?: (atMs: number) => number | undefined }> {
|
|
285
291
|
if (!instanceIds.length) return {}
|
|
286
292
|
const rows = await getRepository(TwinInstance).find({ where: { domain: { id: domainId }, instanceId: In(instanceIds) } })
|
|
287
293
|
const tally = new Map<string, { decl: any[]; n: number }>()
|
|
288
294
|
let utcOffsetMinutes: number | undefined
|
|
295
|
+
let timezone: string | undefined
|
|
289
296
|
for (const row of rows) {
|
|
290
297
|
const model = row.model as any
|
|
291
298
|
if (utcOffsetMinutes === undefined && typeof model?.utcOffsetMinutes === 'number') utcOffsetMinutes = model.utcOffsetMinutes
|
|
299
|
+
if (timezone === undefined && typeof model?.timezone === 'string' && model.timezone) timezone = model.timezone
|
|
292
300
|
for (const e of (model?.equipment ?? []) as any[]) {
|
|
293
301
|
const decl = e?.workCalendar
|
|
294
302
|
if (!Array.isArray(decl) || !decl.length) continue
|
|
@@ -300,7 +308,18 @@ async function shiftContext(
|
|
|
300
308
|
}
|
|
301
309
|
let best: { decl: any[]; n: number } | undefined
|
|
302
310
|
for (const v of tally.values()) if (!best || v.n > best.n) best = v
|
|
303
|
-
|
|
311
|
+
/*
|
|
312
|
+
* 이름이 있으면 **푸는 함수**로 바꿔 넘긴다. 접기가 레코드마다 부르므로 그 순간의 오프셋이 나오고,
|
|
313
|
+
* 일광절약시간 전환이 든 구간도 맞게 세어진다(§`FoldOptions.offsetAt`).
|
|
314
|
+
*
|
|
315
|
+
* 수(`utcOffsetMinutes`)도 함께 남긴다 — 이름이 없는 공간에서는 그것이 유일한 답이고, 이름이 있어도
|
|
316
|
+
* 그 순간을 못 푸는 경우의 마지막 기댈 자리다.
|
|
317
|
+
*/
|
|
318
|
+
return {
|
|
319
|
+
...(best ? { shifts: best.decl } : {}),
|
|
320
|
+
...(utcOffsetMinutes !== undefined ? { utcOffsetMinutes } : {}),
|
|
321
|
+
...(timezone ? { offsetAt: (atMs: number) => utcOffsetOf(timezone, new Date(atMs)) } : {})
|
|
322
|
+
}
|
|
304
323
|
}
|
|
305
324
|
|
|
306
325
|
async function locationAreaMap(
|