@operato/twin-kernel 0.7.40 → 0.7.42
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 +2 -2
- package/dist/contract.js +4 -4
- package/dist/domain-catalog.d.ts +1 -1
- package/dist/domain-catalog.js +1 -1
- package/dist/ems-kernel.js +1 -1
- package/dist/face2-adapter.d.ts +41 -1
- package/dist/face2-adapter.js +47 -2
- package/dist/flow-engine.js +7 -0
- package/dist/kernel.d.ts +1 -1
- package/dist/kernel.js +1 -1
- package/dist/mes-kernel.js +25 -4
- package/dist/observed-reducer.d.ts +1 -1
- package/dist-cjs/index.cjs +57 -9
- package/package.json +1 -1
package/dist/contract.d.ts
CHANGED
|
@@ -1531,11 +1531,11 @@ export interface CommandAck {
|
|
|
1531
1531
|
error?: string;
|
|
1532
1532
|
}
|
|
1533
1533
|
/**
|
|
1534
|
-
* 호스트-facing 커맨드 채널(비동기) — 씬 컴포넌트 등 클라이언트가 트윈 호스트에 커맨드를
|
|
1534
|
+
* 호스트-facing 커맨드 채널(비동기) — 씬 컴포넌트 등 클라이언트가 트윈 호스트에 커맨드를 보내는 계약.
|
|
1535
1535
|
* 커널 in-process `dispatch`(동기)와 달리 원격 호스트(GraphQL/HTTP/…) 전송을 추상화.
|
|
1536
1536
|
* 씬 컨트롤 컴포넌트는 이 인터페이스에만 의존(커널 레벨) — concrete 전송은 각 호스트(things-factory 등)가 주입.
|
|
1537
1537
|
* → 컴포넌트가 특정 호스트 구현(things-factory)에 갇히지 않고, 어디서든 개발/재사용 가능.
|
|
1538
|
-
* 컴포넌트는 tenantId 등을 모르는 부분 커맨드를
|
|
1538
|
+
* 컴포넌트는 tenantId 등을 모르는 부분 커맨드를 보내고, 호스트가 보강한다(commandId/type/args 만 제공).
|
|
1539
1539
|
*/
|
|
1540
1540
|
export interface TwinCommandChannel {
|
|
1541
1541
|
dispatch(command: Pick<Command, 'commandId' | 'type' | 'args'>): Promise<CommandAck>;
|
package/dist/contract.js
CHANGED
|
@@ -150,7 +150,7 @@ export function testPassedAt(r, at) {
|
|
|
150
150
|
return false;
|
|
151
151
|
if (!r.expiresAt || !at)
|
|
152
152
|
return r.result === 'pass';
|
|
153
|
-
return
|
|
153
|
+
return parsedMs(at) <= parsedMs(r.expiresAt);
|
|
154
154
|
}
|
|
155
155
|
/**
|
|
156
156
|
* 이 개체가 **요구된 시험들을 만족하나** — 등급이 요구하고 개체가 기록을 든다.
|
|
@@ -293,8 +293,8 @@ export function priorityRank(p) {
|
|
|
293
293
|
export function dueStatusOf(x, nowIso) {
|
|
294
294
|
if (!x.endTime || !nowIso)
|
|
295
295
|
return undefined;
|
|
296
|
-
const due =
|
|
297
|
-
const now =
|
|
296
|
+
const due = parsedMs(x.endTime);
|
|
297
|
+
const now = parsedMs(nowIso);
|
|
298
298
|
if (!Number.isFinite(due) || !Number.isFinite(now))
|
|
299
299
|
return undefined;
|
|
300
300
|
return now > due ? 'late' : 'on-time';
|
|
@@ -663,7 +663,7 @@ export function capabilityOf(r, ctx) {
|
|
|
663
663
|
if (r.status === 'down')
|
|
664
664
|
return { available: false, reason: 'down' };
|
|
665
665
|
if (at) {
|
|
666
|
-
const ms =
|
|
666
|
+
const ms = parsedMs(at);
|
|
667
667
|
if (Number.isFinite(ms)) {
|
|
668
668
|
const why = offCalendarReasonAt(r, ms, ctx?.utcOffsetMinutes);
|
|
669
669
|
if (why === 'non-working')
|
package/dist/domain-catalog.d.ts
CHANGED
|
@@ -225,7 +225,7 @@ export declare const propertiesOf: (axis: string) => TwinPropertyInfo[];
|
|
|
225
225
|
*/
|
|
226
226
|
export declare const axisSource: (axis: string) => "document" | "state" | "journal";
|
|
227
227
|
/**
|
|
228
|
-
* 문서에서 이 축을 찾을 자리 — **문서 축이 아니면
|
|
228
|
+
* 문서에서 이 축을 찾을 자리 — **문서 축이 아니면 오류를 낸다.**
|
|
229
229
|
*
|
|
230
230
|
* 상태·저널 축에는 board 안의 자리가 없다. 그런데 옛 소비처는 `info.path` 를 무조건 읽었으므로,
|
|
231
231
|
* 그 자리가 `undefined` 면 board 루트를 읽고 **오류 없이 0 을 답한다**(이 프로젝트가 겪은 실패 모양).
|
package/dist/domain-catalog.js
CHANGED
|
@@ -202,7 +202,7 @@ export const propertiesOf = (axis) => TWIN_PROPERTIES.filter(p => p.axis === axi
|
|
|
202
202
|
*/
|
|
203
203
|
export const axisSource = (axis) => TWIN_AXES.find(a => a.axis === axis)?.source ?? 'document';
|
|
204
204
|
/**
|
|
205
|
-
* 문서에서 이 축을 찾을 자리 — **문서 축이 아니면
|
|
205
|
+
* 문서에서 이 축을 찾을 자리 — **문서 축이 아니면 오류를 낸다.**
|
|
206
206
|
*
|
|
207
207
|
* 상태·저널 축에는 board 안의 자리가 없다. 그런데 옛 소비처는 `info.path` 를 무조건 읽었으므로,
|
|
208
208
|
* 그 자리가 `undefined` 면 board 루트를 읽고 **오류 없이 0 을 답한다**(이 프로젝트가 겪은 실패 모양).
|
package/dist/ems-kernel.js
CHANGED
|
@@ -621,7 +621,7 @@ export class EmsKernel extends FlowEngine {
|
|
|
621
621
|
* **아무 일도 하지 않는다** — 그런데 조용히 넘기지 않는다: 이 커널이 그런 요청을 받았다는 것은
|
|
622
622
|
* **배선이 잘못됐다는 사실**이고(EMS 트윈에 물류 명령을 보낸 것), 조용히 넘기면 그 사실이 사라진다.
|
|
623
623
|
*
|
|
624
|
-
*
|
|
624
|
+
* 오류를 내지도 않는다: 저널 재생 중이라면 트윈 전체가 멈춘다. 그래서 커널이 담을 줄 모르는 사건을
|
|
625
625
|
* 세는 자리(`ObservedReducer.unhandled`)와 같은 규율으로, **개수를 세어 상태로 낸다.**
|
|
626
626
|
*/
|
|
627
627
|
flowRequests = new Map();
|
package/dist/face2-adapter.d.ts
CHANGED
|
@@ -59,7 +59,37 @@ export interface AggregationEventMapping {
|
|
|
59
59
|
readPoint?: MapValue;
|
|
60
60
|
bizLocation?: MapValue;
|
|
61
61
|
}
|
|
62
|
-
|
|
62
|
+
/**
|
|
63
|
+
* **변환의 사실** — 무엇이 들어가 무엇이 나왔나(EPCIS `TransformationEvent`).
|
|
64
|
+
*
|
|
65
|
+
* ── 왜 이 갈래가 필요한가 (2026-08-21) ─────────────────────────────────────
|
|
66
|
+
* 정규 레코드 계약이 `ObjectEvent`·`AggregationEvent` 둘만 다뤄서, 원본이 「이 로트를 소비해 이 제품을
|
|
67
|
+
* 만들었다」고 말해도 실을 자리가 없었다. 그 사실이 문 앞에서 사라진다.
|
|
68
|
+
*
|
|
69
|
+
* **HACCP 회수 범위 판정이 정확히 이 자리를 요구한다** — 「이 로트가 어느 제품에 들어갔나」다. 로트를
|
|
70
|
+
* 개체로 식별할 수 없는 현장(연속량·분할 로트)에서는 소비를 `inputQuantityList` 의 **LGTIN 클래스**
|
|
71
|
+
* (`urn:epc:class:lgtin:<프리픽스>.<품번>.<로트>`)로 말한다 — 표준이 로트 단위 클래스를 그렇게 정한다.
|
|
72
|
+
*
|
|
73
|
+
* `action` 이 없다: 표준이 `TransformationEvent` 에 `action` 을 두지 않는다(들어감과 나옴이 곧 뜻이다).
|
|
74
|
+
*/
|
|
75
|
+
export interface TransformationEventMapping {
|
|
76
|
+
type: 'TransformationEvent';
|
|
77
|
+
bizStep: MapValue;
|
|
78
|
+
disposition?: MapValue;
|
|
79
|
+
/** 소비된 개체들("$.inputEPCList") — 문자열 배열. */
|
|
80
|
+
inputEPCList?: MapValue;
|
|
81
|
+
/** 소비된 클래스+수량("$.inputQuantityList") — 로트 단위 소비가 이 자리다. */
|
|
82
|
+
inputQuantityList?: MapValue;
|
|
83
|
+
/** 산출된 개체들("$.outputEPCList"). */
|
|
84
|
+
outputEPCList?: MapValue;
|
|
85
|
+
/** 산출된 클래스+수량("$.outputQuantityList"). */
|
|
86
|
+
outputQuantityList?: MapValue;
|
|
87
|
+
/** 여러 이벤트를 한 변환으로 잇는 식별자("$.transformationID") — 한 오더의 여러 단계가 이것으로 묶인다. */
|
|
88
|
+
transformationID?: MapValue;
|
|
89
|
+
readPoint?: MapValue;
|
|
90
|
+
bizLocation?: MapValue;
|
|
91
|
+
}
|
|
92
|
+
export type EventMapping = ObjectEventMapping | AggregationEventMapping | TransformationEventMapping;
|
|
63
93
|
/**
|
|
64
94
|
* 이 품목 레코드가 **담김의 사실**인가 — 라우팅 판정을 한 곳에 둔다(소비처가 각자 짐작하지 않게).
|
|
65
95
|
*
|
|
@@ -67,6 +97,16 @@ export type EventMapping = ObjectEventMapping | AggregationEventMapping;
|
|
|
67
97
|
* 있으면 담김으로 본다 — 둘 다 있으면 커넥터가 무엇을 말하려는지 알 수 없으므로 담김으로 받지 않는다.
|
|
68
98
|
*/
|
|
69
99
|
export declare function isAggregationRecord(record: unknown): boolean;
|
|
100
|
+
/**
|
|
101
|
+
* 이 레코드가 **변환의 사실**인가 — 라우팅 판정을 한 곳에 둔다(소비처가 각자 짐작하지 않게).
|
|
102
|
+
*
|
|
103
|
+
* 변환은 **들어간 것과 나온 것**으로 말한다. 개체 하나의 관측(`epc`)이나 담김(`parentID`)과 섞이지
|
|
104
|
+
* 않게, 그 둘이 없고 입력·출력 중 하나라도 있으면 변환으로 본다.
|
|
105
|
+
*
|
|
106
|
+
* 한쪽만 있는 것도 변환이다 — 표준이 소실(N→0)과 생성(0→N)을 같은 이벤트로 표현한다. 둘 다 없으면
|
|
107
|
+
* 변환이 아니다(무엇이 바뀌었는지 말하지 않은 것이다).
|
|
108
|
+
*/
|
|
109
|
+
export declare function isTransformationRecord(record: unknown): boolean;
|
|
70
110
|
/** 룰: 소스 레코드의 판별자(sourceType)로 매핑을 고른다. */
|
|
71
111
|
export interface AdapterRule {
|
|
72
112
|
sourceType: string;
|
package/dist/face2-adapter.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* 매핑 엔진은 walking-skeleton 범위에서 무의존 declarative spec 사용.
|
|
9
9
|
* (jsonata/템플릿 엔진 최종 선택은 열린 결정 — face2-adapters.md. 커널 zero-dep 유지 위해 여기선 미도입.)
|
|
10
10
|
*/
|
|
11
|
-
import { aggregationEvent, objectEvent, validateEpcisEvent } from "./epcis.js";
|
|
11
|
+
import { aggregationEvent, objectEvent, transformationEvent, validateEpcisEvent } from "./epcis.js";
|
|
12
12
|
/**
|
|
13
13
|
* 이 품목 레코드가 **담김의 사실**인가 — 라우팅 판정을 한 곳에 둔다(소비처가 각자 짐작하지 않게).
|
|
14
14
|
*
|
|
@@ -21,6 +21,24 @@ export function isAggregationRecord(record) {
|
|
|
21
21
|
const r = record;
|
|
22
22
|
return typeof r.parentID === 'string' && r.parentID.trim().length > 0 && r.epc === undefined;
|
|
23
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* 이 레코드가 **변환의 사실**인가 — 라우팅 판정을 한 곳에 둔다(소비처가 각자 짐작하지 않게).
|
|
26
|
+
*
|
|
27
|
+
* 변환은 **들어간 것과 나온 것**으로 말한다. 개체 하나의 관측(`epc`)이나 담김(`parentID`)과 섞이지
|
|
28
|
+
* 않게, 그 둘이 없고 입력·출력 중 하나라도 있으면 변환으로 본다.
|
|
29
|
+
*
|
|
30
|
+
* 한쪽만 있는 것도 변환이다 — 표준이 소실(N→0)과 생성(0→N)을 같은 이벤트로 표현한다. 둘 다 없으면
|
|
31
|
+
* 변환이 아니다(무엇이 바뀌었는지 말하지 않은 것이다).
|
|
32
|
+
*/
|
|
33
|
+
export function isTransformationRecord(record) {
|
|
34
|
+
if (!record || typeof record !== 'object')
|
|
35
|
+
return false;
|
|
36
|
+
const r = record;
|
|
37
|
+
if (r.epc !== undefined || r.parentID !== undefined)
|
|
38
|
+
return false;
|
|
39
|
+
const has = (k) => Array.isArray(r[k]) && r[k].length > 0;
|
|
40
|
+
return has('inputEPCList') || has('inputQuantityList') || has('outputEPCList') || has('outputQuantityList');
|
|
41
|
+
}
|
|
24
42
|
function get(obj, path) {
|
|
25
43
|
return path.split('.').reduce((o, k) => (o == null ? o : o[k]), obj);
|
|
26
44
|
}
|
|
@@ -83,12 +101,38 @@ function resolveQuantityList(v, record, field, errors) {
|
|
|
83
101
|
/** 단일 레코드 → 정규 EPCIS 이벤트 + 매핑에서 드러난 문제(검증은 `ingest` 가 이어서 한다). */
|
|
84
102
|
export function mapRecordChecked(record, mapping, eventTime) {
|
|
85
103
|
const errors = [];
|
|
86
|
-
const action = (resolve(mapping.action, record, 'action', errors) ?? '');
|
|
87
104
|
const bizStep = resolve(mapping.bizStep, record, 'bizStep', errors) ?? '';
|
|
88
105
|
const disposition = resolve(mapping.disposition, record, 'disposition', errors);
|
|
89
106
|
const readPoint = resolve(mapping.readPoint, record, 'readPoint', errors);
|
|
90
107
|
const bizLocation = resolve(mapping.bizLocation, record, 'bizLocation', errors);
|
|
108
|
+
if (mapping.type === 'TransformationEvent') {
|
|
109
|
+
const inputEPCList = resolveList(mapping.inputEPCList, record, 'inputEPCList', errors);
|
|
110
|
+
const inputQuantityList = resolveQuantityList(mapping.inputQuantityList, record, 'inputQuantityList', errors);
|
|
111
|
+
const outputEPCList = resolveList(mapping.outputEPCList, record, 'outputEPCList', errors);
|
|
112
|
+
const outputQuantityList = resolveQuantityList(mapping.outputQuantityList, record, 'outputQuantityList', errors);
|
|
113
|
+
const transformationID = resolve(mapping.transformationID, record, 'transformationID', errors);
|
|
114
|
+
/* 들어간 것도 나온 것도 없으면 무엇이 바뀌었는지 말하지 않은 것이다 — 검증에 넘기기 전에 여기서
|
|
115
|
+
말해 준다(검증 메시지는 이벤트를 가리키고, 이 메시지는 **매핑**을 가리킨다). */
|
|
116
|
+
if (!inputEPCList.length && !inputQuantityList.length && !outputEPCList.length && !outputQuantityList.length) {
|
|
117
|
+
errors.push('입력과 출력이 모두 비었다 — 변환은 들어간 것이나 나온 것 중 하나는 말해야 한다');
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
event: transformationEvent({
|
|
121
|
+
eventTime, bizStep, disposition,
|
|
122
|
+
/* 없는 쪽은 **필드를 만들지 않는다** — 빈 배열을 실으면 「없다」고 말하는 것이 된다.
|
|
123
|
+
소실(N→0)과 생성(0→N)은 표준이 인정하는 변환이고, 그때 한쪽이 아예 없는 것이 사실이다. */
|
|
124
|
+
...(inputEPCList.length ? { inputEPCList } : {}),
|
|
125
|
+
...(inputQuantityList.length ? { inputQuantityList } : {}),
|
|
126
|
+
...(outputEPCList.length ? { outputEPCList } : {}),
|
|
127
|
+
...(outputQuantityList.length ? { outputQuantityList } : {}),
|
|
128
|
+
...(transformationID ? { transformationID } : {}),
|
|
129
|
+
readPoint, bizLocation
|
|
130
|
+
}),
|
|
131
|
+
errors
|
|
132
|
+
};
|
|
133
|
+
}
|
|
91
134
|
if (mapping.type === 'AggregationEvent') {
|
|
135
|
+
const action = (resolve(mapping.action, record, 'action', errors) ?? '');
|
|
92
136
|
const parentID = resolve(mapping.parentID, record, 'parentID', errors) ?? '';
|
|
93
137
|
const childEPCs = resolveList(mapping.childEPCs, record, 'childEPCs', errors);
|
|
94
138
|
const childQuantityList = resolveQuantityList(mapping.childQuantityList, record, 'childQuantityList', errors);
|
|
@@ -103,6 +147,7 @@ export function mapRecordChecked(record, mapping, eventTime) {
|
|
|
103
147
|
errors
|
|
104
148
|
};
|
|
105
149
|
}
|
|
150
|
+
const action = (resolve(mapping.action, record, 'action', errors) ?? '');
|
|
106
151
|
const epc = resolve(mapping.epc, record, 'epc', errors);
|
|
107
152
|
const quantityList = resolveQuantityList(mapping.quantityList, record, 'quantityList', errors);
|
|
108
153
|
/* 개체도 수량도 없으면 무엇을 관측했는지 말하지 않은 것이다 — 검증에 넘기기 전에 여기서 말해 준다
|
package/dist/flow-engine.js
CHANGED
|
@@ -2084,6 +2084,13 @@ export class FlowEngine {
|
|
|
2084
2084
|
* 시뮬레이션에서 이것은 **우리 계산의 결함**이다(우리가 없는 물건을 소비하라고 요구했다). 그래서
|
|
2085
2085
|
* 멈춘다. 미러는 처방이 다르다 — 원본이 진실이므로 받아들이고 어긋난 횟수를 남긴다.
|
|
2086
2086
|
*
|
|
2087
|
+
* ── 자리 규칙은 **여기서 확인하지 않는다** (2026-08-21에 호출부를 확인한 결과) ────
|
|
2088
|
+
* 「자재는 작업이 일어나는 자리에 있어야 소비된다」는 규칙이 있고, 그 확인은 **소비 경로**
|
|
2089
|
+
* (`claimMaterials`)가 이미 한다(그 자리에 있는 것만 선택한다). 이 원시에 같은 확인을 추가하면
|
|
2090
|
+
* 정상 경로를 막는다: MES 선언 경로는 자재를 **라인사이드 보관 자리**에서 선택하고 변환은
|
|
2091
|
+
* **공정 자리**에서 일어난다(`mes-kernel` 의 라우트 단계 — 두 자리가 다른 것이 그 경로의 설계다).
|
|
2092
|
+
* 추가하려면 먼저 그 경로가 물건을 공정 자리로 이동시키게 만들어야 하고, 그것은 별개의 결정이다.
|
|
2093
|
+
*
|
|
2087
2094
|
* ── 미러를 멈추면 안 되는 이유 (2026-08-20, 시험으로 확인) ─────────────────
|
|
2088
2095
|
* 씨앗으로 세운 커널은 그대로 `tick` 한다(§`hydrateObserved`). 원본이 확보분 하나를 누락한
|
|
2089
2096
|
* 스냅샷을 주면 — 실 연동에서 흔한 빈틈이다 — 그 커널은 **첫 틱에 멈춘다.** 원본의 한 건이
|
package/dist/kernel.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ export declare class WmsKernel extends FlowEngine {
|
|
|
16
16
|
*
|
|
17
17
|
* 여기서 이름을 여는 것이 답이 아니다 — 이 경로는 부품·라우트가 내장 상수인 레거시 시나리오라,
|
|
18
18
|
* 이름만 열면 **지원하는 척**이 된다(MES 가 같은 이유로 같은 결론을 냈다). 답은 요구를 분명히 말하는
|
|
19
|
-
* 것이고,
|
|
19
|
+
* 것이고, 오류를 내면 호스트가 그 트윈만 멈추고 이유를 등록부에 적는다(`tick-failed`) — 다른 트윈은 계속 돈다.
|
|
20
20
|
* 조용한 정지는 그 사실조차 남기지 않는다.
|
|
21
21
|
*/
|
|
22
22
|
private builtInLocation;
|
package/dist/kernel.js
CHANGED
|
@@ -43,7 +43,7 @@ export class WmsKernel extends FlowEngine {
|
|
|
43
43
|
*
|
|
44
44
|
* 여기서 이름을 여는 것이 답이 아니다 — 이 경로는 부품·라우트가 내장 상수인 레거시 시나리오라,
|
|
45
45
|
* 이름만 열면 **지원하는 척**이 된다(MES 가 같은 이유로 같은 결론을 냈다). 답은 요구를 분명히 말하는
|
|
46
|
-
* 것이고,
|
|
46
|
+
* 것이고, 오류를 내면 호스트가 그 트윈만 멈추고 이유를 등록부에 적는다(`tick-failed`) — 다른 트윈은 계속 돈다.
|
|
47
47
|
* 조용한 정지는 그 사실조차 남기지 않는다.
|
|
48
48
|
*/
|
|
49
49
|
builtInLocation(type, what) {
|
package/dist/mes-kernel.js
CHANGED
|
@@ -417,13 +417,34 @@ export class MesKernel extends FlowEngine {
|
|
|
417
417
|
}
|
|
418
418
|
const rc = this.recipeDef(o);
|
|
419
419
|
const picks = [];
|
|
420
|
+
/*
|
|
421
|
+
* ── 자리별 색인을 사용한다 (2026-08-21) ────────────────────────────────────
|
|
422
|
+
* 예전에는 투입 줄마다 `[...this.items.values()].filter(...)` 로 **물품 전체를 순회**했다. 오더 하나에
|
|
423
|
+
* 투입 줄이 셋이면 세 번이다. 규모 기준선이 그것을 보여 주었다 — 물품이 4배 늘 때 주문당 비용이
|
|
424
|
+
* 3.3~3.5배가 됐다(거의 선형). 목표 규모는 물품 10만이고, 그때 이 순회는 틱을 밀어 낸다.
|
|
425
|
+
*
|
|
426
|
+
* 필터 조건이 `gtin` + `disposition` + **자리 타입**이므로, 자리 타입에 해당하는 자리들만 찾아
|
|
427
|
+
* `items.at(자리)` 색인을 사용하면 그 자리에 있는 것만 본다. 자리 수는 물품 수와 무관하게 작다.
|
|
428
|
+
*/
|
|
429
|
+
const locsOfType = new Map();
|
|
430
|
+
for (const n of this.locations.values()) {
|
|
431
|
+
const bin = locsOfType.get(n.type);
|
|
432
|
+
if (bin)
|
|
433
|
+
bin.push(n);
|
|
434
|
+
else
|
|
435
|
+
locsOfType.set(n.type, [n]);
|
|
436
|
+
}
|
|
420
437
|
for (const line of rc.inputs) {
|
|
421
438
|
const g = this.classOf(line.material);
|
|
422
|
-
/* 자리 필터는 남는다 — 완제품도 `sellable` 이라, 반제품→완제품 체인에서 이것이 없으면 산출물을 자재로
|
|
439
|
+
/* 자리 필터는 남는다 — 완제품도 `sellable` 이라, 반제품→완제품 체인에서 이것이 없으면 산출물을 자재로 소비한다. */
|
|
423
440
|
const fromType = this.locationTypeOfMaterial(line.material);
|
|
424
|
-
const available = [
|
|
425
|
-
|
|
426
|
-
|
|
441
|
+
const available = [];
|
|
442
|
+
for (const n of locsOfType.get(fromType) ?? []) {
|
|
443
|
+
for (const i of this.items.at(n.id)) {
|
|
444
|
+
if (i.gtin === g && i.disposition === DISP.sellable)
|
|
445
|
+
available.push({ epc: i.epc, location: i.location, qty: 1 });
|
|
446
|
+
}
|
|
447
|
+
}
|
|
427
448
|
const chosen = this.policy.selectStock({ gtin: g, qty: line.qty, available });
|
|
428
449
|
if (chosen.length < line.qty)
|
|
429
450
|
return; // 자재 부족 → 대기
|
|
@@ -55,7 +55,7 @@ export interface ProjectedState {
|
|
|
55
55
|
* 아무것도 담기지 않는다. 그것을 조용히 두면 트윈은 「정상인데 빈」 모습이 된다.
|
|
56
56
|
*
|
|
57
57
|
* 비어 있지 않으면 뜻은 하나다: **이 트윈에 맞지 않는 어휘가 들어오고 있다**(커넥터 배선 오류이거나
|
|
58
|
-
* 커널이 낡았다). 세는 것에 그치고 판단은 소비처가 한다 — 여기서
|
|
58
|
+
* 커널이 낡았다). 세는 것에 그치고 판단은 소비처가 한다 — 여기서 오류를 내면 저널 재생이 멈춘다.
|
|
59
59
|
*/
|
|
60
60
|
unhandled?: {
|
|
61
61
|
eventType: string;
|
package/dist-cjs/index.cjs
CHANGED
|
@@ -125,6 +125,7 @@ __export(index_exports, {
|
|
|
125
125
|
isEnergyRecord: () => isEnergyRecord,
|
|
126
126
|
isEquipmentLevel: () => isEquipmentLevel,
|
|
127
127
|
isOperationalRecord: () => isOperationalRecord,
|
|
128
|
+
isTransformationRecord: () => isTransformationRecord,
|
|
128
129
|
isoDurationHours: () => isoDurationHours,
|
|
129
130
|
itemKeyOf: () => itemKeyOf,
|
|
130
131
|
levelOfLocationType: () => levelOfLocationType,
|
|
@@ -263,7 +264,7 @@ function hierarchyOf(s, levelOfType) {
|
|
|
263
264
|
function testPassedAt(r, at) {
|
|
264
265
|
if (r.result !== "pass") return false;
|
|
265
266
|
if (!r.expiresAt || !at) return r.result === "pass";
|
|
266
|
-
return
|
|
267
|
+
return parsedMs(at) <= parsedMs(r.expiresAt);
|
|
267
268
|
}
|
|
268
269
|
function meetsTests(required, results, at) {
|
|
269
270
|
if (!required.length) return true;
|
|
@@ -326,8 +327,8 @@ function priorityRank(p) {
|
|
|
326
327
|
}
|
|
327
328
|
function dueStatusOf(x, nowIso) {
|
|
328
329
|
if (!x.endTime || !nowIso) return void 0;
|
|
329
|
-
const due =
|
|
330
|
-
const now =
|
|
330
|
+
const due = parsedMs(x.endTime);
|
|
331
|
+
const now = parsedMs(nowIso);
|
|
331
332
|
if (!Number.isFinite(due) || !Number.isFinite(now)) return void 0;
|
|
332
333
|
return now > due ? "late" : "on-time";
|
|
333
334
|
}
|
|
@@ -495,7 +496,7 @@ function capabilityOf(r, ctx) {
|
|
|
495
496
|
if (r.held) return { available: false, reason: "held" };
|
|
496
497
|
if (r.status === "down") return { available: false, reason: "down" };
|
|
497
498
|
if (at) {
|
|
498
|
-
const ms2 =
|
|
499
|
+
const ms2 = parsedMs(at);
|
|
499
500
|
if (Number.isFinite(ms2)) {
|
|
500
501
|
const why = offCalendarReasonAt(r, ms2, ctx?.utcOffsetMinutes);
|
|
501
502
|
if (why === "non-working") return { available: false, reason: "resting" };
|
|
@@ -2807,6 +2808,13 @@ function isAggregationRecord(record) {
|
|
|
2807
2808
|
const r = record;
|
|
2808
2809
|
return typeof r.parentID === "string" && r.parentID.trim().length > 0 && r.epc === void 0;
|
|
2809
2810
|
}
|
|
2811
|
+
function isTransformationRecord(record) {
|
|
2812
|
+
if (!record || typeof record !== "object") return false;
|
|
2813
|
+
const r = record;
|
|
2814
|
+
if (r.epc !== void 0 || r.parentID !== void 0) return false;
|
|
2815
|
+
const has = (k) => Array.isArray(r[k]) && r[k].length > 0;
|
|
2816
|
+
return has("inputEPCList") || has("inputQuantityList") || has("outputEPCList") || has("outputQuantityList");
|
|
2817
|
+
}
|
|
2810
2818
|
function get(obj, path) {
|
|
2811
2819
|
return path.split(".").reduce((o, k) => o == null ? o : o[k], obj);
|
|
2812
2820
|
}
|
|
@@ -2843,19 +2851,46 @@ function resolveQuantityList(v, record, field, errors) {
|
|
|
2843
2851
|
}
|
|
2844
2852
|
function mapRecordChecked(record, mapping, eventTime) {
|
|
2845
2853
|
const errors = [];
|
|
2846
|
-
const action = resolve(mapping.action, record, "action", errors) ?? "";
|
|
2847
2854
|
const bizStep = resolve(mapping.bizStep, record, "bizStep", errors) ?? "";
|
|
2848
2855
|
const disposition = resolve(mapping.disposition, record, "disposition", errors);
|
|
2849
2856
|
const readPoint = resolve(mapping.readPoint, record, "readPoint", errors);
|
|
2850
2857
|
const bizLocation = resolve(mapping.bizLocation, record, "bizLocation", errors);
|
|
2858
|
+
if (mapping.type === "TransformationEvent") {
|
|
2859
|
+
const inputEPCList = resolveList(mapping.inputEPCList, record, "inputEPCList", errors);
|
|
2860
|
+
const inputQuantityList = resolveQuantityList(mapping.inputQuantityList, record, "inputQuantityList", errors);
|
|
2861
|
+
const outputEPCList = resolveList(mapping.outputEPCList, record, "outputEPCList", errors);
|
|
2862
|
+
const outputQuantityList = resolveQuantityList(mapping.outputQuantityList, record, "outputQuantityList", errors);
|
|
2863
|
+
const transformationID = resolve(mapping.transformationID, record, "transformationID", errors);
|
|
2864
|
+
if (!inputEPCList.length && !inputQuantityList.length && !outputEPCList.length && !outputQuantityList.length) {
|
|
2865
|
+
errors.push("\uC785\uB825\uACFC \uCD9C\uB825\uC774 \uBAA8\uB450 \uBE44\uC5C8\uB2E4 \u2014 \uBCC0\uD658\uC740 \uB4E4\uC5B4\uAC04 \uAC83\uC774\uB098 \uB098\uC628 \uAC83 \uC911 \uD558\uB098\uB294 \uB9D0\uD574\uC57C \uD55C\uB2E4");
|
|
2866
|
+
}
|
|
2867
|
+
return {
|
|
2868
|
+
event: transformationEvent({
|
|
2869
|
+
eventTime,
|
|
2870
|
+
bizStep,
|
|
2871
|
+
disposition,
|
|
2872
|
+
/* 없는 쪽은 **필드를 만들지 않는다** — 빈 배열을 실으면 「없다」고 말하는 것이 된다.
|
|
2873
|
+
소실(N→0)과 생성(0→N)은 표준이 인정하는 변환이고, 그때 한쪽이 아예 없는 것이 사실이다. */
|
|
2874
|
+
...inputEPCList.length ? { inputEPCList } : {},
|
|
2875
|
+
...inputQuantityList.length ? { inputQuantityList } : {},
|
|
2876
|
+
...outputEPCList.length ? { outputEPCList } : {},
|
|
2877
|
+
...outputQuantityList.length ? { outputQuantityList } : {},
|
|
2878
|
+
...transformationID ? { transformationID } : {},
|
|
2879
|
+
readPoint,
|
|
2880
|
+
bizLocation
|
|
2881
|
+
}),
|
|
2882
|
+
errors
|
|
2883
|
+
};
|
|
2884
|
+
}
|
|
2851
2885
|
if (mapping.type === "AggregationEvent") {
|
|
2886
|
+
const action2 = resolve(mapping.action, record, "action", errors) ?? "";
|
|
2852
2887
|
const parentID = resolve(mapping.parentID, record, "parentID", errors) ?? "";
|
|
2853
2888
|
const childEPCs = resolveList(mapping.childEPCs, record, "childEPCs", errors);
|
|
2854
2889
|
const childQuantityList = resolveQuantityList(mapping.childQuantityList, record, "childQuantityList", errors);
|
|
2855
2890
|
return {
|
|
2856
2891
|
event: aggregationEvent({
|
|
2857
2892
|
eventTime,
|
|
2858
|
-
action,
|
|
2893
|
+
action: action2,
|
|
2859
2894
|
bizStep,
|
|
2860
2895
|
disposition,
|
|
2861
2896
|
parentID,
|
|
@@ -2868,6 +2903,7 @@ function mapRecordChecked(record, mapping, eventTime) {
|
|
|
2868
2903
|
errors
|
|
2869
2904
|
};
|
|
2870
2905
|
}
|
|
2906
|
+
const action = resolve(mapping.action, record, "action", errors) ?? "";
|
|
2871
2907
|
const epc = resolve(mapping.epc, record, "epc", errors);
|
|
2872
2908
|
const quantityList = resolveQuantityList(mapping.quantityList, record, "quantityList", errors);
|
|
2873
2909
|
if (!epc && !quantityList.length) {
|
|
@@ -5995,7 +6031,7 @@ var WmsKernel = class extends FlowEngine {
|
|
|
5995
6031
|
*
|
|
5996
6032
|
* 여기서 이름을 여는 것이 답이 아니다 — 이 경로는 부품·라우트가 내장 상수인 레거시 시나리오라,
|
|
5997
6033
|
* 이름만 열면 **지원하는 척**이 된다(MES 가 같은 이유로 같은 결론을 냈다). 답은 요구를 분명히 말하는
|
|
5998
|
-
* 것이고,
|
|
6034
|
+
* 것이고, 오류를 내면 호스트가 그 트윈만 멈추고 이유를 등록부에 적는다(`tick-failed`) — 다른 트윈은 계속 돈다.
|
|
5999
6035
|
* 조용한 정지는 그 사실조차 남기지 않는다.
|
|
6000
6036
|
*/
|
|
6001
6037
|
builtInLocation(type, what) {
|
|
@@ -6826,10 +6862,21 @@ var MesKernel = class extends FlowEngine {
|
|
|
6826
6862
|
}
|
|
6827
6863
|
const rc = this.recipeDef(o);
|
|
6828
6864
|
const picks = [];
|
|
6865
|
+
const locsOfType = /* @__PURE__ */ new Map();
|
|
6866
|
+
for (const n of this.locations.values()) {
|
|
6867
|
+
const bin = locsOfType.get(n.type);
|
|
6868
|
+
if (bin) bin.push(n);
|
|
6869
|
+
else locsOfType.set(n.type, [n]);
|
|
6870
|
+
}
|
|
6829
6871
|
for (const line of rc.inputs) {
|
|
6830
6872
|
const g = this.classOf(line.material);
|
|
6831
6873
|
const fromType = this.locationTypeOfMaterial(line.material);
|
|
6832
|
-
const available = [
|
|
6874
|
+
const available = [];
|
|
6875
|
+
for (const n of locsOfType.get(fromType) ?? []) {
|
|
6876
|
+
for (const i of this.items.at(n.id)) {
|
|
6877
|
+
if (i.gtin === g && i.disposition === DISP.sellable) available.push({ epc: i.epc, location: i.location, qty: 1 });
|
|
6878
|
+
}
|
|
6879
|
+
}
|
|
6833
6880
|
const chosen = this.policy.selectStock({ gtin: g, qty: line.qty, available });
|
|
6834
6881
|
if (chosen.length < line.qty) return;
|
|
6835
6882
|
picks.push(...chosen);
|
|
@@ -7361,7 +7408,7 @@ var EmsKernel = class extends FlowEngine {
|
|
|
7361
7408
|
* **아무 일도 하지 않는다** — 그런데 조용히 넘기지 않는다: 이 커널이 그런 요청을 받았다는 것은
|
|
7362
7409
|
* **배선이 잘못됐다는 사실**이고(EMS 트윈에 물류 명령을 보낸 것), 조용히 넘기면 그 사실이 사라진다.
|
|
7363
7410
|
*
|
|
7364
|
-
*
|
|
7411
|
+
* 오류를 내지도 않는다: 저널 재생 중이라면 트윈 전체가 멈춘다. 그래서 커널이 담을 줄 모르는 사건을
|
|
7365
7412
|
* 세는 자리(`ObservedReducer.unhandled`)와 같은 규율으로, **개수를 세어 상태로 낸다.**
|
|
7366
7413
|
*/
|
|
7367
7414
|
flowRequests = /* @__PURE__ */ new Map();
|
|
@@ -8356,6 +8403,7 @@ function retiredVocabularyIn(line) {
|
|
|
8356
8403
|
isEnergyRecord,
|
|
8357
8404
|
isEquipmentLevel,
|
|
8358
8405
|
isOperationalRecord,
|
|
8406
|
+
isTransformationRecord,
|
|
8359
8407
|
isoDurationHours,
|
|
8360
8408
|
itemKeyOf,
|
|
8361
8409
|
levelOfLocationType,
|
package/package.json
CHANGED