@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
|
@@ -69,7 +69,15 @@ test('EMS 밖에도 공용 어휘가 있다 — 속도는 어느 트윈에서나
|
|
|
69
69
|
/* 읽는 코드가 있는 것만 — 판정 기준 셋(주의)과 속도. 새 효과를 붙이면 이 목록도 함께 자란다. */
|
|
70
70
|
assert.deepEqual(
|
|
71
71
|
specs.map(s => s.id).sort(),
|
|
72
|
-
[
|
|
72
|
+
[
|
|
73
|
+
'attention.congestionRatio',
|
|
74
|
+
'attention.scrapMinSamples',
|
|
75
|
+
'attention.scrapRate',
|
|
76
|
+
'operation.duration',
|
|
77
|
+
'operation.setupDuration',
|
|
78
|
+
'operation.yield',
|
|
79
|
+
'speed'
|
|
80
|
+
],
|
|
73
81
|
'읽는 코드가 있는 것만'
|
|
74
82
|
)
|
|
75
83
|
/* 모르는 속성은 이제 「어휘가 없다」가 아니라 **「그 속성을 읽지 않는다」**로 거절된다(더 정확하다). */
|
|
@@ -552,3 +560,102 @@ test('두 겹이 함께 얹힌다 — 트윈 값이 현장 값을 이긴다(시
|
|
|
552
560
|
assert.deepEqual((out.model as any).localDurations, { putaway: 'PT120S' }, '좁은 겹이 이긴다')
|
|
553
561
|
assert.deepEqual(out.unreadable, [])
|
|
554
562
|
})
|
|
563
|
+
|
|
564
|
+
/*
|
|
565
|
+
* ── 공정 모수: 수율·셋업 (2026-08-19) ───────────────────────────────────────
|
|
566
|
+
* 양품률 기본값은 커널 소스의 상수(0.8)였고 **그 상수가 불량 과다 주목 신호까지 만들었다.** 현장이 자기
|
|
567
|
+
* 수를 말할 자리를 준다. 사람은 퍼센트로 말하고(80=80%), 커널은 비율(0..1)을 읽는다 — 환산은 한 곳에서.
|
|
568
|
+
*/
|
|
569
|
+
test('수율은 퍼센트로 받아 비율로 얹는다 — 80 이 0.8 이다(한 곳에서만 나눈다)', () => {
|
|
570
|
+
const { accepted, rejected } = validateLocalDeclarations('mes', [
|
|
571
|
+
{ target: 'operation', id: 'weld', properties: [{ id: 'operation.yield', value: '95' }] }
|
|
572
|
+
])
|
|
573
|
+
assert.deepEqual(rejected, [])
|
|
574
|
+
const { model, unreadable } = applyLocalDeclarations({ locations: [], equipment: [] }, accepted)
|
|
575
|
+
assert.deepEqual(unreadable, [])
|
|
576
|
+
assert.deepEqual((model as any).localParams, { weld: { yield: '0.95' } })
|
|
577
|
+
assert.equal((model as any).localDurations, undefined, '모수는 시간 표를 건드리지 않는다')
|
|
578
|
+
})
|
|
579
|
+
|
|
580
|
+
test('수율은 퍼센트 범위를 지킨다 — 0.8 을 넣으면 걸린다(SOC 에서 겪은 함정)', () => {
|
|
581
|
+
const low = validateLocalDeclarations('mes', [
|
|
582
|
+
{ target: 'operation', id: 'weld', properties: [{ id: 'operation.yield', value: '0.8' }] }
|
|
583
|
+
])
|
|
584
|
+
assert.equal(low.rejected[0]?.reason, 'out-of-range')
|
|
585
|
+
const high = validateLocalDeclarations('mes', [
|
|
586
|
+
{ target: 'operation', id: 'weld', properties: [{ id: 'operation.yield', value: '150' }] }
|
|
587
|
+
])
|
|
588
|
+
assert.equal(high.rejected[0]?.reason, 'out-of-range')
|
|
589
|
+
})
|
|
590
|
+
|
|
591
|
+
test('셋업은 단위와 함께 받아 ISO 8601 로 얹는다 — 시간 표가 아니라 모수 표에', () => {
|
|
592
|
+
const { accepted } = validateLocalDeclarations('mes', [
|
|
593
|
+
{ target: 'operation', id: 'paint', properties: [{ id: 'operation.setupDuration', value: '7.5', uom: 'MIN' }] }
|
|
594
|
+
])
|
|
595
|
+
const { model } = applyLocalDeclarations({ locations: [], equipment: [] }, accepted)
|
|
596
|
+
assert.deepEqual((model as any).localParams, { paint: { setupDuration: 'PT450S' } })
|
|
597
|
+
})
|
|
598
|
+
|
|
599
|
+
test('셋업도 단위 없이 받지 않는다 — 7 초와 7 분은 60배 다르다', () => {
|
|
600
|
+
const { rejected } = validateLocalDeclarations('mes', [
|
|
601
|
+
{ target: 'operation', id: 'paint', properties: [{ id: 'operation.setupDuration', value: '7' }] }
|
|
602
|
+
])
|
|
603
|
+
assert.equal(rejected[0]?.reason, 'uom-required')
|
|
604
|
+
})
|
|
605
|
+
|
|
606
|
+
test('한 공정에 시간·수율·셋업을 함께 얹는다 — 서로 덮지 않는다', () => {
|
|
607
|
+
const { accepted } = validateLocalDeclarations('mes', [
|
|
608
|
+
{
|
|
609
|
+
target: 'operation',
|
|
610
|
+
id: 'weld',
|
|
611
|
+
properties: [
|
|
612
|
+
{ id: 'operation.duration', value: '3', uom: 'MIN' },
|
|
613
|
+
{ id: 'operation.yield', value: '92' },
|
|
614
|
+
{ id: 'operation.setupDuration', value: '30', uom: 'SEC' }
|
|
615
|
+
]
|
|
616
|
+
}
|
|
617
|
+
])
|
|
618
|
+
const { model } = applyLocalDeclarations({ locations: [], equipment: [] }, accepted)
|
|
619
|
+
assert.deepEqual((model as any).localDurations, { weld: 'PT180S' })
|
|
620
|
+
assert.deepEqual((model as any).localParams, { weld: { yield: '0.92', setupDuration: 'PT30S' } })
|
|
621
|
+
})
|
|
622
|
+
|
|
623
|
+
test('모수 철회는 모수 표에서 뺀다 — 시간 선언은 그대로 남는다', () => {
|
|
624
|
+
const declared = [
|
|
625
|
+
{
|
|
626
|
+
target: 'operation' as const,
|
|
627
|
+
id: 'weld',
|
|
628
|
+
properties: [
|
|
629
|
+
{ id: 'operation.duration', value: '3', uom: 'MIN' },
|
|
630
|
+
{ id: 'operation.yield', value: '92' }
|
|
631
|
+
]
|
|
632
|
+
}
|
|
633
|
+
]
|
|
634
|
+
const applied = applyLocalDeclarations({ locations: [], equipment: [] }, declared)
|
|
635
|
+
const { layer, revoked, notFound } = revokeLocalDeclarations(applied.layer, [
|
|
636
|
+
{ target: 'operation', id: 'weld', propertyId: 'operation.yield' }
|
|
637
|
+
])
|
|
638
|
+
assert.deepEqual(notFound, [])
|
|
639
|
+
assert.deepEqual(layer[0].properties.map(p => p.id), ['operation.duration'], '시간 선언은 겹에 남는다')
|
|
640
|
+
const back = restoreRevoked(applied.model, revoked)
|
|
641
|
+
assert.equal((back.model as any).localParams, undefined, '빈 껍데기를 남기지 않는다')
|
|
642
|
+
assert.deepEqual((back.model as any).localDurations, { weld: 'PT180S' }, '시간은 그대로')
|
|
643
|
+
})
|
|
644
|
+
|
|
645
|
+
test('없는 모수를 철회했다고 말하지 않는다 — 조용한 성공이 가장 비싸다', () => {
|
|
646
|
+
const back = restoreRevoked({ locations: [], equipment: [], localParams: { weld: { yield: '0.9' } } }, [
|
|
647
|
+
{ target: 'operation', id: 'weld', propertyId: 'operation.setupDuration', replaced: null }
|
|
648
|
+
])
|
|
649
|
+
assert.deepEqual(back.restored, [])
|
|
650
|
+
assert.deepEqual(back.missingTargets, [{ target: 'operation', id: 'weld' }])
|
|
651
|
+
assert.deepEqual((back.model as any).localParams, { weld: { yield: '0.9' } }, '남의 값을 건드리지 않는다')
|
|
652
|
+
})
|
|
653
|
+
|
|
654
|
+
test('두 겹이 함께 얹혀도 모수가 섞이지 않는다 — 좁은 겹이 이긴다', () => {
|
|
655
|
+
const out = applyDeclarationLayers(
|
|
656
|
+
{ locations: [], equipment: [] },
|
|
657
|
+
[{ target: 'operation', id: 'weld', properties: [{ id: 'operation.yield', value: '80' }] }],
|
|
658
|
+
[{ target: 'operation', id: 'weld', properties: [{ id: 'operation.yield', value: '95' }] }]
|
|
659
|
+
)
|
|
660
|
+
assert.deepEqual((out.model as any).localParams, { weld: { yield: '0.95' } })
|
|
661
|
+
})
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 실측 양품률 — **이력에서 배운 수율이 선언값·상수를 이긴다.**
|
|
3
|
+
*
|
|
4
|
+
* ── 왜 (2026-08-19) ─────────────────────────────────────────────────────────
|
|
5
|
+
* 소요시간에는 이력 층이 있었는데 수율에는 없었다. 양품률 기본값은 커널 소스의 상수(0.8)였고 **그 상수가
|
|
6
|
+
* 불량 과다 주목 신호까지 만들었다.** 이제 커널이 판정을 작업 완료에 적고(`outcome`), 폴드가 축으로 들고,
|
|
7
|
+
* KPI 가 종류별로 세고(`quality`), 이 모듈이 비율로 바꿔 커널 시임에 넣는다.
|
|
8
|
+
*
|
|
9
|
+
* 이 시험이 지키는 것:
|
|
10
|
+
* ① 분모는 **판정된 것만** — 이동·체류를 섞으면 양품률이 낮게 나온다.
|
|
11
|
+
* ② 표본이 모자라면 **배우지 않는다** — 비율은 드문 사건을 재는 값이라 작은 표본이 잡음을 만든다.
|
|
12
|
+
* ③ 0 도 사실이다 — 전량 불량이면 0 을 배운다(「값이 없다」와 다르다).
|
|
13
|
+
* ④ 모르는 종류에는 `undefined` — 그러면 커널이 선언값·상수로 내려간다.
|
|
14
|
+
* ⑤ 배우지 못한 것을 **밝힌다** — 조용히 일부만 반영하지 않는다.
|
|
15
|
+
*/
|
|
16
|
+
import { test } from 'node:test'
|
|
17
|
+
import assert from 'node:assert/strict'
|
|
18
|
+
import { readFileSync } from 'node:fs'
|
|
19
|
+
import { fileURLToPath } from 'node:url'
|
|
20
|
+
|
|
21
|
+
import { buildYieldEstimator, DEFAULT_YIELD_MIN_SAMPLES } from '../server/engine/measured-yield.ts'
|
|
22
|
+
|
|
23
|
+
const group = (key: string, good: number, scrap: number) => ({ key, quality: { good, scrap } })
|
|
24
|
+
|
|
25
|
+
test('판정된 것만 분모다 — 이동·체류가 섞이지 않는다', () => {
|
|
26
|
+
/* tasks 는 1000 건이어도 판정은 40 건뿐인 종류가 정상이다(중간 스테이션은 품질을 가리지 않는다). */
|
|
27
|
+
const r = buildYieldEstimator([{ key: 'assembly', tasks: 1000, quality: { good: 36, scrap: 4 } } as any])
|
|
28
|
+
assert.equal(r.learned.assembly, 0.9, '36/40 = 0.9')
|
|
29
|
+
assert.deepEqual(r.samples.assembly, { good: 36, scrap: 4 })
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test('표본이 모자라면 배우지 않고 그 사실을 낸다 — 세 건 중 한 건 불량은 33% 가 아니다', () => {
|
|
33
|
+
const r = buildYieldEstimator([group('weld', 2, 1)])
|
|
34
|
+
assert.deepEqual(r.learned, {})
|
|
35
|
+
assert.deepEqual(r.skipped, [{ kind: 'weld', judged: 3 }])
|
|
36
|
+
assert.equal(r.estimator, undefined, '배운 것이 없으면 시임을 만들지 않는다')
|
|
37
|
+
assert.equal(r.minSamples, DEFAULT_YIELD_MIN_SAMPLES)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test('하한은 소요보다 높다 — 비율은 드문 사건을 재는 값이다', () => {
|
|
41
|
+
/* 불량률 5% 인 공정은 20건에서 불량이 한 건이다 — 그 아래로 내려가면 이력이 잡음이 된다. */
|
|
42
|
+
assert.ok(DEFAULT_YIELD_MIN_SAMPLES >= 20, `하한이 ${DEFAULT_YIELD_MIN_SAMPLES} 이면 비율에 너무 낮다`)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test('전량 불량은 0 을 배운다 — 「값이 없다」와 구별한다', () => {
|
|
46
|
+
const r = buildYieldEstimator([group('paint', 0, 25)])
|
|
47
|
+
assert.equal(r.learned.paint, 0)
|
|
48
|
+
assert.equal(r.estimator!.estimate({ kind: 'paint' }), 0, '0 을 답한다(undefined 로 뭉개지 않는다)')
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test('판정이 아예 없는 종류는 건너뛴 것이 아니다 — 품질을 가리지 않는 공정이 정상이다', () => {
|
|
52
|
+
const r = buildYieldEstimator([{ key: 'putaway', tasks: 500 } as any])
|
|
53
|
+
assert.deepEqual(r.learned, {})
|
|
54
|
+
assert.deepEqual(r.skipped, [], '표본 부족으로 세지 않는다')
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('모르는 종류에는 undefined — 커널이 선언값·상수로 내려갈 수 있게', () => {
|
|
58
|
+
const r = buildYieldEstimator([group('weld', 30, 2)])
|
|
59
|
+
assert.equal(r.estimator!.estimate({ kind: 'weld' }), 30 / 32)
|
|
60
|
+
assert.equal(r.estimator!.estimate({ kind: 'paint' }), undefined)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
test("'unknown' 축은 쓰지 않는다 — 종류를 모르는 기록을 특정 종류의 근거로 삼지 않는다", () => {
|
|
64
|
+
const r = buildYieldEstimator([group('unknown', 100, 5)])
|
|
65
|
+
assert.deepEqual(r.learned, {})
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test('빈 입력에도 답을 만든다 — 부르는 쪽이 조건문을 두 벌 갖지 않게', () => {
|
|
69
|
+
for (const input of [undefined, []]) {
|
|
70
|
+
const r = buildYieldEstimator(input as any)
|
|
71
|
+
assert.deepEqual(r.learned, {})
|
|
72
|
+
assert.equal(r.estimator, undefined)
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
/*
|
|
77
|
+
* ── 사슬이 이어져 있나 (소스로 지킨다) ──────────────────────────────────────
|
|
78
|
+
* 이 층은 넷이 이어져야 뜻이 있다: 커널이 판정을 적고 → 폴드가 축으로 들고 → KPI 가 세고 → 여기서 비율.
|
|
79
|
+
* 한 칸만 빠져도 화면은 「이력에서 배운 값」이라고 말하면서 상수로 돈다. 실 앱에서 관측하려면 라인이
|
|
80
|
+
* 한 바퀴 돌아야 하므로(판정은 완료에서 일어난다), 경로는 여기서 지킨다.
|
|
81
|
+
*/
|
|
82
|
+
test('사슬 넷이 이어져 있다 — 판정→축→집계→비율', () => {
|
|
83
|
+
const hostSrc = (f: string) => readFileSync(fileURLToPath(new URL(`../server/${f}`, import.meta.url)), 'utf-8')
|
|
84
|
+
assert.match(hostSrc('engine/kpi-fold.ts'), /facets\.outcome/, 'KPI 가 판정 축을 읽는다')
|
|
85
|
+
assert.match(hostSrc('engine/kpi-fold.ts'), /quality\?: \{ good: number; scrap: number \}/, '종류별로 센다')
|
|
86
|
+
const engine = hostSrc('engine/twin-engine.ts')
|
|
87
|
+
assert.match(engine, /buildYieldEstimator\(kpi\?\.groups\?\.items/, '같은 폴드에서 배운다(두 번 접지 않는다)')
|
|
88
|
+
assert.match(engine, /kernel\.yieldEstimator = yields\.estimator/, '커널 시임에 넣는다')
|
|
89
|
+
assert.match(engine, /yieldOf missing/, '옛 커널이면 그 사실을 말한다')
|
|
90
|
+
})
|
|
@@ -93,8 +93,35 @@ test('이력 목록은 선언 대조에도 쓰인다 — 오타를 그때부터
|
|
|
93
93
|
*/
|
|
94
94
|
test('시뮬 기동이 모드를 적는다 — 「돌고 있나」를 묻는 쪽이 답을 받아야 한다', () => {
|
|
95
95
|
const src = readFileSync(fileURLToPath(new URL('../server/engine/twin-engine.ts', import.meta.url)), 'utf-8')
|
|
96
|
-
assert.match(src, /
|
|
96
|
+
assert.match(src, /mode: 'sim',\s*\n\s*runtime/, '시뮬 런타임도 모드를 든다')
|
|
97
97
|
/* 답이 갈리는 자리 — 도는 트윈이면 「재기동 때 받는다」로 말해야 한다. */
|
|
98
98
|
const mut = readFileSync(fileURLToPath(new URL('../server/service/twin-model/twin-model-mutation.ts', import.meta.url)), 'utf-8')
|
|
99
99
|
assert.match(mut, /running \? 'on-restart' : 'stored'/)
|
|
100
100
|
})
|
|
101
|
+
|
|
102
|
+
/*
|
|
103
|
+
* ── 공정 모수도 목록에 실린다 (2026-08-19) ──────────────────────────────────
|
|
104
|
+
* 수율·셋업은 시간과 다른 표에 담긴다(`localParams`). 목록이 그것을 함께 들지 않으면 선언해 둔 값이
|
|
105
|
+
* 화면에서 사라지고, 그러면 철회할 자리도 없어진다. 원천이 말한 모수도 함께 들어 견줄 수 있게 한다.
|
|
106
|
+
*/
|
|
107
|
+
test('선언한 모수와 원천 모수를 함께 든다 — 무엇을 고쳤나가 보인다', () => {
|
|
108
|
+
const model = {
|
|
109
|
+
operations: [{ key: 'weld', label: 'Welding', intent: 'process', parameters: [{ id: 'yield', value: '0.7' }] }],
|
|
110
|
+
localParams: { weld: { yield: '0.92', setupDuration: 'PT30S' } }
|
|
111
|
+
}
|
|
112
|
+
const rows = operationBasis(model, kernelReporting([{ kind: 'weld', duration: 'declared' }]))
|
|
113
|
+
assert.deepEqual(rows, [
|
|
114
|
+
{
|
|
115
|
+
kind: 'weld',
|
|
116
|
+
label: 'Welding',
|
|
117
|
+
referenceParams: { yield: '0.7' },
|
|
118
|
+
basis: 'declared',
|
|
119
|
+
localParams: { yield: '0.92', setupDuration: 'PT30S' }
|
|
120
|
+
}
|
|
121
|
+
])
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
test('모수만 선언한 공정도 목록에 남는다 — 커널이 아직 돌리지 않았어도', () => {
|
|
125
|
+
const rows = operationBasis({ localParams: { inspect: { yield: '0.99' } } }, undefined)
|
|
126
|
+
assert.deepEqual(rows, [{ kind: 'inspect', localParams: { yield: '0.99' } }])
|
|
127
|
+
})
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* **라이브 문이 운영 사실을 안다** — 미러가 시뮬보다 가난하지 않게 (ADR-0029 어휘 넓히기, 2026-08-19).
|
|
3
|
+
*
|
|
4
|
+
* ── 무엇이 막혀 있었나 ──────────────────────────────────────────────────────
|
|
5
|
+
* 이 문(`ingestCanonicalRecords`)은 어휘를 셋만 알았다: EPCIS 품목 · 에너지 계량 · 설비 에너지. 그리고
|
|
6
|
+
* **모르는 것은 EPCIS 로 떨어졌다** — 작업 사실을 넣으면 `epc` 가 없어 거부됐고, 원본은 「이 작업이
|
|
7
|
+
* 끝났다」를 말할 길이 없었다. 시뮬 커널만 그것을 알고 미러는 영원히 몰랐다.
|
|
8
|
+
*
|
|
9
|
+
* ── 이 시험이 지키는 것 ─────────────────────────────────────────────────────
|
|
10
|
+
* ① 네 어휘가 **한 문**에서 갈린다(판정은 커널의 것이고 여기서 다시 짐작하지 않는다).
|
|
11
|
+
* ② 운영 사실이 자기 봉투로 나온다 — EPCIS 로 감싸지지 않는다.
|
|
12
|
+
* ③ 섞여 와도 갈린다(실 피드는 한 배치에 여러 어휘가 섞인다).
|
|
13
|
+
* ④ 거부는 이유와 함께 남는다 — 피드 결선이 그 이유를 사람에게 말한다.
|
|
14
|
+
*/
|
|
15
|
+
import { test } from 'node:test'
|
|
16
|
+
import assert from 'node:assert/strict'
|
|
17
|
+
import { readFileSync } from 'node:fs'
|
|
18
|
+
import { fileURLToPath } from 'node:url'
|
|
19
|
+
|
|
20
|
+
import { ingestCanonicalRecords } from '../server/engine/canonical-ingest.ts'
|
|
21
|
+
|
|
22
|
+
const AT = '2026-08-19T09:00:00.000Z'
|
|
23
|
+
|
|
24
|
+
test('운영 사실이 자기 봉투로 나온다 — EPCIS 로 감싸지지 않는다', () => {
|
|
25
|
+
const { accepted, rejected } = ingestCanonicalRecords(
|
|
26
|
+
[{ taskId: 'tk-1', kind: 'putaway', status: 'completed' } as any],
|
|
27
|
+
'domain-1',
|
|
28
|
+
AT
|
|
29
|
+
)
|
|
30
|
+
assert.deepEqual(rejected, [])
|
|
31
|
+
assert.equal(accepted.length, 1)
|
|
32
|
+
assert.equal(accepted[0].eventType, 'task.status')
|
|
33
|
+
assert.equal(accepted[0].eventTime, AT, '시각을 주지 않으면 이 문이 정한 기본 시각을 쓴다')
|
|
34
|
+
assert.equal(accepted[0].tenantId, 'domain-1')
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test('네 어휘가 한 배치에서 갈린다 — 실 피드는 섞여 온다', () => {
|
|
38
|
+
const { accepted, rejected } = ingestCanonicalRecords(
|
|
39
|
+
[
|
|
40
|
+
{ epc: 'urn:epc:id:sgtin:1.1.1', action: 'ADD', bizStep: 'urn:epcglobal:cbv:bizstep:receiving', readPoint: 'dock-1' },
|
|
41
|
+
{ meterId: 'm-1', kW: 120 },
|
|
42
|
+
{ equipmentId: 'pv-1', generatedKW: 40 },
|
|
43
|
+
{ moverId: 'fk-1', kind: 'forklift', status: 'busy' }, // vocabulary-guard: allow 저널 와이어 필드
|
|
44
|
+
{ orderId: 'or-1', kind: 'outbound', status: 'picking', requested: 10, fulfilled: 3 }
|
|
45
|
+
] as any,
|
|
46
|
+
'domain-1',
|
|
47
|
+
AT
|
|
48
|
+
)
|
|
49
|
+
assert.deepEqual(rejected, [], '어느 어휘도 남의 길로 떨어지지 않는다')
|
|
50
|
+
assert.deepEqual(accepted.map(e => e.eventType).sort(), [
|
|
51
|
+
'energy.equipment', 'energy.measured', 'epcis.ObjectEvent', 'equipment.status', 'order.status'
|
|
52
|
+
].sort())
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test('거부는 이유와 함께 남는다 — 유효한 것은 함께 통과한다', () => {
|
|
56
|
+
const { accepted, rejected } = ingestCanonicalRecords(
|
|
57
|
+
[
|
|
58
|
+
{ taskId: 'tk-1', kind: 'pick', status: 'completed' },
|
|
59
|
+
{ taskId: 'tk-2', kind: 'pick', status: 'FINISHED' } // 커널이 접을 수 없는 낱말
|
|
60
|
+
] as any,
|
|
61
|
+
'domain-1',
|
|
62
|
+
AT
|
|
63
|
+
)
|
|
64
|
+
assert.equal(accepted.length, 1)
|
|
65
|
+
assert.equal(rejected.length, 1)
|
|
66
|
+
assert.ok(rejected[0].errors.join(' ').length > 0)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
test('피드 결선이 거부를 조용히 버리지 않는다 — 이유를 사람에게 말한다', () => {
|
|
70
|
+
const src = readFileSync(fileURLToPath(new URL('../server/service/reference/reference-live.ts', import.meta.url)), 'utf-8')
|
|
71
|
+
assert.match(src, /record\(s\) rejected/, '거부 수와 이유를 남긴다')
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
/*
|
|
75
|
+
* ── 담김도 이 문을 지난다 (2026-08-19) ──────────────────────────────────────
|
|
76
|
+
* 실 앱에서 한 바퀴 돌리자 「옮기지 못했다」 로그가 `epcis.AggregationEvent` 를 짚었다 — 관측 리듀서는 그
|
|
77
|
+
* 사실로 자식 물품에 `parent` 를 붙이는데(3D 가 「이 상자가 어느 팔레트에 실렸나」를 묻는 자리) 문에 그
|
|
78
|
+
* 어휘가 없었다.
|
|
79
|
+
*/
|
|
80
|
+
test('담김 레코드가 자기 봉투로 나온다 — 개체 관측과 한 배치에 섞여 와도 갈린다', () => {
|
|
81
|
+
const { accepted, rejected } = ingestCanonicalRecords(
|
|
82
|
+
[
|
|
83
|
+
{ epc: 'urn:epc:id:sgtin:1.1.1', action: 'ADD', bizStep: 'urn:epcglobal:cbv:bizstep:receiving', readPoint: 'dock-1' },
|
|
84
|
+
{ parentID: 'urn:epc:id:sscc:0614141.0000000001', childEPCs: ['urn:epc:id:sgtin:1.1.1'], action: 'ADD', bizStep: 'urn:epcglobal:cbv:bizstep:packing', readPoint: 'packing' }
|
|
85
|
+
] as any,
|
|
86
|
+
'domain-1',
|
|
87
|
+
AT
|
|
88
|
+
)
|
|
89
|
+
assert.deepEqual(rejected, [])
|
|
90
|
+
assert.deepEqual(accepted.map(e => e.eventType), ['epcis.ObjectEvent', 'epcis.AggregationEvent'])
|
|
91
|
+
assert.equal((accepted[1].data as any).parentID, 'urn:epc:id:sscc:0614141.0000000001')
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
test('자리를 구조째 실으면 거부한다 — 예전에는 "[object Object]" 로 굳어 저널에 쌓였다', () => {
|
|
95
|
+
const { accepted, rejected } = ingestCanonicalRecords(
|
|
96
|
+
[{ epc: 'urn:epc:id:sgtin:1.1.1', action: 'ADD', bizStep: 'urn:epcglobal:cbv:bizstep:receiving', readPoint: { id: 'dock-1' } }] as any,
|
|
97
|
+
'domain-1',
|
|
98
|
+
AT
|
|
99
|
+
)
|
|
100
|
+
assert.equal(accepted.length, 0)
|
|
101
|
+
assert.match(rejected[0].errors.join(' '), /readPoint/)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
test('이 문이 어휘가 넷임을 스스로 말한다 — 다음 사람이 세 어휘로 읽지 않게', () => {
|
|
105
|
+
const src = readFileSync(fileURLToPath(new URL('../server/engine/canonical-ingest.ts', import.meta.url)), 'utf-8')
|
|
106
|
+
assert.match(src, /어휘가 넷이다/)
|
|
107
|
+
assert.match(src, /isOperationalRecord/, '판정은 커널의 것을 부른다(여기서 다시 짐작하지 않는다)')
|
|
108
|
+
})
|
|
@@ -102,8 +102,8 @@ before(async () => {
|
|
|
102
102
|
q = new TwinJournalQuery()
|
|
103
103
|
ctx = { state: { domain: { id: domainId } } }
|
|
104
104
|
|
|
105
|
-
await TwinEngine.ingestMaster(domainId, mesMaster())
|
|
106
|
-
await TwinEngine.ingestMaster(domainId, wmsMaster())
|
|
105
|
+
await TwinEngine.ingestMaster(domainId, mesMaster(), 'reset')
|
|
106
|
+
await TwinEngine.ingestMaster(domainId, wmsMaster(), 'reset')
|
|
107
107
|
})
|
|
108
108
|
|
|
109
109
|
after(async () => {
|
|
@@ -122,7 +122,7 @@ test('실행 중이 아니면 그 사실을 알린다 — "없는 것" 과 구
|
|
|
122
122
|
})
|
|
123
123
|
|
|
124
124
|
test('돌고 있으면 구간의 양을 종류로 갈라 낸다 — 표준 등식이 성립한다', async () => {
|
|
125
|
-
await TwinEngine.startFromRegistry(domainId, MES, '
|
|
125
|
+
await TwinEngine.startFromRegistry(domainId, MES, 'reset')
|
|
126
126
|
const rows = await ask(MES)
|
|
127
127
|
assert.equal(rows[0].reason, undefined)
|
|
128
128
|
|
|
@@ -138,7 +138,7 @@ test('돌고 있으면 구간의 양을 종류로 갈라 낸다 — 표준 등
|
|
|
138
138
|
})
|
|
139
139
|
|
|
140
140
|
test('공정을 선언하지 않은 트윈은 판정 대상이 아니다 — 0 으로 채우면 없는 병목이 생긴다', async () => {
|
|
141
|
-
await TwinEngine.startFromRegistry(domainId, WMS, '
|
|
141
|
+
await TwinEngine.startFromRegistry(domainId, WMS, 'reset')
|
|
142
142
|
const rows = await q.twinOperationsCapability(WMS, undefined, 24, WINDOW.fromTime, WINDOW.toTime, WINDOW.fromTime, ctx)
|
|
143
143
|
assert.equal(rows[0].reason, 'no-operations')
|
|
144
144
|
assert.equal(rows[0].report, undefined)
|
|
@@ -184,7 +184,7 @@ before(async () => {
|
|
|
184
184
|
q = new TwinModelItemQuery()
|
|
185
185
|
ctx = { state: { domain: { id: domainId } } }
|
|
186
186
|
|
|
187
|
-
await TwinEngine.ingestMaster(domainId, master())
|
|
187
|
+
await TwinEngine.ingestMaster(domainId, master(), 'reset')
|
|
188
188
|
})
|
|
189
189
|
|
|
190
190
|
after(async () => {
|
|
@@ -94,3 +94,31 @@ test('공정 시간이 등록돼 있고, 그 값을 실어 보내는 자리를
|
|
|
94
94
|
assert.match(layer, /isoDurationOf/, '커널이 읽는 표기로 바꾼다')
|
|
95
95
|
assert.match(hostSrc('engine/twin-engine.ts'), /declareDurations\(declared\)/, '기동·구조전환 때 커널에 싣는다')
|
|
96
96
|
})
|
|
97
|
+
|
|
98
|
+
/*
|
|
99
|
+
* 모수도 같은 배선을 지난다 (2026-08-19).
|
|
100
|
+
*
|
|
101
|
+
* 이 시험이 소스를 보는 이유: 모수를 읽는 순간은 **작업이 완료될 때**라서 실 앱에서 관측하려면 라인이
|
|
102
|
+
* 한 바퀴 돌아야 한다(복원된 25분 작업이 쌓인 트윈에서는 그 관측이 오래 걸린다 — 실제로 그랬다).
|
|
103
|
+
* 값이 커널까지 가는 **경로**는 여기서 지키고, 커널이 그 값을 쓰는지는 커널 시험이 값으로 지킨다.
|
|
104
|
+
*/
|
|
105
|
+
test('수율·셋업이 커널 창구까지 배선돼 있다 — 등록만 하고 나르지 않으면 화면이 거짓을 말한다', () => {
|
|
106
|
+
const layer = hostSrc('engine/local-declarations.ts')
|
|
107
|
+
assert.match(layer, /OPERATION_PROPERTY\.yield/, '선언 겹이 수율을 안다')
|
|
108
|
+
assert.match(layer, /OPERATION_PROPERTY\.setupDuration/, '선언 겹이 셋업을 안다')
|
|
109
|
+
/* 퍼센트 → 비율 환산은 **한 곳**이어야 한다(두 곳이면 100 이 두 번 걸린다). */
|
|
110
|
+
const divisions = layer.match(/pct \/ 100/g) ?? []
|
|
111
|
+
assert.equal(divisions.length, 1, `퍼센트 환산은 한 자리에서만 (발견 ${divisions.length})`)
|
|
112
|
+
assert.match(hostSrc('engine/twin-engine.ts'), /declareParameters\(params\)/, '기동·구조전환 때 커널에 싣는다')
|
|
113
|
+
/* 커널이 그 창구를 갖지 않은 버전이면 **말한다** — 조용히 상수로 돌지 않게. */
|
|
114
|
+
assert.match(hostSrc('engine/twin-engine.ts'), /declareParameters missing/, '버전 어긋남을 경고로 드러낸다')
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
test('공정 모수가 등록부에 있고 읽는 자리를 가리킨다', () => {
|
|
118
|
+
for (const id of ['operation.yield', 'operation.setupDuration']) {
|
|
119
|
+
const e = PROPERTY_EFFECTS.find(x => x.id === id)!
|
|
120
|
+
assert.ok(e, `${id} 가 등록부에 있어야 한다`)
|
|
121
|
+
assert.deepEqual(e.axes, ['operations'])
|
|
122
|
+
assert.match(e.readBy, /declareParameters/, '어느 창구로 가는지 말한다')
|
|
123
|
+
}
|
|
124
|
+
})
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* **재기동 정책** — 개명(ADR-0029 §2)과 그 규율 둘.
|
|
3
|
+
*
|
|
4
|
+
* ── 무엇을 바꿨나 ───────────────────────────────────────────────────────────
|
|
5
|
+
* `realityMode`(`mirror`·`sim-world`·`sim-experiment`) → `restartPolicy`(`resync`·`resume`·`reset`).
|
|
6
|
+
* 「현실이 어디서 오나」는 ADR-0029 가 **원본의 종류**로 옮긴 물음이고, 코드가 이 축으로 실제로 가른 것은
|
|
7
|
+
* **재기동 거동 하나**였다 — 그것은 원본의 성질이 아니라 트윈과 자기 과거의 관계다. 그래서 축은 남고
|
|
8
|
+
* 이름이 바뀌었다.
|
|
9
|
+
*
|
|
10
|
+
* ── 지키는 규율 둘 (계획서 §6) ──────────────────────────────────────────────
|
|
11
|
+
* ① **모르는 값은 던진다.** 예전에는 미선언이 조용히 `sim-experiment`(=저널 초기화)로 떨어졌다 — 값이
|
|
12
|
+
* 하나 어긋나는 순간 이력을 지우는 길이다. 기본값을 없앴다.
|
|
13
|
+
* ② **옛 어휘를 번역하지 않는다.** 번역을 두면 옛 값이 살아남고 다시 저장된다. 저장된 값은 한 번
|
|
14
|
+
* 옮기고(데이터 이관), 코드는 새 어휘만 안다 — 다만 무엇으로 옮겨야 하는지는 오류 문장이 말한다.
|
|
15
|
+
*/
|
|
16
|
+
import { test } from 'node:test'
|
|
17
|
+
import assert from 'node:assert/strict'
|
|
18
|
+
import { readdirSync, readFileSync, statSync } from 'node:fs'
|
|
19
|
+
import { fileURLToPath } from 'node:url'
|
|
20
|
+
import { join } from 'node:path'
|
|
21
|
+
|
|
22
|
+
import { readRestartPolicy, isRestartPolicy, RESTART_POLICIES } from '../server/engine/restart-policy.ts'
|
|
23
|
+
|
|
24
|
+
test('셋뿐이다 — 그리고 그 셋을 읽는다', () => {
|
|
25
|
+
assert.deepEqual([...RESTART_POLICIES], ['resync', 'resume', 'reset'])
|
|
26
|
+
for (const p of RESTART_POLICIES) assert.equal(readRestartPolicy(p, 'test'), p)
|
|
27
|
+
assert.equal(readRestartPolicy(' resume ', 'test'), 'resume', '공백은 다듬는다')
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
test('미선언은 던진다 — 기본값으로 메우면 저널이 지워지는 길이 열린다', () => {
|
|
31
|
+
for (const raw of [undefined, null, '', ' ', 42, {}]) {
|
|
32
|
+
assert.throws(() => readRestartPolicy(raw, 'twin "x"'), /twin "x": restart policy/)
|
|
33
|
+
}
|
|
34
|
+
/* 무엇을 해야 하는지 문장이 말한다 — 코드를 그대로 던지지 않는다. */
|
|
35
|
+
try {
|
|
36
|
+
readRestartPolicy(undefined, 'twin "x"')
|
|
37
|
+
assert.fail('던져야 한다')
|
|
38
|
+
} catch (e: any) {
|
|
39
|
+
assert.match(e.message, /resync \| resume \| reset/)
|
|
40
|
+
assert.match(e.message, /no default/)
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
test('옛 어휘도 던진다 — 다만 무엇으로 옮길지 말한다(번역해 주지 않는다)', () => {
|
|
45
|
+
const cases: [string, string][] = [
|
|
46
|
+
['mirror', 'resync'],
|
|
47
|
+
['sim-world', 'resume'],
|
|
48
|
+
['sim-experiment', 'reset']
|
|
49
|
+
]
|
|
50
|
+
for (const [old, next] of cases) {
|
|
51
|
+
try {
|
|
52
|
+
readRestartPolicy(old, 'twin "x"')
|
|
53
|
+
assert.fail(`${old} 은 던져야 한다`)
|
|
54
|
+
} catch (e: any) {
|
|
55
|
+
assert.match(e.message, /old vocabulary/)
|
|
56
|
+
assert.match(e.message, new RegExp(`"${next}"`), '옮길 값을 말한다')
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test('묻는 자리도 있다 — 화면·검사는 던지지 않고 판별한다', () => {
|
|
62
|
+
assert.equal(isRestartPolicy('resume'), true)
|
|
63
|
+
assert.equal(isRestartPolicy('sim-world'), false)
|
|
64
|
+
assert.equal(isRestartPolicy(undefined), false)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
/*
|
|
68
|
+
* ── 옛 어휘가 코드로 되돌아오지 않게 (계획서 §6 의 검사) ────────────────────
|
|
69
|
+
* ADR-0034 의 `local-declarations.test.ts` 가 `site*` 에 대해 하는 것과 같은 방식이다. 개명은 저장값·타입·
|
|
70
|
+
* 화면에 걸쳐 있어서, 한 곳만 되돌아와도 그 자리가 조용히 다르게 재기동한다.
|
|
71
|
+
*/
|
|
72
|
+
const OLD_TOKENS = [/\brealityMode\b/, /\bRealityMode\b/, /'sim-world'/, /'sim-experiment'/, /DEFAULT_REALITY_MODE/]
|
|
73
|
+
|
|
74
|
+
function sources(root: string): { file: string; src: string }[] {
|
|
75
|
+
const base = fileURLToPath(new URL(root, import.meta.url))
|
|
76
|
+
const out: { file: string; src: string }[] = []
|
|
77
|
+
const walk = (dir: string) => {
|
|
78
|
+
for (const name of readdirSync(dir)) {
|
|
79
|
+
if (name === 'node_modules' || name.startsWith('dist')) continue
|
|
80
|
+
const path = join(dir, name)
|
|
81
|
+
if (statSync(path).isDirectory()) { walk(path); continue }
|
|
82
|
+
if (name.endsWith('.ts')) out.push({ file: path.slice(base.length + 1), src: readFileSync(path, 'utf-8') })
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
walk(base)
|
|
86
|
+
return out
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
test('옛 어휘가 코드에 없다 — 이력을 설명하는 주석만 예외다', () => {
|
|
90
|
+
const offenders: string[] = []
|
|
91
|
+
for (const { file, src } of [...sources('../server'), ...sources('../test')]) {
|
|
92
|
+
/* 이력 설명(「예전에는 …」)과 이관 안내(restart-policy.ts)는 남아야 한다 — 그것이 개명의 근거다. */
|
|
93
|
+
if (file.endsWith('restart-policy.ts') || file.endsWith('restart-policy.test.ts')) continue
|
|
94
|
+
for (const line of src.split('\n')) {
|
|
95
|
+
const isComment = /^\s*(\*|\/\/|\/\*)/.test(line)
|
|
96
|
+
if (isComment) continue
|
|
97
|
+
if (OLD_TOKENS.some(re => re.test(line))) offenders.push(`${file}: ${line.trim().slice(0, 80)}`)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
assert.deepEqual(offenders, [], `옛 어휘가 코드로 돌아왔다:\n${offenders.join('\n')}`)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
test('기본값 상수가 없다 — 미선언을 우리가 고르지 않는다', () => {
|
|
104
|
+
const engine = readFileSync(fileURLToPath(new URL('../server/engine/twin-engine.ts', import.meta.url)), 'utf-8')
|
|
105
|
+
const code = engine
|
|
106
|
+
.split('\n')
|
|
107
|
+
.filter(line => !/^\s*(\*|\/\/|\/\*)/.test(line))
|
|
108
|
+
.join('\n')
|
|
109
|
+
assert.equal(/DEFAULT_RESTART_POLICY/.test(code), false, '기본값 상수가 되살아나면 미선언이 다시 조용해진다')
|
|
110
|
+
assert.match(code, /readRestartPolicy\(/, '읽는 자리는 엄격한 리더를 지난다')
|
|
111
|
+
})
|
|
@@ -73,5 +73,11 @@ test('프로비저닝이 고른 현장을 origin 에 남기고, 다시 읽기가
|
|
|
73
73
|
.join('\n')
|
|
74
74
|
assert.match(code, /kind: 'template', templateId, params: effective, \.\.\.\(spaceId \? \{ spaceId \} : \{\}\)/, '템플릿 트윈이 현장을 기억한다')
|
|
75
75
|
assert.match(code, /const keepSpace = origin\.spaceId \|\| inst\.spaceId/, '기억이 없으면 지금 있는 현장을 지킨다')
|
|
76
|
-
|
|
76
|
+
/* 인자 사이에 재기동 정책이 끼었다(ADR-0029 §2) — 현장을 넘기는 인자는 그대로여야 한다. */
|
|
77
|
+
assert.match(code, /keepSpace \? \{ spaceId: keepSpace \} : undefined/, '다시 읽기가 그 현장으로 넣는다')
|
|
78
|
+
assert.match(
|
|
79
|
+
code,
|
|
80
|
+
/readRestartPolicy\(inst\.restartPolicy, `resync of twin/,
|
|
81
|
+
'다시 읽기는 그 트윈의 정책을 그대로 지킨다(구조만 새로 그린다)'
|
|
82
|
+
)
|
|
77
83
|
})
|