@operato/twin-kernel 0.6.2 → 0.6.4
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/contract.d.ts +33 -0
- package/dist/domain-catalog.d.ts +41 -1
- package/dist/domain-catalog.js +61 -1
- package/dist/flow-engine.js +14 -0
- package/dist/observed-reducer.js +1 -0
- package/dist-cjs/index.cjs +77 -0
- package/package.json +1 -1
package/dist/contract.d.ts
CHANGED
|
@@ -806,6 +806,14 @@ export interface AssetState extends EffectivePeriod {
|
|
|
806
806
|
*/
|
|
807
807
|
effectivity?: Effectivity;
|
|
808
808
|
}
|
|
809
|
+
/**
|
|
810
|
+
* 작업 하나의 관측 상태 — **ISA-95 `SegmentResponse`**(Part 4, 실적).
|
|
811
|
+
*
|
|
812
|
+
* 1차 출처(B2MML v0701): `SegmentResponseType`(B2MML-OperationsPerformance.xsd) — 실적 하나에 응답이
|
|
813
|
+
* 여럿 들리고, 응답마다 어느 공정 구간을 언제 누구·무엇으로 했는지가 실린다(`PersonnelActual` ·
|
|
814
|
+
* `EquipmentActual` · `MaterialActual`). 이 계약의 `personnel`·`resourceRef`·`materialActual` 이 그
|
|
815
|
+
* 자리들이다.
|
|
816
|
+
*/
|
|
809
817
|
export interface TaskState {
|
|
810
818
|
id: string;
|
|
811
819
|
kind: string;
|
|
@@ -856,10 +864,27 @@ export interface TaskState {
|
|
|
856
864
|
uom?: string;
|
|
857
865
|
}[];
|
|
858
866
|
}
|
|
867
|
+
/**
|
|
868
|
+
* 오더 하나의 관측 상태 — **ISA-95 `OperationsRequest`**(Part 4, 일정).
|
|
869
|
+
*
|
|
870
|
+
* 1차 출처(B2MML v0701): `OperationsRequestType`(B2MML-OperationsSchedule.xsd) — 일정 하나에 요구가
|
|
871
|
+
* 여럿 들리고, 요구는 `SegmentRequirement` 로 쪼개진다. 여기 담는 것은 **원본이 낸 요구를 트윈이
|
|
872
|
+
* 관측한 것**이라 계획 자체가 아니다(그 구분이 ADR-0030 의 이유다).
|
|
873
|
+
*
|
|
874
|
+
* 실적 쪽 대응은 `TaskState`(`SegmentResponse`) 다 — 요구와 응답을 한 짝으로 읽어야 "얼마나 시켰고
|
|
875
|
+
* 얼마나 됐나" 가 나온다.
|
|
876
|
+
*/
|
|
859
877
|
export interface OrderState {
|
|
860
878
|
id: string;
|
|
861
879
|
kind: string;
|
|
862
880
|
status: string;
|
|
881
|
+
/**
|
|
882
|
+
* **무엇을 만드나** — GS1 품목 참조(단일 품목 작업지시). 여러 품목이면 `lines[].gtin` 이 말한다.
|
|
883
|
+
*
|
|
884
|
+
* 이 자리가 없던 동안 상태에는 "1개" 만 남고 **무엇을 1개인지** 사라졌다. 화면·예측·계보가 품목을
|
|
885
|
+
* 알 수 없다는 뜻이고, 확보분(`allocated`)이 같은 식으로 빠져 계보의 절반이 사라졌던 것과 같은 부류다.
|
|
886
|
+
*/
|
|
887
|
+
gtin?: string;
|
|
863
888
|
progress?: number;
|
|
864
889
|
held?: boolean;
|
|
865
890
|
/**
|
|
@@ -1236,6 +1261,14 @@ export interface OrderStatusDelta {
|
|
|
1236
1261
|
status: string;
|
|
1237
1262
|
requested: number;
|
|
1238
1263
|
fulfilled: number;
|
|
1264
|
+
/**
|
|
1265
|
+
* **무엇을 만드나** — 상태의 `OrderState.gtin` 과 같은 값.
|
|
1266
|
+
*
|
|
1267
|
+
* 스냅샷에만 실으면 **미러가 이 필드를 못 채운다**(라이브는 이벤트로만 배운다). 같은 사실은
|
|
1268
|
+
* 델타·스냅샷·씨앗 **세 자리**에 함께 실어야 한다 — `allocated` 가 두 자리에만 있어서 웜스타트가
|
|
1269
|
+
* 잃었던 것과 같은 부류다(패리티 가드가 이것을 잡았다).
|
|
1270
|
+
*/
|
|
1271
|
+
gtin?: string;
|
|
1239
1272
|
held?: boolean;
|
|
1240
1273
|
/**
|
|
1241
1274
|
* 오더 라인(SKU+수량) — 선택. 있으면 이행 예측이 "남은 데맨드(라인별 requested-fulfilled)를
|
package/dist/domain-catalog.d.ts
CHANGED
|
@@ -34,15 +34,40 @@ export interface TwinAxisInfo {
|
|
|
34
34
|
* (저장·계약의 이름과 갈라지면 그 순간 방언이 생긴다).
|
|
35
35
|
*/
|
|
36
36
|
axis: string;
|
|
37
|
+
/**
|
|
38
|
+
* 이 축을 **어디서 읽나.**
|
|
39
|
+
*
|
|
40
|
+
* 없으면 `'document'` — 지금까지의 모든 축이 그렇다(선언이고, 인제스트가 쓴다). 그런데 표준이
|
|
41
|
+
* 요구하는 것 중에는 **선언이 아닌 것**이 있다: 작업 일정과 실적은 사람이 적어 두는 값이 아니라
|
|
42
|
+
* 현장이 낳는 사실이고, 그 집은 커널 상태와 저널이다(ISA-95 Part 4).
|
|
43
|
+
*
|
|
44
|
+
* 이 칸이 없으면 축만 늘려도 소비처가 board 에서 찾다가 **언제나 0 을 답한다** — 오류 없이,
|
|
45
|
+
* 그냥 빈 공장처럼. 이 프로젝트가 이미 그 모양으로 한 번 무너졌다(옛 어휘 보드가 자리 0·설비 0).
|
|
46
|
+
*
|
|
47
|
+
* · `document` — 저장된 트윈 모델 안. `path` 가 그 자리를 말한다
|
|
48
|
+
* · `state` — 커널 상태. **지금의 사실**이고 커널이 돌 때만 있다(멈추면 `null`, 0 이 아니다)
|
|
49
|
+
* · `journal` — 저널. 과거까지 있고 커널이 멈춰도 남는다
|
|
50
|
+
*/
|
|
51
|
+
source?: 'document' | 'state' | 'journal';
|
|
52
|
+
/**
|
|
53
|
+
* 이 축이 **시간축을 갖나** — 과거 시점으로 되짚을 수 있는가(저널 폴드).
|
|
54
|
+
*
|
|
55
|
+
* 선언은 시간축이 없다(그 시점의 선언은 구조 리비전이 답한다). 관측은 있다 — 그래서 이 표시가
|
|
56
|
+
* 필요하다: 화면이 시각 커서를 이 축에 걸어도 되는지 알아야 한다.
|
|
57
|
+
*/
|
|
58
|
+
historical?: boolean;
|
|
37
59
|
/**
|
|
38
60
|
* board 안에서의 자리. 최상위면 `axis` 와 같고, 중첩이면 경로다
|
|
39
61
|
* (`productionSpec.definition.recipes`).
|
|
40
62
|
*
|
|
63
|
+
* **`source: 'document'` 일 때만 있다.** 상태·저널 축에는 board 안의 자리가 없다 — 빈 문자열로
|
|
64
|
+
* 두면 소비처가 board 루트를 읽고 통째로 잘못된 답을 만든다.
|
|
65
|
+
*
|
|
41
66
|
* **레시피·라우트는 저장상 `productionSpec` 안에 있지만 개념으로는 1급**이다 — 사람은
|
|
42
67
|
* "레시피" 를 찾지 "생산 정의 안의 레시피" 를 찾지 않는다. 저장 위치가 개념을 가두면
|
|
43
68
|
* 화면이 저장 구조를 흉내 내게 되고, 그건 `ADR-0032`(board 는 캐시) 뒤에 무의미해진다.
|
|
44
69
|
*/
|
|
45
|
-
path
|
|
70
|
+
path?: string;
|
|
46
71
|
/** i18n 키 — 사람 언어는 표현 계층이 렌더한다(L2). */
|
|
47
72
|
label: string;
|
|
48
73
|
/** 표준 온톨로지 투영. 표준에 자리가 없으면 **빈 객체**(숨기지 않는다). */
|
|
@@ -147,6 +172,21 @@ export interface TwinPropertyInfo {
|
|
|
147
172
|
export declare const TWIN_PROPERTIES: TwinPropertyInfo[];
|
|
148
173
|
/** 이 축이 보여 줄 속성들 — 없으면 빈 배열(속성 없는 축이 정상이다). */
|
|
149
174
|
export declare const propertiesOf: (axis: string) => TwinPropertyInfo[];
|
|
175
|
+
/**
|
|
176
|
+
* 이 축은 **선언에서 읽나** — `source` 를 말하지 않은 축은 문서 축이다(기존 14축).
|
|
177
|
+
*
|
|
178
|
+
* 소비처가 `source` 를 직접 비교하면 기본값 규칙이 자리마다 복사되고, 한 곳이 빠지면 그 축은
|
|
179
|
+
* board 루트를 읽어 조용히 잘못된 답을 낸다. 판정은 여기 하나다.
|
|
180
|
+
*/
|
|
181
|
+
export declare const axisSource: (axis: string) => "document" | "state" | "journal";
|
|
182
|
+
/**
|
|
183
|
+
* 문서에서 이 축을 찾을 자리 — **문서 축이 아니면 던진다.**
|
|
184
|
+
*
|
|
185
|
+
* 상태·저널 축에는 board 안의 자리가 없다. 그런데 옛 소비처는 `info.path` 를 무조건 읽었으므로,
|
|
186
|
+
* 그 자리가 `undefined` 면 board 루트를 읽고 **오류 없이 0 을 답한다**(이 프로젝트가 겪은 실패 모양).
|
|
187
|
+
* 그래서 조용히 넘기지 않고 부르는 쪽을 멈춘다.
|
|
188
|
+
*/
|
|
189
|
+
export declare function documentPath(axis: string): string;
|
|
150
190
|
/** 이 축에서 나가는 관계 · 이 축으로 들어오는 관계 — 항목 패널의 "이어진 것" 양방향. */
|
|
151
191
|
export declare const relationsFrom: (axis: string) => TwinRelationInfo[];
|
|
152
192
|
export declare const relationsTo: (axis: string) => TwinRelationInfo[];
|
package/dist/domain-catalog.js
CHANGED
|
@@ -43,7 +43,28 @@ export const TWIN_AXES = [
|
|
|
43
43
|
{ axis: 'routes', path: 'productionSpec.definition.routes', label: 'twin.axis.routes', kind: 'spec',
|
|
44
44
|
standardClass: { isa95: 'OperationsSegment' } },
|
|
45
45
|
{ axis: 'materials', path: 'productionSpec.definition.materials', label: 'twin.axis.materials', kind: 'class',
|
|
46
|
-
standardClass: { isa95: 'MaterialDefinition' } }
|
|
46
|
+
standardClass: { isa95: 'MaterialDefinition' } },
|
|
47
|
+
/*
|
|
48
|
+
* ── ISA-95 Part 4 — 일정과 실적 ────────────────────────────────────────────────
|
|
49
|
+
*
|
|
50
|
+
* 여기부터는 **선언이 아니다.** 오더와 작업은 사람이 board 에 적어 두는 값이 아니라 현장이 낳는
|
|
51
|
+
* 사실이고, 그 집은 커널 상태와 저널이다. 그래서 `source` 가 `'state'` 이고 `path` 가 없다.
|
|
52
|
+
*
|
|
53
|
+
* ── 왜 축이어야 하나 ──────────────────────────────────────────────────────────
|
|
54
|
+
* 이 둘이 축이 아니던 동안 "이 트윈이 표준의 어디를 채우나" 라는 질문은 **절반만** 답했다. Part 2
|
|
55
|
+
* 자원 모델은 여덟이 완전한데 Part 4 는 통째로 비어 있었다 — 런타임에는 실재하는데 조회 모델에는
|
|
56
|
+
* 없었기 때문이다. 실적을 성과 화면이 답한다는 것은 사실이지만, 그건 **집계**이고 개념 자체를
|
|
57
|
+
* 가리켜 걸어 들어갈 자리는 없었다(어느 오더가 어느 공정에서 무엇을 먹었나).
|
|
58
|
+
*
|
|
59
|
+
* ── 왜 테이블로 투영하지 않나 ─────────────────────────────────────────────────
|
|
60
|
+
* 정본이 저널이다. 구조 엔티티는 **원본이 낼 수 있는 것만** 담고(ADR-0032-D), 오더는 원본이 낸
|
|
61
|
+
* 것이지만 트윈이 **관측한** 것이다. 투영은 규모에서 성능이 필요해질 때 KPI 폴드와 같은 급의
|
|
62
|
+
* 별도 결정으로 한다.
|
|
63
|
+
*/
|
|
64
|
+
{ axis: 'orders', label: 'twin.axis.orders', kind: 'instance', source: 'state', historical: true,
|
|
65
|
+
standardClass: { isa95: 'OperationsRequest', epcis: 'TransactionEvent' } },
|
|
66
|
+
{ axis: 'tasks', label: 'twin.axis.tasks', kind: 'instance', source: 'state', historical: true,
|
|
67
|
+
standardClass: { isa95: 'SegmentResponse', epcis: 'TransformationEvent' } }
|
|
47
68
|
];
|
|
48
69
|
/** 관계 전체 — 지도의 선과 항목의 이웃이 여기서 나온다(화면은 이 목록을 갖지 않는다). */
|
|
49
70
|
export const TWIN_RELATIONS = [
|
|
@@ -63,6 +84,23 @@ export const TWIN_RELATIONS = [
|
|
|
63
84
|
{ from: 'recipes', field: 'inputs[].material', target: { kind: 'axis', axis: 'materials' }, via: 'twin.rel.input' },
|
|
64
85
|
{ from: 'recipes', field: 'outputs[].material', target: { kind: 'axis', axis: 'materials' }, via: 'twin.rel.output' },
|
|
65
86
|
{ from: 'routes', field: 'steps[]', target: { kind: 'axis', axis: 'operations' }, via: 'twin.rel.step' },
|
|
87
|
+
/*
|
|
88
|
+
* 일정·실적의 관계 — **관측 관계다.** 선언 관계는 "그렇게 만들기로 했다" 이고, 이것은 "그렇게
|
|
89
|
+
* 일어났다" 다. 관계가 없으면 이 두 축은 지도에서 떠 있는 카드가 된다(그러면 걸어 들어갈 수 없다).
|
|
90
|
+
*/
|
|
91
|
+
/*
|
|
92
|
+
* 오더가 무엇을 만드나 — **자재 키가 아니라 GS1 품목 참조다**(`lines[].gtin`). 자재와 이어 주는
|
|
93
|
+
* 것은 `productionSpec.binding` 이고 그 해소는 호스트가 한다 — 자리·구역과 같은 `external` 이다.
|
|
94
|
+
* 축을 직접 가리키게 적으면 없는 필드를 가리키는 선언이 된다(계약을 보고 고쳤다).
|
|
95
|
+
*/
|
|
96
|
+
{ from: 'orders', field: 'lines[].gtin', target: { kind: 'external', entity: 'gs1.itemRef' }, via: 'twin.rel.item', optional: true },
|
|
97
|
+
{ from: 'tasks', field: 'orderId', target: { kind: 'axis', axis: 'orders' }, via: 'twin.rel.order', optional: true },
|
|
98
|
+
/* 작업의 `kind` 는 그 공정의 키다(라우트의 단계 이름 그대로 — `mes-kernel` 의 공정 목록이 그 증거). */
|
|
99
|
+
{ from: 'tasks', field: 'kind', target: { kind: 'axis', axis: 'operations' }, via: 'twin.rel.operation', optional: true },
|
|
100
|
+
{ from: 'tasks', field: 'fromNode', target: { kind: 'axis', axis: 'locations' }, via: 'twin.rel.from', optional: true },
|
|
101
|
+
{ from: 'tasks', field: 'toNode', target: { kind: 'axis', axis: 'locations' }, via: 'twin.rel.at', optional: true },
|
|
102
|
+
{ from: 'tasks', field: 'resourceRef', target: { kind: 'axis', axis: 'equipment' }, via: 'twin.rel.by', optional: true },
|
|
103
|
+
{ from: 'tasks', field: 'personnel[]', target: { kind: 'axis', axis: 'persons' }, via: 'twin.rel.crew', optional: true },
|
|
66
104
|
/* 공정이 도는 자리·자원 — 항목이 아니라 **타입**을 가리킨다 */
|
|
67
105
|
{ from: 'operations', field: 'locationType', target: { kind: 'type', role: 'location' }, via: 'twin.rel.at', optional: true },
|
|
68
106
|
{ from: 'operations', field: 'resourceType', target: { kind: 'type', role: 'equipment' }, via: 'twin.rel.by', optional: true }
|
|
@@ -89,6 +127,28 @@ export const TWIN_PROPERTIES = [
|
|
|
89
127
|
];
|
|
90
128
|
/** 이 축이 보여 줄 속성들 — 없으면 빈 배열(속성 없는 축이 정상이다). */
|
|
91
129
|
export const propertiesOf = (axis) => TWIN_PROPERTIES.filter(p => p.axis === axis);
|
|
130
|
+
/**
|
|
131
|
+
* 이 축은 **선언에서 읽나** — `source` 를 말하지 않은 축은 문서 축이다(기존 14축).
|
|
132
|
+
*
|
|
133
|
+
* 소비처가 `source` 를 직접 비교하면 기본값 규칙이 자리마다 복사되고, 한 곳이 빠지면 그 축은
|
|
134
|
+
* board 루트를 읽어 조용히 잘못된 답을 낸다. 판정은 여기 하나다.
|
|
135
|
+
*/
|
|
136
|
+
export const axisSource = (axis) => TWIN_AXES.find(a => a.axis === axis)?.source ?? 'document';
|
|
137
|
+
/**
|
|
138
|
+
* 문서에서 이 축을 찾을 자리 — **문서 축이 아니면 던진다.**
|
|
139
|
+
*
|
|
140
|
+
* 상태·저널 축에는 board 안의 자리가 없다. 그런데 옛 소비처는 `info.path` 를 무조건 읽었으므로,
|
|
141
|
+
* 그 자리가 `undefined` 면 board 루트를 읽고 **오류 없이 0 을 답한다**(이 프로젝트가 겪은 실패 모양).
|
|
142
|
+
* 그래서 조용히 넘기지 않고 부르는 쪽을 멈춘다.
|
|
143
|
+
*/
|
|
144
|
+
export function documentPath(axis) {
|
|
145
|
+
const info = TWIN_AXES.find(a => a.axis === axis);
|
|
146
|
+
if (!info)
|
|
147
|
+
throw new Error(`unknown twin axis: "${axis}"`);
|
|
148
|
+
if (axisSource(axis) !== 'document' || !info.path)
|
|
149
|
+
throw new Error(`axis "${axis}" is not read from the document (source: ${axisSource(axis)}) — read it from the ${axisSource(axis)}`);
|
|
150
|
+
return info.path;
|
|
151
|
+
}
|
|
92
152
|
/** 이 축에서 나가는 관계 · 이 축으로 들어오는 관계 — 항목 패널의 "이어진 것" 양방향. */
|
|
93
153
|
export const relationsFrom = (axis) => TWIN_RELATIONS.filter(r => r.from === axis);
|
|
94
154
|
export const relationsTo = (axis) => TWIN_RELATIONS.filter(r => r.target.kind === 'axis' && r.target.axis === axis);
|
package/dist/flow-engine.js
CHANGED
|
@@ -524,6 +524,7 @@ export class FlowEngine {
|
|
|
524
524
|
/* 확보분과 거래번호도 함께 옮긴다 — 여기서 떨구면 위에서 실어 온 것이 마지막 한 걸음에서
|
|
525
525
|
사라진다(같은 사실이 델타·스냅샷·이 변환 **세 길**을 지난다). */
|
|
526
526
|
...(o.allocated?.length ? { allocated: o.allocated } : {}),
|
|
527
|
+
...(o.gtin ? { gtin: o.gtin } : {}),
|
|
527
528
|
...(o.bizTransaction ? { bizTransaction: o.bizTransaction } : {})
|
|
528
529
|
}));
|
|
529
530
|
for (const o of observedOrders) {
|
|
@@ -541,6 +542,8 @@ export class FlowEngine {
|
|
|
541
542
|
/* **확보해 둔 것과 거래번호를 이어받는다.** 비우면 되살아난 오더가 아무것도 안 잡은 것처럼
|
|
542
543
|
보이고, 그 오더의 진행 중 작업이 완료될 때 계보가 입력 없이 나간다. 상태가 말해 주지
|
|
543
544
|
않으면(옛 저널) 그때는 비는 것이 사실이다 — 없는 것을 지어내지 않는다. */
|
|
545
|
+
/* 품목도 함께 — 씨앗이 흘리면 되살아난 오더가 무엇을 만드는지 모른다. */
|
|
546
|
+
...(o.gtin ? { gtin: o.gtin } : {}),
|
|
544
547
|
bizTransaction: o.bizTransaction ?? '', allocated: (o.allocated ?? []).slice(), picked: [], shipmentEpc: null, lines,
|
|
545
548
|
/* **보류를 이어받는다** — 잃으면 씨앗이 사람이 일부러 멈춘 오더를 다시 계획해 내보낸다.
|
|
546
549
|
씨앗 왕복 대조가 이것을 잡았다(그 전에는 사람이 코드를 읽어야만 알 수 있었다). */
|
|
@@ -863,6 +866,16 @@ export class FlowEngine {
|
|
|
863
866
|
id: o.id, kind: o.kind, status: o.status,
|
|
864
867
|
progress: o.requested ? o.fulfilled / o.requested : 0,
|
|
865
868
|
requested: o.requested, fulfilled: o.fulfilled,
|
|
869
|
+
/*
|
|
870
|
+
* **무엇을 만드는 오더인가** — 이것이 없으면 "1개" 만 남고 무엇을 1개인지 사라진다.
|
|
871
|
+
*
|
|
872
|
+
* 커널은 오더를 만들 때부터 이 값을 들고 있는데(`gtin: product.gtin`) 스냅샷이 흘리고 있었다.
|
|
873
|
+
* 그러면 화면·예측·계보가 품목을 알 수 없다 — 확보분(`allocated`)이 같은 식으로 빠져 계보의
|
|
874
|
+
* 절반이 사라졌던 것과 같은 부류다(그때 배운 것을 여기서도 적용한다).
|
|
875
|
+
*
|
|
876
|
+
* 라인이 있는 오더는 `lines[].gtin` 이 품목을 말하고, 단일 품목 작업지시는 이 자리가 말한다.
|
|
877
|
+
*/
|
|
878
|
+
...(o.gtin ? { gtin: o.gtin } : {}),
|
|
866
879
|
...(o.lines?.length ? { lines: o.lines.map(l => ({ gtin: l.gtin, requested: l.requested })) } : {}),
|
|
867
880
|
...(o.priority !== undefined ? { priority: o.priority } : {}),
|
|
868
881
|
...(o.startTime ? { startTime: o.startTime } : {}),
|
|
@@ -1474,6 +1487,7 @@ export class FlowEngine {
|
|
|
1474
1487
|
...(o.lines?.length ? { lines: o.lines.map(l => ({ gtin: l.gtin, requested: l.requested })) } : {}),
|
|
1475
1488
|
/* 진행 중 할당과 거래번호를 함께 싣는다 — 이것이 없으면 웜스타트가 잃고, 되살아난 오더의
|
|
1476
1489
|
계보가 입력 없이 나간다(무엇이 무엇으로 바뀌었나의 절반이 사라진다). */
|
|
1490
|
+
...(o.gtin ? { gtin: o.gtin } : {}),
|
|
1477
1491
|
...(o.allocated?.length ? { allocated: o.allocated.slice() } : {}),
|
|
1478
1492
|
...(o.bizTransaction ? { bizTransaction: o.bizTransaction } : {})
|
|
1479
1493
|
});
|
package/dist/observed-reducer.js
CHANGED
|
@@ -344,6 +344,7 @@ export class ObservedReducer {
|
|
|
344
344
|
...(d.endTime ? { endTime: d.endTime } : {}),
|
|
345
345
|
/* 진행 중 확보분과 거래번호 — **미러도 같은 필드를 채운다.** 시뮬만 아는 사실을 두면
|
|
346
346
|
그 위에서 세운 예측이 두 구동에서 달라진다(패리티 검사가 지키는 것이 이것이다). */
|
|
347
|
+
...(d.gtin ? { gtin: d.gtin } : {}),
|
|
347
348
|
...(d.allocated?.length ? { allocated: d.allocated.slice() } : {}),
|
|
348
349
|
...(d.bizTransaction ? { bizTransaction: d.bizTransaction } : {}),
|
|
349
350
|
held: d.held
|
package/dist-cjs/index.cjs
CHANGED
|
@@ -71,6 +71,7 @@ __export(index_exports, {
|
|
|
71
71
|
aggregationEvent: () => aggregationEvent,
|
|
72
72
|
analyzeCapacity: () => analyzeCapacity,
|
|
73
73
|
axisInfo: () => axisInfo,
|
|
74
|
+
axisSource: () => axisSource,
|
|
74
75
|
capabilitiesForType: () => capabilitiesForType,
|
|
75
76
|
classClosure: () => classClosure,
|
|
76
77
|
compareStates: () => compareStates,
|
|
@@ -79,6 +80,7 @@ __export(index_exports, {
|
|
|
79
80
|
conversionFactorOf: () => conversionFactorOf,
|
|
80
81
|
counterfactualAt: () => counterfactualAt,
|
|
81
82
|
deriveAttentions: () => deriveAttentions,
|
|
83
|
+
documentPath: () => documentPath,
|
|
82
84
|
dueStatusOf: () => dueStatusOf,
|
|
83
85
|
effectivityAt: () => effectivityAt,
|
|
84
86
|
fefoPolicy: () => fefoPolicy,
|
|
@@ -1183,6 +1185,7 @@ var ObservedReducer = class {
|
|
|
1183
1185
|
...d.endTime ? { endTime: d.endTime } : {},
|
|
1184
1186
|
/* 진행 중 확보분과 거래번호 — **미러도 같은 필드를 채운다.** 시뮬만 아는 사실을 두면
|
|
1185
1187
|
그 위에서 세운 예측이 두 구동에서 달라진다(패리티 검사가 지키는 것이 이것이다). */
|
|
1188
|
+
...d.gtin ? { gtin: d.gtin } : {},
|
|
1186
1189
|
...d.allocated?.length ? { allocated: d.allocated.slice() } : {},
|
|
1187
1190
|
...d.bizTransaction ? { bizTransaction: d.bizTransaction } : {},
|
|
1188
1191
|
held: d.held
|
|
@@ -1672,6 +1675,39 @@ var TWIN_AXES = [
|
|
|
1672
1675
|
label: "twin.axis.materials",
|
|
1673
1676
|
kind: "class",
|
|
1674
1677
|
standardClass: { isa95: "MaterialDefinition" }
|
|
1678
|
+
},
|
|
1679
|
+
/*
|
|
1680
|
+
* ── ISA-95 Part 4 — 일정과 실적 ────────────────────────────────────────────────
|
|
1681
|
+
*
|
|
1682
|
+
* 여기부터는 **선언이 아니다.** 오더와 작업은 사람이 board 에 적어 두는 값이 아니라 현장이 낳는
|
|
1683
|
+
* 사실이고, 그 집은 커널 상태와 저널이다. 그래서 `source` 가 `'state'` 이고 `path` 가 없다.
|
|
1684
|
+
*
|
|
1685
|
+
* ── 왜 축이어야 하나 ──────────────────────────────────────────────────────────
|
|
1686
|
+
* 이 둘이 축이 아니던 동안 "이 트윈이 표준의 어디를 채우나" 라는 질문은 **절반만** 답했다. Part 2
|
|
1687
|
+
* 자원 모델은 여덟이 완전한데 Part 4 는 통째로 비어 있었다 — 런타임에는 실재하는데 조회 모델에는
|
|
1688
|
+
* 없었기 때문이다. 실적을 성과 화면이 답한다는 것은 사실이지만, 그건 **집계**이고 개념 자체를
|
|
1689
|
+
* 가리켜 걸어 들어갈 자리는 없었다(어느 오더가 어느 공정에서 무엇을 먹었나).
|
|
1690
|
+
*
|
|
1691
|
+
* ── 왜 테이블로 투영하지 않나 ─────────────────────────────────────────────────
|
|
1692
|
+
* 정본이 저널이다. 구조 엔티티는 **원본이 낼 수 있는 것만** 담고(ADR-0032-D), 오더는 원본이 낸
|
|
1693
|
+
* 것이지만 트윈이 **관측한** 것이다. 투영은 규모에서 성능이 필요해질 때 KPI 폴드와 같은 급의
|
|
1694
|
+
* 별도 결정으로 한다.
|
|
1695
|
+
*/
|
|
1696
|
+
{
|
|
1697
|
+
axis: "orders",
|
|
1698
|
+
label: "twin.axis.orders",
|
|
1699
|
+
kind: "instance",
|
|
1700
|
+
source: "state",
|
|
1701
|
+
historical: true,
|
|
1702
|
+
standardClass: { isa95: "OperationsRequest", epcis: "TransactionEvent" }
|
|
1703
|
+
},
|
|
1704
|
+
{
|
|
1705
|
+
axis: "tasks",
|
|
1706
|
+
label: "twin.axis.tasks",
|
|
1707
|
+
kind: "instance",
|
|
1708
|
+
source: "state",
|
|
1709
|
+
historical: true,
|
|
1710
|
+
standardClass: { isa95: "SegmentResponse", epcis: "TransformationEvent" }
|
|
1675
1711
|
}
|
|
1676
1712
|
];
|
|
1677
1713
|
var TWIN_RELATIONS = [
|
|
@@ -1691,6 +1727,23 @@ var TWIN_RELATIONS = [
|
|
|
1691
1727
|
{ from: "recipes", field: "inputs[].material", target: { kind: "axis", axis: "materials" }, via: "twin.rel.input" },
|
|
1692
1728
|
{ from: "recipes", field: "outputs[].material", target: { kind: "axis", axis: "materials" }, via: "twin.rel.output" },
|
|
1693
1729
|
{ from: "routes", field: "steps[]", target: { kind: "axis", axis: "operations" }, via: "twin.rel.step" },
|
|
1730
|
+
/*
|
|
1731
|
+
* 일정·실적의 관계 — **관측 관계다.** 선언 관계는 "그렇게 만들기로 했다" 이고, 이것은 "그렇게
|
|
1732
|
+
* 일어났다" 다. 관계가 없으면 이 두 축은 지도에서 떠 있는 카드가 된다(그러면 걸어 들어갈 수 없다).
|
|
1733
|
+
*/
|
|
1734
|
+
/*
|
|
1735
|
+
* 오더가 무엇을 만드나 — **자재 키가 아니라 GS1 품목 참조다**(`lines[].gtin`). 자재와 이어 주는
|
|
1736
|
+
* 것은 `productionSpec.binding` 이고 그 해소는 호스트가 한다 — 자리·구역과 같은 `external` 이다.
|
|
1737
|
+
* 축을 직접 가리키게 적으면 없는 필드를 가리키는 선언이 된다(계약을 보고 고쳤다).
|
|
1738
|
+
*/
|
|
1739
|
+
{ from: "orders", field: "lines[].gtin", target: { kind: "external", entity: "gs1.itemRef" }, via: "twin.rel.item", optional: true },
|
|
1740
|
+
{ from: "tasks", field: "orderId", target: { kind: "axis", axis: "orders" }, via: "twin.rel.order", optional: true },
|
|
1741
|
+
/* 작업의 `kind` 는 그 공정의 키다(라우트의 단계 이름 그대로 — `mes-kernel` 의 공정 목록이 그 증거). */
|
|
1742
|
+
{ from: "tasks", field: "kind", target: { kind: "axis", axis: "operations" }, via: "twin.rel.operation", optional: true },
|
|
1743
|
+
{ from: "tasks", field: "fromNode", target: { kind: "axis", axis: "locations" }, via: "twin.rel.from", optional: true },
|
|
1744
|
+
{ from: "tasks", field: "toNode", target: { kind: "axis", axis: "locations" }, via: "twin.rel.at", optional: true },
|
|
1745
|
+
{ from: "tasks", field: "resourceRef", target: { kind: "axis", axis: "equipment" }, via: "twin.rel.by", optional: true },
|
|
1746
|
+
{ from: "tasks", field: "personnel[]", target: { kind: "axis", axis: "persons" }, via: "twin.rel.crew", optional: true },
|
|
1694
1747
|
/* 공정이 도는 자리·자원 — 항목이 아니라 **타입**을 가리킨다 */
|
|
1695
1748
|
{ from: "operations", field: "locationType", target: { kind: "type", role: "location" }, via: "twin.rel.at", optional: true },
|
|
1696
1749
|
{ from: "operations", field: "resourceType", target: { kind: "type", role: "equipment" }, via: "twin.rel.by", optional: true }
|
|
@@ -1744,6 +1797,14 @@ var TWIN_PROPERTIES = [
|
|
|
1744
1797
|
}
|
|
1745
1798
|
];
|
|
1746
1799
|
var propertiesOf = (axis) => TWIN_PROPERTIES.filter((p) => p.axis === axis);
|
|
1800
|
+
var axisSource = (axis) => TWIN_AXES.find((a) => a.axis === axis)?.source ?? "document";
|
|
1801
|
+
function documentPath(axis) {
|
|
1802
|
+
const info = TWIN_AXES.find((a) => a.axis === axis);
|
|
1803
|
+
if (!info) throw new Error(`unknown twin axis: "${axis}"`);
|
|
1804
|
+
if (axisSource(axis) !== "document" || !info.path)
|
|
1805
|
+
throw new Error(`axis "${axis}" is not read from the document (source: ${axisSource(axis)}) \u2014 read it from the ${axisSource(axis)}`);
|
|
1806
|
+
return info.path;
|
|
1807
|
+
}
|
|
1747
1808
|
var relationsFrom = (axis) => TWIN_RELATIONS.filter((r) => r.from === axis);
|
|
1748
1809
|
var relationsTo = (axis) => TWIN_RELATIONS.filter((r) => r.target.kind === "axis" && r.target.axis === axis);
|
|
1749
1810
|
function axisInfo(axis) {
|
|
@@ -2510,6 +2571,7 @@ var FlowEngine = class {
|
|
|
2510
2571
|
/* 확보분과 거래번호도 함께 옮긴다 — 여기서 떨구면 위에서 실어 온 것이 마지막 한 걸음에서
|
|
2511
2572
|
사라진다(같은 사실이 델타·스냅샷·이 변환 **세 길**을 지난다). */
|
|
2512
2573
|
...o.allocated?.length ? { allocated: o.allocated } : {},
|
|
2574
|
+
...o.gtin ? { gtin: o.gtin } : {},
|
|
2513
2575
|
...o.bizTransaction ? { bizTransaction: o.bizTransaction } : {}
|
|
2514
2576
|
}));
|
|
2515
2577
|
for (const o of observedOrders) {
|
|
@@ -2525,6 +2587,8 @@ var FlowEngine = class {
|
|
|
2525
2587
|
/* **확보해 둔 것과 거래번호를 이어받는다.** 비우면 되살아난 오더가 아무것도 안 잡은 것처럼
|
|
2526
2588
|
보이고, 그 오더의 진행 중 작업이 완료될 때 계보가 입력 없이 나간다. 상태가 말해 주지
|
|
2527
2589
|
않으면(옛 저널) 그때는 비는 것이 사실이다 — 없는 것을 지어내지 않는다. */
|
|
2590
|
+
/* 품목도 함께 — 씨앗이 흘리면 되살아난 오더가 무엇을 만드는지 모른다. */
|
|
2591
|
+
...o.gtin ? { gtin: o.gtin } : {},
|
|
2528
2592
|
bizTransaction: o.bizTransaction ?? "",
|
|
2529
2593
|
allocated: (o.allocated ?? []).slice(),
|
|
2530
2594
|
picked: [],
|
|
@@ -2830,6 +2894,16 @@ var FlowEngine = class {
|
|
|
2830
2894
|
progress: o.requested ? o.fulfilled / o.requested : 0,
|
|
2831
2895
|
requested: o.requested,
|
|
2832
2896
|
fulfilled: o.fulfilled,
|
|
2897
|
+
/*
|
|
2898
|
+
* **무엇을 만드는 오더인가** — 이것이 없으면 "1개" 만 남고 무엇을 1개인지 사라진다.
|
|
2899
|
+
*
|
|
2900
|
+
* 커널은 오더를 만들 때부터 이 값을 들고 있는데(`gtin: product.gtin`) 스냅샷이 흘리고 있었다.
|
|
2901
|
+
* 그러면 화면·예측·계보가 품목을 알 수 없다 — 확보분(`allocated`)이 같은 식으로 빠져 계보의
|
|
2902
|
+
* 절반이 사라졌던 것과 같은 부류다(그때 배운 것을 여기서도 적용한다).
|
|
2903
|
+
*
|
|
2904
|
+
* 라인이 있는 오더는 `lines[].gtin` 이 품목을 말하고, 단일 품목 작업지시는 이 자리가 말한다.
|
|
2905
|
+
*/
|
|
2906
|
+
...o.gtin ? { gtin: o.gtin } : {},
|
|
2833
2907
|
...o.lines?.length ? { lines: o.lines.map((l) => ({ gtin: l.gtin, requested: l.requested })) } : {},
|
|
2834
2908
|
...o.priority !== void 0 ? { priority: o.priority } : {},
|
|
2835
2909
|
...o.startTime ? { startTime: o.startTime } : {},
|
|
@@ -3385,6 +3459,7 @@ var FlowEngine = class {
|
|
|
3385
3459
|
...o.lines?.length ? { lines: o.lines.map((l) => ({ gtin: l.gtin, requested: l.requested })) } : {},
|
|
3386
3460
|
/* 진행 중 할당과 거래번호를 함께 싣는다 — 이것이 없으면 웜스타트가 잃고, 되살아난 오더의
|
|
3387
3461
|
계보가 입력 없이 나간다(무엇이 무엇으로 바뀌었나의 절반이 사라진다). */
|
|
3462
|
+
...o.gtin ? { gtin: o.gtin } : {},
|
|
3388
3463
|
...o.allocated?.length ? { allocated: o.allocated.slice() } : {},
|
|
3389
3464
|
...o.bizTransaction ? { bizTransaction: o.bizTransaction } : {}
|
|
3390
3465
|
});
|
|
@@ -4986,6 +5061,7 @@ function retiredVocabularyIn(line) {
|
|
|
4986
5061
|
aggregationEvent,
|
|
4987
5062
|
analyzeCapacity,
|
|
4988
5063
|
axisInfo,
|
|
5064
|
+
axisSource,
|
|
4989
5065
|
capabilitiesForType,
|
|
4990
5066
|
classClosure,
|
|
4991
5067
|
compareStates,
|
|
@@ -4994,6 +5070,7 @@ function retiredVocabularyIn(line) {
|
|
|
4994
5070
|
conversionFactorOf,
|
|
4995
5071
|
counterfactualAt,
|
|
4996
5072
|
deriveAttentions,
|
|
5073
|
+
documentPath,
|
|
4997
5074
|
dueStatusOf,
|
|
4998
5075
|
effectivityAt,
|
|
4999
5076
|
fefoPolicy,
|
package/package.json
CHANGED