@operato/ops-contract 0.9.5 → 0.9.7

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, tense: 'standing' },
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, tense: 'standing' },
170
+ { from: 'persons', field: 'homeLocation', target: { kind: 'axis', axis: 'locations' }, via: 'ops.rel.home', optional: true, tense: 'standing' },
171
+ { from: 'assets', field: 'homeLocation', target: { kind: 'axis', axis: 'locations' }, via: 'ops.rel.home', optional: true, tense: 'standing' },
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, tense: 'standing' },
174
+ { from: 'assets', field: 'assetClassIds[]', target: { kind: 'axis', axis: 'assetClasses' }, via: 'ops.rel.class', optional: true, tense: 'standing' },
175
+ { from: 'equipment', field: 'kind', target: { kind: 'type', role: 'equipment' }, via: 'ops.rel.type', tense: 'standing' },
176
+ { from: 'locations', field: 'type', target: { kind: 'type', role: 'location' }, via: 'ops.rel.type', tense: 'standing' },
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, tense: 'standing' },
179
+ { from: 'recipes', field: 'inputs[].material', target: { kind: 'axis', axis: 'materials' }, via: 'ops.rel.input', tense: 'standing' },
180
+ { from: 'recipes', field: 'outputs[].material', target: { kind: 'axis', axis: 'materials' }, via: 'ops.rel.output', tense: 'standing' },
181
+ { from: 'routes', field: 'steps[]', target: { kind: 'axis', axis: 'operations' }, via: 'ops.rel.step', tense: 'standing' },
182
182
  /*
183
183
  * 일정·실적의 관계 — **관측 관계다.** 선언 관계는 "그렇게 만들기로 했다" 이고, 이것은 "그렇게
184
184
  * 일어났다" 다. 관계가 없으면 이 두 축은 지도에서 연결되지 않은 카드가 된다(그러면 걸어 들어갈 수 없다).
@@ -188,14 +188,24 @@ 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, tense: 'standing' },
192
+ /*
193
+ * ── 작업의 관계는 `tense` 를 **선언하지 않는다** ────────────────────────────
194
+ * 여섯 줄 모두 비어 있는 것이 판단이다. 작업의 시간축은 **그 줄의 상태**가 정한다 — 끝난 작업이
195
+ * 이 설비를 가리키면 지난 일이고, 아직인 작업이 가리키면 앞의 일이다. 한 선언이 둘 다이다.
196
+ *
197
+ * 여기에 `standing` 을 적으면 화면은 「이 설비에 걸린 작업 열」을 시간이 없는 사실로 묶고, **세워도
198
+ * 되나의 답이 정확히 뒤집힌다.** 채우고 싶은 힘이 있는 자리이므로 적어 둔다: 비어 있음이 답이다.
199
+ *
200
+ * 줄마다 다른 것은 선언이 아니라 **읽는 쪽이 상태를 보고 판정한다**(`TaskState.status`).
201
+ */
202
+ { from: 'tasks', field: 'orderId', target: { kind: 'axis', axis: 'orders' }, via: 'ops.rel.order', optional: true },
193
203
  /* 작업의 `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 },
204
+ { from: 'tasks', field: 'kind', target: { kind: 'axis', axis: 'operations' }, via: 'ops.rel.operation', optional: true },
205
+ { from: 'tasks', field: 'fromNode', target: { kind: 'axis', axis: 'locations' }, via: 'ops.rel.from', optional: true },
206
+ { from: 'tasks', field: 'toNode', target: { kind: 'axis', axis: 'locations' }, via: 'ops.rel.at', optional: true },
207
+ { from: 'tasks', field: 'resourceRef', target: { kind: 'axis', axis: 'equipment' }, via: 'ops.rel.by', optional: true },
208
+ { from: 'tasks', field: 'personnel[]', target: { kind: 'axis', axis: 'persons' }, via: 'ops.rel.crew', optional: true },
199
209
  /*
200
210
  * 물품의 관계 (2026-08-22) — 없으면 축이 **걸어 들어갈 수 없는 목록**이 된다.
201
211
  *
@@ -205,15 +215,25 @@ export const TWIN_RELATIONS = [
205
215
  * `parent` 는 **물품 축을 자기 자신으로** 가리킨다(팔레트에 담긴 상자 — EPCIS `AggregationEvent`).
206
216
  * `carriedBy` 는 다른 축이다 — 반복사용 자산(GRAI)이 물류단위를 실어 나른다(§`FlowItem.carriedBy`).
207
217
  *
208
- * 관계 이름은 **소문자 한 낱말**이다(`twin.rel.<name>`) — 기존 열여덟 개가 그 규율이고 시험이 지킨다.
218
+ * 관계 이름은 **소문자 한 낱말**이다(`ops.rel.<name>`) — 기존 열여덟 개가 그 규율이고 시험이 지킨다.
209
219
  *
210
220
  * 품목(`gtin`)은 오더와 **같은 규율**이다: 자재 키가 아니라 GS1 품목 참조이므로 축을 직접 가리키지
211
221
  * 않고 `external` 로 둔다. 축을 가리키게 적으면 없는 필드를 가리키는 선언이 된다.
212
222
  */
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 },
223
+ /*
224
+ * ── 물품의 지금 자리는 낱말에 **안 들어간다** ───────────────────────────
225
+ * `location`·`parent`·`carriedBy` 지난 일도 앞의 일도 아니고, `standing`(시간이 없는 사실)도
226
+ * 아니다 **지금 그렇고 바뀐다.** 붙박인 자리(`homeLocation`)와 정반대 성질이고, 줄이 같은
227
+ * 낱말을 받으면 화면이 「이 상자는 A 에 있다」를 등급처럼 담아 둔다(캐시해도 되는 사실로 읽는다).
228
+ *
229
+ * 그래서 비워 둔다. **낱말이 하나 없다** — `current`(지금의 사실, 다시 물어야 한다)가 그 자리다.
230
+ * 열거를 늘리는 것은 소비처가 실제로 그 구별에 걸릴 때 하고, 그때까지 미선언으로 남긴다
231
+ * (짐작으로 채우면 그 뒤로는 아무도 이 물음을 다시 안 한다).
232
+ */
233
+ { from: 'items', field: 'location', target: { kind: 'axis', axis: 'locations' }, via: 'ops.rel.at' },
234
+ { from: 'items', field: 'parent', target: { kind: 'axis', axis: 'items' }, via: 'ops.rel.parent', optional: true },
235
+ { from: 'items', field: 'carriedBy', target: { kind: 'axis', axis: 'assets' }, via: 'ops.rel.asset', optional: true },
236
+ { from: 'items', field: 'gtin', target: { kind: 'external', entity: 'gs1.itemRef' }, via: 'ops.rel.item', optional: true, tense: 'standing' },
217
237
  /*
218
238
  * 자격을 검증한 시험 — **여덟 갈래.** 자원(개체)과 등급 양쪽이 가리킨다: 표준이 그 둘 모두에 이
219
239
  * 참조를 두었기 때문이다(개체는 "이 사람이 통과했다", 등급은 "이 자격은 이 시험을 요구한다").
@@ -221,17 +241,17 @@ export const TWIN_RELATIONS = [
221
241
  * **자리(`locations`)에는 없다** — 표준이 자리를 시험 대상으로 두지 않았고 그 비대칭은 표준의
222
242
  * 판단이다(`contract.ts` 가 "우리도 자리에 넣지 않는다" 고 적었다).
223
243
  */
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 },
244
+ { from: 'persons', field: 'testSpecificationIds[]', target: { kind: 'axis', axis: 'testSpecifications' }, via: 'ops.rel.test', optional: true, tense: 'standing' },
245
+ { from: 'personnelClasses', field: 'testSpecificationIds[]', target: { kind: 'axis', axis: 'testSpecifications' }, via: 'ops.rel.test', optional: true, tense: 'standing' },
246
+ { from: 'equipment', field: 'testSpecificationIds[]', target: { kind: 'axis', axis: 'testSpecifications' }, via: 'ops.rel.test', optional: true, tense: 'standing' },
247
+ { from: 'equipmentClasses', field: 'testSpecificationIds[]', target: { kind: 'axis', axis: 'testSpecifications' }, via: 'ops.rel.test', optional: true, tense: 'standing' },
248
+ { from: 'assets', field: 'testSpecificationIds[]', target: { kind: 'axis', axis: 'testSpecifications' }, via: 'ops.rel.test', optional: true, tense: 'standing' },
249
+ { from: 'assetClasses', field: 'testSpecificationIds[]', target: { kind: 'axis', axis: 'testSpecifications' }, via: 'ops.rel.test', optional: true, tense: 'standing' },
250
+ { from: 'materialDefinitions', field: 'testSpecificationIds[]', target: { kind: 'axis', axis: 'testSpecifications' }, via: 'ops.rel.test', optional: true, tense: 'standing' },
251
+ { from: 'materialClasses', field: 'testSpecificationIds[]', target: { kind: 'axis', axis: 'testSpecifications' }, via: 'ops.rel.test', optional: true, tense: 'standing' },
232
252
  /* 공정이 도는 자리·자원 — 항목이 아니라 **타입**을 가리킨다 */
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 }
253
+ { from: 'operations', field: 'locationType', target: { kind: 'type', role: 'location' }, via: 'ops.rel.at', optional: true, tense: 'standing' },
254
+ { from: 'operations', field: 'resourceType', target: { kind: 'type', role: 'equipment' }, via: 'ops.rel.by', optional: true, tense: 'standing' }
235
255
  ];
