@operato/ops-contract 0.9.5 → 0.9.6

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.
@@ -1477,6 +1477,28 @@ export type OeeMissing =
1477
1477
  | 'produced-quantity'
1478
1478
  /** 실 생산 시간이 0 이다 — 성능률의 분모가 없다. */
1479
1479
  | 'actual-production-time';
1480
+ /**
1481
+ * 설비의 소속이 어느 단인가 — 자리(작업 셀)인가 구역(라인·구역)인가.
1482
+ *
1483
+ * ISA-95 의 설비 계층에서 설비는 어느 단에도 있을 수 있다. 라인에 딸린 부대 설비(배기·집진·세척·
1484
+ * 출입 설비)가 그 경우다 — 어느 작업 셀에도 안 붙는다.
1485
+ */
1486
+ export type HomeKind = 'location' | 'area';
1487
+ /**
1488
+ * 그 설비의 소속과 단 — **없으면 자리다.**
1489
+ *
1490
+ * 소비처가 저마다 `?? 'location'` 을 적지 않게 한 자리에 둔다. 이 기본값은 짐작이 아니다: 이 칸이
1491
+ * 생기기 전의 모든 값은 자리였고(커널이 그것만 받았다) 필드 이름 자체가 `homeLocation` 이다.
1492
+ *
1493
+ * 소속 자체가 없으면 답하지 않는다 — 단만 있고 대상이 없는 것은 뜻이 없다.
1494
+ */
1495
+ export declare function homeOf(e: {
1496
+ homeLocation?: string;
1497
+ homeLocationKind?: HomeKind;
1498
+ } | undefined): {
1499
+ id: string;
1500
+ kind: HomeKind;
1501
+ } | undefined;
1480
1502
  export interface EquipmentState extends EffectivePeriod {
1481
1503
  id: string;
1482
1504
  kind: string;
@@ -1504,6 +1526,28 @@ export interface EquipmentState extends EffectivePeriod {
1504
1526
  * 그래서 이동 설비가 한 번 움직이면 원래 소속을 아무도 알 수 없었다.
1505
1527
  */
1506
1528
  homeLocation?: string;
1529
+ /**
1530
+ * 그 소속이 **어느 단인가** — 자리(작업 셀)인가 구역(라인·구역)인가.
1531
+ *
1532
+ * ── 왜 열었나 (2026-09-05 실측) ────────────────────────────────────────────
1533
+ * 미라텍 라인에 에어 샤워가 있다. 청정실 출입구이고 **제품에 손대지 않는다** — 등급도
1534
+ * `EC-CAM-FACILITY`(부대 설비)다. 그 라인의 셋은 다 공정 셀인데 에어 샤워는 그 셀들에 들어가기
1535
+ * **전** 출입구라 어느 셀에도 안 붙는다.
1536
+ *
1537
+ * 원본이 그것을 선언한다 — `homeLocationType: production-line`. 그런데 커널이 자리만 받아서 그
1538
+ * 설비 하나가 「이 트윈에 없는 것을 가리킨다」로 남았다.
1539
+ *
1540
+ * ISA-95 로도 맞다: 설비 계층은 Site › Area › ProductionLine › WorkCell 이고 **설비는 어느 단에도
1541
+ * 있을 수 있다.** 라인 배기·집진·세척처럼 라인에 딸린 설비는 흔한 모양이다.
1542
+ *
1543
+ * ── 셀을 지어 붙이지 않는다 ────────────────────────────────────────────────
1544
+ * 「청정실 출입」 셀을 만들어 거기 붙이면 그 라인이 **네 공정으로 보인다.** 실제는 셋이다. 원본이
1545
+ * 그러지 말라고 명시했고, 지어낸 자리는 화면이 실제 배치로 읽는다.
1546
+ *
1547
+ * 이 필드의 주석이 이미 「이 라인의 설비 가동률」이라고 적고 있었다 — 뜻은 처음부터 소속이었고
1548
+ * 구현만 자리를 요구했다.
1549
+ */
1550
+ homeLocationKind?: HomeKind;
1507
1551
  status: string;
1508
1552
  taskId?: string;
1509
1553
  motion?: EquipmentMotion;
package/dist/contract.js CHANGED
@@ -1013,6 +1013,18 @@ export function observationAt(observations, propertyId, at) {
1013
1013
  }
1014
1014
  return best;
1015
1015
  }
1016
+ /**
1017
+ * 그 설비의 소속과 단 — **없으면 자리다.**
1018
+ *
1019
+ * 소비처가 저마다 `?? 'location'` 을 적지 않게 한 자리에 둔다. 이 기본값은 짐작이 아니다: 이 칸이
1020
+ * 생기기 전의 모든 값은 자리였고(커널이 그것만 받았다) 필드 이름 자체가 `homeLocation` 이다.
1021
+ *
1022
+ * 소속 자체가 없으면 답하지 않는다 — 단만 있고 대상이 없는 것은 뜻이 없다.
1023
+ */
1024
+ export function homeOf(e) {
1025
+ const id = e?.homeLocation;
1026
+ return id ? { id, kind: e?.homeLocationKind ?? 'location' } : undefined;
1027
+ }
1016
1028
  // ── 운영 델타(비-EPCIS) — State 채널의 나머지 절반 ──────────────────────────
1017
1029
  // EPCIS 이벤트는 재고/위치만 재구성 가능. tasks·equipment·orders 의 운영 상태는
1018
1030
  // 이 델타로 미러한다. envelope.eventType = 'task.status' | 'equipment.status' | 'order.status'.
@@ -164,21 +164,21 @@ export function tenseOf(r) {
164
164
  /** 관계 전체 — 지도의 선과 항목의 이웃이 여기서 나온다(화면은 이 목록을 갖지 않는다). */
165
165
  export const TWIN_RELATIONS = [
166
166
  /* 자리가 속한 구역 — **board 밖**(호스트의 `TwinArea`). 해소는 호스트가 한다. */
167
- { from: 'locations', field: 'parentId', target: { kind: 'external', entity: 'space.area' }, via: 'twin.rel.area', optional: true },
167
+ { from: 'locations', field: 'parentId', target: { kind: 'external', entity: 'space.area' }, via: 'ops.rel.area', optional: true },
168
168
  /* 자원이 사는 자리 */
169
- { from: 'equipment', field: 'homeLocation', target: { kind: 'axis', axis: 'locations' }, via: 'twin.rel.home', optional: true },
170
- { from: 'persons', field: 'homeLocation', target: { kind: 'axis', axis: 'locations' }, via: 'twin.rel.home', optional: true },
171
- { from: 'assets', field: 'homeLocation', target: { kind: 'axis', axis: 'locations' }, via: 'twin.rel.home', optional: true },
169
+ { from: 'equipment', field: 'homeLocation', target: { kind: 'axis', axis: 'locations' }, via: 'ops.rel.home', optional: true },
170
+ { from: 'persons', field: 'homeLocation', target: { kind: 'axis', axis: 'locations' }, via: 'ops.rel.home', optional: true },
171
+ { from: 'assets', field: 'homeLocation', target: { kind: 'axis', axis: 'locations' }, via: 'ops.rel.home', optional: true },
172
172
  /* 자원의 등급 — 설비만 `kind`(타입)로 가고 사람·자산은 등급 목록으로 간다. 표준의 비대칭 그대로. */
173
- { from: 'persons', field: 'personnelClassIds[]', target: { kind: 'axis', axis: 'personnelClasses' }, via: 'twin.rel.class', optional: true },
174
- { from: 'assets', field: 'assetClassIds[]', target: { kind: 'axis', axis: 'assetClasses' }, via: 'twin.rel.class', optional: true },
175
- { from: 'equipment', field: 'kind', target: { kind: 'type', role: 'equipment' }, via: 'twin.rel.type' },
176
- { from: 'locations', field: 'type', target: { kind: 'type', role: 'location' }, via: 'twin.rel.type' },
173
+ { from: 'persons', field: 'personnelClassIds[]', target: { kind: 'axis', axis: 'personnelClasses' }, via: 'ops.rel.class', optional: true },
174
+ { from: 'assets', field: 'assetClassIds[]', target: { kind: 'axis', axis: 'assetClasses' }, via: 'ops.rel.class', optional: true },
175
+ { from: 'equipment', field: 'kind', target: { kind: 'type', role: 'equipment' }, via: 'ops.rel.type' },
176
+ { from: 'locations', field: 'type', target: { kind: 'type', role: 'location' }, via: 'ops.rel.type' },
177
177
  /* 생산 — 레시피에서 시작해 라우트·공정·품목으로 퍼진다 */
178
- { from: 'recipes', field: 'route', target: { kind: 'axis', axis: 'routes' }, via: 'twin.rel.route', optional: true },
179
- { from: 'recipes', field: 'inputs[].material', target: { kind: 'axis', axis: 'materials' }, via: 'twin.rel.input' },
180
- { from: 'recipes', field: 'outputs[].material', target: { kind: 'axis', axis: 'materials' }, via: 'twin.rel.output' },
181
- { from: 'routes', field: 'steps[]', target: { kind: 'axis', axis: 'operations' }, via: 'twin.rel.step' },
178
+ { from: 'recipes', field: 'route', target: { kind: 'axis', axis: 'routes' }, via: 'ops.rel.route', optional: true },
179
+ { from: 'recipes', field: 'inputs[].material', target: { kind: 'axis', axis: 'materials' }, via: 'ops.rel.input' },
180
+ { from: 'recipes', field: 'outputs[].material', target: { kind: 'axis', axis: 'materials' }, via: 'ops.rel.output' },
181
+ { from: 'routes', field: 'steps[]', target: { kind: 'axis', axis: 'operations' }, via: 'ops.rel.step' },
182
182
  /*
183
183
  * 일정·실적의 관계 — **관측 관계다.** 선언 관계는 "그렇게 만들기로 했다" 이고, 이것은 "그렇게
184
184
  * 일어났다" 다. 관계가 없으면 이 두 축은 지도에서 연결되지 않은 카드가 된다(그러면 걸어 들어갈 수 없다).
@@ -188,14 +188,14 @@ export const TWIN_RELATIONS = [
188
188
  * 것은 `productionSpec.binding` 이고 그 해소는 호스트가 한다 — 자리·구역과 같은 `external` 이다.
189
189
  * 축을 직접 가리키게 적으면 없는 필드를 가리키는 선언이 된다(계약을 보고 고쳤다).
190
190
  */
191
- { from: 'orders', field: 'lines[].gtin', target: { kind: 'external', entity: 'gs1.itemRef' }, via: 'twin.rel.item', optional: true },
192
- { from: 'tasks', field: 'orderId', target: { kind: 'axis', axis: 'orders' }, via: 'twin.rel.order', optional: true },
191
+ { from: 'orders', field: 'lines[].gtin', target: { kind: 'external', entity: 'gs1.itemRef' }, via: 'ops.rel.item', optional: true },
192
+ { from: 'tasks', field: 'orderId', target: { kind: 'axis', axis: 'orders' }, via: 'ops.rel.order', optional: true },
193
193
  /* 작업의 `kind` 는 그 공정의 키다(라우트의 단계 이름 그대로 — `mes-kernel` 의 공정 목록이 그 증거). */
194
- { from: 'tasks', field: 'kind', target: { kind: 'axis', axis: 'operations' }, via: 'twin.rel.operation', optional: true },
195
- { from: 'tasks', field: 'fromNode', target: { kind: 'axis', axis: 'locations' }, via: 'twin.rel.from', optional: true },
196
- { from: 'tasks', field: 'toNode', target: { kind: 'axis', axis: 'locations' }, via: 'twin.rel.at', optional: true },
197
- { from: 'tasks', field: 'resourceRef', target: { kind: 'axis', axis: 'equipment' }, via: 'twin.rel.by', optional: true },
198
- { from: 'tasks', field: 'personnel[]', target: { kind: 'axis', axis: 'persons' }, via: 'twin.rel.crew', optional: true },
194
+ { from: 'tasks', field: 'kind', target: { kind: 'axis', axis: 'operations' }, via: 'ops.rel.operation', optional: true },
195
+ { from: 'tasks', field: 'fromNode', target: { kind: 'axis', axis: 'locations' }, via: 'ops.rel.from', optional: true },
196
+ { from: 'tasks', field: 'toNode', target: { kind: 'axis', axis: 'locations' }, via: 'ops.rel.at', optional: true },
197
+ { from: 'tasks', field: 'resourceRef', target: { kind: 'axis', axis: 'equipment' }, via: 'ops.rel.by', optional: true },
198
+ { from: 'tasks', field: 'personnel[]', target: { kind: 'axis', axis: 'persons' }, via: 'ops.rel.crew', optional: true },
199
199
  /*
200
200
  * 물품의 관계 (2026-08-22) — 없으면 축이 **걸어 들어갈 수 없는 목록**이 된다.
201
201
  *
@@ -205,15 +205,15 @@ export const TWIN_RELATIONS = [
205
205
  * `parent` 는 **물품 축을 자기 자신으로** 가리킨다(팔레트에 담긴 상자 — EPCIS `AggregationEvent`).
206
206
  * `carriedBy` 는 다른 축이다 — 반복사용 자산(GRAI)이 물류단위를 실어 나른다(§`FlowItem.carriedBy`).
207
207
  *
208
- * 관계 이름은 **소문자 한 낱말**이다(`twin.rel.<name>`) — 기존 열여덟 개가 그 규율이고 시험이 지킨다.
208
+ * 관계 이름은 **소문자 한 낱말**이다(`ops.rel.<name>`) — 기존 열여덟 개가 그 규율이고 시험이 지킨다.
209
209
  *
210
210
  * 품목(`gtin`)은 오더와 **같은 규율**이다: 자재 키가 아니라 GS1 품목 참조이므로 축을 직접 가리키지
211
211
  * 않고 `external` 로 둔다. 축을 가리키게 적으면 없는 필드를 가리키는 선언이 된다.
212
212
  */
213
- { from: 'items', field: 'location', target: { kind: 'axis', axis: 'locations' }, via: 'twin.rel.at' },
214
- { from: 'items', field: 'parent', target: { kind: 'axis', axis: 'items' }, via: 'twin.rel.parent', optional: true },
215
- { from: 'items', field: 'carriedBy', target: { kind: 'axis', axis: 'assets' }, via: 'twin.rel.asset', optional: true },
216
- { from: 'items', field: 'gtin', target: { kind: 'external', entity: 'gs1.itemRef' }, via: 'twin.rel.item', optional: true },
213
+ { from: 'items', field: 'location', target: { kind: 'axis', axis: 'locations' }, via: 'ops.rel.at' },
214
+ { from: 'items', field: 'parent', target: { kind: 'axis', axis: 'items' }, via: 'ops.rel.parent', optional: true },
215
+ { from: 'items', field: 'carriedBy', target: { kind: 'axis', axis: 'assets' }, via: 'ops.rel.asset', optional: true },
216
+ { from: 'items', field: 'gtin', target: { kind: 'external', entity: 'gs1.itemRef' }, via: 'ops.rel.item', optional: true },
217
217
  /*
218
218
  * 자격을 검증한 시험 — **여덟 갈래.** 자원(개체)과 등급 양쪽이 가리킨다: 표준이 그 둘 모두에 이
219
219
  * 참조를 두었기 때문이다(개체는 "이 사람이 통과했다", 등급은 "이 자격은 이 시험을 요구한다").
@@ -221,17 +221,17 @@ export const TWIN_RELATIONS = [
221
221
  * **자리(`locations`)에는 없다** — 표준이 자리를 시험 대상으로 두지 않았고 그 비대칭은 표준의
222
222
  * 판단이다(`contract.ts` 가 "우리도 자리에 넣지 않는다" 고 적었다).
223
223
  */
224
- { from: 'persons', field: 'testSpecificationIds[]', target: { kind: 'axis', axis: 'testSpecifications' }, via: 'twin.rel.test', optional: true },
225
- { from: 'personnelClasses', field: 'testSpecificationIds[]', target: { kind: 'axis', axis: 'testSpecifications' }, via: 'twin.rel.test', optional: true },
226
- { from: 'equipment', field: 'testSpecificationIds[]', target: { kind: 'axis', axis: 'testSpecifications' }, via: 'twin.rel.test', optional: true },
227
- { from: 'equipmentClasses', field: 'testSpecificationIds[]', target: { kind: 'axis', axis: 'testSpecifications' }, via: 'twin.rel.test', optional: true },
228
- { from: 'assets', field: 'testSpecificationIds[]', target: { kind: 'axis', axis: 'testSpecifications' }, via: 'twin.rel.test', optional: true },
229
- { from: 'assetClasses', field: 'testSpecificationIds[]', target: { kind: 'axis', axis: 'testSpecifications' }, via: 'twin.rel.test', optional: true },
230
- { from: 'materialDefinitions', field: 'testSpecificationIds[]', target: { kind: 'axis', axis: 'testSpecifications' }, via: 'twin.rel.test', optional: true },
231
- { from: 'materialClasses', field: 'testSpecificationIds[]', target: { kind: 'axis', axis: 'testSpecifications' }, via: 'twin.rel.test', optional: true },
224
+ { from: 'persons', field: 'testSpecificationIds[]', target: { kind: 'axis', axis: 'testSpecifications' }, via: 'ops.rel.test', optional: true },
225
+ { from: 'personnelClasses', field: 'testSpecificationIds[]', target: { kind: 'axis', axis: 'testSpecifications' }, via: 'ops.rel.test', optional: true },
226
+ { from: 'equipment', field: 'testSpecificationIds[]', target: { kind: 'axis', axis: 'testSpecifications' }, via: 'ops.rel.test', optional: true },
227
+ { from: 'equipmentClasses', field: 'testSpecificationIds[]', target: { kind: 'axis', axis: 'testSpecifications' }, via: 'ops.rel.test', optional: true },
228
+ { from: 'assets', field: 'testSpecificationIds[]', target: { kind: 'axis', axis: 'testSpecifications' }, via: 'ops.rel.test', optional: true },
229
+ { from: 'assetClasses', field: 'testSpecificationIds[]', target: { kind: 'axis', axis: 'testSpecifications' }, via: 'ops.rel.test', optional: true },
230
+ { from: 'materialDefinitions', field: 'testSpecificationIds[]', target: { kind: 'axis', axis: 'testSpecifications' }, via: 'ops.rel.test', optional: true },
231
+ { from: 'materialClasses', field: 'testSpecificationIds[]', target: { kind: 'axis', axis: 'testSpecifications' }, via: 'ops.rel.test', optional: true },
232
232
  /* 공정이 도는 자리·자원 — 항목이 아니라 **타입**을 가리킨다 */
233
- { from: 'operations', field: 'locationType', target: { kind: 'type', role: 'location' }, via: 'twin.rel.at', optional: true },
234
- { from: 'operations', field: 'resourceType', target: { kind: 'type', role: 'equipment' }, via: 'twin.rel.by', optional: true }
233
+ { from: 'operations', field: 'locationType', target: { kind: 'type', role: 'location' }, via: 'ops.rel.at', optional: true },
234
+ { from: 'operations', field: 'resourceType', target: { kind: 'type', role: 'equipment' }, via: 'ops.rel.by', optional: true }
235
235
  ];
236
236
  /**
237
237
  * 오늘 **실제로 짝이 서는 것**만 선언한다. 그럴듯한 속성을 미리 적어 두면 화면이 빈칸을 줄줄이
@@ -120,6 +120,7 @@ __export(index_exports, {
120
120
  graiUri: () => graiUri,
121
121
  gs1KeyDigitViolation: () => gs1KeyDigitViolation,
122
122
  hierarchyOf: () => hierarchyOf,
123
+ homeOf: () => homeOf,
123
124
  identityGroundingOf: () => identityGroundingOf,
124
125
  inWorkCalendar: () => inWorkCalendar,
125
126
  inWorkCalendarAt: () => inWorkCalendarAt,
@@ -829,6 +830,10 @@ function observationAt(observations, propertyId, at2) {
829
830
  }
830
831
  return best;
831
832
  }
833
+ function homeOf(e) {
834
+ const id = e?.homeLocation;
835
+ return id ? { id, kind: e?.homeLocationKind ?? "location" } : void 0;
836
+ }
832
837
  var OP_EVENT = {
833
838
  task: "task.status",
834
839
  equipment: "equipment.status",
@@ -1873,21 +1878,21 @@ function tenseOf(r) {
1873
1878
  }
1874
1879
  var TWIN_RELATIONS = [
1875
1880
  /* 자리가 속한 구역 — **board 밖**(호스트의 `TwinArea`). 해소는 호스트가 한다. */
1876
- { from: "locations", field: "parentId", target: { kind: "external", entity: "space.area" }, via: "twin.rel.area", optional: true },
1881
+ { from: "locations", field: "parentId", target: { kind: "external", entity: "space.area" }, via: "ops.rel.area", optional: true },
1877
1882
  /* 자원이 사는 자리 */
1878
- { from: "equipment", field: "homeLocation", target: { kind: "axis", axis: "locations" }, via: "twin.rel.home", optional: true },
1879
- { from: "persons", field: "homeLocation", target: { kind: "axis", axis: "locations" }, via: "twin.rel.home", optional: true },
1880
- { from: "assets", field: "homeLocation", target: { kind: "axis", axis: "locations" }, via: "twin.rel.home", optional: true },
1883
+ { from: "equipment", field: "homeLocation", target: { kind: "axis", axis: "locations" }, via: "ops.rel.home", optional: true },
1884
+ { from: "persons", field: "homeLocation", target: { kind: "axis", axis: "locations" }, via: "ops.rel.home", optional: true },
1885
+ { from: "assets", field: "homeLocation", target: { kind: "axis", axis: "locations" }, via: "ops.rel.home", optional: true },
1881
1886
  /* 자원의 등급 — 설비만 `kind`(타입)로 가고 사람·자산은 등급 목록으로 간다. 표준의 비대칭 그대로. */
1882
- { from: "persons", field: "personnelClassIds[]", target: { kind: "axis", axis: "personnelClasses" }, via: "twin.rel.class", optional: true },
1883
- { from: "assets", field: "assetClassIds[]", target: { kind: "axis", axis: "assetClasses" }, via: "twin.rel.class", optional: true },
1884
- { from: "equipment", field: "kind", target: { kind: "type", role: "equipment" }, via: "twin.rel.type" },
1885
- { from: "locations", field: "type", target: { kind: "type", role: "location" }, via: "twin.rel.type" },
1887
+ { from: "persons", field: "personnelClassIds[]", target: { kind: "axis", axis: "personnelClasses" }, via: "ops.rel.class", optional: true },
1888
+ { from: "assets", field: "assetClassIds[]", target: { kind: "axis", axis: "assetClasses" }, via: "ops.rel.class", optional: true },
1889
+ { from: "equipment", field: "kind", target: { kind: "type", role: "equipment" }, via: "ops.rel.type" },
1890
+ { from: "locations", field: "type", target: { kind: "type", role: "location" }, via: "ops.rel.type" },
1886
1891
  /* 생산 — 레시피에서 시작해 라우트·공정·품목으로 퍼진다 */
1887
- { from: "recipes", field: "route", target: { kind: "axis", axis: "routes" }, via: "twin.rel.route", optional: true },
1888
- { from: "recipes", field: "inputs[].material", target: { kind: "axis", axis: "materials" }, via: "twin.rel.input" },
1889
- { from: "recipes", field: "outputs[].material", target: { kind: "axis", axis: "materials" }, via: "twin.rel.output" },
1890
- { from: "routes", field: "steps[]", target: { kind: "axis", axis: "operations" }, via: "twin.rel.step" },
1892
+ { from: "recipes", field: "route", target: { kind: "axis", axis: "routes" }, via: "ops.rel.route", optional: true },
1893
+ { from: "recipes", field: "inputs[].material", target: { kind: "axis", axis: "materials" }, via: "ops.rel.input" },
1894
+ { from: "recipes", field: "outputs[].material", target: { kind: "axis", axis: "materials" }, via: "ops.rel.output" },
1895
+ { from: "routes", field: "steps[]", target: { kind: "axis", axis: "operations" }, via: "ops.rel.step" },
1891
1896
  /*
1892
1897
  * 일정·실적의 관계 — **관측 관계다.** 선언 관계는 "그렇게 만들기로 했다" 이고, 이것은 "그렇게
1893
1898
  * 일어났다" 다. 관계가 없으면 이 두 축은 지도에서 연결되지 않은 카드가 된다(그러면 걸어 들어갈 수 없다).
@@ -1897,14 +1902,14 @@ var TWIN_RELATIONS = [
1897
1902
  * 것은 `productionSpec.binding` 이고 그 해소는 호스트가 한다 — 자리·구역과 같은 `external` 이다.
1898
1903
  * 축을 직접 가리키게 적으면 없는 필드를 가리키는 선언이 된다(계약을 보고 고쳤다).
1899
1904
  */
1900
- { from: "orders", field: "lines[].gtin", target: { kind: "external", entity: "gs1.itemRef" }, via: "twin.rel.item", optional: true },
1901
- { from: "tasks", field: "orderId", target: { kind: "axis", axis: "orders" }, via: "twin.rel.order", optional: true },
1905
+ { from: "orders", field: "lines[].gtin", target: { kind: "external", entity: "gs1.itemRef" }, via: "ops.rel.item", optional: true },
1906
+ { from: "tasks", field: "orderId", target: { kind: "axis", axis: "orders" }, via: "ops.rel.order", optional: true },
1902
1907
  /* 작업의 `kind` 는 그 공정의 키다(라우트의 단계 이름 그대로 — `mes-kernel` 의 공정 목록이 그 증거). */
1903
- { from: "tasks", field: "kind", target: { kind: "axis", axis: "operations" }, via: "twin.rel.operation", optional: true },
1904
- { from: "tasks", field: "fromNode", target: { kind: "axis", axis: "locations" }, via: "twin.rel.from", optional: true },
1905
- { from: "tasks", field: "toNode", target: { kind: "axis", axis: "locations" }, via: "twin.rel.at", optional: true },
1906
- { from: "tasks", field: "resourceRef", target: { kind: "axis", axis: "equipment" }, via: "twin.rel.by", optional: true },
1907
- { from: "tasks", field: "personnel[]", target: { kind: "axis", axis: "persons" }, via: "twin.rel.crew", optional: true },
1908
+ { from: "tasks", field: "kind", target: { kind: "axis", axis: "operations" }, via: "ops.rel.operation", optional: true },
1909
+ { from: "tasks", field: "fromNode", target: { kind: "axis", axis: "locations" }, via: "ops.rel.from", optional: true },
1910
+ { from: "tasks", field: "toNode", target: { kind: "axis", axis: "locations" }, via: "ops.rel.at", optional: true },
1911
+ { from: "tasks", field: "resourceRef", target: { kind: "axis", axis: "equipment" }, via: "ops.rel.by", optional: true },
1912
+ { from: "tasks", field: "personnel[]", target: { kind: "axis", axis: "persons" }, via: "ops.rel.crew", optional: true },
1908
1913
  /*
1909
1914
  * 물품의 관계 (2026-08-22) — 없으면 축이 **걸어 들어갈 수 없는 목록**이 된다.
1910
1915
  *
@@ -1914,15 +1919,15 @@ var TWIN_RELATIONS = [
1914
1919
  * `parent` 는 **물품 축을 자기 자신으로** 가리킨다(팔레트에 담긴 상자 — EPCIS `AggregationEvent`).
1915
1920
  * `carriedBy` 는 다른 축이다 — 반복사용 자산(GRAI)이 물류단위를 실어 나른다(§`FlowItem.carriedBy`).
1916
1921
  *
1917
- * 관계 이름은 **소문자 한 낱말**이다(`twin.rel.<name>`) — 기존 열여덟 개가 그 규율이고 시험이 지킨다.
1922
+ * 관계 이름은 **소문자 한 낱말**이다(`ops.rel.<name>`) — 기존 열여덟 개가 그 규율이고 시험이 지킨다.
1918
1923
  *
1919
1924
  * 품목(`gtin`)은 오더와 **같은 규율**이다: 자재 키가 아니라 GS1 품목 참조이므로 축을 직접 가리키지
1920
1925
  * 않고 `external` 로 둔다. 축을 가리키게 적으면 없는 필드를 가리키는 선언이 된다.
1921
1926
  */
1922
- { from: "items", field: "location", target: { kind: "axis", axis: "locations" }, via: "twin.rel.at" },
1923
- { from: "items", field: "parent", target: { kind: "axis", axis: "items" }, via: "twin.rel.parent", optional: true },
1924
- { from: "items", field: "carriedBy", target: { kind: "axis", axis: "assets" }, via: "twin.rel.asset", optional: true },
1925
- { from: "items", field: "gtin", target: { kind: "external", entity: "gs1.itemRef" }, via: "twin.rel.item", optional: true },
1927
+ { from: "items", field: "location", target: { kind: "axis", axis: "locations" }, via: "ops.rel.at" },
1928
+ { from: "items", field: "parent", target: { kind: "axis", axis: "items" }, via: "ops.rel.parent", optional: true },
1929
+ { from: "items", field: "carriedBy", target: { kind: "axis", axis: "assets" }, via: "ops.rel.asset", optional: true },
1930
+ { from: "items", field: "gtin", target: { kind: "external", entity: "gs1.itemRef" }, via: "ops.rel.item", optional: true },
1926
1931
  /*
1927
1932
  * 자격을 검증한 시험 — **여덟 갈래.** 자원(개체)과 등급 양쪽이 가리킨다: 표준이 그 둘 모두에 이
1928
1933
  * 참조를 두었기 때문이다(개체는 "이 사람이 통과했다", 등급은 "이 자격은 이 시험을 요구한다").
@@ -1930,17 +1935,17 @@ var TWIN_RELATIONS = [
1930
1935
  * **자리(`locations`)에는 없다** — 표준이 자리를 시험 대상으로 두지 않았고 그 비대칭은 표준의
1931
1936
  * 판단이다(`contract.ts` 가 "우리도 자리에 넣지 않는다" 고 적었다).
1932
1937
  */
1933
- { from: "persons", field: "testSpecificationIds[]", target: { kind: "axis", axis: "testSpecifications" }, via: "twin.rel.test", optional: true },
1934
- { from: "personnelClasses", field: "testSpecificationIds[]", target: { kind: "axis", axis: "testSpecifications" }, via: "twin.rel.test", optional: true },
1935
- { from: "equipment", field: "testSpecificationIds[]", target: { kind: "axis", axis: "testSpecifications" }, via: "twin.rel.test", optional: true },
1936
- { from: "equipmentClasses", field: "testSpecificationIds[]", target: { kind: "axis", axis: "testSpecifications" }, via: "twin.rel.test", optional: true },
1937
- { from: "assets", field: "testSpecificationIds[]", target: { kind: "axis", axis: "testSpecifications" }, via: "twin.rel.test", optional: true },
1938
- { from: "assetClasses", field: "testSpecificationIds[]", target: { kind: "axis", axis: "testSpecifications" }, via: "twin.rel.test", optional: true },
1939
- { from: "materialDefinitions", field: "testSpecificationIds[]", target: { kind: "axis", axis: "testSpecifications" }, via: "twin.rel.test", optional: true },
1940
- { from: "materialClasses", field: "testSpecificationIds[]", target: { kind: "axis", axis: "testSpecifications" }, via: "twin.rel.test", optional: true },
1938
+ { from: "persons", field: "testSpecificationIds[]", target: { kind: "axis", axis: "testSpecifications" }, via: "ops.rel.test", optional: true },
1939
+ { from: "personnelClasses", field: "testSpecificationIds[]", target: { kind: "axis", axis: "testSpecifications" }, via: "ops.rel.test", optional: true },
1940
+ { from: "equipment", field: "testSpecificationIds[]", target: { kind: "axis", axis: "testSpecifications" }, via: "ops.rel.test", optional: true },
1941
+ { from: "equipmentClasses", field: "testSpecificationIds[]", target: { kind: "axis", axis: "testSpecifications" }, via: "ops.rel.test", optional: true },
1942
+ { from: "assets", field: "testSpecificationIds[]", target: { kind: "axis", axis: "testSpecifications" }, via: "ops.rel.test", optional: true },
1943
+ { from: "assetClasses", field: "testSpecificationIds[]", target: { kind: "axis", axis: "testSpecifications" }, via: "ops.rel.test", optional: true },
1944
+ { from: "materialDefinitions", field: "testSpecificationIds[]", target: { kind: "axis", axis: "testSpecifications" }, via: "ops.rel.test", optional: true },
1945
+ { from: "materialClasses", field: "testSpecificationIds[]", target: { kind: "axis", axis: "testSpecifications" }, via: "ops.rel.test", optional: true },
1941
1946
  /* 공정이 도는 자리·자원 — 항목이 아니라 **타입**을 가리킨다 */
1942
- { from: "operations", field: "locationType", target: { kind: "type", role: "location" }, via: "twin.rel.at", optional: true },
1943
- { from: "operations", field: "resourceType", target: { kind: "type", role: "equipment" }, via: "twin.rel.by", optional: true }
1947
+ { from: "operations", field: "locationType", target: { kind: "type", role: "location" }, via: "ops.rel.at", optional: true },
1948
+ { from: "operations", field: "resourceType", target: { kind: "type", role: "equipment" }, via: "ops.rel.by", optional: true }
1944
1949
  ];
1945
1950
  var TWIN_PROPERTIES = [
1946
1951
  /* 공정 — 선언(ISO 8601 기간)과 관측(저널에서 계산한 분포)이 둘 다 있는 유일한 짝. */
@@ -4410,6 +4415,7 @@ function commandSpecGaps(specs, command) {
4410
4415
  graiUri,
4411
4416
  gs1KeyDigitViolation,
4412
4417
  hierarchyOf,
4418
+ homeOf,
4413
4419
  identityGroundingOf,
4414
4420
  inWorkCalendar,
4415
4421
  inWorkCalendarAt,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@operato/ops-contract",
3
- "version": "0.9.5",
3
+ "version": "0.9.6",
4
4
  "description": "Operations domain contract — the standard vocabulary that producers and readers agree on (EPCIS 2.0/GS1, ISA-95, IEC 61850/ISO 50001). Types, guards, validation. No state, no engine.",
5
5
  "type": "module",
6
6
  "main": "./dist-cjs/index.cjs",