@things-factory/headless-twin 10.0.19 → 10.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-server/engine/canonical-ingest.d.ts +10 -1
- package/dist-server/engine/canonical-ingest.js +110 -33
- package/dist-server/engine/canonical-ingest.js.map +1 -1
- package/dist-server/engine/event-time.d.ts +25 -0
- package/dist-server/engine/event-time.js +98 -0
- package/dist-server/engine/event-time.js.map +1 -0
- package/dist-server/engine/index.d.ts +2 -0
- package/dist-server/engine/index.js +23 -0
- package/dist-server/engine/index.js.map +1 -1
- package/dist-server/engine/ingest-dedupe.d.ts +18 -6
- package/dist-server/engine/ingest-dedupe.js +201 -15
- package/dist-server/engine/ingest-dedupe.js.map +1 -1
- package/dist-server/engine/ingest-health.d.ts +60 -1
- package/dist-server/engine/ingest-health.js +43 -8
- package/dist-server/engine/ingest-health.js.map +1 -1
- package/dist-server/engine/kpi-fold.d.ts +19 -0
- package/dist-server/engine/kpi-fold.js +4 -1
- package/dist-server/engine/kpi-fold.js.map +1 -1
- package/dist-server/engine/kpi-query.js +23 -3
- package/dist-server/engine/kpi-query.js.map +1 -1
- package/dist-server/engine/oee-accumulator.d.ts +35 -2
- package/dist-server/engine/oee-accumulator.js +56 -4
- package/dist-server/engine/oee-accumulator.js.map +1 -1
- package/dist-server/engine/twin-engine.d.ts +72 -3
- package/dist-server/engine/twin-engine.js +118 -5
- package/dist-server/engine/twin-engine.js.map +1 -1
- package/dist-server/index.js +21 -0
- package/dist-server/index.js.map +1 -1
- package/dist-server/migrations/1786600000000-RenameOperatoMesAdapterType.d.ts +5 -0
- package/dist-server/migrations/1786600000000-RenameOperatoMesAdapterType.js +66 -0
- package/dist-server/migrations/1786600000000-RenameOperatoMesAdapterType.js.map +1 -0
- package/dist-server/migrations/1786700000000-AddDispatchRetryColumns.d.ts +5 -0
- package/dist-server/migrations/1786700000000-AddDispatchRetryColumns.js +79 -0
- package/dist-server/migrations/1786700000000-AddDispatchRetryColumns.js.map +1 -0
- package/dist-server/migrations/index.js +5 -1
- package/dist-server/migrations/index.js.map +1 -1
- package/dist-server/routes.js +12 -20
- package/dist-server/routes.js.map +1 -1
- package/dist-server/service/actuation/actuation-advisor.d.ts +60 -0
- package/dist-server/service/actuation/actuation-advisor.js +53 -0
- package/dist-server/service/actuation/actuation-advisor.js.map +1 -0
- package/dist-server/service/actuation/actuation-outcome.d.ts +52 -0
- package/dist-server/service/actuation/actuation-outcome.js +34 -0
- package/dist-server/service/actuation/actuation-outcome.js.map +1 -0
- package/dist-server/service/actuation/actuation-rule-resolver.d.ts +19 -0
- package/dist-server/service/actuation/actuation-rule-resolver.js +161 -0
- package/dist-server/service/actuation/actuation-rule-resolver.js.map +1 -0
- package/dist-server/service/actuation/actuation-rule.d.ts +55 -0
- package/dist-server/service/actuation/actuation-rule.js +130 -0
- package/dist-server/service/actuation/actuation-rule.js.map +1 -0
- package/dist-server/service/actuation/approval-gateway.d.ts +20 -0
- package/dist-server/service/actuation/approval-gateway.js +14 -0
- package/dist-server/service/actuation/approval-gateway.js.map +1 -0
- package/dist-server/service/actuation/catch-up-effects.d.ts +16 -0
- package/dist-server/service/actuation/catch-up-effects.js +120 -0
- package/dist-server/service/actuation/catch-up-effects.js.map +1 -0
- package/dist-server/service/actuation/command-dispatcher.d.ts +206 -8
- package/dist-server/service/actuation/command-dispatcher.js +191 -14
- package/dist-server/service/actuation/command-dispatcher.js.map +1 -1
- package/dist-server/service/actuation/command-store.d.ts +10 -2
- package/dist-server/service/actuation/command-store.js +168 -5
- package/dist-server/service/actuation/command-store.js.map +1 -1
- package/dist-server/service/actuation/dispatch-after-approval.d.ts +11 -0
- package/dist-server/service/actuation/dispatch-after-approval.js +73 -0
- package/dist-server/service/actuation/dispatch-after-approval.js.map +1 -0
- package/dist-server/service/actuation/effect-match.d.ts +53 -0
- package/dist-server/service/actuation/effect-match.js +65 -0
- package/dist-server/service/actuation/effect-match.js.map +1 -0
- package/dist-server/service/actuation/index.d.ts +21 -2
- package/dist-server/service/actuation/index.js +21 -2
- package/dist-server/service/actuation/index.js.map +1 -1
- package/dist-server/service/actuation/retry-plan.d.ts +63 -0
- package/dist-server/service/actuation/retry-plan.js +99 -0
- package/dist-server/service/actuation/retry-plan.js.map +1 -0
- package/dist-server/service/actuation/rule-evaluate.d.ts +127 -0
- package/dist-server/service/actuation/rule-evaluate.js +205 -0
- package/dist-server/service/actuation/rule-evaluate.js.map +1 -0
- package/dist-server/service/actuation/rule-loop.d.ts +5 -0
- package/dist-server/service/actuation/rule-loop.js +156 -0
- package/dist-server/service/actuation/rule-loop.js.map +1 -0
- package/dist-server/service/actuation/rule-runner.d.ts +52 -0
- package/dist-server/service/actuation/rule-runner.js +135 -0
- package/dist-server/service/actuation/rule-runner.js.map +1 -0
- package/dist-server/service/actuation/rule-wiring.d.ts +24 -0
- package/dist-server/service/actuation/rule-wiring.js +80 -0
- package/dist-server/service/actuation/rule-wiring.js.map +1 -0
- package/dist-server/service/actuation/settle-effects.d.ts +21 -0
- package/dist-server/service/actuation/settle-effects.js +101 -0
- package/dist-server/service/actuation/settle-effects.js.map +1 -0
- package/dist-server/service/actuation/sweep-dispatch.d.ts +37 -0
- package/dist-server/service/actuation/sweep-dispatch.js +125 -0
- package/dist-server/service/actuation/sweep-dispatch.js.map +1 -0
- package/dist-server/service/actuation/twin-command-resolver.d.ts +120 -0
- package/dist-server/service/actuation/twin-command-resolver.js +406 -0
- package/dist-server/service/actuation/twin-command-resolver.js.map +1 -0
- package/dist-server/service/actuation/twin-command.d.ts +81 -0
- package/dist-server/service/actuation/twin-command.js +60 -0
- package/dist-server/service/actuation/twin-command.js.map +1 -1
- package/dist-server/service/actuation/twin-now.d.ts +41 -0
- package/dist-server/service/actuation/twin-now.js +65 -0
- package/dist-server/service/actuation/twin-now.js.map +1 -0
- package/dist-server/service/index.d.ts +5 -2
- package/dist-server/service/index.js +8 -0
- package/dist-server/service/index.js.map +1 -1
- package/dist-server/service/reference/actuation-routing.d.ts +60 -0
- package/dist-server/service/reference/actuation-routing.js +126 -0
- package/dist-server/service/reference/actuation-routing.js.map +1 -0
- package/dist-server/service/reference/actuation-target.d.ts +65 -0
- package/dist-server/service/reference/actuation-target.js +94 -0
- package/dist-server/service/reference/actuation-target.js.map +1 -0
- package/dist-server/service/reference/connection-portability-resolver.d.ts +29 -0
- package/dist-server/service/reference/connection-portability-resolver.js +181 -0
- package/dist-server/service/reference/connection-portability-resolver.js.map +1 -0
- package/dist-server/service/reference/connection-portability.d.ts +138 -0
- package/dist-server/service/reference/connection-portability.js +230 -0
- package/dist-server/service/reference/connection-portability.js.map +1 -0
- package/dist-server/service/reference/fill-cadence.d.ts +17 -0
- package/dist-server/service/reference/fill-cadence.js +31 -0
- package/dist-server/service/reference/fill-cadence.js.map +1 -0
- package/dist-server/service/reference/fill-dedupe.d.ts +16 -0
- package/dist-server/service/reference/fill-dedupe.js +63 -0
- package/dist-server/service/reference/fill-dedupe.js.map +1 -0
- package/dist-server/service/reference/fill-loop.d.ts +35 -0
- package/dist-server/service/reference/fill-loop.js +187 -0
- package/dist-server/service/reference/fill-loop.js.map +1 -0
- package/dist-server/service/reference/fill-span.d.ts +10 -0
- package/dist-server/service/reference/fill-span.js +37 -0
- package/dist-server/service/reference/fill-span.js.map +1 -0
- package/dist-server/service/reference/hook-contract.d.ts +88 -4
- package/dist-server/service/reference/hook-contract.js +105 -4
- package/dist-server/service/reference/hook-contract.js.map +1 -1
- package/dist-server/service/reference/hook-rejected.d.ts +25 -0
- package/dist-server/service/reference/hook-rejected.js +48 -0
- package/dist-server/service/reference/hook-rejected.js.map +1 -0
- package/dist-server/service/reference/index.d.ts +4 -1
- package/dist-server/service/reference/index.js +5 -1
- package/dist-server/service/reference/index.js.map +1 -1
- package/dist-server/service/reference/live-cadence.d.ts +68 -0
- package/dist-server/service/reference/live-cadence.js +44 -0
- package/dist-server/service/reference/live-cadence.js.map +1 -0
- package/dist-server/service/reference/live-feed-lease.d.ts +15 -0
- package/dist-server/service/reference/live-feed-lease.js +56 -0
- package/dist-server/service/reference/live-feed-lease.js.map +1 -0
- package/dist-server/service/reference/live-ingest.d.ts +21 -0
- package/dist-server/service/reference/live-ingest.js +66 -0
- package/dist-server/service/reference/live-ingest.js.map +1 -0
- package/dist-server/service/reference/reference-adapter.d.ts +291 -2
- package/dist-server/service/reference/reference-adapter.js +19 -1
- package/dist-server/service/reference/reference-adapter.js.map +1 -1
- package/dist-server/service/reference/reference-fill-run.d.ts +50 -0
- package/dist-server/service/reference/reference-fill-run.js +142 -0
- package/dist-server/service/reference/reference-fill-run.js.map +1 -0
- package/dist-server/service/reference/reference-fill.d.ts +90 -0
- package/dist-server/service/reference/reference-fill.js +141 -0
- package/dist-server/service/reference/reference-fill.js.map +1 -0
- package/dist-server/service/reference/reference-hook.d.ts +22 -0
- package/dist-server/service/reference/reference-hook.js +135 -55
- package/dist-server/service/reference/reference-hook.js.map +1 -1
- package/dist-server/service/reference/reference-link-groups.d.ts +10 -0
- package/dist-server/service/reference/reference-link-groups.js +15 -0
- package/dist-server/service/reference/reference-link-groups.js.map +1 -0
- package/dist-server/service/reference/reference-links.d.ts +14 -0
- package/dist-server/service/reference/reference-links.js +73 -0
- package/dist-server/service/reference/reference-links.js.map +1 -0
- package/dist-server/service/reference/reference-live.d.ts +1 -0
- package/dist-server/service/reference/reference-live.js +32 -6
- package/dist-server/service/reference/reference-live.js.map +1 -1
- package/dist-server/service/reference/reference-master.d.ts +56 -2
- package/dist-server/service/reference/reference-master.js +33 -3
- package/dist-server/service/reference/reference-master.js.map +1 -1
- package/dist-server/service/reference/reference-resolver.d.ts +27 -0
- package/dist-server/service/reference/reference-resolver.js +79 -1
- package/dist-server/service/reference/reference-resolver.js.map +1 -1
- package/dist-server/service/twin-event/twin-event-keys.d.ts +31 -3
- package/dist-server/service/twin-event/twin-event-keys.js +36 -4
- package/dist-server/service/twin-event/twin-event-keys.js.map +1 -1
- package/dist-server/service/twin-event/twin-event.d.ts +1 -0
- package/dist-server/service/twin-event/twin-event.js +11 -2
- package/dist-server/service/twin-event/twin-event.js.map +1 -1
- package/dist-server/service/twin-model/choose-recipe.d.ts +15 -0
- package/dist-server/service/twin-model/choose-recipe.js +33 -0
- package/dist-server/service/twin-model/choose-recipe.js.map +1 -0
- package/dist-server/service/twin-model/external-resolver.d.ts +76 -0
- package/dist-server/service/twin-model/external-resolver.js +109 -0
- package/dist-server/service/twin-model/external-resolver.js.map +1 -0
- package/dist-server/service/twin-model/project-structure.js +16 -3
- package/dist-server/service/twin-model/project-structure.js.map +1 -1
- package/dist-server/service/twin-model/twin-equipment.d.ts +17 -0
- package/dist-server/service/twin-model/twin-equipment.js +10 -0
- package/dist-server/service/twin-model/twin-equipment.js.map +1 -1
- package/dist-server/service/twin-model/twin-lineage-query.js +75 -1
- package/dist-server/service/twin-model/twin-lineage-query.js.map +1 -1
- package/dist-server/service/twin-model/twin-model-item-query.js +94 -5
- package/dist-server/service/twin-model/twin-model-item-query.js.map +1 -1
- package/dist-server/service/twin-model/twin-model-tree-query.js.map +1 -1
- package/dist-server/service/twin-space/twin-space.d.ts +29 -0
- package/dist-server/service/twin-space/twin-space.js +13 -0
- package/dist-server/service/twin-space/twin-space.js.map +1 -1
- package/dist-server/service/twin-subject/event-subjects.d.ts +3 -1
- package/dist-server/service/twin-subject/event-subjects.js +8 -1
- package/dist-server/service/twin-subject/event-subjects.js.map +1 -1
- package/dist-shared/entity-delta.js +1 -1
- package/dist-shared/entity-delta.js.map +1 -1
- package/package.json +7 -7
- package/server/engine/canonical-ingest.ts +119 -33
- package/server/engine/event-time.ts +96 -0
- package/server/engine/index.ts +20 -0
- package/server/engine/ingest-dedupe.ts +203 -27
- package/server/engine/ingest-health.ts +91 -6
- package/server/engine/kpi-fold.ts +24 -1
- package/server/engine/kpi-query.ts +23 -4
- package/server/engine/oee-accumulator.ts +80 -8
- package/server/engine/twin-engine.ts +119 -5
- package/server/index.ts +23 -0
- package/server/migrations/1786600000000-RenameOperatoMesAdapterType.ts +66 -0
- package/server/migrations/1786700000000-AddDispatchRetryColumns.ts +82 -0
- package/server/migrations/index.ts +5 -1
- package/server/routes.ts +12 -24
- package/server/service/actuation/actuation-advisor.ts +124 -0
- package/server/service/actuation/actuation-outcome.ts +97 -0
- package/server/service/actuation/actuation-rule-resolver.ts +147 -0
- package/server/service/actuation/actuation-rule.ts +139 -0
- package/server/service/actuation/approval-gateway.ts +44 -0
- package/server/service/actuation/catch-up-effects.ts +152 -0
- package/server/service/actuation/command-dispatcher.ts +401 -20
- package/server/service/actuation/command-store.ts +182 -8
- package/server/service/actuation/dispatch-after-approval.ts +71 -0
- package/server/service/actuation/effect-match.ts +124 -0
- package/server/service/actuation/index.ts +21 -2
- package/server/service/actuation/retry-plan.ts +145 -0
- package/server/service/actuation/rule-evaluate.ts +352 -0
- package/server/service/actuation/rule-loop.ts +160 -0
- package/server/service/actuation/rule-runner.ts +216 -0
- package/server/service/actuation/rule-wiring.ts +84 -0
- package/server/service/actuation/settle-effects.ts +115 -0
- package/server/service/actuation/sweep-dispatch.ts +179 -0
- package/server/service/actuation/twin-command-resolver.ts +387 -0
- package/server/service/actuation/twin-command.ts +128 -1
- package/server/service/actuation/twin-now.ts +97 -0
- package/server/service/index.ts +9 -1
- package/server/service/reference/actuation-routing.ts +165 -0
- package/server/service/reference/actuation-target.ts +163 -0
- package/server/service/reference/connection-portability-resolver.ts +171 -0
- package/server/service/reference/connection-portability.ts +285 -0
- package/server/service/reference/fill-cadence.ts +29 -0
- package/server/service/reference/fill-dedupe.ts +66 -0
- package/server/service/reference/fill-loop.ts +153 -0
- package/server/service/reference/fill-span.ts +31 -0
- package/server/service/reference/hook-contract.ts +185 -5
- package/server/service/reference/hook-rejected.ts +53 -0
- package/server/service/reference/index.ts +5 -1
- package/server/service/reference/live-cadence.ts +129 -0
- package/server/service/reference/live-feed-lease.ts +56 -0
- package/server/service/reference/live-ingest.ts +94 -0
- package/server/service/reference/reference-adapter.ts +327 -5
- package/server/service/reference/reference-fill-run.ts +165 -0
- package/server/service/reference/reference-fill.ts +223 -0
- package/server/service/reference/reference-hook.ts +162 -56
- package/server/service/reference/reference-link-groups.ts +21 -0
- package/server/service/reference/reference-links.ts +77 -0
- package/server/service/reference/reference-live.ts +30 -5
- package/server/service/reference/reference-master.ts +90 -5
- package/server/service/reference/reference-resolver.ts +77 -2
- package/server/service/twin-event/twin-event-keys.ts +37 -4
- package/server/service/twin-event/twin-event.ts +18 -1
- package/server/service/twin-model/choose-recipe.ts +31 -0
- package/server/service/twin-model/external-resolver.ts +161 -0
- package/server/service/twin-model/project-structure.ts +16 -3
- package/server/service/twin-model/twin-equipment.ts +22 -0
- package/server/service/twin-model/twin-lineage-query.ts +82 -1
- package/server/service/twin-model/twin-model-item-query.ts +95 -4
- package/server/service/twin-model/twin-model-tree-query.ts +1 -1
- package/server/service/twin-space/twin-space.ts +39 -0
- package/server/service/twin-subject/event-subjects.ts +8 -1
- package/shared/entity-delta.ts +1 -1
- package/test/ack-shape-alignment.test.ts +97 -0
- package/test/actuation-approval-door.test.ts +421 -0
- package/test/actuation-dispatch.test.ts +16 -3
- package/test/actuation-effect.test.ts +162 -0
- package/test/actuation-outcome.test.ts +152 -0
- package/test/actuation-rule-runner.test.ts +281 -0
- package/test/actuation-rule.test.ts +266 -0
- package/test/actuation-seam.test.ts +249 -0
- package/test/actuation-settle.test.ts +164 -0
- package/test/actuation-target.test.ts +145 -0
- package/test/canonical-ingest-vocabularies.test.ts +49 -2
- package/test/capability-mapping.test.ts +1 -1
- package/test/choose-recipe.test.ts +117 -0
- package/test/command-store-writes-columns.test.ts +126 -0
- package/test/connection-portability-doors.test.ts +87 -0
- package/test/connector-capability-declaration.test.ts +54 -4
- package/test/declaration-reaches-model.test.ts +43 -0
- package/test/dedupe-transformation.test.ts +112 -0
- package/test/entity-delta.test.ts +1 -1
- package/test/event-subjects.test.ts +2 -1
- package/test/external-resolver.test.ts +171 -0
- package/test/fill-dedupe.test.ts +59 -0
- package/test/fill-loop.test.ts +35 -0
- package/test/first-envelope.test.ts +153 -0
- package/test/fold-shift-dst.test.ts +110 -0
- package/test/home-in-area.test.ts +90 -0
- package/test/hook-rejected-shape.test.ts +86 -0
- package/test/hook-response-sum.test.ts +193 -0
- package/test/hook-sequence.test.ts +1 -1
- package/test/ingest-dedupe.test.ts +56 -0
- package/test/ingest-expected-quiet.test.ts +111 -0
- package/test/ingest-idempotent.test.ts +79 -0
- package/test/ingest-reconcile-four-ways.test.ts +97 -0
- package/test/ingest-rules-header.test.ts +100 -0
- package/test/ingest-window-rolls-unobserved.test.ts +72 -0
- package/test/intake-mapping-parity.test.ts +175 -0
- package/test/live-cadence.test.ts +170 -0
- package/test/live-feed-lease.test.ts +63 -0
- package/test/live-mirror-parity.test.ts +1 -1
- package/test/oee-accumulator.test.ts +3 -3
- package/test/oee-site-calendar.test.ts +146 -0
- package/test/order-and-transaction-split.test.ts +107 -0
- package/test/reference-fill.test.ts +235 -0
- package/test/reference-hook.test.ts +1 -1
- package/test/reference-links.test.ts +56 -0
- package/test/retry-plan.test.ts +139 -0
- package/test/sweep-dispatch.test.ts +191 -0
- package/test/twin-event-keys.test.ts +20 -6
- package/test/twin-model-item-db.test.ts +6 -6
- package/test/twin-model-tree-db.test.ts +6 -6
- package/tsconfig.shared.tsbuildinfo +1 -1
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 같은 사실을 두 번 받았을 때 한 번으로 세는 근거 — **정체**.
|
|
3
|
+
*
|
|
4
|
+
* 정체를 못 만들면 그 사실은 dedupe 밖이고, 다시 받을 때마다 저널에 새로 앉는다. backfill 이 그것을
|
|
5
|
+
* 드러냈다(2026-09-06).
|
|
6
|
+
*/
|
|
7
|
+
import { test } from 'node:test'
|
|
8
|
+
import assert from 'node:assert/strict'
|
|
9
|
+
|
|
10
|
+
import { factIdentity, takeUnidentified } from '../server/engine/ingest-dedupe.js'
|
|
11
|
+
|
|
12
|
+
/* ── 주체가 subjectId 인 사실 ───────────────────────────────────────────────── */
|
|
13
|
+
|
|
14
|
+
test('★ 처분과 시험은 subjectId 가 주체다 — 그 이름이 없으면 정체가 안 만들어진다', () => {
|
|
15
|
+
/*
|
|
16
|
+
* [측정함 2026-09-06] `fillTwinScope` 를 돌릴 때마다 처분 4건 · 시험 3건이 다시 앉았다. 나머지
|
|
17
|
+
* 34건은 걸러지는데 이 둘만 계속 늘었다.
|
|
18
|
+
*
|
|
19
|
+
* 원인은 `discriminatorsOf` 에 `subjectId` 가 없어서 `parts` 가 비고, `factIdentity` 가
|
|
20
|
+
* `undefined` 를 낸 것이다. 정체가 없으면 dedupe 밖이다.
|
|
21
|
+
*/
|
|
22
|
+
const disposition = {
|
|
23
|
+
eventType: 'nonconformance.disposition',
|
|
24
|
+
eventTime: '2026-09-01T02:50:51.832Z',
|
|
25
|
+
data: { subjectId: 'LOT-A', decision: 'scrap', quantity: 50 }
|
|
26
|
+
}
|
|
27
|
+
const id = factIdentity(disposition)
|
|
28
|
+
assert.ok(id, '정체를 못 만들면 다시 받을 때마다 새로 앉는다')
|
|
29
|
+
assert.ok(id!.includes('subject=LOT-A'), `주체가 정체에 안 들어갔다: ${id}`)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test('★ 다른 로트의 같은 처분은 다른 사실이다', () => {
|
|
33
|
+
const at = '2026-09-01T02:50:51.832Z'
|
|
34
|
+
const a = factIdentity({ eventType: 'nonconformance.disposition', eventTime: at, data: { subjectId: 'LOT-A', decision: 'scrap' } })
|
|
35
|
+
const b = factIdentity({ eventType: 'nonconformance.disposition', eventTime: at, data: { subjectId: 'LOT-F', decision: 'scrap' } })
|
|
36
|
+
assert.notEqual(a, b, '두 로트의 처분이 한 사실로 뭉쳤다')
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
test('★ 같은 사실을 다시 받으면 같은 정체다 — backfill 이 저널을 안 부풀린다', () => {
|
|
40
|
+
const rec = { eventType: 'nonconformance.disposition', eventTime: '2026-09-01T02:50:51.832Z', data: { subjectId: 'LOT-A', decision: 'scrap', quantity: 50 } }
|
|
41
|
+
assert.equal(factIdentity(rec), factIdentity({ ...rec, data: { ...rec.data } }))
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
test('★ 시험 결과는 명세도 가른다 — 같은 대상에 시험이 여럿이다', () => {
|
|
45
|
+
const at = '2026-09-01T00:00:00.000Z'
|
|
46
|
+
const a = factIdentity({ eventType: 'test.result', eventTime: at, data: { subjectId: 'EQ-1', specId: 'CP-PBA-INCOMING' } })
|
|
47
|
+
const b = factIdentity({ eventType: 'test.result', eventTime: at, data: { subjectId: 'EQ-1', specId: 'CP-PBA-TEST' } })
|
|
48
|
+
assert.notEqual(a, b, '같은 대상의 두 시험이 한 사실로 뭉쳤다')
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test('★ 정체를 못 만든 것을 센다 — 조용히 dedupe 밖으로 나가지 않는다', () => {
|
|
52
|
+
takeUnidentified()
|
|
53
|
+
assert.equal(factIdentity({ eventType: 'made.up.kind', eventTime: '2026-09-01T00:00:00.000Z', data: {} }), undefined)
|
|
54
|
+
assert.deepEqual(takeUnidentified(), { 'made.up.kind': 1 })
|
|
55
|
+
assert.deepEqual(takeUnidentified(), {}, '읽고 비운다')
|
|
56
|
+
})
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* ★ **지금은 조용한 것이 정상이다** — 밤마다 나던 거짓 빨강.
|
|
3
|
+
*
|
|
4
|
+
* ── 무엇이 틀렸나 (2026-09-04) ──────────────────────────────────────────────
|
|
5
|
+
* 유입 건강이 **한 자로 전부를 쟀다.** 10분 안에 아무것도 오지 않으면 「유입 없음」이다. 그런데
|
|
6
|
+
* 태양광은 밤에 발전하지 않고, 업무시간만 도는 원천은 저녁에 아무것도 내지 않는다. 정상인데
|
|
7
|
+
* **매일 밤 절반이 빨갛게 났다.**
|
|
8
|
+
*
|
|
9
|
+
* 거짓 빨강이 쌓이면 진짜 빨강도 같이 안 읽힌다. 오늘 UX 레인이 「늘 참인 경고는 벽지가 된다」고
|
|
10
|
+
* 한 것과 같은 부류다 — 사흘이면 그 자리에 무엇이 적혀도 안 읽는다.
|
|
11
|
+
*
|
|
12
|
+
* 그리고 15분마다 긷는 원천은 긷는 사이마다 빨갛다. 이 파일이 고치는 코드가 그 한계를 **스스로
|
|
13
|
+
* 주석에 적어 두고 있었다**: 「주기를 알게 되면 그 값으로 견주는 것이 맞다」.
|
|
14
|
+
*
|
|
15
|
+
* ── 초록도 빨강도 아닌 셋째 값 ──────────────────────────────────────────────
|
|
16
|
+
* 조용한 시간을 초록으로 칠하면 **밤에 정말 끊긴 원천**을 아침까지 아무도 모른다. 그래서 답은
|
|
17
|
+
* 「지금은 안 오는 것이 맞다」이고, 그것은 다른 값이어야 한다.
|
|
18
|
+
*/
|
|
19
|
+
import { test } from 'node:test'
|
|
20
|
+
import assert from 'node:assert/strict'
|
|
21
|
+
|
|
22
|
+
import { syncVerdictOf, STALE_AFTER_MS } from '../server/engine/ingest-health.js'
|
|
23
|
+
|
|
24
|
+
const NOW = 1_800_000_000_000
|
|
25
|
+
const long = NOW - STALE_AFTER_MS - 60_000 /* 한 창을 넘긴 침묵 */
|
|
26
|
+
|
|
27
|
+
const at = (over: Record<string, unknown> = {}) =>
|
|
28
|
+
syncVerdictOf({
|
|
29
|
+
feedState: 'attached',
|
|
30
|
+
recent: { offered: 100, rejected: 0 },
|
|
31
|
+
lastAtMs: NOW - 1_000,
|
|
32
|
+
nowMs: NOW,
|
|
33
|
+
lastUndeliveredAtMs: -1,
|
|
34
|
+
offeredTotal: 100,
|
|
35
|
+
...over
|
|
36
|
+
} as any)
|
|
37
|
+
|
|
38
|
+
/* ── 조용한 시간 ──────────────────────────────────────────────────────────── */
|
|
39
|
+
|
|
40
|
+
test('★ 조용한 시간의 긴 침묵은 「유입 없음」이 아니다', () => {
|
|
41
|
+
assert.equal(at({ lastAtMs: long, quietNow: true }), 'expected-quiet')
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
test('★ 같은 침묵이 활동 시간이면 「유입 없음」이다', () => {
|
|
45
|
+
assert.equal(at({ lastAtMs: long, quietNow: false }), 'no-traffic')
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
test('★ 달력을 선언하지 않았으면 지금까지처럼 답한다 — 모르는 것을 「조용할 때」로 읽지 않는다', () => {
|
|
49
|
+
/*
|
|
50
|
+
* `quietNow` 를 주지 않은 원천이 전부 「지금은 조용한 시간」이 되면, 정말 끊긴 원천이 통째로
|
|
51
|
+
* 조용해진다. 없으면 옛 거동 그대로다.
|
|
52
|
+
*/
|
|
53
|
+
assert.equal(at({ lastAtMs: long }), 'no-traffic')
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('★ 조용한 시간이어도 사실이 사라지고 있으면 그것이 먼저다', () => {
|
|
57
|
+
/*
|
|
58
|
+
* 버려지는 중은 **몇 시든 문제**다. 밤이라고 덮으면 밤새 새는 것을 아침에 안다.
|
|
59
|
+
*/
|
|
60
|
+
const v = at({ lastAtMs: long, quietNow: true, lastUndeliveredAtMs: NOW - 1_000 })
|
|
61
|
+
assert.equal(v, 'twin-not-running')
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test('★ 조용한 시간이어도 결선이 없으면 그것이 먼저다', () => {
|
|
65
|
+
assert.equal(at({ feedState: 'detached', lastAtMs: long, quietNow: true }), 'not-connected')
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test('★ 한 번도 받지 않았는데 지금이 조용한 시간이면 「모른다」가 아니다', () => {
|
|
69
|
+
/*
|
|
70
|
+
* 밤에 처음 붙인 태양광 연결이 「기다린다」로 보이면, 사람이 아침까지 무엇을 기다리는지 모른다.
|
|
71
|
+
*/
|
|
72
|
+
assert.equal(at({ lastAtMs: -1, recent: null, offeredTotal: 0, quietNow: true }), 'expected-quiet')
|
|
73
|
+
assert.equal(at({ lastAtMs: -1, recent: null, offeredTotal: 0 }), 'unknown')
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test('★ 창이 비었어도 조용한 시간이면 정상이다', () => {
|
|
77
|
+
assert.equal(at({ recent: { offered: 0, rejected: 0 }, quietNow: true }), 'expected-quiet')
|
|
78
|
+
assert.equal(at({ recent: { offered: 0, rejected: 0 } }), 'no-traffic')
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
test('조용한 시간에도 받고 있으면 받는 중이다 — 침묵이 아니면 이 축이 개입하지 않는다', () => {
|
|
82
|
+
assert.equal(at({ quietNow: true }), 'syncing')
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
/* ── 원천이 선언한 주기 ──────────────────────────────────────────────────── */
|
|
86
|
+
|
|
87
|
+
test('★ 15분마다 긷는 원천은 그 주기 안에서 정상이다', () => {
|
|
88
|
+
/*
|
|
89
|
+
* 창 하나(10분)로 재면 긷는 사이마다 빨갛다. 원천이 주기를 선언하면 그것으로 견준다.
|
|
90
|
+
*/
|
|
91
|
+
const twelveMin = NOW - 12 * 60_000
|
|
92
|
+
|
|
93
|
+
assert.equal(at({ lastAtMs: twelveMin }), 'no-traffic', '선언이 없으면 옛 거동')
|
|
94
|
+
assert.equal(at({ lastAtMs: twelveMin, expectedSilenceMs: 20 * 60_000 }), 'syncing')
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
test('★ 선언한 주기를 넘기면 그때는 「유입 없음」이다 — 선언이 면죄부가 아니다', () => {
|
|
98
|
+
const thirtyMin = NOW - 30 * 60_000
|
|
99
|
+
assert.equal(at({ lastAtMs: thirtyMin, expectedSilenceMs: 20 * 60_000 }), 'no-traffic')
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
test('0이나 음수 주기는 선언이 아니다 — 창 하나로 돌아간다', () => {
|
|
103
|
+
for (const bad of [0, -1, NaN]) {
|
|
104
|
+
assert.equal(at({ lastAtMs: long, expectedSilenceMs: bad }), 'no-traffic', `${bad} 를 주기로 받았다`)
|
|
105
|
+
}
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
test('★ 주기와 달력은 다른 축이다 — 주기를 넘겼는데 조용한 시간이면 정상이다', () => {
|
|
109
|
+
const thirtyMin = NOW - 30 * 60_000
|
|
110
|
+
assert.equal(at({ lastAtMs: thirtyMin, expectedSilenceMs: 20 * 60_000, quietNow: true }), 'expected-quiet')
|
|
111
|
+
})
|
|
@@ -130,3 +130,82 @@ test('트윈마다 따로 센다 — 한 트윈의 되풀이가 다른 트윈을
|
|
|
130
130
|
const { duplicates } = b.filter([energy('inv-1', 1)])
|
|
131
131
|
assert.equal(duplicates, 0)
|
|
132
132
|
})
|
|
133
|
+
|
|
134
|
+
/* ════════════════════════════════════════════════════════════════════════════
|
|
135
|
+
* ★ **구별할 값이 봉투 안에 있으면 다 본다** — 골라 쓰고 나머지를 버리던 자리.
|
|
136
|
+
*
|
|
137
|
+
* ── 실측 (2026-09-05, 인티그레이션·plant 레인) ─────────────────────────────
|
|
138
|
+
* plant 이 실적 둘을 보냈는데 한 건이 저널에 안 남았다.
|
|
139
|
+
*
|
|
140
|
+
* seq 36 quality.output moverId EQ-CT-MWELD-01 · taskId PR-CT-WIRE-01 · goodCount 12
|
|
141
|
+
* seq 41 quality.output moverId EQ-CT-MWELD-01 · taskId PR-CT-WIRE-02 · goodCount 9
|
|
142
|
+
*
|
|
143
|
+
* 시각이 같았고(원본이 끝시각을 복사) 대상을 **먼저 걸리는 하나**로 골라서 `moverId` 가 이겼다.
|
|
144
|
+
* 거부가 아니라 **되풀이로 세어졌다** — 유입 장부에 `offered 5 · rejected 0 · journaled 3`.
|
|
145
|
+
*
|
|
146
|
+
* 원인이 시험 데이터였다고 해서 거동이 옳아지지 않는다. 두 봉투는 `taskId` 도 수량도 달랐고,
|
|
147
|
+
* **구별할 값이 이미 봉투 안에 있었는데 판정이 그것을 안 봤다.**
|
|
148
|
+
*
|
|
149
|
+
* ── 이 파일이 스스로 적어 둔 값 ───────────────────────────────────────────
|
|
150
|
+
* 「모르는 것을 같다고 판단하면 서로 다른 사실이 하나로 합쳐진다.
|
|
151
|
+
* 알리지 않고 잃는 것보다 두 번 세는 것이 낫다.」
|
|
152
|
+
*
|
|
153
|
+
* 지금 거동은 **알리지 않고 잃는 쪽**이었다.
|
|
154
|
+
* ════════════════════════════════════════════════════════════════════════════ */
|
|
155
|
+
|
|
156
|
+
const AT_36 = '2026-09-05T06:50:00.000Z'
|
|
157
|
+
const quality = (taskId: string, goodCount: number) => ({
|
|
158
|
+
eventType: 'quality.output',
|
|
159
|
+
data: { moverId: 'EQ-CT-MWELD-01', taskId, goodCount, at: AT_36 }
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
test('★ 한 설비가 두 작업을 같은 시각에 보고해도 두 사실이다 — 실제로 한 건을 잃던 그 줄', () => {
|
|
163
|
+
const a = factIdentity(quality('PR-CT-WIRE-01', 12))
|
|
164
|
+
const b = factIdentity(quality('PR-CT-WIRE-02', 9))
|
|
165
|
+
|
|
166
|
+
assert.ok(a && b, '정체를 못 만들면 거르지 않는다 — 이 검사가 뜻이 없다')
|
|
167
|
+
assert.notEqual(a, b, '작업이 다른 두 실적이 한 사실이 됐다')
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
test('★ 같은 봉투를 다시 보내면 여전히 같은 사실이다 — 넓혀도 되풀이는 잡힌다', () => {
|
|
171
|
+
/*
|
|
172
|
+
* 넓히는 것이 「되풀이를 놓친다」로 이어지지 않는다는 것이 이 변경의 근거다. 같은 봉투는 이
|
|
173
|
+
* 값들이 전부 같으므로 정체도 같다.
|
|
174
|
+
*/
|
|
175
|
+
assert.equal(factIdentity(quality('PR-CT-WIRE-01', 12)), factIdentity(quality('PR-CT-WIRE-01', 12)))
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
test('★ 한 개체의 서로 다른 관측을 가른다 — EPCIS 는 그것이 정상이다', () => {
|
|
179
|
+
/*
|
|
180
|
+
* 저널 246만 건에 옛 규칙을 다시 걸어 여섯 묶음이 나왔고 전부 이 모양이었다. 한 물건이 한
|
|
181
|
+
* 순간에 입고이면서 개시일 수 있다 — 표준에서 이상한 일이 아닌데 규칙이 그 셋을 안 봤다.
|
|
182
|
+
*/
|
|
183
|
+
const epc = 'urn:epc:id:sgtin:9521321.011111.1'
|
|
184
|
+
const receiving = {
|
|
185
|
+
eventType: 'epcis.ObjectEvent',
|
|
186
|
+
data: { epcList: [epc], bizStep: 'receiving', action: 'ADD', disposition: 'sellable_accessible', eventTime: AT_36 }
|
|
187
|
+
}
|
|
188
|
+
const commissioning = {
|
|
189
|
+
eventType: 'epcis.ObjectEvent',
|
|
190
|
+
data: { epcList: [epc], bizStep: 'commissioning', action: 'OBSERVE', disposition: 'reserved', eventTime: AT_36 }
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
assert.notEqual(factIdentity(receiving), factIdentity(commissioning), '입고와 개시가 한 사실이 됐다')
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
test('★ 참조가 달라도 갈린다 — 주체만 보고 나머지를 버리지 않는다', () => {
|
|
197
|
+
/*
|
|
198
|
+
* 「주체를 먼저 본다」는 판단 자체는 옳았다. 틀린 것은 **고르고 나머지를 버린 것**이다.
|
|
199
|
+
*/
|
|
200
|
+
const one = { eventType: 'task.status', data: { taskId: 'T-1', orderId: 'order-1', at: AT_36 } }
|
|
201
|
+
const two = { eventType: 'task.status', data: { taskId: 'T-1', orderId: 'order-2', at: AT_36 } }
|
|
202
|
+
|
|
203
|
+
assert.notEqual(factIdentity(one), factIdentity(two))
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
test('★ 이 검사가 실제로 무언가를 잰다 — 견준 것이 0이면 빨강', () => {
|
|
207
|
+
/* 가르는 값이 하나도 없으면 정체를 안 만든다(거르지 않고 통과) — 그 경계가 살아 있는지. */
|
|
208
|
+
assert.equal(factIdentity({ eventType: 'x', data: { at: AT_36 } }), undefined, '대상 없는 봉투에 정체를 만들었다')
|
|
209
|
+
assert.equal(factIdentity({ eventType: 'x', data: { taskId: 'T-1' } }), undefined, '시각 없는 봉투에 정체를 만들었다')
|
|
210
|
+
assert.ok(factIdentity(quality('T-1', 1))!.includes('task=T-1'), '작업이 정체에 안 들어갔다')
|
|
211
|
+
})
|
|
@@ -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
|
+
})
|