@operato/twin-kernel 0.6.1 → 0.6.3
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 +43 -0
- package/dist/domain-catalog.d.ts +41 -1
- package/dist/domain-catalog.js +61 -1
- package/dist/flow-engine.js +18 -3
- package/dist/observed-reducer.js +4 -0
- package/dist-cjs/index.cjs +85 -4
- 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,6 +864,16 @@ 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;
|
|
@@ -874,6 +892,22 @@ export interface OrderState {
|
|
|
874
892
|
requested?: number;
|
|
875
893
|
fulfilled?: number;
|
|
876
894
|
lines?: ObservedOrderLine[];
|
|
895
|
+
/**
|
|
896
|
+
* 이 오더가 **확보해 둔 물품들** — 진행 중인 할당.
|
|
897
|
+
*
|
|
898
|
+
* 이것이 상태에 없으면 **웜스타트가 잃는다.** 잃으면 되살아난 오더는 아무것도 안 잡은 것처럼
|
|
899
|
+
* 보이고, 그 오더에 딸린 진행 중 작업이 완료될 때 계보(`TransformationEvent`)가 **입력 없이**
|
|
900
|
+
* 나간다 — "무엇이 무엇으로 바뀌었나" 의 절반이 사라진다. 실제로 그렇게 되고 있었다.
|
|
901
|
+
*
|
|
902
|
+
* 씨앗이 잃은 것은 예측도 모른다 — 진행 중 자재를 안 잡은 것으로 놓고 미래를 굴리면 답이
|
|
903
|
+
* 낙관 쪽으로 치우친다(`hydrateObserved` 가 상태를 지키는 이유와 같다).
|
|
904
|
+
*/
|
|
905
|
+
allocated?: string[];
|
|
906
|
+
/**
|
|
907
|
+
* 이 오더의 거래번호(작업지시·PO·SO) — 계보와 EPCIS 이벤트가 **무슨 거래로** 일어났는지 잇는 축.
|
|
908
|
+
* 이것도 상태에 없어서 되살아난 오더는 빈 문자열을 갖고, 이후 모든 이벤트의 거래번호가 빈다.
|
|
909
|
+
*/
|
|
910
|
+
bizTransaction?: string;
|
|
877
911
|
/** 우선순위 — 표준 `OperationsRequest.Priority`. 작은 값이 급하다. 오더 할당 순서를 정한다. */
|
|
878
912
|
priority?: number;
|
|
879
913
|
/** 예정 착수 — 표준 `OperationsRequest.StartTime`. */
|
|
@@ -1230,6 +1264,15 @@ export interface OrderStatusDelta {
|
|
|
1230
1264
|
priority?: number;
|
|
1231
1265
|
startTime?: ISOTime;
|
|
1232
1266
|
endTime?: ISOTime;
|
|
1267
|
+
/**
|
|
1268
|
+
* 이 오더가 **확보해 둔 물품들**과 그 **거래번호** — 진행 중인 할당.
|
|
1269
|
+
*
|
|
1270
|
+
* 델타가 나르지 않으면 웜스타트가 잃는다. 잃으면 되살아난 오더는 아무것도 안 잡은 것처럼
|
|
1271
|
+
* 보이고, 그 오더의 진행 중 작업이 완료될 때 계보(`TransformationEvent`)가 **입력 없이** 나간다 —
|
|
1272
|
+
* "무엇이 무엇으로 바뀌었나" 의 절반이 사라진다. 실제로 그렇게 되고 있었다.
|
|
1273
|
+
*/
|
|
1274
|
+
allocated?: string[];
|
|
1275
|
+
bizTransaction?: string;
|
|
1233
1276
|
}
|
|
1234
1277
|
export declare const CMD: {
|
|
1235
1278
|
readonly orderHold: "order.hold";
|
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
|
@@ -520,7 +520,11 @@ export class FlowEngine {
|
|
|
520
520
|
없다.** 라이브 트윈이 납기 초과를 한 번도 보고하지 못한 원인 중 하나였다. */
|
|
521
521
|
...(o.endTime ? { endTime: o.endTime } : {}),
|
|
522
522
|
...(o.startTime ? { startTime: o.startTime } : {}),
|
|
523
|
-
...(o.priority !== undefined ? { priority: o.priority } : {})
|
|
523
|
+
...(o.priority !== undefined ? { priority: o.priority } : {}),
|
|
524
|
+
/* 확보분과 거래번호도 함께 옮긴다 — 여기서 떨구면 위에서 실어 온 것이 마지막 한 걸음에서
|
|
525
|
+
사라진다(같은 사실이 델타·스냅샷·이 변환 **세 길**을 지난다). */
|
|
526
|
+
...(o.allocated?.length ? { allocated: o.allocated } : {}),
|
|
527
|
+
...(o.bizTransaction ? { bizTransaction: o.bizTransaction } : {})
|
|
524
528
|
}));
|
|
525
529
|
for (const o of observedOrders) {
|
|
526
530
|
const lines = (o.lines ?? []).map(l => ({ gtin: l.gtin, requested: l.requested - (l.fulfilled ?? 0) })).filter(l => l.requested > 0);
|
|
@@ -534,7 +538,10 @@ export class FlowEngine {
|
|
|
534
538
|
continue; // 이미 이행 완료 → 예측 대상 아님
|
|
535
539
|
this.orders.set(o.orderId, {
|
|
536
540
|
id: o.orderId, kind: o.kind, status: 'created', requested: remaining, fulfilled: 0,
|
|
537
|
-
|
|
541
|
+
/* **확보해 둔 것과 거래번호를 이어받는다.** 비우면 되살아난 오더가 아무것도 안 잡은 것처럼
|
|
542
|
+
보이고, 그 오더의 진행 중 작업이 완료될 때 계보가 입력 없이 나간다. 상태가 말해 주지
|
|
543
|
+
않으면(옛 저널) 그때는 비는 것이 사실이다 — 없는 것을 지어내지 않는다. */
|
|
544
|
+
bizTransaction: o.bizTransaction ?? '', allocated: (o.allocated ?? []).slice(), picked: [], shipmentEpc: null, lines,
|
|
538
545
|
/* **보류를 이어받는다** — 잃으면 씨앗이 사람이 일부러 멈춘 오더를 다시 계획해 내보낸다.
|
|
539
546
|
씨앗 왕복 대조가 이것을 잡았다(그 전에는 사람이 코드를 읽어야만 알 수 있었다). */
|
|
540
547
|
...(o.held ? { held: true } : {}),
|
|
@@ -860,6 +867,10 @@ export class FlowEngine {
|
|
|
860
867
|
...(o.priority !== undefined ? { priority: o.priority } : {}),
|
|
861
868
|
...(o.startTime ? { startTime: o.startTime } : {}),
|
|
862
869
|
...(o.endTime ? { endTime: o.endTime } : {}),
|
|
870
|
+
/* 진행 중 확보분과 거래번호 — **스냅샷에도** 실어야 한다. 델타에만 실으면 스냅샷으로
|
|
871
|
+
재기동하는 경로(체크포인트)에서 그대로 잃는다(같은 사실을 두 길로 나르는 값이다). */
|
|
872
|
+
...(o.allocated?.length ? { allocated: o.allocated.slice() } : {}),
|
|
873
|
+
...(o.bizTransaction ? { bizTransaction: o.bizTransaction } : {}),
|
|
863
874
|
held: o.held
|
|
864
875
|
})),
|
|
865
876
|
attentions: this.computeAttentions(),
|
|
@@ -1460,7 +1471,11 @@ export class FlowEngine {
|
|
|
1460
1471
|
...(o.priority !== undefined ? { priority: o.priority } : {}),
|
|
1461
1472
|
...(o.startTime ? { startTime: o.startTime } : {}),
|
|
1462
1473
|
...(o.endTime ? { endTime: o.endTime } : {}),
|
|
1463
|
-
...(o.lines?.length ? { lines: o.lines.map(l => ({ gtin: l.gtin, requested: l.requested })) } : {})
|
|
1474
|
+
...(o.lines?.length ? { lines: o.lines.map(l => ({ gtin: l.gtin, requested: l.requested })) } : {}),
|
|
1475
|
+
/* 진행 중 할당과 거래번호를 함께 싣는다 — 이것이 없으면 웜스타트가 잃고, 되살아난 오더의
|
|
1476
|
+
계보가 입력 없이 나간다(무엇이 무엇으로 바뀌었나의 절반이 사라진다). */
|
|
1477
|
+
...(o.allocated?.length ? { allocated: o.allocated.slice() } : {}),
|
|
1478
|
+
...(o.bizTransaction ? { bizTransaction: o.bizTransaction } : {})
|
|
1464
1479
|
});
|
|
1465
1480
|
}
|
|
1466
1481
|
// ── 내부 mechanics ─────────────────────────────────────────────────────────
|
package/dist/observed-reducer.js
CHANGED
|
@@ -342,6 +342,10 @@ export class ObservedReducer {
|
|
|
342
342
|
...(d.priority !== undefined ? { priority: d.priority } : {}),
|
|
343
343
|
...(d.startTime ? { startTime: d.startTime } : {}),
|
|
344
344
|
...(d.endTime ? { endTime: d.endTime } : {}),
|
|
345
|
+
/* 진행 중 확보분과 거래번호 — **미러도 같은 필드를 채운다.** 시뮬만 아는 사실을 두면
|
|
346
|
+
그 위에서 세운 예측이 두 구동에서 달라진다(패리티 검사가 지키는 것이 이것이다). */
|
|
347
|
+
...(d.allocated?.length ? { allocated: d.allocated.slice() } : {}),
|
|
348
|
+
...(d.bizTransaction ? { bizTransaction: d.bizTransaction } : {}),
|
|
345
349
|
held: d.held
|
|
346
350
|
});
|
|
347
351
|
break;
|
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,
|
|
@@ -1181,6 +1183,10 @@ var ObservedReducer = class {
|
|
|
1181
1183
|
...d.priority !== void 0 ? { priority: d.priority } : {},
|
|
1182
1184
|
...d.startTime ? { startTime: d.startTime } : {},
|
|
1183
1185
|
...d.endTime ? { endTime: d.endTime } : {},
|
|
1186
|
+
/* 진행 중 확보분과 거래번호 — **미러도 같은 필드를 채운다.** 시뮬만 아는 사실을 두면
|
|
1187
|
+
그 위에서 세운 예측이 두 구동에서 달라진다(패리티 검사가 지키는 것이 이것이다). */
|
|
1188
|
+
...d.allocated?.length ? { allocated: d.allocated.slice() } : {},
|
|
1189
|
+
...d.bizTransaction ? { bizTransaction: d.bizTransaction } : {},
|
|
1184
1190
|
held: d.held
|
|
1185
1191
|
});
|
|
1186
1192
|
break;
|
|
@@ -1668,6 +1674,39 @@ var TWIN_AXES = [
|
|
|
1668
1674
|
label: "twin.axis.materials",
|
|
1669
1675
|
kind: "class",
|
|
1670
1676
|
standardClass: { isa95: "MaterialDefinition" }
|
|
1677
|
+
},
|
|
1678
|
+
/*
|
|
1679
|
+
* ── ISA-95 Part 4 — 일정과 실적 ────────────────────────────────────────────────
|
|
1680
|
+
*
|
|
1681
|
+
* 여기부터는 **선언이 아니다.** 오더와 작업은 사람이 board 에 적어 두는 값이 아니라 현장이 낳는
|
|
1682
|
+
* 사실이고, 그 집은 커널 상태와 저널이다. 그래서 `source` 가 `'state'` 이고 `path` 가 없다.
|
|
1683
|
+
*
|
|
1684
|
+
* ── 왜 축이어야 하나 ──────────────────────────────────────────────────────────
|
|
1685
|
+
* 이 둘이 축이 아니던 동안 "이 트윈이 표준의 어디를 채우나" 라는 질문은 **절반만** 답했다. Part 2
|
|
1686
|
+
* 자원 모델은 여덟이 완전한데 Part 4 는 통째로 비어 있었다 — 런타임에는 실재하는데 조회 모델에는
|
|
1687
|
+
* 없었기 때문이다. 실적을 성과 화면이 답한다는 것은 사실이지만, 그건 **집계**이고 개념 자체를
|
|
1688
|
+
* 가리켜 걸어 들어갈 자리는 없었다(어느 오더가 어느 공정에서 무엇을 먹었나).
|
|
1689
|
+
*
|
|
1690
|
+
* ── 왜 테이블로 투영하지 않나 ─────────────────────────────────────────────────
|
|
1691
|
+
* 정본이 저널이다. 구조 엔티티는 **원본이 낼 수 있는 것만** 담고(ADR-0032-D), 오더는 원본이 낸
|
|
1692
|
+
* 것이지만 트윈이 **관측한** 것이다. 투영은 규모에서 성능이 필요해질 때 KPI 폴드와 같은 급의
|
|
1693
|
+
* 별도 결정으로 한다.
|
|
1694
|
+
*/
|
|
1695
|
+
{
|
|
1696
|
+
axis: "orders",
|
|
1697
|
+
label: "twin.axis.orders",
|
|
1698
|
+
kind: "instance",
|
|
1699
|
+
source: "state",
|
|
1700
|
+
historical: true,
|
|
1701
|
+
standardClass: { isa95: "OperationsRequest", epcis: "TransactionEvent" }
|
|
1702
|
+
},
|
|
1703
|
+
{
|
|
1704
|
+
axis: "tasks",
|
|
1705
|
+
label: "twin.axis.tasks",
|
|
1706
|
+
kind: "instance",
|
|
1707
|
+
source: "state",
|
|
1708
|
+
historical: true,
|
|
1709
|
+
standardClass: { isa95: "SegmentResponse", epcis: "TransformationEvent" }
|
|
1671
1710
|
}
|
|
1672
1711
|
];
|
|
1673
1712
|
var TWIN_RELATIONS = [
|
|
@@ -1687,6 +1726,23 @@ var TWIN_RELATIONS = [
|
|
|
1687
1726
|
{ from: "recipes", field: "inputs[].material", target: { kind: "axis", axis: "materials" }, via: "twin.rel.input" },
|
|
1688
1727
|
{ from: "recipes", field: "outputs[].material", target: { kind: "axis", axis: "materials" }, via: "twin.rel.output" },
|
|
1689
1728
|
{ from: "routes", field: "steps[]", target: { kind: "axis", axis: "operations" }, via: "twin.rel.step" },
|
|
1729
|
+
/*
|
|
1730
|
+
* 일정·실적의 관계 — **관측 관계다.** 선언 관계는 "그렇게 만들기로 했다" 이고, 이것은 "그렇게
|
|
1731
|
+
* 일어났다" 다. 관계가 없으면 이 두 축은 지도에서 떠 있는 카드가 된다(그러면 걸어 들어갈 수 없다).
|
|
1732
|
+
*/
|
|
1733
|
+
/*
|
|
1734
|
+
* 오더가 무엇을 만드나 — **자재 키가 아니라 GS1 품목 참조다**(`lines[].gtin`). 자재와 이어 주는
|
|
1735
|
+
* 것은 `productionSpec.binding` 이고 그 해소는 호스트가 한다 — 자리·구역과 같은 `external` 이다.
|
|
1736
|
+
* 축을 직접 가리키게 적으면 없는 필드를 가리키는 선언이 된다(계약을 보고 고쳤다).
|
|
1737
|
+
*/
|
|
1738
|
+
{ from: "orders", field: "lines[].gtin", target: { kind: "external", entity: "gs1.itemRef" }, via: "twin.rel.item", optional: true },
|
|
1739
|
+
{ from: "tasks", field: "orderId", target: { kind: "axis", axis: "orders" }, via: "twin.rel.order", optional: true },
|
|
1740
|
+
/* 작업의 `kind` 는 그 공정의 키다(라우트의 단계 이름 그대로 — `mes-kernel` 의 공정 목록이 그 증거). */
|
|
1741
|
+
{ from: "tasks", field: "kind", target: { kind: "axis", axis: "operations" }, via: "twin.rel.operation", optional: true },
|
|
1742
|
+
{ from: "tasks", field: "fromNode", target: { kind: "axis", axis: "locations" }, via: "twin.rel.from", optional: true },
|
|
1743
|
+
{ from: "tasks", field: "toNode", target: { kind: "axis", axis: "locations" }, via: "twin.rel.at", optional: true },
|
|
1744
|
+
{ from: "tasks", field: "resourceRef", target: { kind: "axis", axis: "equipment" }, via: "twin.rel.by", optional: true },
|
|
1745
|
+
{ from: "tasks", field: "personnel[]", target: { kind: "axis", axis: "persons" }, via: "twin.rel.crew", optional: true },
|
|
1690
1746
|
/* 공정이 도는 자리·자원 — 항목이 아니라 **타입**을 가리킨다 */
|
|
1691
1747
|
{ from: "operations", field: "locationType", target: { kind: "type", role: "location" }, via: "twin.rel.at", optional: true },
|
|
1692
1748
|
{ from: "operations", field: "resourceType", target: { kind: "type", role: "equipment" }, via: "twin.rel.by", optional: true }
|
|
@@ -1740,6 +1796,14 @@ var TWIN_PROPERTIES = [
|
|
|
1740
1796
|
}
|
|
1741
1797
|
];
|
|
1742
1798
|
var propertiesOf = (axis) => TWIN_PROPERTIES.filter((p) => p.axis === axis);
|
|
1799
|
+
var axisSource = (axis) => TWIN_AXES.find((a) => a.axis === axis)?.source ?? "document";
|
|
1800
|
+
function documentPath(axis) {
|
|
1801
|
+
const info = TWIN_AXES.find((a) => a.axis === axis);
|
|
1802
|
+
if (!info) throw new Error(`unknown twin axis: "${axis}"`);
|
|
1803
|
+
if (axisSource(axis) !== "document" || !info.path)
|
|
1804
|
+
throw new Error(`axis "${axis}" is not read from the document (source: ${axisSource(axis)}) \u2014 read it from the ${axisSource(axis)}`);
|
|
1805
|
+
return info.path;
|
|
1806
|
+
}
|
|
1743
1807
|
var relationsFrom = (axis) => TWIN_RELATIONS.filter((r) => r.from === axis);
|
|
1744
1808
|
var relationsTo = (axis) => TWIN_RELATIONS.filter((r) => r.target.kind === "axis" && r.target.axis === axis);
|
|
1745
1809
|
function axisInfo(axis) {
|
|
@@ -2502,7 +2566,11 @@ var FlowEngine = class {
|
|
|
2502
2566
|
없다.** 라이브 트윈이 납기 초과를 한 번도 보고하지 못한 원인 중 하나였다. */
|
|
2503
2567
|
...o.endTime ? { endTime: o.endTime } : {},
|
|
2504
2568
|
...o.startTime ? { startTime: o.startTime } : {},
|
|
2505
|
-
...o.priority !== void 0 ? { priority: o.priority } : {}
|
|
2569
|
+
...o.priority !== void 0 ? { priority: o.priority } : {},
|
|
2570
|
+
/* 확보분과 거래번호도 함께 옮긴다 — 여기서 떨구면 위에서 실어 온 것이 마지막 한 걸음에서
|
|
2571
|
+
사라진다(같은 사실이 델타·스냅샷·이 변환 **세 길**을 지난다). */
|
|
2572
|
+
...o.allocated?.length ? { allocated: o.allocated } : {},
|
|
2573
|
+
...o.bizTransaction ? { bizTransaction: o.bizTransaction } : {}
|
|
2506
2574
|
}));
|
|
2507
2575
|
for (const o of observedOrders) {
|
|
2508
2576
|
const lines = (o.lines ?? []).map((l) => ({ gtin: l.gtin, requested: l.requested - (l.fulfilled ?? 0) })).filter((l) => l.requested > 0);
|
|
@@ -2514,8 +2582,11 @@ var FlowEngine = class {
|
|
|
2514
2582
|
status: "created",
|
|
2515
2583
|
requested: remaining,
|
|
2516
2584
|
fulfilled: 0,
|
|
2517
|
-
|
|
2518
|
-
|
|
2585
|
+
/* **확보해 둔 것과 거래번호를 이어받는다.** 비우면 되살아난 오더가 아무것도 안 잡은 것처럼
|
|
2586
|
+
보이고, 그 오더의 진행 중 작업이 완료될 때 계보가 입력 없이 나간다. 상태가 말해 주지
|
|
2587
|
+
않으면(옛 저널) 그때는 비는 것이 사실이다 — 없는 것을 지어내지 않는다. */
|
|
2588
|
+
bizTransaction: o.bizTransaction ?? "",
|
|
2589
|
+
allocated: (o.allocated ?? []).slice(),
|
|
2519
2590
|
picked: [],
|
|
2520
2591
|
shipmentEpc: null,
|
|
2521
2592
|
lines,
|
|
@@ -2823,6 +2894,10 @@ var FlowEngine = class {
|
|
|
2823
2894
|
...o.priority !== void 0 ? { priority: o.priority } : {},
|
|
2824
2895
|
...o.startTime ? { startTime: o.startTime } : {},
|
|
2825
2896
|
...o.endTime ? { endTime: o.endTime } : {},
|
|
2897
|
+
/* 진행 중 확보분과 거래번호 — **스냅샷에도** 실어야 한다. 델타에만 실으면 스냅샷으로
|
|
2898
|
+
재기동하는 경로(체크포인트)에서 그대로 잃는다(같은 사실을 두 길로 나르는 값이다). */
|
|
2899
|
+
...o.allocated?.length ? { allocated: o.allocated.slice() } : {},
|
|
2900
|
+
...o.bizTransaction ? { bizTransaction: o.bizTransaction } : {},
|
|
2826
2901
|
held: o.held
|
|
2827
2902
|
})),
|
|
2828
2903
|
attentions: this.computeAttentions(),
|
|
@@ -3367,7 +3442,11 @@ var FlowEngine = class {
|
|
|
3367
3442
|
...o.priority !== void 0 ? { priority: o.priority } : {},
|
|
3368
3443
|
...o.startTime ? { startTime: o.startTime } : {},
|
|
3369
3444
|
...o.endTime ? { endTime: o.endTime } : {},
|
|
3370
|
-
...o.lines?.length ? { lines: o.lines.map((l) => ({ gtin: l.gtin, requested: l.requested })) } : {}
|
|
3445
|
+
...o.lines?.length ? { lines: o.lines.map((l) => ({ gtin: l.gtin, requested: l.requested })) } : {},
|
|
3446
|
+
/* 진행 중 할당과 거래번호를 함께 싣는다 — 이것이 없으면 웜스타트가 잃고, 되살아난 오더의
|
|
3447
|
+
계보가 입력 없이 나간다(무엇이 무엇으로 바뀌었나의 절반이 사라진다). */
|
|
3448
|
+
...o.allocated?.length ? { allocated: o.allocated.slice() } : {},
|
|
3449
|
+
...o.bizTransaction ? { bizTransaction: o.bizTransaction } : {}
|
|
3371
3450
|
});
|
|
3372
3451
|
}
|
|
3373
3452
|
// ── 내부 mechanics ─────────────────────────────────────────────────────────
|
|
@@ -4967,6 +5046,7 @@ function retiredVocabularyIn(line) {
|
|
|
4967
5046
|
aggregationEvent,
|
|
4968
5047
|
analyzeCapacity,
|
|
4969
5048
|
axisInfo,
|
|
5049
|
+
axisSource,
|
|
4970
5050
|
capabilitiesForType,
|
|
4971
5051
|
classClosure,
|
|
4972
5052
|
compareStates,
|
|
@@ -4975,6 +5055,7 @@ function retiredVocabularyIn(line) {
|
|
|
4975
5055
|
conversionFactorOf,
|
|
4976
5056
|
counterfactualAt,
|
|
4977
5057
|
deriveAttentions,
|
|
5058
|
+
documentPath,
|
|
4978
5059
|
dueStatusOf,
|
|
4979
5060
|
effectivityAt,
|
|
4980
5061
|
fefoPolicy,
|
package/package.json
CHANGED