@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,97 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* ★ **셈이 닫혀야 한다** — 두 갈래로는 큰 수가 「사라졌다」로 떠 있게 된다.
|
|
3
|
+
*
|
|
4
|
+
* ── 실측 (2026-09-05, 인티그레이션·plant 레인) ─────────────────────────────
|
|
5
|
+
* plant 아웃박스와 저널을 손으로 맞춰 보니 정확히 닫혔다.
|
|
6
|
+
*
|
|
7
|
+
* 나간 것 157 = 앉은 것 46 + 거절 2 + 어휘 1 + 되풀이 108
|
|
8
|
+
*
|
|
9
|
+
* 되풀이 108은 하네스가 같은 실적을 같은 시각으로 다시 보낸 것이고 **합친 것이 옳은 거동**이다.
|
|
10
|
+
* 어휘 1은 저널에 안 가는 것이 옳다(시각축이 없다).
|
|
11
|
+
*
|
|
12
|
+
* ── 왜 두 갈래로는 안 되나 ────────────────────────────────────────────────
|
|
13
|
+
* 두 갈래 157 − 46 − 2 = 109 「설명 못 함」
|
|
14
|
+
* 네 갈래 157 − 46 − 2 − 108 − 1 = 0
|
|
15
|
+
*
|
|
16
|
+
* **109 를 「사라졌다」로 내면 그날부터 아무도 그 수를 안 본다.** 늘 큰 수가 떠 있으면 진짜로 하나
|
|
17
|
+
* 사라진 날을 못 본다 — 오늘 이 저장소에서 「늘 참인 경고는 벽지가 된다」로 여러 번 나온 것이다.
|
|
18
|
+
*
|
|
19
|
+
* ── 그리고 되풀이 108은 잰 값이 아니었다 ──────────────────────────────────
|
|
20
|
+
* 뺄셈으로만 구할 수 있었다. 어휘 항이 없어서다. 그 뺄셈을 코드가 하려면 넷이 다 있어야 한다.
|
|
21
|
+
*/
|
|
22
|
+
import { test } from 'node:test'
|
|
23
|
+
import assert from 'node:assert/strict'
|
|
24
|
+
|
|
25
|
+
import {
|
|
26
|
+
newIngestLedger,
|
|
27
|
+
recordIngest,
|
|
28
|
+
recordDuplicates,
|
|
29
|
+
recordVocabulary,
|
|
30
|
+
recordJournalWrite
|
|
31
|
+
} from '../server/engine/ingest-health.js'
|
|
32
|
+
|
|
33
|
+
const T0 = 1_757_000_000_000
|
|
34
|
+
|
|
35
|
+
test('★ 넷을 다 세면 셈이 0으로 닫힌다 — 실측한 그 수로', () => {
|
|
36
|
+
const ledger = newIngestLedger()
|
|
37
|
+
|
|
38
|
+
/* 나간 것 157 · 거절 2 */
|
|
39
|
+
recordIngest(ledger, 157, [{ errors: ['x'] }, { errors: ['y'] }], T0)
|
|
40
|
+
/* 저널에 앉은 것 46 */
|
|
41
|
+
recordJournalWrite(ledger, 46, T0)
|
|
42
|
+
/* 합친 것 108 */
|
|
43
|
+
recordDuplicates(ledger, 108, T0)
|
|
44
|
+
/* 저널에 안 가는 어휘 1 */
|
|
45
|
+
recordVocabulary(ledger, 1)
|
|
46
|
+
|
|
47
|
+
const w = ledger.window
|
|
48
|
+
const unexplained = w.offered - w.rows - w.rejected - ledger.duplicates - w.vocabulary
|
|
49
|
+
|
|
50
|
+
assert.equal(unexplained, 0, `설명 못 한 것이 ${unexplained} 남았다 — 셈이 안 닫힌다`)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test('★ 어휘 항이 없으면 그 수가 「사라졌다」로 남는다 — 이 항의 존재 이유', () => {
|
|
54
|
+
const ledger = newIngestLedger()
|
|
55
|
+
recordIngest(ledger, 20, undefined, T0)
|
|
56
|
+
recordJournalWrite(ledger, 17, T0)
|
|
57
|
+
recordVocabulary(ledger, 1)
|
|
58
|
+
|
|
59
|
+
const w = ledger.window
|
|
60
|
+
/* 어휘를 안 빼면 셋이 빈 것으로 보인다 — 실제로 사라진 것은 둘이다. */
|
|
61
|
+
assert.equal(w.offered - w.rows, 3, '실측 상황을 못 만들고 있다')
|
|
62
|
+
assert.equal(w.offered - w.rows - w.vocabulary, 2, '어휘를 빼도 수가 안 맞는다')
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test('★ 어휘는 저널 수에 안 들어간다 — 두 번 세면 셈이 음수가 된다', () => {
|
|
66
|
+
const ledger = newIngestLedger()
|
|
67
|
+
recordIngest(ledger, 1, undefined, T0)
|
|
68
|
+
recordVocabulary(ledger, 1)
|
|
69
|
+
|
|
70
|
+
assert.equal(ledger.window.rows, 0, '어휘가 저널 수에 섞였다')
|
|
71
|
+
assert.equal(ledger.window.vocabulary, 1)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test('★ 창이 갈리면 각 창이 자기 어휘를 든다 — 누적으로 비교하면 안 된다', () => {
|
|
75
|
+
/*
|
|
76
|
+
* 인티그레이션 레인이 짚은 것: `mes-line-a` 가 커서 48 · 저널 11 인데 결함이 아니다(그 사이
|
|
77
|
+
* 저널을 비웠다). **창 단위로** 맞춰야 한다.
|
|
78
|
+
*/
|
|
79
|
+
const ledger = newIngestLedger()
|
|
80
|
+
recordIngest(ledger, 5, undefined, T0)
|
|
81
|
+
recordVocabulary(ledger, 2)
|
|
82
|
+
assert.equal(ledger.window.vocabulary, 2)
|
|
83
|
+
|
|
84
|
+
/* 창을 넘긴다 — 새 창은 0에서 시작한다. */
|
|
85
|
+
recordIngest(ledger, 5, undefined, T0 + 700_000)
|
|
86
|
+
assert.equal(ledger.window.vocabulary, 0, '새 창이 앞 창의 어휘를 물고 왔다')
|
|
87
|
+
assert.equal(ledger.vocabulary, 2, '누적은 남아야 한다')
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
test('★ 이 검사가 실제로 무언가를 잰다 — 견준 것이 0이면 빨강', () => {
|
|
91
|
+
const ledger = newIngestLedger()
|
|
92
|
+
assert.equal(ledger.window.vocabulary, 0, '새 창에 그 항이 없다')
|
|
93
|
+
recordVocabulary(ledger, 0)
|
|
94
|
+
assert.equal(ledger.vocabulary, 0, '0을 세었다 — 모르면 안 적는다')
|
|
95
|
+
recordVocabulary(ledger, -3)
|
|
96
|
+
assert.equal(ledger.vocabulary, 0, '음수를 세었다')
|
|
97
|
+
})
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 트윈의 고정 유입 규칙이 **표준 헤더 필드를 세 갈래 모두에서** 통과시키나.
|
|
3
|
+
*
|
|
4
|
+
* ── 왜 필요한가 (2026-09-07) ────────────────────────────────────────────────
|
|
5
|
+
* 같은 결함이 이 저장소에서 네 번 났고, 네 번 다 **층이 달랐다.**
|
|
6
|
+
*
|
|
7
|
+
* ```
|
|
8
|
+
* 1 EpcisHeaderOptions 에 칸이 없다 objectEvent 만 자기 인자로 갖고 있었다
|
|
9
|
+
* 2 매핑 스펙에 칸이 없다 커넥터가 선언할 자리가 없었다
|
|
10
|
+
* 3 이 파일의 규칙 셋에 칸이 없다 선언해도 여기서 사라졌다
|
|
11
|
+
* 4 받는 쪽 — 이건 처음부터 서 있었다
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* 세 층이 각각 새는 자리였고, **아래 두 층을 고친 뒤에도 위층에서 계속 사라졌다.** 계약을 열고
|
|
15
|
+
* 「됐다」고 한 것이 아니라 인티그레이션 레인이 이 규칙 그대로 `mapRecord` 를 돌려 봐서 찾았다.
|
|
16
|
+
*
|
|
17
|
+
* 그래서 이 시험은 **계약이 아니라 이 파일의 규칙**을 돌린다. 계약 쪽 시험이 초록이어도 여기서
|
|
18
|
+
* 사라질 수 있다는 것이 오늘 실제로 일어난 일이다.
|
|
19
|
+
*/
|
|
20
|
+
import test from 'node:test'
|
|
21
|
+
import assert from 'node:assert/strict'
|
|
22
|
+
|
|
23
|
+
import { mapRecordChecked } from '@operato/ops-contract'
|
|
24
|
+
|
|
25
|
+
import { canonicalRulesForTest } from '../server/engine/canonical-ingest.js'
|
|
26
|
+
|
|
27
|
+
const AT = '2026-09-07T02:00:00.000Z'
|
|
28
|
+
|
|
29
|
+
/** 세 갈래가 각자 성립하는 최소 레코드 — 헤더 값은 같은 것을 싣는다. */
|
|
30
|
+
const HEADER = {
|
|
31
|
+
bizStep: 'urn:epcglobal:cbv:bizstep:storing',
|
|
32
|
+
disposition: 'urn:epcglobal:cbv:disp:in_progress',
|
|
33
|
+
readPoint: 'urn:epc:id:sgln:0614141.00001.0',
|
|
34
|
+
bizLocation: 'urn:epc:id:sgln:0614141.00002.0',
|
|
35
|
+
bizTransactionList: [{ type: 'urn:epcglobal:cbv:btt:po', bizTransaction: 'urn:epcglobal:cbv:bt:0614141:order-33' }],
|
|
36
|
+
errorDeclaration: { declarationTime: AT, reason: 'urn:epcglobal:cbv:er:incorrect_data' }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const RECORDS: { sourceType: string; record: Record<string, unknown> }[] = [
|
|
40
|
+
{ sourceType: 'twin', record: { ...HEADER, action: 'OBSERVE', epc: 'urn:epc:id:sgtin:0614141.107346.2018' } },
|
|
41
|
+
{
|
|
42
|
+
sourceType: 'twin-transformation',
|
|
43
|
+
record: {
|
|
44
|
+
...HEADER,
|
|
45
|
+
inputEPCList: ['urn:epc:id:sgtin:0614141.107346.1000'],
|
|
46
|
+
outputEPCList: ['urn:epc:id:sgtin:0614141.107346.2000'],
|
|
47
|
+
transformationID: 'urn:uuid:11111111-2222-3333-4444-555555555555'
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
sourceType: 'twin-aggregation',
|
|
52
|
+
record: { ...HEADER, action: 'ADD', parentID: 'urn:epc:id:sscc:0614141.1234567890', childEPCs: ['urn:epc:id:sgtin:0614141.107346.2018'] }
|
|
53
|
+
}
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
function mapWith(sourceType: string, record: Record<string, unknown>): any {
|
|
57
|
+
const rule = canonicalRulesForTest().find(r => r.sourceType === sourceType)
|
|
58
|
+
|
|
59
|
+
assert.ok(rule, `${sourceType} 규칙이 없다 — 갈래가 사라졌다`)
|
|
60
|
+
|
|
61
|
+
return mapRecordChecked(record as any, rule!.mapping as any, AT)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
test('규칙이 셋이다 — 갈래가 줄면 그 사실이 조용히 사라진다', () => {
|
|
65
|
+
assert.deepEqual(
|
|
66
|
+
canonicalRulesForTest().map(r => r.sourceType).sort(),
|
|
67
|
+
['twin', 'twin-aggregation', 'twin-transformation']
|
|
68
|
+
)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
for (const { sourceType, record } of RECORDS) {
|
|
72
|
+
test(`${sourceType} 이 오더 참조를 커널까지 넘긴다 — 이 파일에서 사라지던 자리다`, () => {
|
|
73
|
+
const { event, errors } = mapWith(sourceType, record)
|
|
74
|
+
|
|
75
|
+
assert.deepEqual(errors, [], `${sourceType}: 성립하는 레코드인데 오류가 났다`)
|
|
76
|
+
assert.deepEqual(
|
|
77
|
+
event.bizTransactionList,
|
|
78
|
+
HEADER.bizTransactionList,
|
|
79
|
+
`${sourceType}: 오더 참조가 사라졌다 — 커넥터가 실어도 저널의 biz_transaction 이 빈다`
|
|
80
|
+
)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
test(`${sourceType} 이 나머지 헤더 필드도 함께 넘긴다 — 하나만 보면 다음 것이 또 샌다`, () => {
|
|
84
|
+
const { event } = mapWith(sourceType, record)
|
|
85
|
+
|
|
86
|
+
assert.equal(event.bizStep, HEADER.bizStep)
|
|
87
|
+
assert.equal(event.disposition, HEADER.disposition)
|
|
88
|
+
assert.equal(event.readPoint?.id, HEADER.readPoint)
|
|
89
|
+
assert.equal(event.bizLocation?.id, HEADER.bizLocation)
|
|
90
|
+
assert.equal(event.errorDeclaration?.declarationTime, AT)
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
test('오더 참조를 안 실으면 그 칸을 만들지 않는다 — 빈 배열은 「참조가 없다」와 다르다', () => {
|
|
95
|
+
const { bizTransactionList: _drop, ...bare } = RECORDS[0].record
|
|
96
|
+
const { event, errors } = mapWith('twin', bare)
|
|
97
|
+
|
|
98
|
+
assert.deepEqual(errors, [])
|
|
99
|
+
assert.equal('bizTransactionList' in event, false)
|
|
100
|
+
})
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* ★ **보는 사람이 없어도 창이 닫힌다** — 아무도 안 보는 트윈일수록 기록이 없었다.
|
|
3
|
+
*
|
|
4
|
+
* ── 실측 (2026-09-05) ──────────────────────────────────────────────────────
|
|
5
|
+
* mes-miratek 사실 46건 · 62분에 흩어짐 창 1개
|
|
6
|
+
* mes-line-a 사실 11건 창 48개
|
|
7
|
+
*
|
|
8
|
+
* **사실 수와 창 수가 거꾸로다.** 창은 「다음 사건이 왔을 때」와 「누가 물어볼 때」만 굴렀고,
|
|
9
|
+
* `ingestHealthOf()` 가 굴리는 쪽이 실제로 창을 만들고 있었다 — 점검표·화면이 계속 묻는 트윈만
|
|
10
|
+
* 기록이 쌓였다.
|
|
11
|
+
*
|
|
12
|
+
* 그래서 **아무도 안 보는 트윈일수록 기록이 없었다.** 사실이 조용히 사라지는 것을 잡으려고 만든
|
|
13
|
+
* 장부가 정확히 그 반대로 돌았다. 실제로 137봉투가 장부 없이 지나갔다(인티그레이션 레인 실측).
|
|
14
|
+
*
|
|
15
|
+
* ── 왜 주기인가 ────────────────────────────────────────────────────────────
|
|
16
|
+
* 창이 닫히는 이유는 **시간이 지난 것**이다. 사건도 질의도 그것을 알려 주는 신호일 뿐이고 둘 다
|
|
17
|
+
* 안 올 수 있다. 시간은 늘 지나므로 주기가 그 사실에 맞는 신호다.
|
|
18
|
+
*/
|
|
19
|
+
import { test } from 'node:test'
|
|
20
|
+
import assert from 'node:assert/strict'
|
|
21
|
+
|
|
22
|
+
import { INGEST_WINDOW_MS, newIngestLedger, recordJournalWrite, rollIngestWindow } from '../server/engine/ingest-health.js'
|
|
23
|
+
|
|
24
|
+
const T0 = 1_757_000_000_000
|
|
25
|
+
|
|
26
|
+
test('★ 사건도 질의도 없이 시간만 지나면 창이 닫힌다', () => {
|
|
27
|
+
const ledger = newIngestLedger()
|
|
28
|
+
const closed: any[] = []
|
|
29
|
+
|
|
30
|
+
/* 사실 하나가 들어와 창이 열린다. */
|
|
31
|
+
recordJournalWrite(ledger, 1, T0, w => closed.push(w))
|
|
32
|
+
assert.equal(closed.length, 0, '열리자마자 닫히면 안 된다')
|
|
33
|
+
|
|
34
|
+
/* 그 뒤로 아무 일도 없다 — 주기만 온다. */
|
|
35
|
+
rollIngestWindow(ledger, T0 + INGEST_WINDOW_MS + 1, INGEST_WINDOW_MS, w => closed.push(w))
|
|
36
|
+
|
|
37
|
+
assert.equal(closed.length, 1, '시간이 지났는데 창이 안 닫혔다 — 기록이 안 남는다')
|
|
38
|
+
assert.equal(closed[0].rows, 1, '닫힌 창이 그 사실을 안 들었다')
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('★ 창 길이 전에는 안 닫는다 — 주기가 창을 잘게 쪼개면 안 된다', () => {
|
|
42
|
+
const ledger = newIngestLedger()
|
|
43
|
+
const closed: any[] = []
|
|
44
|
+
recordJournalWrite(ledger, 1, T0)
|
|
45
|
+
|
|
46
|
+
/* 주기(30초)가 창 길이(10분)보다 짧다 — 여러 번 와도 안 닫혀야 한다. */
|
|
47
|
+
for (let i = 1; i <= 10; i++) rollIngestWindow(ledger, T0 + i * 30_000, INGEST_WINDOW_MS, w => closed.push(w))
|
|
48
|
+
|
|
49
|
+
assert.equal(closed.length, 0, `주기가 창을 ${closed.length}개로 쪼갰다`)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('★ 조용한 구간도 창으로 남는다 — expected-quiet 판정이 그 기록 위에 선다', () => {
|
|
53
|
+
/*
|
|
54
|
+
* 사실이 하나도 안 온 구간이 표에 남아야 「조용한 것이 정상인가」를 판정할 수 있다. 그 구간이
|
|
55
|
+
* 아예 기록되지 않으면 조용함과 기록 없음이 구별되지 않는다.
|
|
56
|
+
*/
|
|
57
|
+
const ledger = newIngestLedger()
|
|
58
|
+
const closed: any[] = []
|
|
59
|
+
recordJournalWrite(ledger, 1, T0, w => closed.push(w))
|
|
60
|
+
|
|
61
|
+
/* 두 창을 통째로 건너뛴다 — 그 사이 아무 사실도 없었다. */
|
|
62
|
+
rollIngestWindow(ledger, T0 + INGEST_WINDOW_MS * 2 + 1, INGEST_WINDOW_MS, w => closed.push(w))
|
|
63
|
+
|
|
64
|
+
assert.ok(closed.length >= 1, '건너뛴 구간이 하나도 안 남았다')
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
test('★ 이 검사가 실제로 무언가를 잰다 — 견준 것이 0이면 빨강', () => {
|
|
68
|
+
assert.ok(INGEST_WINDOW_MS > 0, `창 길이가 ${INGEST_WINDOW_MS} 다`)
|
|
69
|
+
const ledger = newIngestLedger()
|
|
70
|
+
recordJournalWrite(ledger, 3, T0)
|
|
71
|
+
assert.equal(ledger.window.rows, 3, '장부가 사실을 안 세고 있다 — 이 검사가 아무것도 안 잰다')
|
|
72
|
+
})
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* ★ **접수 타입에 있는 칸은 매핑도 옮긴다** — 문턱에서 조용히 버려지던 자리.
|
|
3
|
+
*
|
|
4
|
+
* ── 오늘 이 부류가 일곱 번이었다 (2026-09-04) ──────────────────────────────
|
|
5
|
+
* ActuationAck 이 세 칸을 버린다고 말한다 타입과 값 사이
|
|
6
|
+
* save 가 새 칸을 안 쓴다 칸과 쓰는 길
|
|
7
|
+
* 질의가 noteNext 를 안 낸다 저장과 화면 사이
|
|
8
|
+
* 커넥터가 등급 셋을 읽어 놓고 버린다 원본 쪽
|
|
9
|
+
* homeLocation vs homeLocationId 낱말 하나
|
|
10
|
+
* equipmentClassIds 칸이 접수면에 없다 ← 이 검사가 막는 것
|
|
11
|
+
* materialActual 안쪽을 안 본다 검사가 얕다
|
|
12
|
+
*
|
|
13
|
+
* **일곱 다 오류가 안 나서** 검사로만 잡혔다.
|
|
14
|
+
*
|
|
15
|
+
* ── 이 자리가 특히 조용하다 ─────────────────────────────────────────────────
|
|
16
|
+
* 접수 타입(`RefEquipment` …)에 칸이 있어도 매핑 함수가 안 옮기면 **원본이 채운 값이 문턱에서
|
|
17
|
+
* 사라진다.** 타입만 봐서는 안 보인다 — 타입은 「받는다」고 말하고 있으니까.
|
|
18
|
+
*
|
|
19
|
+
* 실제로 그렇게 났다: 설비 22대에 등급을 다 채웠는데 등급 목록만 서고 소속은 아무도 없었다.
|
|
20
|
+
* 화면에서 등급을 골라도 설비가 안 나왔고, 거절도 경고도 없었다.
|
|
21
|
+
*
|
|
22
|
+
* ── 왜 원문을 읽나 ──────────────────────────────────────────────────────────
|
|
23
|
+
* 매핑은 객체 리터럴이라 런타임에 「무엇을 옮기나」를 물을 수 없다(입력을 다 채워 넣어 답을 보는
|
|
24
|
+
* 방법이 있지만, 그러면 선택 필드마다 픽스처를 손으로 적어야 하고 그 손이 바로 빠뜨리는 자리다).
|
|
25
|
+
* 원문에서 읽으면 **이름을 손으로 적지 않아** 타입이 늘 때 검사가 함께 따라간다.
|
|
26
|
+
*/
|
|
27
|
+
import { test } from 'node:test'
|
|
28
|
+
import assert from 'node:assert/strict'
|
|
29
|
+
import { readFileSync } from 'node:fs'
|
|
30
|
+
|
|
31
|
+
const SRC = readFileSync(new URL('../server/service/reference/reference-master.ts', import.meta.url), 'utf8')
|
|
32
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
33
|
+
.replace(/\/\/[^\n]*/g, '')
|
|
34
|
+
|
|
35
|
+
/** 접수 타입 하나의 칸 이름들 — `export interface X { … }` 의 깊이 1 필드. */
|
|
36
|
+
function intakeFields(name: string): string[] {
|
|
37
|
+
const start = SRC.indexOf(`export interface ${name} {`)
|
|
38
|
+
assert.ok(start >= 0, `접수 타입을 못 찾았다: ${name} — 이 검사가 따라가야 한다`)
|
|
39
|
+
const body = SRC.slice(SRC.indexOf('{', start))
|
|
40
|
+
const end = body.indexOf('\n}')
|
|
41
|
+
assert.ok(end > 0, `${name} 의 끝을 못 찾았다`)
|
|
42
|
+
return [...new Set([...body.slice(0, end).matchAll(/^\s{2}([a-zA-Z][\w]*)\??:/gm)].map(m => m[1]))]
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 매핑 함수가 **읽는** 칸들 — `e.foo` · `a.foo` 처럼 원본 줄에서 꺼내는 이름.
|
|
47
|
+
*
|
|
48
|
+
* 옮긴 뒤 이름이 바뀌는 것도 있다(`homeLocationId → homeLocation`). 그래서 「나가는 이름」이 아니라
|
|
49
|
+
* **「읽는 이름」**을 센다 — 읽지 않으면 옮길 수도 없다.
|
|
50
|
+
*/
|
|
51
|
+
function readFields(varName: string): string[] {
|
|
52
|
+
return [...new Set([...SRC.matchAll(new RegExp(`\\b${varName}\\.([a-zA-Z][\\w]*)`, 'g'))].map(m => m[1]))]
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 일부러 안 옮기는 칸 — 이유와 함께.
|
|
57
|
+
*
|
|
58
|
+
* 비어 있는 것이 정상이 아니다: 접수면이 받아 두고 커널에 안 넘기는 값이 있으면 **왜 받는지**가
|
|
59
|
+
* 적혀 있어야 한다. 안 적으면 다음 사람이 「원래 그런가 보다」로 넘긴다.
|
|
60
|
+
*/
|
|
61
|
+
const NOT_MAPPED: Record<string, { fields: string[]; why: string }> = {
|
|
62
|
+
RefEquipment: {
|
|
63
|
+
fields: [],
|
|
64
|
+
why: ''
|
|
65
|
+
},
|
|
66
|
+
RefAsset: {
|
|
67
|
+
fields: [],
|
|
68
|
+
why: ''
|
|
69
|
+
},
|
|
70
|
+
RefPerson: {
|
|
71
|
+
fields: [],
|
|
72
|
+
why: ''
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const CASES = [
|
|
77
|
+
{ intake: 'RefEquipment', reads: 'e' },
|
|
78
|
+
{ intake: 'RefAsset', reads: 'a' },
|
|
79
|
+
{ intake: 'RefPerson', reads: 'p' }
|
|
80
|
+
] as const
|
|
81
|
+
|
|
82
|
+
test('★ 접수 타입의 칸을 매핑이 다 읽는다 — 안 읽으면 원본이 채운 값이 문턱에서 사라진다', () => {
|
|
83
|
+
const dropped: string[] = []
|
|
84
|
+
|
|
85
|
+
for (const c of CASES) {
|
|
86
|
+
const excused = new Set(NOT_MAPPED[c.intake]?.fields ?? [])
|
|
87
|
+
const read = new Set(readFields(c.reads))
|
|
88
|
+
|
|
89
|
+
for (const f of intakeFields(c.intake)) {
|
|
90
|
+
if (!read.has(f) && !excused.has(f)) dropped.push(`${c.intake}.${f}`)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
assert.deepEqual(
|
|
95
|
+
dropped,
|
|
96
|
+
[],
|
|
97
|
+
'접수 타입은 받는다고 말하는데 매핑이 안 읽는다. 원본이 채운 값이 조용히 사라진다:\n ' +
|
|
98
|
+
dropped.join('\n ') +
|
|
99
|
+
'\n매핑에 넣거나, 일부러 안 넘긴다면 NOT_MAPPED 에 이유를 적는다.'
|
|
100
|
+
)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
test('★ 등급 소속 셋이 다 접수면을 지난다 — 이름으로 못 박는다', () => {
|
|
104
|
+
/*
|
|
105
|
+
* 위 검사는 목록 대조라 예외 표에 올리면 통과한다. 이 셋은 **소속이 없으면 등급으로 부르는
|
|
106
|
+
* 요구가 채워지지 않는** 자리여서 예외가 될 수 없다.
|
|
107
|
+
*/
|
|
108
|
+
for (const [type, field, reads] of [
|
|
109
|
+
['RefEquipment', 'equipmentClassIds', 'e'],
|
|
110
|
+
['RefAsset', 'assetClassIds', 'a'],
|
|
111
|
+
['RefPerson', 'personnelClassIds', 'p']
|
|
112
|
+
] as const) {
|
|
113
|
+
assert.ok(intakeFields(type).includes(field), `${type} 에 ${field} 칸이 없다`)
|
|
114
|
+
assert.ok(readFields(reads).includes(field), `매핑이 ${type}.${field} 를 안 읽는다 — 소속이 문턱에서 사라진다`)
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
test('★ 사람도 정체성 자리를 갖는다 — 그리고 그 자리는 개인정보가 아니다', () => {
|
|
119
|
+
/*
|
|
120
|
+
* ── 왜 열었나 (2026-09-05, 인티그레이션 레인이 물음) ────────────────────────
|
|
121
|
+
* 설비에서 이 자리를 만든 이유가 사람에게도 그대로 걸린다 — 지역 이름은 그 현장 안에서만
|
|
122
|
+
* 고유하다. `E-1001` 이 다른 공장에도 있으면 여러 현장을 함께 보는 순간 한쪽이 사라진다.
|
|
123
|
+
*
|
|
124
|
+
* ── 왜 이 검사에 규약을 함께 적나 ──────────────────────────────────────────
|
|
125
|
+
* 설비와 사람이 갈리는 자리가 있다. 사람의 「전역에서 고유한 값」은 사원번호·카드번호처럼
|
|
126
|
+
* **그 사람에 관한 사실**이고, 저널은 되풀이 읽히고 미러로 복제되고 구독으로 방송된다 —
|
|
127
|
+
* 한 번 들어가면 지워지지 않는다.
|
|
128
|
+
*
|
|
129
|
+
* 이 칸이 요구하는 것은 **현장을 앞에 붙여 전역에서 고유해진 이름**이다(`objectUri` 가 만든다).
|
|
130
|
+
* 그 규약이 주석에만 있으면 다음 커넥터가 카드번호를 넣는다 — 그러니 소스에 그 문장이 실제로
|
|
131
|
+
* 있는지를 검사가 지킨다.
|
|
132
|
+
*/
|
|
133
|
+
assert.ok(intakeFields('RefPerson').includes('identity'), 'RefPerson 에 identity 칸이 없다')
|
|
134
|
+
assert.ok(readFields('p').includes('identity'), '매핑이 사람의 identity 를 안 읽는다')
|
|
135
|
+
|
|
136
|
+
/* 규약이 글로 남아 있나 — 지우면 이 줄이 빨개진다. */
|
|
137
|
+
const raw = readFileSync(new URL('../server/service/reference/reference-master.ts', import.meta.url), 'utf8')
|
|
138
|
+
const start = raw.indexOf('export interface RefPerson')
|
|
139
|
+
const personDoc = raw.slice(start, raw.indexOf('\n}', start))
|
|
140
|
+
assert.ok(personDoc.includes('개인정보'), '개인정보를 넣지 않는다는 규약이 사라졌다')
|
|
141
|
+
assert.ok(personDoc.includes('objectUri'), '무엇을 넣어야 하는지(이름공간 아래 식별자)가 사라졌다')
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
test('★ 정체성 낱말이 세 자리에서 같다 — 마스터가 내는 이름과 접수면이 받는 이름', () => {
|
|
145
|
+
/*
|
|
146
|
+
* 설비는 `identity` 로 바꿨는데 자산은 `gs1Id` 로 남아 있었다. 마스터는 둘 다 `identity` 로
|
|
147
|
+
* 내므로 자산 쪽은 **원본이 채우는 순간 조용히 사라졌다** — 값이 다 비어 있어 아직 잃은 것은
|
|
148
|
+
* 없었다(인티그레이션 레인 실측).
|
|
149
|
+
*
|
|
150
|
+
* 자리(`RefLocation`)는 `gs1Id` 가 맞다 — 거기는 실제로 SGLN 이 온다.
|
|
151
|
+
*/
|
|
152
|
+
assert.ok(intakeFields('RefEquipment').includes('identity'), '설비 정체성 이름이 바뀌었다')
|
|
153
|
+
assert.ok(intakeFields('RefAsset').includes('identity'), '자산이 아직 마스터와 다른 이름을 쓴다')
|
|
154
|
+
assert.ok(!intakeFields('RefAsset').includes('gs1Id'), '자산에 옛 이름이 남아 있다')
|
|
155
|
+
assert.ok(intakeFields('RefLocation').includes('gs1Id'), '자리는 SGLN 이 오므로 그 이름이 맞다')
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
test('★ 이 검사가 실제로 무언가를 잰다 — 견준 것이 0이면 빨강', () => {
|
|
159
|
+
/*
|
|
160
|
+
* 정규식이 모양을 못 따라가면 목록이 비고, 그러면 위 검사가 **초록인데 아무것도 안 재는 상태**가
|
|
161
|
+
* 된다. 오늘 이 저장소에서 그 부류를 여러 번 잡았다.
|
|
162
|
+
*/
|
|
163
|
+
for (const c of CASES) {
|
|
164
|
+
const fields = intakeFields(c.intake)
|
|
165
|
+
const read = readFields(c.reads)
|
|
166
|
+
assert.ok(fields.length > 5, `${c.intake} 칸을 ${fields.length}개만 읽었다`)
|
|
167
|
+
assert.ok(read.length > 5, `${c.reads}.* 를 ${read.length}개만 읽었다`)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/* 이유 없는 예외는 예외가 아니다 — 비어 있는 항목은 예외가 아니라 자리표시다. */
|
|
171
|
+
for (const [type, e] of Object.entries(NOT_MAPPED)) {
|
|
172
|
+
if (!e.fields.length) continue
|
|
173
|
+
assert.ok(e.why?.trim(), `${type} 의 예외에 이유가 없다`)
|
|
174
|
+
}
|
|
175
|
+
})
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* ★ **원천마다 시간의 결이 다르다** — 밤마다 나던 거짓 빨강의 나머지 반쪽.
|
|
3
|
+
*
|
|
4
|
+
* 판정은 이미 두 값을 받을 수 있다(`syncVerdictOf` 의 `expectedSilenceMs`·`quietNow`). 없던 것은
|
|
5
|
+
* **원천이 그것을 말할 자리**였다. 인티그레이션 레인이 다섯을 재 왔다.
|
|
6
|
+
*
|
|
7
|
+
* ppms 발전 일몰에 멈춘다 — 두 사이트가 같은 분 19:25 에 함께
|
|
8
|
+
* ppms 계측 밤에도 온다 · 15분마다
|
|
9
|
+
* ppms 가격 화·목만 거래된다 · 그 날 하루 한 번
|
|
10
|
+
* plant 근무시간 창 · 훅이라 「보내면 온다」
|
|
11
|
+
* chef 조업시간 창 · 같음
|
|
12
|
+
*
|
|
13
|
+
* 셋이 다 다르다 — 해에 매인 것, 사람 근무에 매인 것, 거래일에 매인 것. 그런데 **두 축으로
|
|
14
|
+
* 덮인다**: 「지금이 활동 시각인가」와 「그 안에서 얼마나 뜸한가」.
|
|
15
|
+
*/
|
|
16
|
+
import { test } from 'node:test'
|
|
17
|
+
import assert from 'node:assert/strict'
|
|
18
|
+
|
|
19
|
+
import { cadenceOf, ingestExpectation, type LiveCadence } from '../server/service/reference/live-cadence.js'
|
|
20
|
+
|
|
21
|
+
const NOW = 1_800_000_000_000
|
|
22
|
+
|
|
23
|
+
/* ── 축을 찾는다 ──────────────────────────────────────────────────────────── */
|
|
24
|
+
|
|
25
|
+
test('★ 선언한 축을 찾는다', () => {
|
|
26
|
+
const cadences: LiveCadence[] = [
|
|
27
|
+
{ axis: 'equipment.status', windowKind: 'daylight' },
|
|
28
|
+
{ axis: 'quality.output', expectedSilenceMs: 900_000 }
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
assert.equal(cadenceOf(cadences, 'quality.output')?.expectedSilenceMs, 900_000)
|
|
32
|
+
assert.equal(cadenceOf(cadences, 'equipment.status')?.windowKind, 'daylight')
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test('★ 선언하지 않은 축은 없다고 답한다 — 옛 거동으로 견주게', () => {
|
|
36
|
+
assert.equal(cadenceOf([{ axis: 'quality.output' }], 'order.status'), undefined)
|
|
37
|
+
assert.equal(cadenceOf(undefined, 'quality.output'), undefined)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
/* ── 판정에 넘길 두 값으로 옮긴다 ───────────────────────────────────────── */
|
|
41
|
+
|
|
42
|
+
test('★ 선언이 없으면 아무것도 답하지 않는다 — 「모른다」를 「안 와도 된다」로 접지 않는다', () => {
|
|
43
|
+
/*
|
|
44
|
+
* 여기서 `quietNow: true` 를 내면 **선언 안 한 원천이 전부 조용해진다.** 정말 끊긴 것도
|
|
45
|
+
* 조용해지고, 그러면 이 축을 만든 목적을 잃는다.
|
|
46
|
+
*/
|
|
47
|
+
assert.deepEqual(ingestExpectation(undefined, { nowMs: NOW }), {})
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
test('★ 15분마다 긷는 원천 — 주기만 넘긴다', () => {
|
|
51
|
+
const out = ingestExpectation({ axis: 'quality.output', expectedSilenceMs: 900_000 }, { nowMs: NOW })
|
|
52
|
+
|
|
53
|
+
assert.equal(out.expectedSilenceMs, 900_000)
|
|
54
|
+
assert.equal(out.quietNow, undefined, '창을 선언하지 않았는데 시각을 단언했다')
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('0이나 음수 주기는 선언이 아니다', () => {
|
|
58
|
+
for (const bad of [0, -1, NaN]) {
|
|
59
|
+
const out = ingestExpectation({ axis: 'a', expectedSilenceMs: bad }, { nowMs: NOW })
|
|
60
|
+
assert.equal(out.expectedSilenceMs, undefined, `${bad} 를 주기로 받았다`)
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
/* ── 「보내면 온다」 ──────────────────────────────────────────────────────── */
|
|
65
|
+
|
|
66
|
+
test('★ 훅으로 미는 축은 침묵으로 판정하지 않는다 — plant 가 그렇다', () => {
|
|
67
|
+
/*
|
|
68
|
+
* 보낼 것이 없으면 안 보내는 것이 정상이다. 「주기가 없다」와 「모른다」를 가르는 값이다.
|
|
69
|
+
*/
|
|
70
|
+
const out = ingestExpectation({ axis: 'order.status', pushed: true }, { nowMs: NOW })
|
|
71
|
+
|
|
72
|
+
assert.equal(out.quietNow, true)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
test('★ 미는 축은 창 판정을 기다리지 않는다 — 판정 수단이 없어도 답한다', () => {
|
|
76
|
+
const out = ingestExpectation(
|
|
77
|
+
{ axis: 'order.status', pushed: true, window: [{ id: 'day', entryType: 'production', startTime: '09:00', endTime: '18:00' }] },
|
|
78
|
+
{ nowMs: NOW } /* inWindow 를 주지 않았다 */
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
assert.equal(out.quietNow, true)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
/* ── 창 ──────────────────────────────────────────────────────────────────── */
|
|
85
|
+
|
|
86
|
+
test('★ 활동 시각 안이면 조용할 때가 아니다', () => {
|
|
87
|
+
const out = ingestExpectation(
|
|
88
|
+
{ axis: 'quality.output', window: [{ id: 'day', entryType: 'production', startTime: '09:00', endTime: '18:00' }] },
|
|
89
|
+
{ nowMs: NOW, inWindow: () => true }
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
assert.equal(out.quietNow, false)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
test('★ 활동 시각 밖이면 조용할 때다', () => {
|
|
96
|
+
const out = ingestExpectation(
|
|
97
|
+
{ axis: 'quality.output', window: [{ id: 'day', entryType: 'production', startTime: '09:00', endTime: '18:00' }] },
|
|
98
|
+
{ nowMs: NOW, inWindow: () => false }
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
assert.equal(out.quietNow, true)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
test('★ 판정 수단을 안 받았으면 시각을 단언하지 않는다 — 지어낸 값을 만들지 않는다', () => {
|
|
105
|
+
/*
|
|
106
|
+
* 부르는 쪽이 달력 판정 함수를 주지 않았는데 우리가 「활동 시각이다」로 답하면, 창을 선언한
|
|
107
|
+
* 원천이 **창이 없는 것과 같아진다.** 그것은 조용한 실패다.
|
|
108
|
+
*/
|
|
109
|
+
const out = ingestExpectation(
|
|
110
|
+
{ axis: 'quality.output', window: [{ id: 'day', entryType: 'production', startTime: '09:00', endTime: '18:00' }] },
|
|
111
|
+
{ nowMs: NOW }
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
assert.equal(out.quietNow, undefined)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
test('빈 창은 창이 아니다', () => {
|
|
118
|
+
assert.equal(ingestExpectation({ axis: 'a', window: [] }, { nowMs: NOW, inWindow: () => false }).quietNow, undefined)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
/* ── 두 축이 함께 온다 ──────────────────────────────────────────────────── */
|
|
122
|
+
|
|
123
|
+
test('★ 주기와 창을 같이 선언할 수 있다 — 인증서 가격이 그 모양이다', () => {
|
|
124
|
+
/*
|
|
125
|
+
* 화·목만 거래되고 그 날 하루 한 번 온다. 「매일 와야 한다」로 재면 주 5일 중 3일이 빨갛고,
|
|
126
|
+
* 「주 2회」로 재면 이어 쓴 날을 못 본다. 두 축이면 둘 다 답한다.
|
|
127
|
+
*/
|
|
128
|
+
const out = ingestExpectation(
|
|
129
|
+
{
|
|
130
|
+
axis: 'quality.output',
|
|
131
|
+
expectedSilenceMs: 26 * 3_600_000,
|
|
132
|
+
window: [
|
|
133
|
+
{ id: 'tue', entryType: 'production', startTime: '00:00', endTime: '23:59' },
|
|
134
|
+
{ id: 'thu', entryType: 'production', startTime: '00:00', endTime: '23:59' }
|
|
135
|
+
]
|
|
136
|
+
},
|
|
137
|
+
{ nowMs: NOW, inWindow: () => false }
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
assert.equal(out.expectedSilenceMs, 26 * 3_600_000)
|
|
141
|
+
assert.equal(out.quietNow, true, '거래일이 아니면 조용한 것이 정상이다')
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
/* ── 「해 있는 동안」은 호스트가 채운다 ──────────────────────────────────── */
|
|
145
|
+
|
|
146
|
+
test('★ 종류만 선언한 것으로는 창 판정을 하지 않는다 — 호스트가 채워야 한다', () => {
|
|
147
|
+
/*
|
|
148
|
+
* 커널은 일출·일몰을 계산하지 않는다(굴절·고위도·박명 정의가 다 선택이고, 정하면 방언이다).
|
|
149
|
+
* 원천은 종류만 말하고, 좌표를 아는 호스트가 날마다 구체적인 구간을 채운다.
|
|
150
|
+
*
|
|
151
|
+
* 그러니 `windowKind` 만 있고 `window` 가 비어 있으면 **아직 채워지지 않은 것**이고, 그때
|
|
152
|
+
* 판정하지 않는다. `false` 로 접으면 태양광이 밤에도 활동 시각인 것이 된다.
|
|
153
|
+
*/
|
|
154
|
+
const out = ingestExpectation({ axis: 'equipment.status', windowKind: 'daylight' }, { nowMs: NOW, inWindow: () => true })
|
|
155
|
+
|
|
156
|
+
assert.equal(out.quietNow, undefined)
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
test('★ 호스트가 채우면 그것으로 판정한다', () => {
|
|
160
|
+
const out = ingestExpectation(
|
|
161
|
+
{
|
|
162
|
+
axis: 'equipment.status',
|
|
163
|
+
windowKind: 'daylight',
|
|
164
|
+
window: [{ id: 'today', entryType: 'production', startTime: '06:12', endTime: '19:25' }]
|
|
165
|
+
},
|
|
166
|
+
{ nowMs: NOW, inWindow: () => false }
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
assert.equal(out.quietNow, true)
|
|
170
|
+
})
|