@operato/twin-kernel 0.2.3 → 0.4.0

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.
@@ -15,14 +15,14 @@ export interface MaterialDef {
15
15
  identity?: Identity;
16
16
  }
17
17
  /** 로케이션(수동 위치) 타입. */
18
- export interface NodeTypeDef {
18
+ export interface LocationTypeDef {
19
19
  key: string;
20
20
  label: string;
21
21
  standardClass?: StandardClass;
22
22
  identity?: Identity;
23
23
  capabilities?: string[];
24
24
  }
25
- /** 자원(능동 설비·무버) 타입. */
25
+ /** 자원(능동 설비·설비) 타입. */
26
26
  export interface ResourceTypeDef {
27
27
  key: string;
28
28
  label: string;
@@ -91,8 +91,8 @@ export interface OperationDef {
91
91
  key: string;
92
92
  label: string;
93
93
  intent: OperationIntent;
94
- /** 오퍼레이션이 수행되는 노드 타입(NodeTypeDef.key). 커널이 nodeByType 로 위치 해소. */
95
- nodeType?: string;
94
+ /** 오퍼레이션이 수행되는 자리 타입(LocationTypeDef.key). 커널이 locationByType 로 위치 해소. */
95
+ locationType?: string;
96
96
  /** 요구 자원 종류(ResourceTypeDef.key). transport/process 는 자원 필요, dwell 은 무자원. */
97
97
  resourceType?: string;
98
98
  /** CBV bizStep URN(방출 이벤트 어휘). */
@@ -139,6 +139,51 @@ export interface OperationDef {
139
139
  equipmentClass?: string;
140
140
  quantity: number;
141
141
  }[];
142
+ /**
143
+ * 필요·산출 자재 — **ISA-95 `OperationsSegment.MaterialSpecification`**(`OpMaterialSpecificationType`).
144
+ *
145
+ * ── 4대 자원 중 자재만 빠져 있었다 ────────────────────────────────────────
146
+ * 인원·설비·자산 셋은 공정이 "등급 + 수량" 으로 요구하는데 **자재만 그 자리가 없었다.** 그래서
147
+ * 트레일러 조립 공장을 모델링해도 *"차축 하나에 바퀴 둘"* 을 말할 방법이 없고, 부품이 없어서
148
+ * 라인이 서는 상황이 **예측에 아예 나타나지 않는다** — 자재는 현장에서 사람만큼 자주 부족하다.
149
+ *
150
+ * ── BOM 은 어디 있나 (1차 출처 확인) ─────────────────────────────────────
151
+ * `MaterialDefinition.AssemblyDefinition` 은 **재귀 구조**(무엇이 무엇으로 이루어지나)일 뿐
152
+ * **수량이 없다.** 수량은 공정 쪽 `MaterialSpecification` 이 든다(`Quantity` + `MaterialUse`).
153
+ * 즉 표준에서 **BOM 의 "몇 개" 는 공정의 사실**이다 — 같은 부품이라도 공정마다 소요가 다르다.
154
+ *
155
+ * `use` 는 표준 `MaterialUse` 를 소문자로 쓴다(우리 어휘 규약). 지금 커널이 소비하는 것은
156
+ * `consumed`(작업이 시작되려면 있어야 하고 시작 시 빠진다)뿐이고, 나머지는 **선언만 받아 둔다** —
157
+ * 자리가 없으면 사실이 들어오지 못한다.
158
+ */
159
+ materialSpecification?: OpMaterialSpecification[];
160
+ }
161
+ /**
162
+ * 공정 하나의 자재 명세 — **ISA-95 `OpMaterialSpecificationType`** 의 우리 부분집합.
163
+ *
164
+ * 1차 출처: `ID` · `MaterialClassID*` · `MaterialDefinitionID*` · `MaterialLotID*` · `MaterialSubLotID*` ·
165
+ * `Description*` · **`MaterialUse`** · `HierarchyScope` · `StorageLocation` · `SpatialDefinition` ·
166
+ * **`Quantity*`** · `AssemblySpecification*`(재귀) · `AssemblyType` · `AssemblyRelationship` ·
167
+ * `MaterialSpecificationProperty*` · `TestSpecificationID*`.
168
+ *
169
+ * 우리는 **요구를 표현하는 데 필요한 것**만 든다(인원·자산 명세와 같은 모양). 로트·하위로트 지목,
170
+ * 재귀 조립 명세, 저장 위치 한정은 아직 없다 — 필요해질 때 표준 이름 그대로 얹는다.
171
+ */
172
+ export interface OpMaterialSpecification {
173
+ /** 표준 `ID` — 명세를 가리키는 이름(선택). */
174
+ id?: string;
175
+ /** 품목 등급으로 요구 — 표준 `MaterialClassID`. */
176
+ materialClass?: string;
177
+ /** 특정 품목으로 요구 — 표준 `MaterialDefinitionID`. 등급과 함께 주면 품목이 좁은 쪽이다. */
178
+ materialDefinition?: string;
179
+ /**
180
+ * 이 공정에서의 쓰임 — 표준 `MaterialUse`.
181
+ * `consumed` 없어지며 들어간다 · `produced` 만들어져 나온다 · `consumable` 쓰이지만 제품에 남지 않는다.
182
+ */
183
+ use: 'consumed' | 'produced' | 'consumable';
184
+ /** 수량 — 표준 `Quantity`. 단위 미지정이면 개수(EPCIS 규약과 같다). */
185
+ quantity: number;
186
+ uom?: string;
142
187
  }
143
188
  /** 라우트(오퍼레이션 시퀀스) — ISA-95 ProcessSegment 연결. */
144
189
  export interface RouteDef {
@@ -174,7 +219,7 @@ export interface DomainDefinition {
174
219
  label: string;
175
220
  vocabulary?: DomainVocabulary;
176
221
  materials?: MaterialDef[];
177
- nodeTypes: NodeTypeDef[];
222
+ locationTypes: LocationTypeDef[];
178
223
  resourceTypes: ResourceTypeDef[];
179
224
  operations?: OperationDef[];
180
225
  routes?: RouteDef[];
@@ -41,24 +41,24 @@ export function validateDomainDefinition(def) {
41
41
  v.push('id 누락');
42
42
  if (typeof def.label !== 'string' || !def.label)
43
43
  v.push('label 누락');
44
- if (!Array.isArray(def.nodeTypes) || def.nodeTypes.length === 0)
45
- v.push('nodeTypes 비어있음');
44
+ if (!Array.isArray(def.locationTypes) || def.locationTypes.length === 0)
45
+ v.push('locationTypes 비어있음');
46
46
  if (!Array.isArray(def.resourceTypes))
47
47
  v.push('resourceTypes 배열 아님');
48
- const nodeKeys = new Set((def.nodeTypes || []).map(n => n.key));
48
+ const locationKeys = new Set((def.locationTypes || []).map(n => n.key));
49
49
  const resKeys = new Set((def.resourceTypes || []).map(r => r.key));
50
50
  const matKeys = new Set((def.materials || []).map(m => m.key));
51
51
  const opKeys = new Set((def.operations || []).map(o => o.key));
52
52
  const routeKeys = new Set((def.routes || []).map(r => r.key));
53
- for (const [name, arr] of [['nodeTypes', def.nodeTypes], ['resourceTypes', def.resourceTypes], ['materials', def.materials], ['operations', def.operations], ['routes', def.routes], ['recipes', def.recipes]]) {
53
+ for (const [name, arr] of [['locationTypes', def.locationTypes], ['resourceTypes', def.resourceTypes], ['materials', def.materials], ['operations', def.operations], ['routes', def.routes], ['recipes', def.recipes]]) {
54
54
  for (const d of dupes((arr || []).map(x => x.key)))
55
55
  v.push(`${name} 키 중복: ${d}`);
56
56
  }
57
57
  for (const o of def.operations || []) {
58
58
  if (!INTENTS.includes(o.intent))
59
59
  v.push(`operation '${o.key}' intent 부정: ${o.intent}`);
60
- if (o.nodeType && !nodeKeys.has(o.nodeType))
61
- v.push(`operation '${o.key}' nodeType '${o.nodeType}' 미정의`);
60
+ if (o.locationType && !locationKeys.has(o.locationType))
61
+ v.push(`operation '${o.key}' locationType '${o.locationType}' 미정의`);
62
62
  if (o.resourceType && !resKeys.has(o.resourceType))
63
63
  v.push(`operation '${o.key}' resourceType '${o.resourceType}' 미정의`);
64
64
  if (o.intent === 'dwell' && o.resourceType)
package/dist/epcis.d.ts CHANGED
@@ -9,6 +9,18 @@ export declare const DISP: {
9
9
  readonly in_transit: "urn:epcglobal:cbv:disp:in_transit";
10
10
  readonly non_sellable: "urn:epcglobal:cbv:disp:non_sellable_other";
11
11
  };
12
+ /**
13
+ * **자재 소비·산출의 CBV 단계** — 도메인 무관하게 코어가 쓴다.
14
+ *
15
+ * 업종별 단계(입고·피킹·출하…)는 각 프로파일이 갖지만, "자재가 들어갔다/나왔다" 는 셋 다 하는 일이라
16
+ * 코어에 있어야 한다. 프로파일 하나에 두면 다른 업종이 그것을 가져다 쓰면서 방언이 생긴다.
17
+ */
18
+ export declare const CBV_BIZSTEP: {
19
+ /** 공정에 자재가 들어갔다 — ISA-95 `MaterialUse: Consumed`. */
20
+ readonly consuming: "urn:epcglobal:cbv:bizstep:consuming";
21
+ /** 새 물품이 생겨 계보가 시작된다 — ISA-95 `MaterialUse: Produced`. */
22
+ readonly commissioning: "urn:epcglobal:cbv:bizstep:commissioning";
23
+ };
12
24
  export type EpcisEventType = 'ObjectEvent' | 'AggregationEvent' | 'TransactionEvent' | 'TransformationEvent';
13
25
  export type EpcisAction = 'ADD' | 'OBSERVE' | 'DELETE';
14
26
  /**
package/dist/epcis.js CHANGED
@@ -18,6 +18,18 @@ export const DISP = {
18
18
  in_transit: 'urn:epcglobal:cbv:disp:in_transit',
19
19
  non_sellable: 'urn:epcglobal:cbv:disp:non_sellable_other' // 불량/scrap
20
20
  };
21
+ /**
22
+ * **자재 소비·산출의 CBV 단계** — 도메인 무관하게 코어가 쓴다.
23
+ *
24
+ * 업종별 단계(입고·피킹·출하…)는 각 프로파일이 갖지만, "자재가 들어갔다/나왔다" 는 셋 다 하는 일이라
25
+ * 코어에 있어야 한다. 프로파일 하나에 두면 다른 업종이 그것을 가져다 쓰면서 방언이 생긴다.
26
+ */
27
+ export const CBV_BIZSTEP = {
28
+ /** 공정에 자재가 들어갔다 — ISA-95 `MaterialUse: Consumed`. */
29
+ consuming: 'urn:epcglobal:cbv:bizstep:consuming',
30
+ /** 새 물품이 생겨 계보가 시작된다 — ISA-95 `MaterialUse: Produced`. */
31
+ commissioning: 'urn:epcglobal:cbv:bizstep:commissioning'
32
+ };
21
33
  // ── GS1 EPC URI 헬퍼 (표준) ────────────────────────────────────────────────
22
34
  /** SSCC (물류단위: 팔레트/화물/트레일러) — 결정적 카운터 기반. */
23
35
  export function ssccUri(companyPrefix, serial) {
@@ -13,5 +13,35 @@ export declare class EventJournal {
13
13
  /** sim 시각(ISO eventTime) 이하 이벤트. */
14
14
  untilSimTime(iso: string): CanonicalEnvelope[];
15
15
  }
16
- /** 이벤트열 → 상태 재구성(시간여행). board = 마스터(토폴로지·무버). */
16
+ /** 이벤트열 → 상태 재구성(시간여행). board = 마스터(토폴로지·설비). */
17
17
  export declare function replay(board: BoardDef, events: readonly CanonicalEnvelope[]): ProjectedState;
18
+ /** 한 구조 아래에서 일어난 이벤트들 — 재생의 한 마디. */
19
+ export interface StructureSegment {
20
+ board: BoardDef;
21
+ events: readonly CanonicalEnvelope[];
22
+ }
23
+ /** 구조가 바뀐 지점마다 무엇이 사라졌는지. 조용히 넘어가지 않는다. */
24
+ export interface StructureShift {
25
+ index: number;
26
+ locationsAdded: number;
27
+ locationsDropped: number;
28
+ equipmentDropped: number;
29
+ personsDropped: number;
30
+ assetsDropped: number;
31
+ }
32
+ /**
33
+ * **구조가 바뀐 이력까지 이어서 재생한다.**
34
+ *
35
+ * 공장은 바뀐다. 지금까지는 구조가 바뀌면 저널을 지우는 것이 유일한 길이었다 — 안 지우면 옛 이벤트를
36
+ * 새 공장에 대고 접게 되어 이력이 거짓말을 하기 때문이다(도장 부스가 둘이던 시절의 사실을 여섯 개짜리
37
+ * 공장에 접는다). 역사를 잃거나 거짓말을 하거나, 둘뿐이었다.
38
+ *
39
+ * 셋째 길이 이것이다: 마디마다 **그때의 구조**로 접고, 경계에서 구조만 갈아탄다(관측된 사실은 이어
40
+ * 간다). 그러면 이력이 "그때 그 공장의 사실" 로 계속 읽힌다.
41
+ *
42
+ * 경계에서 사라진 자원은 결과에 실어 보낸다 — 수가 줄어든 것을 사용자가 눈치채지 못하면 안 된다.
43
+ */
44
+ export declare function replaySegments(segments: readonly StructureSegment[]): {
45
+ state: ProjectedState;
46
+ shifts: StructureShift[];
47
+ };
@@ -32,10 +32,36 @@ export class EventJournal {
32
32
  return this.events.filter(e => e.eventTime <= iso);
33
33
  }
34
34
  }
35
- /** 이벤트열 → 상태 재구성(시간여행). board = 마스터(토폴로지·무버). */
35
+ /** 이벤트열 → 상태 재구성(시간여행). board = 마스터(토폴로지·설비). */
36
36
  export function replay(board, events) {
37
37
  const proj = new StateProjector(board);
38
38
  for (const e of events)
39
39
  proj.apply(e);
40
40
  return proj.snapshot();
41
41
  }
42
+ /**
43
+ * **구조가 바뀐 이력까지 이어서 재생한다.**
44
+ *
45
+ * 공장은 바뀐다. 지금까지는 구조가 바뀌면 저널을 지우는 것이 유일한 길이었다 — 안 지우면 옛 이벤트를
46
+ * 새 공장에 대고 접게 되어 이력이 거짓말을 하기 때문이다(도장 부스가 둘이던 시절의 사실을 여섯 개짜리
47
+ * 공장에 접는다). 역사를 잃거나 거짓말을 하거나, 둘뿐이었다.
48
+ *
49
+ * 셋째 길이 이것이다: 마디마다 **그때의 구조**로 접고, 경계에서 구조만 갈아탄다(관측된 사실은 이어
50
+ * 간다). 그러면 이력이 "그때 그 공장의 사실" 로 계속 읽힌다.
51
+ *
52
+ * 경계에서 사라진 자원은 결과에 실어 보낸다 — 수가 줄어든 것을 사용자가 눈치채지 못하면 안 된다.
53
+ */
54
+ export function replaySegments(segments) {
55
+ if (!segments.length)
56
+ throw new Error('재생할 마디가 없다 — 구조를 하나도 주지 않았다');
57
+ const proj = new StateProjector(segments[0].board);
58
+ const shifts = [];
59
+ for (let i = 0; i < segments.length; i++) {
60
+ /* 첫 마디는 생성자가 이미 그 구조로 섰다 — 두 번 세우면 관측 전 상태를 다시 덮는다. */
61
+ if (i > 0)
62
+ shifts.push({ index: i, ...proj.adoptStructure(segments[i].board) });
63
+ for (const e of segments[i].events)
64
+ proj.apply(e);
65
+ }
66
+ return { state: proj.snapshot(), shifts };
67
+ }