@things-factory/headless-twin 10.0.17 → 10.0.18
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 +12 -85
- package/dist-server/engine/canonical-ingest.js +92 -26
- package/dist-server/engine/canonical-ingest.js.map +1 -1
- package/dist-server/engine/energy-topology.js +3 -3
- package/dist-server/engine/energy-topology.js.map +1 -1
- package/dist-server/engine/kpi-fold.d.ts +91 -3
- package/dist-server/engine/kpi-fold.js +149 -25
- package/dist-server/engine/kpi-fold.js.map +1 -1
- package/dist-server/engine/kpi-query.js +99 -11
- package/dist-server/engine/kpi-query.js.map +1 -1
- package/dist-server/engine/local-declarations.js +6 -5
- package/dist-server/engine/local-declarations.js.map +1 -1
- package/dist-server/engine/model-vocabulary.js +2 -2
- package/dist-server/engine/model-vocabulary.js.map +1 -1
- package/dist-server/engine/oee-accumulator.d.ts +2 -1
- package/dist-server/engine/oee-accumulator.js +3 -2
- package/dist-server/engine/oee-accumulator.js.map +1 -1
- package/dist-server/engine/twin-engine.d.ts +72 -2
- package/dist-server/engine/twin-engine.js +150 -4
- package/dist-server/engine/twin-engine.js.map +1 -1
- package/dist-server/routes.js +1 -1
- package/dist-server/routes.js.map +1 -1
- package/dist-server/service/index.d.ts +1 -1
- package/dist-server/service/index.js +3 -0
- package/dist-server/service/index.js.map +1 -1
- package/dist-server/service/reference/reference-live.js +1 -1
- package/dist-server/service/reference/reference-live.js.map +1 -1
- package/dist-server/service/reference/reference-master.d.ts +11 -2
- package/dist-server/service/reference/reference-master.js +1 -1
- package/dist-server/service/reference/reference-master.js.map +1 -1
- package/dist-server/service/reference/reference-resolver.js +2 -2
- package/dist-server/service/reference/reference-resolver.js.map +1 -1
- package/dist-server/service/reference/template-registry.d.ts +1 -1
- package/dist-server/service/reference/template-registry.js.map +1 -1
- package/dist-server/service/twin-backfill/backfill-period-facts.d.ts +24 -0
- package/dist-server/service/twin-backfill/backfill-period-facts.js +96 -0
- package/dist-server/service/twin-backfill/backfill-period-facts.js.map +1 -0
- package/dist-server/service/twin-backfill/backfill-shape.d.ts +34 -0
- package/dist-server/service/twin-backfill/backfill-shape.js +75 -0
- package/dist-server/service/twin-backfill/backfill-shape.js.map +1 -0
- package/dist-server/service/twin-backfill/index.d.ts +2 -0
- package/dist-server/service/twin-backfill/index.js +6 -0
- package/dist-server/service/twin-backfill/index.js.map +1 -0
- package/dist-server/service/twin-backfill/twin-backfill-resolver.d.ts +10 -0
- package/dist-server/service/twin-backfill/twin-backfill-resolver.js +44 -0
- package/dist-server/service/twin-backfill/twin-backfill-resolver.js.map +1 -0
- package/dist-server/service/twin-control/twin-control-mutation.js +2 -2
- package/dist-server/service/twin-control/twin-control-mutation.js.map +1 -1
- package/dist-server/service/twin-lifecycle/domain-catalog.js +2 -1
- package/dist-server/service/twin-lifecycle/domain-catalog.js.map +1 -1
- package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js.map +1 -1
- package/dist-server/service/twin-model/name-index.js +1 -1
- package/dist-server/service/twin-model/name-index.js.map +1 -1
- package/dist-server/service/twin-model/project-structure.js +4 -3
- package/dist-server/service/twin-model/project-structure.js.map +1 -1
- package/dist-server/service/twin-model/twin-equipment.d.ts +1 -1
- package/dist-server/service/twin-model/twin-equipment.js +2 -2
- package/dist-server/service/twin-model/twin-equipment.js.map +1 -1
- package/dist-server/service/twin-model/twin-model-item-query.js +1 -1
- package/dist-server/service/twin-model/twin-model-item-query.js.map +1 -1
- package/dist-server/service/twin-model/twin-model-query.js +4 -4
- package/dist-server/service/twin-model/twin-model-query.js.map +1 -1
- package/dist-server/service/twin-model/twin-model-tree-query.js +1 -1
- package/dist-server/service/twin-model/twin-model-tree-query.js.map +1 -1
- package/dist-server/service/twin-space/twin-space-resolver.js +1 -1
- package/dist-server/service/twin-space/twin-space-resolver.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 -6
- package/server/engine/canonical-ingest.ts +72 -84
- package/server/engine/energy-topology.ts +1 -1
- package/server/engine/kpi-fold.ts +219 -36
- package/server/engine/kpi-query.ts +97 -4
- package/server/engine/local-declarations.ts +2 -1
- package/server/engine/model-vocabulary.ts +1 -1
- package/server/engine/oee-accumulator.ts +4 -2
- package/server/engine/twin-engine.ts +163 -5
- package/server/routes.ts +6 -1
- package/server/service/index.ts +3 -0
- package/server/service/reference/reference-live.ts +8 -2
- package/server/service/reference/reference-master.ts +12 -3
- package/server/service/reference/reference-resolver.ts +1 -1
- package/server/service/reference/template-registry.ts +1 -1
- package/server/service/twin-backfill/backfill-period-facts.ts +168 -0
- package/server/service/twin-backfill/backfill-shape.ts +86 -0
- package/server/service/twin-backfill/index.ts +3 -0
- package/server/service/twin-backfill/twin-backfill-resolver.ts +35 -0
- package/server/service/twin-control/twin-control-mutation.ts +1 -1
- package/server/service/twin-lifecycle/domain-catalog.ts +2 -1
- package/server/service/twin-lifecycle/twin-lifecycle-mutation.ts +1 -1
- package/server/service/twin-model/name-index.ts +1 -1
- package/server/service/twin-model/project-structure.ts +3 -2
- package/server/service/twin-model/twin-equipment.ts +14 -2
- package/server/service/twin-model/twin-model-item-query.ts +1 -1
- package/server/service/twin-model/twin-model-query.ts +2 -2
- package/server/service/twin-model/twin-model-tree-query.ts +1 -1
- package/server/service/twin-space/twin-space-resolver.ts +1 -1
- package/shared/entity-delta.ts +1 -1
- package/test/axis-read.test.ts +4 -3
- package/test/backfill-shape.test.ts +89 -0
- package/test/broadcast-cost-baseline.test.ts +2 -1
- package/test/canonical-ingest-vocabularies.test.ts +82 -3
- package/test/capability-mapping.test.ts +1 -1
- package/test/contract-layer-guard.test.ts +48 -0
- package/test/declaration-reaches-model.test.ts +2 -0
- package/test/equipment-identity-reaches.test.ts +57 -0
- package/test/fact-scope-wiring.test.ts +96 -0
- package/test/generated-not-counted.test.ts +99 -0
- package/test/ingest-bench.test.ts +2 -1
- package/test/ingest-wiring-guard.test.ts +197 -0
- package/test/kpi-fold.test.ts +81 -0
- package/test/live-mirror-parity.test.ts +6 -3
- package/test/local-declarations.test.ts +3 -2
- package/test/master-to-twin.test.ts +2 -2
- package/test/oee-accumulator.test.ts +18 -3
- package/test/projection-reaches-screen.test.ts +5 -0
- package/test/projection-reads-declared.test.ts +109 -0
- package/test/property-effects.test.ts +7 -3
- package/test/scale-twin-bench.test.ts +2 -1
- package/test/vocabulary-guard.test.ts +1 -1
- package/tsconfig.shared.tsbuildinfo +1 -1
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -22,7 +22,7 @@ const log_js_1 = require("./log.js");
|
|
|
22
22
|
const event_type_split_shape_js_1 = require("../service/twin-journal/event-type-split-shape.js");
|
|
23
23
|
const read_time_js_1 = require("./read-time.js");
|
|
24
24
|
const typeorm_1 = require("typeorm");
|
|
25
|
-
const
|
|
25
|
+
const ops_contract_1 = require("@operato/ops-contract");
|
|
26
26
|
const shell_1 = require("@things-factory/shell");
|
|
27
27
|
const twin_event_js_1 = require("../service/twin-event/twin-event.js");
|
|
28
28
|
const twin_instance_js_1 = require("../service/twin-instance/twin-instance.js");
|
|
@@ -41,7 +41,7 @@ const kpi_baseline_js_1 = require("./kpi-baseline.js");
|
|
|
41
41
|
* 나눈 값이라, 두 사실이 한 폴드에 들어와야 창이 어긋날 수 없다. 표본(`energy.measured`)은 읽지 않는다 —
|
|
42
42
|
* 저널에 없고(§4.1 인제스트 계약) 여기서 필요한 것은 마감된 구간뿐이다.
|
|
43
43
|
*/
|
|
44
|
-
const BUSINESS_EVENTS = ['task.status', 'order.status', 'energy.demand.window', 'energy.generation.period', 'energy.usage.period', 'energy.bill'];
|
|
44
|
+
const BUSINESS_EVENTS = ['task.status', 'order.status', 'energy.demand.window', 'energy.generation.period', 'energy.usage.period', 'energy.bill', 'energy.generation.price'];
|
|
45
45
|
/** 한 번에 계산할 이벤트 상한 — 큰 구간이 서버를 붙잡지 않게. */
|
|
46
46
|
const MAX_EVENTS = 20000;
|
|
47
47
|
/**
|
|
@@ -51,7 +51,27 @@ const MAX_EVENTS = 20000;
|
|
|
51
51
|
* 메모리가 그만큼 든다. 셋(종류) × 둘(트윈)까지는 최악에 12만 행이고, 그 이상은 예전 한 질의로 간다 —
|
|
52
52
|
* 느리지만 답은 같다.
|
|
53
53
|
*/
|
|
54
|
-
|
|
54
|
+
/*
|
|
55
|
+
* ── 나눠 물을 수 있는 갈래의 최대 (2026-08-30 실측) ──────────────────────────
|
|
56
|
+
*
|
|
57
|
+
* 6 이었다. 그런데 **한 현장에 트윈이 여럿인 것이 이 제품의 정상**이다(창고·공정·야드가 한 현실을
|
|
58
|
+
* 각각 본다). 트윈 3개 × 종류 6가지 = 18 이라 조건에 안 걸렸고, 한 질의로 떨어졌다.
|
|
59
|
+
*
|
|
60
|
+
* 같은 창·같은 행으로 두 모양을 쟀다(hatio-us, order.status 345만 건이 있는 하루):
|
|
61
|
+
*
|
|
62
|
+
* 한 질의(종류 IN + 시각순) ix_twin_event_1 로 그 날 전체를 걷는다 25.987초 · 20,000건(잘림)
|
|
63
|
+
* 갈래 하나(종류 정확일치) ix_twin_event_7 이 순서까지 준다 0.286초
|
|
64
|
+
*
|
|
65
|
+
* 91배다. 그리고 25초를 기다린 끝에 나오는 값이 **잘린 값**이다 — 사용자는 그 사실을 25초 뒤에 본다.
|
|
66
|
+
*
|
|
67
|
+
* ── 늘리는 대가 ────────────────────────────────────────────────────────────
|
|
68
|
+
* 갈래마다 상한까지 받아야 합칠 때 정확하므로(§`mergeStreams`), 갈래가 많으면 그만큼 든다.
|
|
69
|
+
* 최악은 `갈래 수 × MAX_EVENTS` 행이다. 다만 그 최악은 **모든 갈래가 창을 가득 채울 때**이고,
|
|
70
|
+
* 그때는 어차피 결과가 잘린다(그 사실은 `capped` 로 나간다).
|
|
71
|
+
*
|
|
72
|
+
* 32 는 트윈 4개 × 종류 8가지를 덮는다. 어휘가 더 늘면 이 수를 다시 재고 정한다.
|
|
73
|
+
*/
|
|
74
|
+
const KPI_MAX_SPLIT = 32;
|
|
55
75
|
/**
|
|
56
76
|
* 노드 → 구역 지도. **저널에는 구역이 없다**(노드까지만 있다) — 모델 정의의 `locations[].parentId` 가
|
|
57
77
|
* 마스터 계층에서 온 소속이다. 기동하지 않은 트윈도 모델은 남아 있어 이 경로가 가장 튼튼하다.
|
|
@@ -132,7 +152,7 @@ async function locationAreaMap(domainId, instanceIds, spaceId) {
|
|
|
132
152
|
* 이 집계는 지금 구역 롤업(`rollupOf`)만 쓰므로 결과가 달라지지 않는다. 그래도 여기서
|
|
133
153
|
* 잇는다: 단으로 묻는 질의(`ancestorOfLevel`)가 생길 때 배선을 다시 찾지 않게 한다.
|
|
134
154
|
*/
|
|
135
|
-
h = (0,
|
|
155
|
+
h = (0, ops_contract_1.hierarchyOf)({ locations }, t => (0, ops_contract_1.levelOfLocationType)(t, row.kind));
|
|
136
156
|
}
|
|
137
157
|
catch (e) {
|
|
138
158
|
(0, log_js_1.twinWarn)(`[twin-kpi] "${row.instanceId}": location hierarchy is broken — area rollup skipped for this twin: ${e?.message}`);
|
|
@@ -196,11 +216,11 @@ async function tariffOf(domainId, instanceIds) {
|
|
|
196
216
|
const v = Number(props.get(id));
|
|
197
217
|
return Number.isFinite(v) && v > 0 ? v : undefined;
|
|
198
218
|
};
|
|
199
|
-
const d = num(
|
|
200
|
-
const e = num(
|
|
219
|
+
const d = num(ops_contract_1.EMS_PROPERTY.demandChargePerKW);
|
|
220
|
+
const e = num(ops_contract_1.EMS_PROPERTY.energyChargePerKWh);
|
|
201
221
|
/* 요금적용전력 — 있으면 기본요금이 조건부 파생이 아니라 사실이 된다(§`electricityCost`). */
|
|
202
|
-
const b = num(
|
|
203
|
-
const cur = props.get(
|
|
222
|
+
const b = num(ops_contract_1.EMS_PROPERTY.billingDemandKW);
|
|
223
|
+
const cur = props.get(ops_contract_1.EMS_PROPERTY.currency);
|
|
204
224
|
if (d === undefined && e === undefined)
|
|
205
225
|
continue;
|
|
206
226
|
found.push({
|
|
@@ -234,7 +254,7 @@ async function resourceFeederMap(domainId, instanceIds) {
|
|
|
234
254
|
const model = row.model ?? {};
|
|
235
255
|
const locations = (model.locations ?? []).filter((n) => n?.id);
|
|
236
256
|
const byId = new Map(locations.map((n) => [String(n.id), n]));
|
|
237
|
-
const electrical = new Set(
|
|
257
|
+
const electrical = new Set(ops_contract_1.EMS_LOCATION_TYPES);
|
|
238
258
|
/**
|
|
239
259
|
* 이 자리에서 **전기 상류**로 올라가 처음 만나는 계통의 자리. 없으면 undefined.
|
|
240
260
|
*
|
|
@@ -249,7 +269,7 @@ async function resourceFeederMap(domainId, instanceIds) {
|
|
|
249
269
|
seen.add(String(cur.id)); // 잘못된 모델의 순환이 여기서 멈춘다
|
|
250
270
|
if (electrical.has(String(cur.type)))
|
|
251
271
|
return String(cur.id);
|
|
252
|
-
const up = (0,
|
|
272
|
+
const up = (0, ops_contract_1.electricalUpstreamOf)(locations, String(cur.id));
|
|
253
273
|
cur = up ? byId.get(up) : cur.parentId ? byId.get(String(cur.parentId)) : undefined;
|
|
254
274
|
}
|
|
255
275
|
return undefined;
|
|
@@ -310,6 +330,55 @@ function latestBillOf(domainId, instanceIds) {
|
|
|
310
330
|
}
|
|
311
331
|
return best;
|
|
312
332
|
}
|
|
333
|
+
/** 이 트윈들이 이어받고 있는 **발전 단가** — 창 안에 단가 사실이 없을 때 쓴다. */
|
|
334
|
+
function latestGenerationPriceOf(domainId, instanceIds) {
|
|
335
|
+
let best;
|
|
336
|
+
for (const instanceId of instanceIds) {
|
|
337
|
+
const p = twin_engine_js_1.TwinEngine.snapshot(domainId, instanceId)?.energy?.generationPrice;
|
|
338
|
+
if (!p?.from || !Number.isFinite(Number(p.unitPrice)) || !p.currency)
|
|
339
|
+
continue;
|
|
340
|
+
if (!best || Date.parse(p.from) > Date.parse(best.from)) {
|
|
341
|
+
best = { from: String(p.from), to: String(p.to), unitPrice: Number(p.unitPrice), currency: String(p.currency) };
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return best;
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* 이 트윈들의 **발전 정격** — 이용률의 분모.
|
|
348
|
+
*
|
|
349
|
+
* 커널이 고른다(설비 합 → 자리 합계, 교류 → 직류). 여러 트윈이면 합한다 — 한 현장의 발전은
|
|
350
|
+
* 트윈마다 나뉘어 있어도 같은 현장의 발전이다. 근거가 갈리면 약한 쪽(직류·자리)으로 적는다.
|
|
351
|
+
*/
|
|
352
|
+
function generationRatedOf(domainId, instanceIds) {
|
|
353
|
+
let kW = 0;
|
|
354
|
+
let basis = 'ac';
|
|
355
|
+
let from = 'equipment';
|
|
356
|
+
for (const instanceId of instanceIds) {
|
|
357
|
+
/* 스냅샷에서 읽는다 — 커널이 고른 값이다. 여기서 모델을 다시 뒤지면 고르는 규칙이 두 벌이 된다. */
|
|
358
|
+
const r = twin_engine_js_1.TwinEngine.snapshot(domainId, instanceId)?.energy?.generationRated;
|
|
359
|
+
if (!r || !(Number(r.kW) > 0))
|
|
360
|
+
continue;
|
|
361
|
+
kW += Number(r.kW);
|
|
362
|
+
if (r.basis === 'dc')
|
|
363
|
+
basis = 'dc';
|
|
364
|
+
if (r.from === 'location')
|
|
365
|
+
from = 'location';
|
|
366
|
+
}
|
|
367
|
+
return kW > 0 ? { kW, basis, from } : undefined;
|
|
368
|
+
}
|
|
369
|
+
/** 이 트윈들이 이어받고 있는 **요금 기준** — 이 주기에 적용되는 값(청구서보다 먼저다). */
|
|
370
|
+
function latestTariffBasisOf(domainId, instanceIds) {
|
|
371
|
+
let best;
|
|
372
|
+
for (const instanceId of instanceIds) {
|
|
373
|
+
const b = twin_engine_js_1.TwinEngine.snapshot(domainId, instanceId)?.energy?.tariffBasis;
|
|
374
|
+
if (!b?.from)
|
|
375
|
+
continue;
|
|
376
|
+
if (!best || Date.parse(b.from) > Date.parse(best.from)) {
|
|
377
|
+
best = { from: String(b.from), ...(Number.isFinite(Number(b.billingDemandKW)) ? { billingDemandKW: Number(b.billingDemandKW) } : {}) };
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return best;
|
|
381
|
+
}
|
|
313
382
|
async function energyTopologyOf(domainId, instanceIds, energy) {
|
|
314
383
|
if (!instanceIds.length)
|
|
315
384
|
return undefined;
|
|
@@ -628,7 +697,19 @@ async function computeTwinKpi(input) {
|
|
|
628
697
|
: 'no operational events in the journal yet — there is nothing to measure (this is NOT "zero throughput").'
|
|
629
698
|
};
|
|
630
699
|
}
|
|
631
|
-
|
|
700
|
+
/*
|
|
701
|
+
* ── **미래를 성과 구간으로 삼지 않는다** (2026-08-30) ──────────────────────
|
|
702
|
+
*
|
|
703
|
+
* 마지막 기록이 미래일 수 있다. 한 원본이 **끝나지 않은 이번 달**의 요금 정보를 청구서로 보냈고,
|
|
704
|
+
* 청구서의 사건 시각은 기간의 끝이라 그 사실이 **내일** 일어난 것으로 적혔다. 그러자 구간이
|
|
705
|
+
* `내일 00:00 → 모레 00:00` 이 되어 그 현장의 화면에서 **어제 발전량이 사라졌다.** 오류는 나지
|
|
706
|
+
* 않았고 화면만 비어 있었다.
|
|
707
|
+
*
|
|
708
|
+
* 커널이 이제 그런 사실을 받지 않는다. 그래도 여기서 막는다 — **이미 적힌 것은 사라지지 않고**,
|
|
709
|
+
* 저널은 지우지 않기 때문이다. 그리고 성과는 지나간 일이다. 아직 오지 않은 구간의 성과라는 것은
|
|
710
|
+
* 없다.
|
|
711
|
+
*/
|
|
712
|
+
toMs = Math.min(Date.parse(latest), Date.now());
|
|
632
713
|
basis = 'latest-event';
|
|
633
714
|
}
|
|
634
715
|
const fromMs = input.fromTime ? Date.parse(input.fromTime) : toMs - minutes * 60_000;
|
|
@@ -656,6 +737,10 @@ async function computeTwinKpi(input) {
|
|
|
656
737
|
* **커널 상태에 이어져 있는 마지막 청구서**를 함께 넘긴다(§`EnergyState.lastBill`).
|
|
657
738
|
*/
|
|
658
739
|
const lastBill = latestBillOf(input.domainId, instanceIds);
|
|
740
|
+
const tariffBasis = latestTariffBasisOf(input.domainId, instanceIds);
|
|
741
|
+
/* 이용률의 분모 — 커널이 모델의 선언에서 고른다(설비 합 → 자리 합계, 교류 → 직류). */
|
|
742
|
+
const generationRated = generationRatedOf(input.domainId, instanceIds);
|
|
743
|
+
const generationPrice = latestGenerationPriceOf(input.domainId, instanceIds);
|
|
659
744
|
const kpi = (0, kpi_fold_js_1.foldKpi)(events, { fromMs, toMs }, {
|
|
660
745
|
groupBy: input.groupBy,
|
|
661
746
|
locationArea: areaMap.locationArea,
|
|
@@ -663,6 +748,9 @@ async function computeTwinKpi(input) {
|
|
|
663
748
|
meterFeeder: feederMap.meterFeeder,
|
|
664
749
|
...(tariffFound.tariff ? { tariff: tariffFound.tariff } : {}),
|
|
665
750
|
...(lastBill ? { lastBill } : {}),
|
|
751
|
+
...(tariffBasis ? { tariffBasis } : {}),
|
|
752
|
+
...(generationRated ? { generationRated } : {}),
|
|
753
|
+
...(generationPrice ? { generationPrice } : {}),
|
|
666
754
|
...shiftCtx,
|
|
667
755
|
groupLimit: input.groupLimit
|
|
668
756
|
});
|