236
256
  /**
237
257
  * 오늘 **실제로 짝이 서는 것**만 선언한다. 그럴듯한 속성을 미리 적어 두면 화면이 빈칸을 줄줄이
@@ -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, tense: "standing" },
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, tense: "standing" },
1884
+ { from: "persons", field: "homeLocation", target: { kind: "axis", axis: "locations" }, via: "ops.rel.home", optional: true, tense: "standing" },
1885
+ { from: "assets", field: "homeLocation", target: { kind: "axis", axis: "locations" }, via: "ops.rel.home", optional: true, tense: "standing" },
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, tense: "standing" },
1888
+ { from: "assets", field: "assetClassIds[]", target: { kind: "axis", axis: "assetClasses" }, via: "ops.rel.class", optional: true, tense: "standing" },
1889
+ { from: "equipment", field: "kind", target: { kind: "type", role: "equipment" }, via: "ops.rel.type", tense: "standing" },
1890
+ { from: "locations", field: "type", target: { kind: "type", role: "location" }, via: "ops.rel.type", tense: "standing" },
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, tense: "standing" },
1893
+ { from: "recipes", field: "inputs[].material", target: { kind: "axis", axis: "materials" }, via: "ops.rel.input", tense: "standing" },
1894
+ { from: "recipes", field: "outputs[].material", target: { kind: "axis", axis: "materials" }, via: "ops.rel.output", tense: "standing" },
1895
+ { from: "routes", field: "steps[]", target: { kind: "axis", axis: "operations" }, via: "ops.rel.step", tense: "standing" },
1891
1896
  /*
1892
1897
  * 일정·실적의 관계 — **관측 관계다.** 선언 관계는 "그렇게 만들기로 했다" 이고, 이것은 "그렇게
1893
1898
  * 일어났다" 다. 관계가 없으면 이 두 축은 지도에서 연결되지 않은 카드가 된다(그러면 걸어 들어갈 수 없다).
@@ -1897,14 +1902,24 @@ 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, tense: "standing" },
1906
+ /*
1907
+ * ── 작업의 관계는 `tense` 를 **선언하지 않는다** ────────────────────────────
1908
+ * 여섯 줄 모두 비어 있는 것이 판단이다. 작업의 시간축은 **그 줄의 상태**가 정한다 — 끝난 작업이
1909
+ * 이 설비를 가리키면 지난 일이고, 아직인 작업이 가리키면 앞의 일이다. 한 선언이 둘 다이다.
1910
+ *
1911
+ * 여기에 `standing` 을 적으면 화면은 「이 설비에 걸린 작업 열」을 시간이 없는 사실로 묶고, **세워도
1912
+ * 되나의 답이 정확히 뒤집힌다.** 채우고 싶은 힘이 있는 자리이므로 적어 둔다: 비어 있음이 답이다.
1913
+ *
1914
+ * 줄마다 다른 것은 선언이 아니라 **읽는 쪽이 상태를 보고 판정한다**(`TaskState.status`).
1915
+ */
1916
+ { from: "tasks", field: "orderId", target: { kind: "axis", axis: "orders" }, via: "ops.rel.order", optional: true },
1902
1917
  /* 작업의 `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 },
1918
+ { from: "tasks", field: "kind", target: { kind: "axis", axis: "operations" }, via: "ops.rel.operation", optional: true },
1919
+ { from: "tasks", field: "fromNode", target: { kind: "axis", axis: "locations" }, via: "ops.rel.from", optional: true },
1920
+ { from: "tasks", field: "toNode", target: { kind: "axis", axis: "locations" }, via: "ops.rel.at", optional: true },
1921
+ { from: "tasks", field: "resourceRef", target: { kind: "axis", axis: "equipment" }, via: "ops.rel.by", optional: true },
1922
+ { from: "tasks", field: "personnel[]", target: { kind: "axis", axis: "persons" }, via: "ops.rel.crew", optional: true },
1908
1923
  /*
1909
1924
  * 물품의 관계 (2026-08-22) — 없으면 축이 **걸어 들어갈 수 없는 목록**이 된다.
1910
1925
  *
@@ -1914,15 +1929,25 @@ var TWIN_RELATIONS = [
1914
1929
  * `parent` 는 **물품 축을 자기 자신으로** 가리킨다(팔레트에 담긴 상자 — EPCIS `AggregationEvent`).
1915
1930
  * `carriedBy` 는 다른 축이다 — 반복사용 자산(GRAI)이 물류단위를 실어 나른다(§`FlowItem.carriedBy`).
1916
1931
  *
1917
- * 관계 이름은 **소문자 한 낱말**이다(`twin.rel.<name>`) — 기존 열여덟 개가 그 규율이고 시험이 지킨다.
1932
+ * 관계 이름은 **소문자 한 낱말**이다(`ops.rel.<name>`) — 기존 열여덟 개가 그 규율이고 시험이 지킨다.
1918
1933
  *
1919
1934
  * 품목(`gtin`)은 오더와 **같은 규율**이다: 자재 키가 아니라 GS1 품목 참조이므로 축을 직접 가리키지
1920
1935
  * 않고 `external` 로 둔다. 축을 가리키게 적으면 없는 필드를 가리키는 선언이 된다.
1921
1936
  */
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 },
1937
+ /*
1938
+ * ── 물품의 지금 자리는 낱말에 **안 들어간다** ───────────────────────────
1939
+ * `location`·`parent`·`carriedBy` 지난 일도 앞의 일도 아니고, `standing`(시간이 없는 사실)도
1940
+ * 아니다 **지금 그렇고 바뀐다.** 붙박인 자리(`homeLocation`)와 정반대 성질이고, 줄이 같은
1941
+ * 낱말을 받으면 화면이 「이 상자는 A 에 있다」를 등급처럼 담아 둔다(캐시해도 되는 사실로 읽는다).
1942
+ *
1943
+ * 그래서 비워 둔다. **낱말이 하나 없다** — `current`(지금의 사실, 다시 물어야 한다)가 그 자리다.
1944
+ * 열거를 늘리는 것은 소비처가 실제로 그 구별에 걸릴 때 하고, 그때까지 미선언으로 남긴다
1945
+ * (짐작으로 채우면 그 뒤로는 아무도 이 물음을 다시 안 한다).
1946
+ */
1947
+ { from: "items", field: "location", target: { kind: "axis", axis: "locations" }, via: "ops.rel.at" },
1948
+ { from: "items", field: "parent", target: { kind: "axis", axis: "items" }, via: "ops.rel.parent", optional: true },
1949
+ { from: "items", field: "carriedBy", target: { kind: "axis", axis: "assets" }, via: "ops.rel.asset", optional: true },
1950
+ { from: "items", field: "gtin", target: { kind: "external", entity: "gs1.itemRef" }, via: "ops.rel.item", optional: true, tense: "standing" },
1926
1951
  /*
1927
1952
  * 자격을 검증한 시험 — **여덟 갈래.** 자원(개체)과 등급 양쪽이 가리킨다: 표준이 그 둘 모두에 이
1928
1953
  * 참조를 두었기 때문이다(개체는 "이 사람이 통과했다", 등급은 "이 자격은 이 시험을 요구한다").
@@ -1930,17 +1955,17 @@ var TWIN_RELATIONS = [
1930
1955
  * **자리(`locations`)에는 없다** — 표준이 자리를 시험 대상으로 두지 않았고 그 비대칭은 표준의
1931
1956
  * 판단이다(`contract.ts` 가 "우리도 자리에 넣지 않는다" 고 적었다).
1932
1957
  */
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 },
1958
+ { from: "persons", field: "testSpecificationIds[]", target: { kind: "axis", axis: "testSpecifications" }, via: "ops.rel.test", optional: true, tense: "standing" },
1959
+ { from: "personnelClasses", field: "testSpecificationIds[]", target: { kind: "axis", axis: "testSpecifications" }, via: "ops.rel.test", optional: true, tense: "standing" },
1960
+ { from: "equipment", field: "testSpecificationIds[]", target: { kind: "axis", axis: "testSpecifications" }, via: "ops.rel.test", optional: true, tense: "standing" },
1961
+ { from: "equipmentClasses", field: "testSpecificationIds[]", target: { kind: "axis", axis: "testSpecifications" }, via: "ops.rel.test", optional: true, tense: "standing" },
1962
+ { from: "assets", field: "testSpecificationIds[]", target: { kind: "axis", axis: "testSpecifications" }, via: "ops.rel.test", optional: true, tense: "standing" },
1963
+ { from: "assetClasses", field: "testSpecificationIds[]", target: { kind: "axis", axis: "testSpecifications" }, via: "ops.rel.test", optional: true, tense: "standing" },
1964
+ { from: "materialDefinitions", field: "testSpecificationIds[]", target: { kind: "axis", axis: "testSpecifications" }, via: "ops.rel.test", optional: true, tense: "standing" },
1965
+ { from: "materialClasses", field: "testSpecificationIds[]", target: { kind: "axis", axis: "testSpecifications" }, via: "ops.rel.test", optional: true, tense: "standing" },
1941
1966
  /* 공정이 도는 자리·자원 — 항목이 아니라 **타입**을 가리킨다 */
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 }
1967
+ { from: "operations", field: "locationType", target: { kind: "type", role: "location" }, via: "ops.rel.at", optional: true, tense: "standing" },
1968
+ { from: "operations", field: "resourceType", target: { kind: "type", role: "equipment" }, via: "ops.rel.by", optional: true, tense: "standing" }
1944
1969
  ];
1945
1970
  var TWIN_PROPERTIES = [
1946
1971
  /* 공정 — 선언(ISO 8601 기간)과 관측(저널에서 계산한 분포)이 둘 다 있는 유일한 짝. */
@@ -4410,6 +4435,7 @@ function commandSpecGaps(specs, command) {
4410
4435
  graiUri,
4411
4436
  gs1KeyDigitViolation,
4412
4437
  hierarchyOf,
4438
+ homeOf,
4413
4439
  identityGroundingOf,
4414
4440
  inWorkCalendar,
4415
4441
  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.7",
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",