@things-factory/headless-twin 10.0.10 → 10.0.12
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 +25 -2
- package/dist-server/engine/canonical-ingest.js +48 -10
- package/dist-server/engine/canonical-ingest.js.map +1 -1
- package/dist-server/engine/declared-stimulus.d.ts +43 -0
- package/dist-server/engine/declared-stimulus.js +57 -0
- package/dist-server/engine/declared-stimulus.js.map +1 -0
- package/dist-server/engine/index.d.ts +2 -0
- package/dist-server/engine/index.js +2 -0
- package/dist-server/engine/index.js.map +1 -1
- package/dist-server/engine/kpi-fold.d.ts +12 -0
- package/dist-server/engine/kpi-fold.js +21 -1
- package/dist-server/engine/kpi-fold.js.map +1 -1
- package/dist-server/engine/kpi-query.d.ts +3 -3
- package/dist-server/engine/kpi-query.js +4 -4
- package/dist-server/engine/kpi-query.js.map +1 -1
- package/dist-server/engine/live-feed-registry.d.ts +1 -1
- package/dist-server/engine/live-feed-registry.js +1 -1
- package/dist-server/engine/live-feed-registry.js.map +1 -1
- package/dist-server/engine/local-declarations.d.ts +3 -6
- package/dist-server/engine/local-declarations.js +97 -16
- package/dist-server/engine/local-declarations.js.map +1 -1
- package/dist-server/engine/measured-yield.d.ts +42 -0
- package/dist-server/engine/measured-yield.js +75 -0
- package/dist-server/engine/measured-yield.js.map +1 -0
- package/dist-server/engine/operation-basis.d.ts +16 -0
- package/dist-server/engine/operation-basis.js +20 -2
- package/dist-server/engine/operation-basis.js.map +1 -1
- package/dist-server/engine/property-effects.js +17 -0
- package/dist-server/engine/property-effects.js.map +1 -1
- package/dist-server/engine/restart-policy.d.ts +13 -0
- package/dist-server/engine/restart-policy.js +52 -0
- package/dist-server/engine/restart-policy.js.map +1 -0
- package/dist-server/engine/spec-coverage.d.ts +8 -0
- package/dist-server/engine/spec-coverage.js +3 -1
- package/dist-server/engine/spec-coverage.js.map +1 -1
- package/dist-server/engine/twin-engine.d.ts +48 -15
- package/dist-server/engine/twin-engine.js +179 -39
- package/dist-server/engine/twin-engine.js.map +1 -1
- package/dist-server/index.js +1 -1
- package/dist-server/index.js.map +1 -1
- package/dist-server/service/index.d.ts +1 -1
- package/dist-server/service/reference/control-routing.d.ts +14 -0
- package/dist-server/service/reference/control-routing.js +64 -0
- package/dist-server/service/reference/control-routing.js.map +1 -0
- package/dist-server/service/reference/index.d.ts +1 -0
- package/dist-server/service/reference/index.js +2 -0
- package/dist-server/service/reference/index.js.map +1 -1
- package/dist-server/service/reference/reference-adapter.d.ts +67 -0
- package/dist-server/service/reference/reference-adapter.js +18 -0
- package/dist-server/service/reference/reference-adapter.js.map +1 -1
- package/dist-server/service/reference/reference-live.js +3 -3
- package/dist-server/service/reference/reference-live.js.map +1 -1
- package/dist-server/service/reference/reference-resolver.d.ts +3 -3
- package/dist-server/service/reference/reference-resolver.js +35 -18
- package/dist-server/service/reference/reference-resolver.js.map +1 -1
- package/dist-server/service/twin-audit/command-audit.d.ts +34 -0
- package/dist-server/service/twin-audit/command-audit.js +15 -1
- package/dist-server/service/twin-audit/command-audit.js.map +1 -1
- package/dist-server/service/twin-audit/twin-audit-event.d.ts +3 -0
- package/dist-server/service/twin-audit/twin-audit-event.js +28 -2
- package/dist-server/service/twin-audit/twin-audit-event.js.map +1 -1
- package/dist-server/service/twin-control/twin-control-mutation.d.ts +15 -2
- package/dist-server/service/twin-control/twin-control-mutation.js +85 -34
- package/dist-server/service/twin-control/twin-control-mutation.js.map +1 -1
- package/dist-server/service/twin-event/twin-event.js +1 -1
- package/dist-server/service/twin-event/twin-event.js.map +1 -1
- package/dist-server/service/twin-instance/twin-instance.d.ts +1 -1
- package/dist-server/service/twin-instance/twin-instance.js +2 -2
- package/dist-server/service/twin-instance/twin-instance.js.map +1 -1
- package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js +2 -1
- package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js.map +1 -1
- package/dist-server/service/twin-model/twin-equipment.js +1 -1
- package/dist-server/service/twin-model/twin-equipment.js.map +1 -1
- package/dist-server/service/twin-model/twin-location.js +1 -1
- package/dist-server/service/twin-model/twin-location.js.map +1 -1
- package/dist-server/service/twin-model/twin-model-query.js +1 -1
- package/dist-server/service/twin-model/twin-model-query.js.map +1 -1
- package/dist-server/service/twin-model/twin-operation.js +1 -1
- package/dist-server/service/twin-model/twin-operation.js.map +1 -1
- package/package.json +3 -3
- package/server/engine/canonical-ingest.ts +78 -14
- package/server/engine/declared-stimulus.ts +66 -0
- package/server/engine/index.ts +2 -0
- package/server/engine/kpi-fold.ts +29 -1
- package/server/engine/kpi-query.ts +8 -8
- package/server/engine/live-feed-registry.ts +2 -2
- package/server/engine/local-declarations.ts +97 -19
- package/server/engine/measured-yield.ts +89 -0
- package/server/engine/operation-basis.ts +33 -2
- package/server/engine/property-effects.ts +17 -0
- package/server/engine/restart-policy.ts +55 -0
- package/server/engine/spec-coverage.ts +23 -3
- package/server/engine/twin-engine.ts +201 -42
- package/server/index.ts +1 -1
- package/server/service/reference/control-routing.ts +62 -0
- package/server/service/reference/index.ts +1 -0
- package/server/service/reference/reference-adapter.ts +78 -0
- package/server/service/reference/reference-live.ts +3 -3
- package/server/service/reference/reference-resolver.ts +46 -7
- package/server/service/twin-audit/command-audit.ts +34 -1
- package/server/service/twin-audit/twin-audit-event.ts +50 -4
- package/server/service/twin-control/twin-control-mutation.ts +75 -28
- package/server/service/twin-event/twin-event.ts +10 -2
- package/server/service/twin-instance/twin-instance.ts +16 -10
- package/server/service/twin-lifecycle/twin-lifecycle-mutation.ts +2 -1
- package/server/service/twin-model/twin-equipment.ts +8 -1
- package/server/service/twin-model/twin-location.ts +8 -1
- package/server/service/twin-model/twin-model-query.ts +1 -1
- package/server/service/twin-model/twin-operation.ts +8 -1
- package/test/adopt-structure-live.test.ts +7 -7
- package/test/boot-resume.test.ts +3 -3
- package/test/column-type-portability.test.ts +122 -0
- package/test/control-capability.test.ts +103 -0
- package/test/declared-stimulus.test.ts +88 -0
- package/test/ingest-running-guard.test.ts +5 -5
- package/test/instance-cache-lifecycle.test.ts +1 -1
- package/test/kpi-baseline-db.test.ts +1 -1
- package/test/kpi-query-bench.test.ts +1 -1
- package/test/lineage-survives-restart.test.ts +3 -3
- package/test/live-feed-registry.test.ts +6 -6
- package/test/local-declarations.test.ts +108 -1
- package/test/measured-yield.test.ts +90 -0
- package/test/operation-basis.test.ts +28 -1
- package/test/operational-vocabulary.test.ts +108 -0
- package/test/operations-capability-db.test.ts +4 -4
- package/test/project-structure-db.test.ts +1 -1
- package/test/projection-reaches-screen.test.ts +1 -1
- package/test/property-effects.test.ts +28 -0
- package/test/restart-policy.test.ts +111 -0
- package/test/resync-origin-site.test.ts +7 -1
- package/test/source-outcome-audit.test.ts +104 -0
- package/test/structure-revision-db.test.ts +27 -27
- package/test/tenant-registry-db.test.ts +2 -2
- package/test/twin-model-item-db.test.ts +3 -3
- package/test/twin-model-tree-db.test.ts +7 -7
- package/test/twin-origin-resync.test.ts +4 -4
- package/test/yield-loop.test.ts +144 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { type EnergyRecord, type EnergyEquipmentRecord, type IngestResult } from '@operato/twin-kernel';
|
|
1
|
+
import { type EnergyRecord, type EnergyEquipmentRecord, type OperationalRecord, type IngestResult } from '@operato/twin-kernel';
|
|
2
|
+
/** 개체 관측 — 물건 하나가 어디서 무엇이 됐나. */
|
|
2
3
|
export interface CanonicalRecord {
|
|
3
4
|
epc: string;
|
|
4
5
|
action: string;
|
|
@@ -7,5 +8,27 @@ export interface CanonicalRecord {
|
|
|
7
8
|
readPoint?: string;
|
|
8
9
|
bizLocation?: string;
|
|
9
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* 담김 관측 — **부모와 자식**으로 말한다. 자리 필드는 개체 관측과 같은 이름이다(문자열).
|
|
13
|
+
*
|
|
14
|
+
* 이름은 표준의 것을 그대로 쓴다(`parentID`·`childEPCs`) — 여기서 다시 지으면 매핑이 번역이 되고,
|
|
15
|
+
* 같은 사실에 두 어휘가 생긴다.
|
|
16
|
+
*/
|
|
17
|
+
export interface CanonicalAggregationRecord {
|
|
18
|
+
parentID: string;
|
|
19
|
+
/** 개체로 담긴 자식. 수량으로만 담겼으면 없다(둘 중 하나는 있어야 한다). */
|
|
20
|
+
childEPCs?: string[];
|
|
21
|
+
/** 수량으로 담긴 자식 — `{ epcClass, quantity, uom? }`. 값의 옳고 그름은 커널 검증이 본다. */
|
|
22
|
+
childQuantityList?: {
|
|
23
|
+
epcClass: string;
|
|
24
|
+
quantity?: number;
|
|
25
|
+
uom?: string;
|
|
26
|
+
}[];
|
|
27
|
+
action: string;
|
|
28
|
+
bizStep: string;
|
|
29
|
+
disposition?: string;
|
|
30
|
+
readPoint?: string;
|
|
31
|
+
bizLocation?: string;
|
|
32
|
+
}
|
|
10
33
|
/** 정규 레코드[] → 검증된 CanonicalEnvelope[] (+ 거부분). 순수 함수. 단일→객체도 배열로 정규화. */
|
|
11
|
-
export declare function ingestCanonicalRecords(records: CanonicalRecord | EnergyRecord | EnergyEquipmentRecord | (CanonicalRecord | EnergyRecord | EnergyEquipmentRecord)[] | undefined | null, tenantId: string, defaultEventTime: string): IngestResult;
|
|
34
|
+
export declare function ingestCanonicalRecords(records: CanonicalRecord | CanonicalAggregationRecord | EnergyRecord | EnergyEquipmentRecord | OperationalRecord | (CanonicalRecord | CanonicalAggregationRecord | EnergyRecord | EnergyEquipmentRecord | OperationalRecord)[] | undefined | null, tenantId: string, defaultEventTime: string): IngestResult;
|
|
@@ -6,7 +6,11 @@ const twin_kernel_1 = require("@operato/twin-kernel");
|
|
|
6
6
|
* 정규 레코드 → 커널 검증 ACL → CanonicalEnvelope (P1 seam 코어).
|
|
7
7
|
* 순수(커널만 의존; shell·integration-base 무의존 → node:test 검증 가능).
|
|
8
8
|
* 매핑(레거시 스키마 → 정규)은 이 앞 단계(jsonata step 또는 커넥터 openLiveFeed)가 끝낸다.
|
|
9
|
-
* 여기 도착하는 레코드는 이미 정규
|
|
9
|
+
* 여기 도착하는 레코드는 이미 정규 필드다 — 어휘마다 자기 필드 이름이 계약이다:
|
|
10
|
+
* EPCIS 품목 { epc, action, bizStep, disposition?, readPoint?, bizLocation? }
|
|
11
|
+
* 에너지 계량 { meterId, kW?, kWh?, … } · 설비 에너지 { equipmentId, generatedKW?, soc?, … }
|
|
12
|
+
* 담김(적재) { parentID, childEPCs[] | childQuantityList[], action, bizStep, … }
|
|
13
|
+
* 운영 사실 { taskId | moverId | personId | assetId | orderId, status, … } // vocabulary-guard: allow 저널 와이어 필드
|
|
10
14
|
* 커널은 objectEvent 구성 + EPCIS 검증(위반=오염 차단) + 봉투 래핑만(방언 없음).
|
|
11
15
|
*
|
|
12
16
|
* 두 소비처가 이 단일 코어를 공유(중복 금지):
|
|
@@ -14,7 +18,7 @@ const twin_kernel_1 = require("@operato/twin-kernel");
|
|
|
14
18
|
* - live 커넥터 경로: reference-live(어댑터 openLiveFeed→여기).
|
|
15
19
|
* SoT: design/integration/face2-adapters.md §5(seam)·§7(매핑=밖, 검증=커널).
|
|
16
20
|
*/
|
|
17
|
-
/** 고정 정규 매핑(방언 아님 — 필드명 자체가 정규 계약). sourceType
|
|
21
|
+
/** 고정 정규 매핑(방언 아님 — 필드명 자체가 정규 계약). `sourceType` 으로 어느 품목 사실인지 고른다. */
|
|
18
22
|
const TWIN_INGEST_RULES = [
|
|
19
23
|
{
|
|
20
24
|
sourceType: 'twin',
|
|
@@ -27,34 +31,68 @@ const TWIN_INGEST_RULES = [
|
|
|
27
31
|
readPoint: '$.readPoint',
|
|
28
32
|
bizLocation: '$.bizLocation'
|
|
29
33
|
}
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
/*
|
|
37
|
+
* **담김의 사실** — 무엇이 무엇에 실렸나(팔레트·상자). 오래 비어 있던 자리다: 관측 리듀서는 이 사실을
|
|
38
|
+
* 받으면 자식 물품에 `parent` 를 붙이는데, 문이 `ObjectEvent` 하나만 알아서 원본은 적재를 말할 길이
|
|
39
|
+
* 없었다(시뮬 원본을 붙여 돌려 보고서야 로그로 드러났다).
|
|
40
|
+
*/
|
|
41
|
+
sourceType: 'twin-aggregation',
|
|
42
|
+
mapping: {
|
|
43
|
+
type: 'AggregationEvent',
|
|
44
|
+
action: '$.action',
|
|
45
|
+
bizStep: '$.bizStep',
|
|
46
|
+
disposition: '$.disposition',
|
|
47
|
+
parentID: '$.parentID',
|
|
48
|
+
childEPCs: '$.childEPCs',
|
|
49
|
+
/* 낱개 식별자 없이 **수량으로** 담긴 자식(「이 품번 40개」) — 입고·포장이 이 모양으로 온다. */
|
|
50
|
+
childQuantityList: '$.childQuantityList',
|
|
51
|
+
readPoint: '$.readPoint',
|
|
52
|
+
bizLocation: '$.bizLocation'
|
|
53
|
+
}
|
|
30
54
|
}
|
|
31
55
|
];
|
|
32
56
|
/** 정규 레코드[] → 검증된 CanonicalEnvelope[] (+ 거부분). 순수 함수. 단일→객체도 배열로 정규화. */
|
|
33
57
|
function ingestCanonicalRecords(records, tenantId, defaultEventTime) {
|
|
34
58
|
const arr = Array.isArray(records) ? records : records ? [records] : [];
|
|
35
59
|
/*
|
|
36
|
-
* **어휘가
|
|
37
|
-
* 그래서 계측을 이 길로 넣으면 `epc` 가 없어 거부되거나(그나마 나은 쪽)
|
|
60
|
+
* **어휘가 넷이다** — 물류(EPCIS)·에너지 계량·설비 에너지·운영 사실. 예전에는 이 함수가 무조건
|
|
61
|
+
* `ObjectEvent` 를 만들었고, 그래서 계측을 이 길로 넣으면 `epc` 가 없어 거부되거나(그나마 나은 쪽)
|
|
62
|
+
* 엉뚱한 물품 관측이 됐다.
|
|
63
|
+
*
|
|
64
|
+
* 물류 어휘 안에는 모양이 둘이다(개체 관측 `epc` / 담김 `parentID`+`childEPCs`).
|
|
38
65
|
*
|
|
39
|
-
* 판정은 커널이 한 곳에서 한다(`isEnergyRecord`) — 여기서 다시 짐작하면
|
|
40
|
-
* 봉투는 같은 것을 쓰므로 아래 소비처(미러·저널·방송)는 이 갈림을 알
|
|
66
|
+
* 판정은 커널이 한 곳에서 한다(`isEnergyRecord`·`isOperationalRecord`·`isAggregationRecord`) — 여기서 다시 짐작하면
|
|
67
|
+
* 소비처마다 답이 달라진다. 봉투는 같은 것을 쓰므로 아래 소비처(미러·저널·방송)는 이 갈림을 알
|
|
68
|
+
* 필요가 없다.
|
|
41
69
|
*/
|
|
42
70
|
const energy = arr.filter(twin_kernel_1.isEnergyRecord);
|
|
43
71
|
/* 세 번째 어휘 — 설비가 낸 자기 에너지 상태(발전·저장·감축 여지·개폐 위치). 계량과 갈라 두는
|
|
44
72
|
이유는 커널에 적혀 있다: 계량은 구간에 누적되고, 이것들은 그 설비의 지금이다. */
|
|
45
73
|
const energyEquipment = arr.filter(twin_kernel_1.isEnergyEquipmentRecord);
|
|
46
|
-
|
|
74
|
+
/*
|
|
75
|
+
* 네 번째 어휘 — **운영 사실**(작업·설비·사람·자산·오더·품질). 관측 리듀서는 이것을 오래전부터
|
|
76
|
+
* 접었는데 **들어올 문이 없었다**: 원본이 「이 작업이 끝났다」를 말할 길이 없어 시뮬 커널만 그것을
|
|
77
|
+
* 아는 상태가 남았다(같은 화면이 두 구동에서 다른 것을 말한다).
|
|
78
|
+
*/
|
|
79
|
+
const operational = arr.filter(twin_kernel_1.isOperationalRecord);
|
|
80
|
+
const epcis = arr.filter(r => !(0, twin_kernel_1.isEnergyRecord)(r) && !(0, twin_kernel_1.isEnergyEquipmentRecord)(r) && !(0, twin_kernel_1.isOperationalRecord)(r));
|
|
81
|
+
/* 품목 어휘 안에서도 갈림이 있다(개체 관측 / 담김) — 그 판정도 커널의 것을 부른다. */
|
|
47
82
|
const epcisResult = epcis.length
|
|
48
|
-
? (0, twin_kernel_1.ingest)(epcis.map(r => ({ ...r, sourceType: 'twin' })), TWIN_INGEST_RULES, { tenantId, defaultEventTime })
|
|
83
|
+
? (0, twin_kernel_1.ingest)(epcis.map(r => ({ ...r, sourceType: (0, twin_kernel_1.isAggregationRecord)(r) ? 'twin-aggregation' : 'twin' })), TWIN_INGEST_RULES, { tenantId, defaultEventTime })
|
|
49
84
|
: { accepted: [], rejected: [] };
|
|
50
85
|
const energyResult = energy.length ? (0, twin_kernel_1.ingestEnergyRecords)(energy, { tenantId, defaultEventTime }) : { accepted: [], rejected: [] };
|
|
51
86
|
const equipmentResult = energyEquipment.length
|
|
52
87
|
? (0, twin_kernel_1.ingestEnergyEquipmentRecords)(energyEquipment, { tenantId, defaultEventTime })
|
|
53
88
|
: { accepted: [], rejected: [] };
|
|
89
|
+
const operationalResult = operational.length
|
|
90
|
+
? (0, twin_kernel_1.ingestOperationalRecords)(operational, { tenantId, defaultEventTime })
|
|
91
|
+
: { accepted: [], rejected: [] };
|
|
54
92
|
/* 거부분을 합쳐 돌려준다 — 어느 어휘에서 거부됐는지는 이유 문구가 말한다. */
|
|
55
93
|
return {
|
|
56
|
-
accepted: [...epcisResult.accepted, ...energyResult.accepted, ...equipmentResult.accepted],
|
|
57
|
-
rejected: [...epcisResult.rejected, ...energyResult.rejected, ...equipmentResult.rejected]
|
|
94
|
+
accepted: [...epcisResult.accepted, ...energyResult.accepted, ...equipmentResult.accepted, ...operationalResult.accepted],
|
|
95
|
+
rejected: [...epcisResult.rejected, ...energyResult.rejected, ...equipmentResult.rejected, ...operationalResult.rejected]
|
|
58
96
|
};
|
|
59
97
|
}
|
|
60
98
|
//# sourceMappingURL=canonical-ingest.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"canonical-ingest.js","sourceRoot":"","sources":["../../server/engine/canonical-ingest.ts"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"canonical-ingest.js","sourceRoot":"","sources":["../../server/engine/canonical-ingest.ts"],"names":[],"mappings":"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","sourcesContent":["import { ingest, ingestEnergyRecords, ingestEnergyEquipmentRecords, ingestOperationalRecords, isAggregationRecord,\n isEnergyRecord, isEnergyEquipmentRecord, isOperationalRecord, type AdapterRule, type EnergyRecord,\n type EnergyEquipmentRecord, type OperationalRecord, type IngestResult } from '@operato/twin-kernel'\n\n/*\n * 정규 레코드 → 커널 검증 ACL → CanonicalEnvelope (P1 seam 코어).\n * 순수(커널만 의존; shell·integration-base 무의존 → node:test 검증 가능).\n * 매핑(레거시 스키마 → 정규)은 이 앞 단계(jsonata step 또는 커넥터 openLiveFeed)가 끝낸다.\n * 여기 도착하는 레코드는 이미 정규 필드다 — 어휘마다 자기 필드 이름이 계약이다:\n * EPCIS 품목 { epc, action, bizStep, disposition?, readPoint?, bizLocation? }\n * 에너지 계량 { meterId, kW?, kWh?, … } · 설비 에너지 { equipmentId, generatedKW?, soc?, … }\n * 담김(적재) { parentID, childEPCs[] | childQuantityList[], action, bizStep, … }\n * 운영 사실 { taskId | moverId | personId | assetId | orderId, status, … } // vocabulary-guard: allow 저널 와이어 필드\n * 커널은 objectEvent 구성 + EPCIS 검증(위반=오염 차단) + 봉투 래핑만(방언 없음).\n *\n * 두 소비처가 이 단일 코어를 공유(중복 금지):\n * - sim/scenario 경로: operato-twin twin-ingest 태스크(http-get→jsonata→여기).\n * - live 커넥터 경로: reference-live(어댑터 openLiveFeed→여기).\n * SoT: design/integration/face2-adapters.md §5(seam)·§7(매핑=밖, 검증=커널).\n */\n\n/** 고정 정규 매핑(방언 아님 — 필드명 자체가 정규 계약). `sourceType` 으로 어느 품목 사실인지 고른다. */\nconst TWIN_INGEST_RULES: AdapterRule[] = [\n {\n sourceType: 'twin',\n mapping: {\n type: 'ObjectEvent',\n action: '$.action',\n bizStep: '$.bizStep',\n disposition: '$.disposition',\n epc: '$.epc',\n readPoint: '$.readPoint',\n bizLocation: '$.bizLocation'\n }\n },\n {\n /*\n * **담김의 사실** — 무엇이 무엇에 실렸나(팔레트·상자). 오래 비어 있던 자리다: 관측 리듀서는 이 사실을\n * 받으면 자식 물품에 `parent` 를 붙이는데, 문이 `ObjectEvent` 하나만 알아서 원본은 적재를 말할 길이\n * 없었다(시뮬 원본을 붙여 돌려 보고서야 로그로 드러났다).\n */\n sourceType: 'twin-aggregation',\n mapping: {\n type: 'AggregationEvent',\n action: '$.action',\n bizStep: '$.bizStep',\n disposition: '$.disposition',\n parentID: '$.parentID',\n childEPCs: '$.childEPCs',\n /* 낱개 식별자 없이 **수량으로** 담긴 자식(「이 품번 40개」) — 입고·포장이 이 모양으로 온다. */\n childQuantityList: '$.childQuantityList',\n readPoint: '$.readPoint',\n bizLocation: '$.bizLocation'\n }\n }\n]\n\n/** 개체 관측 — 물건 하나가 어디서 무엇이 됐나. */\nexport interface CanonicalRecord {\n epc: string\n action: string // ADD | OBSERVE | DELETE\n bizStep: string\n disposition?: string\n readPoint?: string\n bizLocation?: string\n}\n\n/**\n * 담김 관측 — **부모와 자식**으로 말한다. 자리 필드는 개체 관측과 같은 이름이다(문자열).\n *\n * 이름은 표준의 것을 그대로 쓴다(`parentID`·`childEPCs`) — 여기서 다시 지으면 매핑이 번역이 되고,\n * 같은 사실에 두 어휘가 생긴다.\n */\nexport interface CanonicalAggregationRecord {\n parentID: string\n /** 개체로 담긴 자식. 수량으로만 담겼으면 없다(둘 중 하나는 있어야 한다). */\n childEPCs?: string[]\n /** 수량으로 담긴 자식 — `{ epcClass, quantity, uom? }`. 값의 옳고 그름은 커널 검증이 본다. */\n childQuantityList?: { epcClass: string; quantity?: number; uom?: string }[]\n action: string // ADD(담다) | DELETE(풀다) | OBSERVE\n bizStep: string\n disposition?: string\n readPoint?: string\n bizLocation?: string\n}\n\n/** 정규 레코드[] → 검증된 CanonicalEnvelope[] (+ 거부분). 순수 함수. 단일→객체도 배열로 정규화. */\nexport function ingestCanonicalRecords(\n records:\n | CanonicalRecord\n | CanonicalAggregationRecord\n | EnergyRecord\n | EnergyEquipmentRecord\n | OperationalRecord\n | (CanonicalRecord | CanonicalAggregationRecord | EnergyRecord | EnergyEquipmentRecord | OperationalRecord)[]\n | undefined\n | null,\n tenantId: string,\n defaultEventTime: string\n): IngestResult {\n const arr = Array.isArray(records) ? records : records ? [records] : []\n /*\n * **어휘가 넷이다** — 물류(EPCIS)·에너지 계량·설비 에너지·운영 사실. 예전에는 이 함수가 무조건\n * `ObjectEvent` 를 만들었고, 그래서 계측을 이 길로 넣으면 `epc` 가 없어 거부되거나(그나마 나은 쪽)\n * 엉뚱한 물품 관측이 됐다.\n *\n * 물류 어휘 안에는 모양이 둘이다(개체 관측 `epc` / 담김 `parentID`+`childEPCs`).\n *\n * 판정은 커널이 한 곳에서 한다(`isEnergyRecord`·`isOperationalRecord`·`isAggregationRecord`) — 여기서 다시 짐작하면\n * 소비처마다 답이 달라진다. 봉투는 같은 것을 쓰므로 아래 소비처(미러·저널·방송)는 이 갈림을 알\n * 필요가 없다.\n */\n const energy = arr.filter(isEnergyRecord) as EnergyRecord[]\n /* 세 번째 어휘 — 설비가 낸 자기 에너지 상태(발전·저장·감축 여지·개폐 위치). 계량과 갈라 두는\n 이유는 커널에 적혀 있다: 계량은 구간에 누적되고, 이것들은 그 설비의 지금이다. */\n const energyEquipment = arr.filter(isEnergyEquipmentRecord) as unknown as EnergyEquipmentRecord[]\n /*\n * 네 번째 어휘 — **운영 사실**(작업·설비·사람·자산·오더·품질). 관측 리듀서는 이것을 오래전부터\n * 접었는데 **들어올 문이 없었다**: 원본이 「이 작업이 끝났다」를 말할 길이 없어 시뮬 커널만 그것을\n * 아는 상태가 남았다(같은 화면이 두 구동에서 다른 것을 말한다).\n */\n const operational = arr.filter(isOperationalRecord) as OperationalRecord[]\n const epcis = arr.filter(r => !isEnergyRecord(r) && !isEnergyEquipmentRecord(r) && !isOperationalRecord(r)) as (CanonicalRecord | CanonicalAggregationRecord)[]\n\n /* 품목 어휘 안에서도 갈림이 있다(개체 관측 / 담김) — 그 판정도 커널의 것을 부른다. */\n const epcisResult = epcis.length\n ? ingest(\n epcis.map(r => ({ ...r, sourceType: isAggregationRecord(r) ? 'twin-aggregation' : 'twin' })),\n TWIN_INGEST_RULES,\n { tenantId, defaultEventTime }\n )\n : { accepted: [], rejected: [] }\n const energyResult = energy.length ? ingestEnergyRecords(energy, { tenantId, defaultEventTime }) : { accepted: [], rejected: [] }\n const equipmentResult = energyEquipment.length\n ? ingestEnergyEquipmentRecords(energyEquipment, { tenantId, defaultEventTime })\n : { accepted: [], rejected: [] }\n const operationalResult = operational.length\n ? ingestOperationalRecords(operational, { tenantId, defaultEventTime })\n : { accepted: [], rejected: [] }\n\n /* 거부분을 합쳐 돌려준다 — 어느 어휘에서 거부됐는지는 이유 문구가 말한다. */\n return {\n accepted: [...epcisResult.accepted, ...energyResult.accepted, ...equipmentResult.accepted, ...operationalResult.accepted],\n rejected: [...epcisResult.rejected, ...energyResult.rejected, ...equipmentResult.rejected, ...operationalResult.rejected]\n }\n}\n"]}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** 자극을 실을지 말지 — 그리고 왜. 부르는 쪽은 이 답만 보고 움직인다. */
|
|
2
|
+
export type StimulusPlan =
|
|
3
|
+
/** 실어라 — 검사를 통과한 선언. */
|
|
4
|
+
{
|
|
5
|
+
action: 'load';
|
|
6
|
+
scenario: any;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* 싣지 않는다 — 그 이유.
|
|
10
|
+
* · `none` — 원본이 자극을 선언하지 않았다(정상: 정적 구조만 반영하는 트윈)
|
|
11
|
+
* · `mirror` — 미러다. 외부 실물이 진실인 트윈에 우리가 자극을 만들면 그것은 현실이 아니다
|
|
12
|
+
* · `no-engine` — 이 런타임에 시나리오 엔진이 없다(관측 구동 커널)
|
|
13
|
+
* · `invalid` — 선언이 커널 규약을 어긴다. **싣지 않고 말한다**(다음 틱에서 터지게 두지 않는다)
|
|
14
|
+
*/
|
|
15
|
+
| {
|
|
16
|
+
action: 'skip';
|
|
17
|
+
reason: 'none' | 'observed' | 'no-engine' | 'invalid';
|
|
18
|
+
detail?: string;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* 저장된 자극 선언을 읽어 **실을지 판정한다** — 커널 검사기를 그대로 쓴다(규칙을 두 벌로 만들지 않는다).
|
|
22
|
+
*
|
|
23
|
+
* `validate` 는 커널의 `validateScenario` 를 넘긴다(호스트가 커널 타입에 묶이지 않게 함수로 받는다).
|
|
24
|
+
* 검사기가 없으면 **싣지 않는다**: 검사할 수 없는 선언을 태우는 것이 가장 비싼 선택이다(서버가 내려간
|
|
25
|
+
* 이력이 있다).
|
|
26
|
+
*/
|
|
27
|
+
export declare function planStimulus(config: any, runtime: {
|
|
28
|
+
hasScenarioEngine: boolean;
|
|
29
|
+
mode?: 'sim' | 'live';
|
|
30
|
+
}, validate?: (scenario: any) => {
|
|
31
|
+
ok: boolean;
|
|
32
|
+
errorCode?: string;
|
|
33
|
+
errorParams?: any;
|
|
34
|
+
} | {
|
|
35
|
+
ok: false;
|
|
36
|
+
errorCode: string;
|
|
37
|
+
}): StimulusPlan;
|
|
38
|
+
/**
|
|
39
|
+
* 선언을 원본 설정에 얹는다(순수) — **기존 설정을 지우지 않는다.**
|
|
40
|
+
*
|
|
41
|
+
* 어댑터 설정에는 접속 정보가 함께 산다. 자극을 넣으면서 그 옆을 덮으면 다음 재동기가 원본을 못 찾는다.
|
|
42
|
+
*/
|
|
43
|
+
export declare function withStimulus(config: any, scenario: any): any;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* **자극은 원본이 선언한다** — 트윈이 아니라.
|
|
4
|
+
*
|
|
5
|
+
* ── 무엇이 났나 (2026-08-19) ────────────────────────────────────────────────
|
|
6
|
+
* 데모의 자극(시나리오)은 시드 코드가 **메모리에만** 실었다: `runtime.scenario.load(...)`. 그래서 트윈을
|
|
7
|
+
* 재기동하면 자극이 사라지고, 구조만 서 있는 트윈이 남는다 — 작업 0·오더 0. 「살아 있는 데모」가
|
|
8
|
+
* **첫 재기동까지만** 사는 것이다(오늘 서버를 여러 번 띄우는 동안 실제로 그 상태였다).
|
|
9
|
+
*
|
|
10
|
+
* ── 왜 트윈에 저장하지 않나 (ADR-0029 · plans/simulator-as-source.md §4) ────
|
|
11
|
+
* 자극은 **원본의 거동**이다: 무엇이 들어오고 무슨 주문이 나는지는 시뮬레이터(원본)가 정하는 사실이고,
|
|
12
|
+
* 트윈은 그것을 반영한다. 트윈에 새 축을 만들면 ADR-0029 가 나중에 걷어내야 하는 표면이 하나 늘고
|
|
13
|
+
* (그 계획서의 §2·§5 가 이미 그 부담을 적어 두었다), 원본에 두면 그 계획의 **첫 조각 앞자리**가 된다:
|
|
14
|
+
* 지금은 호스트가 읽어 커널에 싣고, 나중에 `openLiveFeed` 뒤로 옮기면 같은 선언이 그대로 쓰인다.
|
|
15
|
+
*
|
|
16
|
+
* 그래서 집은 `TwinReference.connectionConfig.scenario` — 어댑터별 설정 자리(이미 `simple-json`)다.
|
|
17
|
+
*
|
|
18
|
+
* ── 판정을 순수 함수로 (이 파일) ────────────────────────────────────────────
|
|
19
|
+
* 「이 트윈에 저장된 자극을 실을 것인가」에는 사람이 틀리기 쉬운 갈림이 셋 있다: 미러에 실으면 **현실을
|
|
20
|
+
* 오염**시키고, 잘못된 선언을 실으면 **다음 틱에서 서버가 내려가고**(실제로 그 이력이 있다), 시나리오
|
|
21
|
+
* 엔진이 없는 런타임에 실으면 조용히 아무 일도 없다. 그래서 판정을 값으로 만들고 시험한다.
|
|
22
|
+
*/
|
|
23
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
|
+
exports.planStimulus = planStimulus;
|
|
25
|
+
exports.withStimulus = withStimulus;
|
|
26
|
+
/**
|
|
27
|
+
* 저장된 자극 선언을 읽어 **실을지 판정한다** — 커널 검사기를 그대로 쓴다(규칙을 두 벌로 만들지 않는다).
|
|
28
|
+
*
|
|
29
|
+
* `validate` 는 커널의 `validateScenario` 를 넘긴다(호스트가 커널 타입에 묶이지 않게 함수로 받는다).
|
|
30
|
+
* 검사기가 없으면 **싣지 않는다**: 검사할 수 없는 선언을 태우는 것이 가장 비싼 선택이다(서버가 내려간
|
|
31
|
+
* 이력이 있다).
|
|
32
|
+
*/
|
|
33
|
+
function planStimulus(config, runtime, validate) {
|
|
34
|
+
const scenario = config?.scenario;
|
|
35
|
+
if (!scenario || typeof scenario !== 'object')
|
|
36
|
+
return { action: 'skip', reason: 'none' };
|
|
37
|
+
/* 관측 트윈을 먼저 막는다 — 「엔진이 없다」로 답하면 이유가 흐려진다(애초에 실을 수 없는 트윈이다). */
|
|
38
|
+
if (runtime.mode === 'live')
|
|
39
|
+
return { action: 'skip', reason: 'observed' };
|
|
40
|
+
if (!runtime.hasScenarioEngine)
|
|
41
|
+
return { action: 'skip', reason: 'no-engine' };
|
|
42
|
+
if (!validate)
|
|
43
|
+
return { action: 'skip', reason: 'invalid', detail: 'no validator available' };
|
|
44
|
+
const v = validate(scenario);
|
|
45
|
+
if (!v?.ok)
|
|
46
|
+
return { action: 'skip', reason: 'invalid', detail: v?.errorCode ?? 'rejected' };
|
|
47
|
+
return { action: 'load', scenario };
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* 선언을 원본 설정에 얹는다(순수) — **기존 설정을 지우지 않는다.**
|
|
51
|
+
*
|
|
52
|
+
* 어댑터 설정에는 접속 정보가 함께 산다. 자극을 넣으면서 그 옆을 덮으면 다음 재동기가 원본을 못 찾는다.
|
|
53
|
+
*/
|
|
54
|
+
function withStimulus(config, scenario) {
|
|
55
|
+
return { ...(config ?? {}), scenario };
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=declared-stimulus.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"declared-stimulus.js","sourceRoot":"","sources":["../../server/engine/declared-stimulus.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;;AAsBH,oCAcC;AAOD,oCAEC;AA9BD;;;;;;GAMG;AACH,SAAgB,YAAY,CAC1B,MAAW,EACX,OAA8D,EAC9D,QAAuH;IAEvH,MAAM,QAAQ,GAAG,MAAM,EAAE,QAAQ,CAAA;IACjC,IAAI,CAAC,QAAQ,IAAI,OAAO,QAAQ,KAAK,QAAQ;QAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,CAAA;IACxF,+DAA+D;IAC/D,IAAI,OAAO,CAAC,IAAI,KAAK,MAAM;QAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,CAAA;IAC1E,IAAI,CAAC,OAAO,CAAC,iBAAiB;QAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,CAAA;IAC9E,IAAI,CAAC,QAAQ;QAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,wBAAwB,EAAE,CAAA;IAC7F,MAAM,CAAC,GAAG,QAAQ,CAAC,QAAQ,CAAC,CAAA;IAC5B,IAAI,CAAC,CAAC,EAAE,EAAE;QAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAG,CAAS,EAAE,SAAS,IAAI,UAAU,EAAE,CAAA;IACrG,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAA;AACrC,CAAC;AAED;;;;GAIG;AACH,SAAgB,YAAY,CAAC,MAAW,EAAE,QAAa;IACrD,OAAO,EAAE,GAAG,CAAC,MAAM,IAAI,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAA;AACxC,CAAC","sourcesContent":["/*\n * **자극은 원본이 선언한다** — 트윈이 아니라.\n *\n * ── 무엇이 났나 (2026-08-19) ────────────────────────────────────────────────\n * 데모의 자극(시나리오)은 시드 코드가 **메모리에만** 실었다: `runtime.scenario.load(...)`. 그래서 트윈을\n * 재기동하면 자극이 사라지고, 구조만 서 있는 트윈이 남는다 — 작업 0·오더 0. 「살아 있는 데모」가\n * **첫 재기동까지만** 사는 것이다(오늘 서버를 여러 번 띄우는 동안 실제로 그 상태였다).\n *\n * ── 왜 트윈에 저장하지 않나 (ADR-0029 · plans/simulator-as-source.md §4) ────\n * 자극은 **원본의 거동**이다: 무엇이 들어오고 무슨 주문이 나는지는 시뮬레이터(원본)가 정하는 사실이고,\n * 트윈은 그것을 반영한다. 트윈에 새 축을 만들면 ADR-0029 가 나중에 걷어내야 하는 표면이 하나 늘고\n * (그 계획서의 §2·§5 가 이미 그 부담을 적어 두었다), 원본에 두면 그 계획의 **첫 조각 앞자리**가 된다:\n * 지금은 호스트가 읽어 커널에 싣고, 나중에 `openLiveFeed` 뒤로 옮기면 같은 선언이 그대로 쓰인다.\n *\n * 그래서 집은 `TwinReference.connectionConfig.scenario` — 어댑터별 설정 자리(이미 `simple-json`)다.\n *\n * ── 판정을 순수 함수로 (이 파일) ────────────────────────────────────────────\n * 「이 트윈에 저장된 자극을 실을 것인가」에는 사람이 틀리기 쉬운 갈림이 셋 있다: 미러에 실으면 **현실을\n * 오염**시키고, 잘못된 선언을 실으면 **다음 틱에서 서버가 내려가고**(실제로 그 이력이 있다), 시나리오\n * 엔진이 없는 런타임에 실으면 조용히 아무 일도 없다. 그래서 판정을 값으로 만들고 시험한다.\n */\n\n/** 자극을 실을지 말지 — 그리고 왜. 부르는 쪽은 이 답만 보고 움직인다. */\nexport type StimulusPlan =\n /** 실어라 — 검사를 통과한 선언. */\n | { action: 'load'; scenario: any }\n /**\n * 싣지 않는다 — 그 이유.\n * · `none` — 원본이 자극을 선언하지 않았다(정상: 정적 구조만 반영하는 트윈)\n * · `mirror` — 미러다. 외부 실물이 진실인 트윈에 우리가 자극을 만들면 그것은 현실이 아니다\n * · `no-engine` — 이 런타임에 시나리오 엔진이 없다(관측 구동 커널)\n * · `invalid` — 선언이 커널 규약을 어긴다. **싣지 않고 말한다**(다음 틱에서 터지게 두지 않는다)\n */\n | { action: 'skip'; reason: 'none' | 'observed' | 'no-engine' | 'invalid'; detail?: string }\n\n/**\n * 저장된 자극 선언을 읽어 **실을지 판정한다** — 커널 검사기를 그대로 쓴다(규칙을 두 벌로 만들지 않는다).\n *\n * `validate` 는 커널의 `validateScenario` 를 넘긴다(호스트가 커널 타입에 묶이지 않게 함수로 받는다).\n * 검사기가 없으면 **싣지 않는다**: 검사할 수 없는 선언을 태우는 것이 가장 비싼 선택이다(서버가 내려간\n * 이력이 있다).\n */\nexport function planStimulus(\n config: any,\n runtime: { hasScenarioEngine: boolean; mode?: 'sim' | 'live' },\n validate?: (scenario: any) => { ok: boolean; errorCode?: string; errorParams?: any } | { ok: false; errorCode: string }\n): StimulusPlan {\n const scenario = config?.scenario\n if (!scenario || typeof scenario !== 'object') return { action: 'skip', reason: 'none' }\n /* 관측 트윈을 먼저 막는다 — 「엔진이 없다」로 답하면 이유가 흐려진다(애초에 실을 수 없는 트윈이다). */\n if (runtime.mode === 'live') return { action: 'skip', reason: 'observed' }\n if (!runtime.hasScenarioEngine) return { action: 'skip', reason: 'no-engine' }\n if (!validate) return { action: 'skip', reason: 'invalid', detail: 'no validator available' }\n const v = validate(scenario)\n if (!v?.ok) return { action: 'skip', reason: 'invalid', detail: (v as any)?.errorCode ?? 'rejected' }\n return { action: 'load', scenario }\n}\n\n/**\n * 선언을 원본 설정에 얹는다(순수) — **기존 설정을 지우지 않는다.**\n *\n * 어댑터 설정에는 접속 정보가 함께 산다. 자극을 넣으면서 그 옆을 덮으면 다음 재동기가 원본을 못 찾는다.\n */\nexport function withStimulus(config: any, scenario: any): any {\n return { ...(config ?? {}), scenario }\n}\n"]}
|
|
@@ -26,4 +26,6 @@ tslib_1.__exportStar(require("./live-feed-registry.js"), exports);
|
|
|
26
26
|
/* 로컬 선언(현장·트윈) — 소비처(twin-ai 도구)가 명세·검사 규칙을 다시 적지 않게. */
|
|
27
27
|
tslib_1.__exportStar(require("./local-declarations.js"), exports);
|
|
28
28
|
tslib_1.__exportStar(require("./operation-basis.js"), exports);
|
|
29
|
+
tslib_1.__exportStar(require("./measured-yield.js"), exports);
|
|
30
|
+
tslib_1.__exportStar(require("./declared-stimulus.js"), exports);
|
|
29
31
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../server/engine/index.ts"],"names":[],"mappings":";;;AAAA,2DAAgC;AAChC,gEAAqC;AACrC,wDAA6B;AAC7B,gEAAqC;AACrC,4DAAiC;AACjC,qEAAqE;AACrE,0DAA+B;AAC/B,qGAA0E;AAC1E,0DAA0D;AAC1D,oGAAyE;AACzE,4CAA4C;AAC5C,kGAAuE;AACvE,sCAAsC;AACtC,0DAA+B;AAC/B,gEAAgE;AAChE,2DAAgC;AAChC,yDAA8B;AAC9B,6DAAkC;AAClC,gEAAqC;AACrC,kEAAuC;AACvC,2DAAgC;AAChC,kEAAuC;AACvC,yDAAyD;AACzD,kEAAuC;AACvC,+DAAoC","sourcesContent":["export * from './twin-engine.js'\nexport * from './canonical-ingest.js'\nexport * from './kpi-fold.js'\nexport * from './property-effects.js'\nexport * from './kpi-baseline.js'\n/* 지표 정본 표(TWIN_METRIC)·목표 판정 — 소비처(twin-ai 등)가 지표 이름을 다시 열거하지 않게. */\nexport * from './kpi-target.js'\nexport * from '@things-factory/headless-twin/dist-shared/kpi-broadcast.js'\n/* 집약 채널 태그(접두사 + 트윈별 태그 생성) — 소비처가 문자열을 다시 적지 않게 내보낸다. */\nexport * from '@things-factory/headless-twin/dist-shared/entity-delta.js'\n/* 공간 계층의 단계 — 씬 표현·성과 카드·AI 가 같은 목록을 쓴다. */\nexport * from '@things-factory/headless-twin/dist-shared/twin-level.js'\n/* 부하 계기판 — 어떤 작업이 이벤트 루프를 얼마나 쓰는지. */\nexport * from './load-meter.js'\n/* 런타임 레지스트리 키 규약 — 소비처(다른 키 캐시·테스트)가 `:` 를 다시 적지 않게 함께 내보낸다. */\nexport * from './runtime-key.js'\nexport * from './kpi-query.js'\nexport * from './spec-coverage.js'\nexport * from './travel-estimator.js'\nexport * from './measured-estimator.js'\nexport * from './model-basis.js'\nexport * from './live-feed-registry.js'\n/* 로컬 선언(현장·트윈) — 소비처(twin-ai 도구)가 명세·검사 규칙을 다시 적지 않게. */\nexport * from './local-declarations.js'\nexport * from './operation-basis.js'\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../server/engine/index.ts"],"names":[],"mappings":";;;AAAA,2DAAgC;AAChC,gEAAqC;AACrC,wDAA6B;AAC7B,gEAAqC;AACrC,4DAAiC;AACjC,qEAAqE;AACrE,0DAA+B;AAC/B,qGAA0E;AAC1E,0DAA0D;AAC1D,oGAAyE;AACzE,4CAA4C;AAC5C,kGAAuE;AACvE,sCAAsC;AACtC,0DAA+B;AAC/B,gEAAgE;AAChE,2DAAgC;AAChC,yDAA8B;AAC9B,6DAAkC;AAClC,gEAAqC;AACrC,kEAAuC;AACvC,2DAAgC;AAChC,kEAAuC;AACvC,yDAAyD;AACzD,kEAAuC;AACvC,+DAAoC;AACpC,8DAAmC;AACnC,iEAAsC","sourcesContent":["export * from './twin-engine.js'\nexport * from './canonical-ingest.js'\nexport * from './kpi-fold.js'\nexport * from './property-effects.js'\nexport * from './kpi-baseline.js'\n/* 지표 정본 표(TWIN_METRIC)·목표 판정 — 소비처(twin-ai 등)가 지표 이름을 다시 열거하지 않게. */\nexport * from './kpi-target.js'\nexport * from '@things-factory/headless-twin/dist-shared/kpi-broadcast.js'\n/* 집약 채널 태그(접두사 + 트윈별 태그 생성) — 소비처가 문자열을 다시 적지 않게 내보낸다. */\nexport * from '@things-factory/headless-twin/dist-shared/entity-delta.js'\n/* 공간 계층의 단계 — 씬 표현·성과 카드·AI 가 같은 목록을 쓴다. */\nexport * from '@things-factory/headless-twin/dist-shared/twin-level.js'\n/* 부하 계기판 — 어떤 작업이 이벤트 루프를 얼마나 쓰는지. */\nexport * from './load-meter.js'\n/* 런타임 레지스트리 키 규약 — 소비처(다른 키 캐시·테스트)가 `:` 를 다시 적지 않게 함께 내보낸다. */\nexport * from './runtime-key.js'\nexport * from './kpi-query.js'\nexport * from './spec-coverage.js'\nexport * from './travel-estimator.js'\nexport * from './measured-estimator.js'\nexport * from './model-basis.js'\nexport * from './live-feed-registry.js'\n/* 로컬 선언(현장·트윈) — 소비처(twin-ai 도구)가 명세·검사 규칙을 다시 적지 않게. */\nexport * from './local-declarations.js'\nexport * from './operation-basis.js'\nexport * from './measured-yield.js'\nexport * from './declared-stimulus.js'\n"]}
|
|
@@ -45,6 +45,18 @@ export interface KpiGroup {
|
|
|
45
45
|
waitTime: DurationStats;
|
|
46
46
|
/** 이 축에 붙어 있던 시간의 합(자원 축에서 특히 의미 있다). */
|
|
47
47
|
busyMs: number;
|
|
48
|
+
/**
|
|
49
|
+
* **품질 판정이 있었던 건수만** 센다 (2026-08-19).
|
|
50
|
+
*
|
|
51
|
+
* 판정이 없는 작업(이동·체류·중간 스테이션)은 어느 쪽에도 세지 않는다 — 모름을 양품으로 세면 양품률이
|
|
52
|
+
* 조용히 100% 로 올라간다. 그래서 `good + scrap` 은 `tasks` 보다 작을 수 있고, 그것이 정상이다.
|
|
53
|
+
*
|
|
54
|
+
* 판정이 하나도 없으면 **필드를 만들지 않는다**(0/0 을 「양품률 0%」로 읽히게 두지 않는다).
|
|
55
|
+
*/
|
|
56
|
+
quality?: {
|
|
57
|
+
good: number;
|
|
58
|
+
scrap: number;
|
|
59
|
+
};
|
|
48
60
|
}
|
|
49
61
|
export interface KpiResult {
|
|
50
62
|
window: {
|
|
@@ -233,6 +233,25 @@ function facetKey(rec, options) {
|
|
|
233
233
|
return 'unknown';
|
|
234
234
|
}
|
|
235
235
|
}
|
|
236
|
+
/**
|
|
237
|
+
* 이 축의 품질 — **판정이 있었던 것만** 센다(없으면 필드를 만들지 않는다).
|
|
238
|
+
*
|
|
239
|
+
* 양품률의 이력이 여기서 나온다: 커널이 작업 완료에 결과를 적고(`TaskStatusDelta.outcome`), 폴드가 축으로
|
|
240
|
+
* 들고(`TaskFacets.outcome`), 여기서 종류별로 센다. 그 사슬이 없으면 「이 공정의 양품률」은 저널에
|
|
241
|
+
* 남지 않는다(설비 누적 카운터는 종류를 모른다).
|
|
242
|
+
*/
|
|
243
|
+
function qualityOf(rows) {
|
|
244
|
+
let good = 0;
|
|
245
|
+
let scrap = 0;
|
|
246
|
+
for (const r of rows) {
|
|
247
|
+
const o = r.facets.outcome;
|
|
248
|
+
if (o === 'good')
|
|
249
|
+
good++;
|
|
250
|
+
else if (o === 'scrap')
|
|
251
|
+
scrap++;
|
|
252
|
+
}
|
|
253
|
+
return good + scrap > 0 ? { quality: { good, scrap } } : {};
|
|
254
|
+
}
|
|
236
255
|
/**
|
|
237
256
|
* 기록을 축으로 묶어 축별 통계를 낸다 — **전체와 같은 규칙**으로(같은 stats 함수, 같은 창).
|
|
238
257
|
*
|
|
@@ -257,7 +276,8 @@ function groupRecords(records, options) {
|
|
|
257
276
|
leadTime: stats(rows.map(r => r.leadMs).filter((v) => v !== undefined)),
|
|
258
277
|
workTime: stats(rows.map(r => r.workMs).filter((v) => v !== undefined)),
|
|
259
278
|
waitTime: stats(rows.map(r => r.waitMs).filter((v) => v !== undefined)),
|
|
260
|
-
busyMs: rows.reduce((a, r) => a + (r.workMs ?? 0), 0)
|
|
279
|
+
busyMs: rows.reduce((a, r) => a + (r.workMs ?? 0), 0),
|
|
280
|
+
...qualityOf(rows)
|
|
261
281
|
}))
|
|
262
282
|
.sort((a, b) => b.tasks - a.tasks || (a.key < b.key ? -1 : 1));
|
|
263
283
|
const limit = Math.max(1, Math.min(100, options.groupLimit ?? 20));
|