@things-factory/headless-twin 10.0.11 → 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 +10 -0
- 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-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-model-query.js +1 -1
- package/dist-server/service/twin-model/twin-model-query.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 +20 -0
- package/server/service/twin-control/twin-control-mutation.ts +75 -28
- 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-model-query.ts +1 -1
- package/test/adopt-structure-live.test.ts +7 -7
- package/test/boot-resume.test.ts +3 -3
- 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
|
@@ -76,7 +76,7 @@ before(async () => {
|
|
|
76
76
|
domainId = d.id
|
|
77
77
|
q = new TwinModelTreeQuery()
|
|
78
78
|
ctx = { state: { domain: { id: domainId } } }
|
|
79
|
-
await TwinEngine.ingestMaster(domainId, master())
|
|
79
|
+
await TwinEngine.ingestMaster(domainId, master(), 'reset')
|
|
80
80
|
})
|
|
81
81
|
|
|
82
82
|
after(async () => {
|
|
@@ -108,13 +108,13 @@ test('수량을 곱해 내려간다 — 그리고 모르면 곱하지 않는다'
|
|
|
108
108
|
{ key: 'frame-line', label: 'f', inputs: [{ material: 'steel', qty: 4 }], outputs: [{ material: 'frame', qty: 1 }] }
|
|
109
109
|
]
|
|
110
110
|
})
|
|
111
|
-
await TwinEngine.ingestMaster(domainId, noQty)
|
|
111
|
+
await TwinEngine.ingestMaster(domainId, noQty, 'reset')
|
|
112
112
|
const t2 = await explode('trailer')
|
|
113
113
|
assert.equal(t2.items.find((n: any) => n.id === 'frame').qty, null, '모르는 수량을 1 로 메우지 않는다')
|
|
114
114
|
assert.equal(t2.items.find((n: any) => n.id === 'steel').qty, null, '모르는 값 아래는 전부 모르는 값이다')
|
|
115
115
|
assert.equal(t2.qtyIncomplete, true, '합계가 성립하지 않는다는 사실을 낸다')
|
|
116
116
|
|
|
117
|
-
await TwinEngine.ingestMaster(domainId, master())
|
|
117
|
+
await TwinEngine.ingestMaster(domainId, master(), 'reset')
|
|
118
118
|
})
|
|
119
119
|
|
|
120
120
|
test('역전개도 같은 선언으로 — 방향만 뒤집는다', async () => {
|
|
@@ -146,11 +146,11 @@ test('사이클은 다시 탐색하지 않고 알린다', async () => {
|
|
|
146
146
|
{ key: 'frame-line', label: 'f', inputs: [{ material: 'trailer', qty: 1 }], outputs: [{ material: 'frame', qty: 1 }] }
|
|
147
147
|
]
|
|
148
148
|
})
|
|
149
|
-
await TwinEngine.ingestMaster(domainId, loop)
|
|
149
|
+
await TwinEngine.ingestMaster(domainId, loop, 'reset')
|
|
150
150
|
const t = await explode('trailer')
|
|
151
151
|
assert.ok(t.items.length < 10, '무한히 자라지 않는다')
|
|
152
152
|
assert.ok(t.cycles.length > 0, '사이클을 만났다는 사실을 말한다')
|
|
153
|
-
await TwinEngine.ingestMaster(domainId, master())
|
|
153
|
+
await TwinEngine.ingestMaster(domainId, master(), 'reset')
|
|
154
154
|
})
|
|
155
155
|
|
|
156
156
|
test('상한에서 끊기면 끊겼다고 알린다 — 조용히 자르지 않는다', async () => {
|
|
@@ -167,10 +167,10 @@ test('선언에 없는 자재를 참조하면 그 사실이 드러난다', async
|
|
|
167
167
|
{ key: 'trailer-line', label: 't', inputs: [{ material: 'no-such-part', qty: 1 }], outputs: [{ material: 'trailer', qty: 1 }] }
|
|
168
168
|
]
|
|
169
169
|
})
|
|
170
|
-
await TwinEngine.ingestMaster(domainId, ghost)
|
|
170
|
+
await TwinEngine.ingestMaster(domainId, ghost, 'reset')
|
|
171
171
|
const t = await explode('trailer')
|
|
172
172
|
const child = t.items.find((n: any) => n.id === 'no-such-part')
|
|
173
173
|
assert.ok(child, '가리켜진 것은 그리되')
|
|
174
174
|
assert.equal(child.known, false, '선언에 없다는 사실을 함께 낸다(있는 척하지 않는다)')
|
|
175
|
-
await TwinEngine.ingestMaster(domainId, master())
|
|
175
|
+
await TwinEngine.ingestMaster(domainId, master(), 'reset')
|
|
176
176
|
})
|
|
@@ -68,7 +68,7 @@ after(async () => {
|
|
|
68
68
|
})
|
|
69
69
|
|
|
70
70
|
test('인제스트가 원천을 각인한다 — 원천이 무엇이든 한 자리에', async () => {
|
|
71
|
-
await TwinEngine.ingestMaster(domainId, master(BORN, 3, { kind: 'template', templateId: 'wh-unit-load', params: { bins: 3 } }))
|
|
71
|
+
await TwinEngine.ingestMaster(domainId, master(BORN, 3, { kind: 'template', templateId: 'wh-unit-load', params: { bins: 3 } }), 'reset')
|
|
72
72
|
assert.deepEqual(await originOf(BORN), { kind: 'template', templateId: 'wh-unit-load', params: { bins: 3 } })
|
|
73
73
|
})
|
|
74
74
|
|
|
@@ -78,7 +78,7 @@ test('원천을 모르는 재프로비전이 이미 아는 것을 지우지 않
|
|
|
78
78
|
* 인제스트한다. 그때 `origin` 을 덮어 비우면 그 트윈은 **조용히 다시 읽을 수 없게 된다.**
|
|
79
79
|
* 그런 종류의 손상은 다음번에 다시 읽으려 할 때까지 아무도 모른다.
|
|
80
80
|
*/
|
|
81
|
-
await TwinEngine.ingestMaster(domainId, master(BORN, 5))
|
|
81
|
+
await TwinEngine.ingestMaster(domainId, master(BORN, 5), 'reset')
|
|
82
82
|
assert.deepEqual(
|
|
83
83
|
await originOf(BORN),
|
|
84
84
|
{ kind: 'template', templateId: 'wh-unit-load', params: { bins: 3 } },
|
|
@@ -90,12 +90,12 @@ test('원천을 모르는 재프로비전이 이미 아는 것을 지우지 않
|
|
|
90
90
|
})
|
|
91
91
|
|
|
92
92
|
test('새 원천을 말하면 갱신된다 — 보존은 침묵에만 적용된다', async () => {
|
|
93
|
-
await TwinEngine.ingestMaster(domainId, master(BORN, 5, { kind: 'reference', source: 'oracle-wms' }))
|
|
93
|
+
await TwinEngine.ingestMaster(domainId, master(BORN, 5, { kind: 'reference', source: 'oracle-wms' }), 'reset')
|
|
94
94
|
assert.deepEqual(await originOf(BORN), { kind: 'reference', source: 'oracle-wms' })
|
|
95
95
|
})
|
|
96
96
|
|
|
97
97
|
test('원천 없이 태어난 트윈은 원천이 없다 — 짐작해 채우지 않는다', async () => {
|
|
98
|
-
await TwinEngine.ingestMaster(domainId, master(ORPHAN, 2))
|
|
98
|
+
await TwinEngine.ingestMaster(domainId, master(ORPHAN, 2), 'reset')
|
|
99
99
|
assert.equal(await originOf(ORPHAN), null, '모르는 것을 아는 척하면 엉뚱한 원본으로 현장을 덮는다')
|
|
100
100
|
})
|
|
101
101
|
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* **한 바퀴** — 커널이 라인을 돌리고, 그 이벤트를 접어, 배운 수율이 다시 커널로 들어간다.
|
|
3
|
+
*
|
|
4
|
+
* ── 왜 이 시험이 필요했나 (2026-08-19) ─────────────────────────────────────
|
|
5
|
+
* 층마다 시험이 있었다(커널 7 · 호스트 9). 그런데 **사슬이 이어졌는지**는 층 시험으로 알 수 없다 —
|
|
6
|
+
* 이벤트 모양이 한 칸만 어긋나도 각 층은 통과하면서 전체는 조용히 상수로 돈다.
|
|
7
|
+
*
|
|
8
|
+
* 실 앱에서 보려고 데모 MES 트윈으로 한 바퀴를 돌리려 했는데 두 번 막혔다: ① 데모의 자극(시나리오)은
|
|
9
|
+
* **시드 때만 실려서** 트윈을 재기동하면 사라진다 ② 모델에서 읽은 값으로 자극을 재구성했지만 부품 도착이
|
|
10
|
+
* 레시피 입력과 맞물리지 않아 라인이 돌지 않았다. 그 두 개는 **데모 데이터의 문제**이고, 우리가 증명해야
|
|
11
|
+
* 하는 것(사슬)과는 다른 문제다.
|
|
12
|
+
*
|
|
13
|
+
* 그래서 여기서 **DB·서버 없이** 진짜 사슬을 돌린다: 커널(MES) → `task.status` 이벤트 → 호스트의 실제
|
|
14
|
+
* 폴드(`foldKpi`) → 실제 추정기(`buildYieldEstimator`) → 커널 시임. 가짜 이벤트를 손으로 만들지 않는다 —
|
|
15
|
+
* 커널이 낸 것을 그대로 접는다.
|
|
16
|
+
*/
|
|
17
|
+
import { test } from 'node:test'
|
|
18
|
+
import assert from 'node:assert/strict'
|
|
19
|
+
import { createRequire } from 'node:module'
|
|
20
|
+
|
|
21
|
+
import { foldKpi } from '../server/engine/kpi-fold.ts'
|
|
22
|
+
import { buildYieldEstimator } from '../server/engine/measured-yield.ts'
|
|
23
|
+
|
|
24
|
+
/* 커널은 한 벌로 묶인 산출물로 들어온다(서브패스는 exports 로 막혀 있다 — property-effects 시험과 같은 이유). */
|
|
25
|
+
const require = createRequire(import.meta.url)
|
|
26
|
+
const { MesKernel, MES_PART_GTINS } = require('@operato/twin-kernel')
|
|
27
|
+
|
|
28
|
+
/** 보드·자극은 커널 시험(`mes.test.ts`)의 것과 같은 모양 — 여기서 공장을 지어내지 않는다. */
|
|
29
|
+
function board() {
|
|
30
|
+
return {
|
|
31
|
+
locations: [
|
|
32
|
+
{ id: 'raw-store', type: 'raw-store', capacity: 500 },
|
|
33
|
+
{ id: 'cut-station', type: 'cut-station', capacity: 20 },
|
|
34
|
+
{ id: 'weld-station', type: 'weld-station', capacity: 20 },
|
|
35
|
+
{ id: 'paint-booth', type: 'paint-booth', capacity: 20 },
|
|
36
|
+
{ id: 'assembly-line', type: 'assembly-line', capacity: 20 },
|
|
37
|
+
{ id: 'fg-store', type: 'fg-store', capacity: 200 }
|
|
38
|
+
],
|
|
39
|
+
equipment: [
|
|
40
|
+
{ id: 'cutter-1', kind: 'cutter', homeLocation: 'cut-station' },
|
|
41
|
+
{ id: 'welder-1', kind: 'welder', homeLocation: 'weld-station' },
|
|
42
|
+
{ id: 'painter-1', kind: 'painter', homeLocation: 'paint-booth' },
|
|
43
|
+
{ id: 'assembler-1', kind: 'assembler', homeLocation: 'assembly-line' }
|
|
44
|
+
]
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const partsInbound = {
|
|
49
|
+
seed: 3,
|
|
50
|
+
generators: [
|
|
51
|
+
{
|
|
52
|
+
kind: 'inbound-arrival',
|
|
53
|
+
rate: { distribution: 'constant', meanPerHour: 720 },
|
|
54
|
+
content: { skuMix: [{ gtin: MES_PART_GTINS.partA, weight: 1 }, { gtin: MES_PART_GTINS.partB, weight: 1 }], qtyPerLine: { min: 1, max: 1 } }
|
|
55
|
+
}
|
|
56
|
+
]
|
|
57
|
+
}
|
|
58
|
+
const workOrders = {
|
|
59
|
+
seed: 7,
|
|
60
|
+
generators: [
|
|
61
|
+
{
|
|
62
|
+
kind: 'outbound-order',
|
|
63
|
+
rate: { distribution: 'constant', meanPerHour: 360 },
|
|
64
|
+
content: { skuMix: [{ gtin: MES_PART_GTINS.partA, weight: 1 }], qtyPerLine: { min: 1, max: 1 } }
|
|
65
|
+
}
|
|
66
|
+
]
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** 라인을 한 바퀴 돌린다 — 부품을 채우고 작업지시를 흘리고 배수한다. */
|
|
70
|
+
function runLine(declaredYieldPct?: number): { events: any[]; kernel: any } {
|
|
71
|
+
const k: any = new MesKernel('plantA')
|
|
72
|
+
const events: any[] = []
|
|
73
|
+
k.onEvent((e: any) => events.push(e))
|
|
74
|
+
k.loadTwinModel(board())
|
|
75
|
+
/* 선언값을 넣어 두면 「실측이 선언을 이긴다」를 같은 라인에서 확인할 수 있다. */
|
|
76
|
+
if (declaredYieldPct !== undefined) k.declareParameters({ assembly: { yield: String(declaredYieldPct / 100) } })
|
|
77
|
+
k.scenario.load(partsInbound)
|
|
78
|
+
k.scenario.start()
|
|
79
|
+
for (let i = 0; i < 400; i++) k.tick(1000)
|
|
80
|
+
k.scenario.pause()
|
|
81
|
+
for (let i = 0; i < 30; i++) k.tick(1000)
|
|
82
|
+
k.scenario.load(workOrders)
|
|
83
|
+
k.scenario.start()
|
|
84
|
+
for (let i = 0; i < 300; i++) k.tick(1000)
|
|
85
|
+
k.scenario.pause()
|
|
86
|
+
for (let i = 0; i < 4000; i++) k.tick(1000)
|
|
87
|
+
return { events, kernel: k }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** 커널 이벤트를 저널 줄 모양으로 — 호스트가 DB 에서 읽는 것과 같은 모양(payload 안에 data). */
|
|
91
|
+
const asJournal = (events: any[]) =>
|
|
92
|
+
events.filter(e => e.eventType === 'task.status').map(e => ({ eventType: 'task.status', eventTime: e.eventTime, payload: { data: e.data } }))
|
|
93
|
+
|
|
94
|
+
const window = (rows: { eventTime: string }[]) => {
|
|
95
|
+
const times = rows.map(r => Date.parse(r.eventTime)).filter(n => Number.isFinite(n))
|
|
96
|
+
return { fromMs: Math.min(...times) - 1000, toMs: Math.max(...times) + 1000 }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
test('한 바퀴: 커널이 판정을 내고 → 폴드가 종류별로 세고 → 추정기가 비율을 배운다', () => {
|
|
100
|
+
const { events } = runLine()
|
|
101
|
+
const rows = asJournal(events)
|
|
102
|
+
assert.ok(rows.length > 50, `작업 전이가 충분히 나야 한다(관측 ${rows.length})`)
|
|
103
|
+
|
|
104
|
+
/* 호스트의 **실제** 폴드로 접는다 — 시험용 계산을 따로 만들지 않는다. */
|
|
105
|
+
const kpi = foldKpi(rows as any, window(rows as any), { groupBy: 'taskKind' })
|
|
106
|
+
const judged = (kpi.groups?.items ?? []).filter(g => g.quality)
|
|
107
|
+
assert.ok(judged.length > 0, '품질이 세어진 종류가 있어야 한다')
|
|
108
|
+
for (const g of judged) {
|
|
109
|
+
assert.ok(g.quality!.good + g.quality!.scrap > 0)
|
|
110
|
+
assert.ok(g.quality!.good + g.quality!.scrap <= g.tasks, '판정 수는 완료 수를 넘지 못한다')
|
|
111
|
+
}
|
|
112
|
+
/* 판정이 없는 종류도 있어야 한다 — 중간 스테이션은 품질을 가리지 않는다(분모에 섞이면 안 된다). */
|
|
113
|
+
assert.ok((kpi.groups?.items ?? []).some(g => !g.quality), '판정하지 않는 공정이 있다')
|
|
114
|
+
|
|
115
|
+
const learned = buildYieldEstimator(kpi.groups?.items, { minSamples: 5 })
|
|
116
|
+
assert.ok(Object.keys(learned.learned).length > 0, '수율을 배워야 한다')
|
|
117
|
+
for (const [kind, ratio] of Object.entries(learned.learned)) {
|
|
118
|
+
assert.ok(ratio >= 0 && ratio <= 1, `${kind} 의 비율이 0..1 이어야 한다(관측 ${ratio})`)
|
|
119
|
+
const s = learned.samples[kind]
|
|
120
|
+
assert.equal(learned.learned[kind], s.good / (s.good + s.scrap), '비율은 판정 수에서 곧바로 나온다')
|
|
121
|
+
}
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
test('배운 값이 커널로 되돌아가 선언값을 이긴다 — 사슬의 마지막 칸', () => {
|
|
125
|
+
/* 선언 60% 로 라인을 돌린다(그 현장의 이력이 그렇게 남는다). */
|
|
126
|
+
const { events } = runLine(60)
|
|
127
|
+
const rows = asJournal(events)
|
|
128
|
+
const kpi = foldKpi(rows as any, window(rows as any), { groupBy: 'taskKind' })
|
|
129
|
+
const learned = buildYieldEstimator(kpi.groups?.items, { minSamples: 5 })
|
|
130
|
+
const kinds = Object.keys(learned.learned)
|
|
131
|
+
assert.ok(kinds.length > 0, '이력에서 배운 종류가 있어야 한다')
|
|
132
|
+
|
|
133
|
+
/* 새 커널에 그 이력을 실으면 — 선언값(90%)이 아니라 이력값으로 답한다. */
|
|
134
|
+
const k2: any = new MesKernel('plantA')
|
|
135
|
+
k2.declareParameters(Object.fromEntries(kinds.map(kind => [kind, { yield: '0.9' }])))
|
|
136
|
+
k2.yieldEstimator = learned.estimator
|
|
137
|
+
for (const kind of kinds) {
|
|
138
|
+
assert.equal(k2.yieldOf(kind), learned.learned[kind], `${kind}: 이력값이 이긴다`)
|
|
139
|
+
assert.deepEqual(k2.specCoverage().operations.find((o: any) => o.kind === kind)?.parameterBasis, { yield: 'measured' })
|
|
140
|
+
}
|
|
141
|
+
/* 이력이 없는 종류는 그대로 선언값이다 — 배운 것만 이긴다(전부 덮지 않는다). */
|
|
142
|
+
k2.declareParameters({ 'never-run': { yield: '0.5' } })
|
|
143
|
+
assert.equal(k2.yieldOf('never-run'), 0.5)
|
|
144
|
+
})
|