@operato/ops-contract 0.9.6 → 0.9.8

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.
@@ -176,19 +176,49 @@ export type TwinRelationTarget = {
176
176
  * ── 왜 필요한가 (MES 레인이 실측으로 올림, 2026-09-05) ─────────────────────
177
177
  * 설비 하나에 걸린 것을 한자리에 보이면, 들어오는 관계라도 뜻이 갈린다.
178
178
  *
179
- * past 이미 일어났다 — 세워도 안 바뀐다 실적 · 상태 구간
179
+ * past 이미 일어났다 — 세워도 안 바뀐다 실적 · 끝난 정비 · 상태 구간
180
+ * current 지금 그렇다 — **끝나면 바뀐다** 정비 중 · 물품의 지금 자리
180
181
  * future 아직이다 — 세우면 이것이 걸린다 정비 계획 · 지시
181
182
  * standing 시간이 없는 사실 등급 · 붙박인 자리 · 종류
182
183
  *
183
184
  * 한 묶음에 담으면 **「걸린 것이 열」인데 아홉이 지난 일인 경우와 아홉이 앞의 일인 경우가 같아
184
185
  * 보인다.** 「이 설비를 세워도 되나」의 답이 정반대인 두 상황이다.
185
186
  *
187
+ * ── `current` 는 나중에 왔다 (2026-09-06) ──────────────────────────────────
188
+ * 처음 셋으로 냈고, 「지금 그렇고 곧 바뀐다」가 그 셋에 안 들어가는 자리를 **두 레인이 각자
189
+ * 만났다.** 미리 넣은 것이 아니라 두 번 걸려서 넣었다.
190
+ *
191
+ * 아키텍트 `items.location` — 이 상자가 지금 A 에 있다. 곧 옮겨진다
192
+ * plant/MES `MaintenanceOrderStatus.IN_PROGRESS` — 지금 이미 서 있다. 끝나면 바뀐다
193
+ *
194
+ * plant 의 상태 넷 중 `IN_PROGRESS` 하나가 셋 어디에도 안 들어갔다: `OPEN` 은 미래,
195
+ * `DONE`·`CANCELLED` 는 과거인데, **정비 중은 「지금 서 있다」이고 「세워도 되나」의 답이 앞의
196
+ * 일과 정반대다**(이미 서 있으니 세울 것이 없다).
197
+ *
198
+ * ── `standing` 과 무엇이 다른가 ────────────────────────────────────────────
199
+ * **다시 물어야 하나**가 갈린다. `standing` 은 등급·종류·붙박인 자리처럼 안 바뀌므로 화면이 담아
200
+ * 두어도 된다. `current` 는 담아 두면 거짓이 된다 — 「이 설비는 정비 중이다」와 「이 상자는 A 에
201
+ * 있다」를 등급처럼 캐시하면 사람이 낡은 사실을 보고 판단한다.
202
+ *
203
+ * 그래서 셋에 억지로 넣지 않고 비워 두었다. 짐작으로 `standing` 을 씌웠으면 그 뒤로 아무도 이
204
+ * 물음을 다시 안 했을 것이다.
205
+ *
186
206
  * ── 왜 `via` 로 가르면 안 되나 ──────────────────────────────────────────────
187
207
  * `via` 는 i18n 키다. **「이것이 지난 일인가」를 말하지 않는다.** 화면이 키 이름을 보고 짐작하게
188
208
  * 되고, 축이 하나 늘 때 한 화면만 따라간다. 읽는 곳은 제품마다여도 **무엇을 아는지는 한 벌**이어야
189
209
  * 한다.
190
210
  */
191
- export type RelationTense = 'past' | 'future' | 'standing';
211
+ export type RelationTense = 'past' | 'current' | 'future' | 'standing';
212
+ /**
213
+ * 화면이 이 관계를 **담아 두어도 되나** — `standing` 만 그렇다.
214
+ *
215
+ * 이 판정을 화면마다 하면 낱말이 하나 늘 때 한 화면만 따라간다. 무엇을 아는지는 한 벌이어야 한다.
216
+ *
217
+ * **미선언은 `false` 다.** 모르면 다시 묻는 쪽이 안전하다 — 낡은 사실을 보이는 것보다 한 번 더
218
+ * 묻는 것이 싸다. `tenseOf` 가 미선언에 `undefined` 를 내는 것과 방향이 다른데, 그것은 「말할 수
219
+ * 없는 것을 단언하지 않는다」이고 이것은 「모를 때 어느 쪽으로 틀리나」다.
220
+ */
221
+ export declare function isCacheableTense(t: RelationTense | undefined): boolean;
192
222
  export interface TwinRelationInfo {
193
223
  /** 출발 축. */
194
224
  from: string;
@@ -149,6 +149,18 @@ export const TWIN_AXES = [
149
149
  { axis: 'demandWindows', path: 'energy.closed', idField: 'startMs', label: 'twin.axis.demandWindows', kind: 'instance',
150
150
  source: 'state', historical: true, standardClass: {}, systems: ['ems'] }
151
151
  ];
152
+ /**
153
+ * 화면이 이 관계를 **담아 두어도 되나** — `standing` 만 그렇다.
154
+ *
155
+ * 이 판정을 화면마다 하면 낱말이 하나 늘 때 한 화면만 따라간다. 무엇을 아는지는 한 벌이어야 한다.
156
+ *
157
+ * **미선언은 `false` 다.** 모르면 다시 묻는 쪽이 안전하다 — 낡은 사실을 보이는 것보다 한 번 더
158
+ * 묻는 것이 싸다. `tenseOf` 가 미선언에 `undefined` 를 내는 것과 방향이 다른데, 그것은 「말할 수
159
+ * 없는 것을 단언하지 않는다」이고 이것은 「모를 때 어느 쪽으로 틀리나」다.
160
+ */
161
+ export function isCacheableTense(t) {
162
+ return t === 'standing';
163
+ }
152
164
  /**
153
165
  * 그 관계가 언제의 일인가 — **모르면 `undefined`.**
154
166
  *
@@ -164,21 +176,21 @@ export function tenseOf(r) {
164
176
  /** 관계 전체 — 지도의 선과 항목의 이웃이 여기서 나온다(화면은 이 목록을 갖지 않는다). */
165
177
  export const TWIN_RELATIONS = [
166
178
  /* 자리가 속한 구역 — **board 밖**(호스트의 `TwinArea`). 해소는 호스트가 한다. */
167
- { from: 'locations', field: 'parentId', target: { kind: 'external', entity: 'space.area' }, via: 'ops.rel.area', optional: true },
179
+ { from: 'locations', field: 'parentId', target: { kind: 'external', entity: 'space.area' }, via: 'ops.rel.area', optional: true, tense: 'standing' },
168
180
  /* 자원이 사는 자리 */
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 },
181
+ { from: 'equipment', field: 'homeLocation', target: { kind: 'axis', axis: 'locations' }, via: 'ops.rel.home', optional: true, tense: 'standing' },
182
+ { from: 'persons', field: 'homeLocation', target: { kind: 'axis', axis: 'locations' }, via: 'ops.rel.home', optional: true, tense: 'standing' },
183
+ { from: 'assets', field: 'homeLocation', target: { kind: 'axis', axis: 'locations' }, via: 'ops.rel.home', optional: true, tense: 'standing' },
172
184
  /* 자원의 등급 — 설비만 `kind`(타입)로 가고 사람·자산은 등급 목록으로 간다. 표준의 비대칭 그대로. */
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' },
185
+ { from: 'persons', field: 'personnelClassIds[]', target: { kind: 'axis', axis: 'personnelClasses' }, via: 'ops.rel.class', optional: true, tense: 'standing' },
186
+ { from: 'assets', field: 'assetClassIds[]', target: { kind: 'axis', axis: 'assetClasses' }, via: 'ops.rel.class', optional: true, tense: 'standing' },
187
+ { from: 'equipment', field: 'kind', target: { kind: 'type', role: 'equipment' }, via: 'ops.rel.type', tense: 'standing' },
188
+ { from: 'locations', field: 'type', target: { kind: 'type', role: 'location' }, via: 'ops.rel.type', tense: 'standing' },
177
189
  /* 생산 — 레시피에서 시작해 라우트·공정·품목으로 퍼진다 */
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' },
190
+ { from: 'recipes', field: 'route', target: { kind: 'axis', axis: 'routes' }, via: 'ops.rel.route', optional: true, tense: 'standing' },
191
+ { from: 'recipes', field: 'inputs[].material', target: { kind: 'axis', axis: 'materials' }, via: 'ops.rel.input', tense: 'standing' },
192
+ { from: 'recipes', field: 'outputs[].material', target: { kind: 'axis', axis: 'materials' }, via: 'ops.rel.output', tense: 'standing' },
193
+ { from: 'routes', field: 'steps[]', target: { kind: 'axis', axis: 'operations' }, via: 'ops.rel.step', tense: 'standing' },
182
194
  /*
183
195
  * 일정·실적의 관계 — **관측 관계다.** 선언 관계는 "그렇게 만들기로 했다" 이고, 이것은 "그렇게
184
196
  * 일어났다" 다. 관계가 없으면 이 두 축은 지도에서 연결되지 않은 카드가 된다(그러면 걸어 들어갈 수 없다).
@@ -188,7 +200,17 @@ export const TWIN_RELATIONS = [
188
200
  * 것은 `productionSpec.binding` 이고 그 해소는 호스트가 한다 — 자리·구역과 같은 `external` 이다.
189
201
  * 축을 직접 가리키게 적으면 없는 필드를 가리키는 선언이 된다(계약을 보고 고쳤다).
190
202
  */
191
- { from: 'orders', field: 'lines[].gtin', target: { kind: 'external', entity: 'gs1.itemRef' }, via: 'ops.rel.item', optional: true },
203
+ { from: 'orders', field: 'lines[].gtin', target: { kind: 'external', entity: 'gs1.itemRef' }, via: 'ops.rel.item', optional: true, tense: 'standing' },
204
+ /*
205
+ * ── 작업의 관계는 `tense` 를 **선언하지 않는다** ────────────────────────────
206
+ * 여섯 줄 모두 비어 있는 것이 판단이다. 작업의 시간축은 **그 줄의 상태**가 정한다 — 끝난 작업이
207
+ * 이 설비를 가리키면 지난 일이고, 아직인 작업이 가리키면 앞의 일이다. 한 선언이 둘 다이다.
208
+ *
209
+ * 여기에 `standing` 을 적으면 화면은 「이 설비에 걸린 작업 열」을 시간이 없는 사실로 묶고, **세워도
210
+ * 되나의 답이 정확히 뒤집힌다.** 채우고 싶은 힘이 있는 자리이므로 적어 둔다: 비어 있음이 답이다.
211
+ *
212
+ * 줄마다 다른 것은 선언이 아니라 **읽는 쪽이 상태를 보고 판정한다**(`TaskState.status`).
213
+ */
192
214
  { from: 'tasks', field: 'orderId', target: { kind: 'axis', axis: 'orders' }, via: 'ops.rel.order', optional: true },
193
215
  /* 작업의 `kind` 는 그 공정의 키다(라우트의 단계 이름 그대로 — `mes-kernel` 의 공정 목록이 그 증거). */
194
216
  { from: 'tasks', field: 'kind', target: { kind: 'axis', axis: 'operations' }, via: 'ops.rel.operation', optional: true },
@@ -210,10 +232,19 @@ export const TWIN_RELATIONS = [
210
232
  * 품목(`gtin`)은 오더와 **같은 규율**이다: 자재 키가 아니라 GS1 품목 참조이므로 축을 직접 가리키지
211
233
  * 않고 `external` 로 둔다. 축을 가리키게 적으면 없는 필드를 가리키는 선언이 된다.
212
234
  */
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 },
235
+ /*
236
+ * ── 물품의 지금 자리는 `current` (2026-09-06) ───────────────────────────
237
+ * `location`·`parent`·`carriedBy` 지난 일도 앞의 일도 아니고 `standing` 아니다 **지금
238
+ * 그렇고 바뀐다.** 붙박인 자리(`homeLocation`)와 정반대 성질이고, 줄이 같은 낱말을 받으면
239
+ * 화면이 「이 상자는 A 에 있다」를 등급처럼 담아 둔다(캐시해도 되는 사실로 읽는다).
240
+ *
241
+ * 하루 비워 두었다. 낱말이 없어서였고, 소비처가 실제로 그 구별에 걸릴 때 늘리기로 했다 — plant 가
242
+ * `MaintenanceOrderStatus.IN_PROGRESS` 로 같은 자리를 만나 걸렸고, 그때 넣었다.
243
+ */
244
+ { from: 'items', field: 'location', target: { kind: 'axis', axis: 'locations' }, via: 'ops.rel.at', tense: 'current' },
245
+ { from: 'items', field: 'parent', target: { kind: 'axis', axis: 'items' }, via: 'ops.rel.parent', optional: true, tense: 'current' },
246
+ { from: 'items', field: 'carriedBy', target: { kind: 'axis', axis: 'assets' }, via: 'ops.rel.asset', optional: true, tense: 'current' },
247
+ { from: 'items', field: 'gtin', target: { kind: 'external', entity: 'gs1.itemRef' }, via: 'ops.rel.item', optional: true, tense: 'standing' },
217
248
  /*
218
249
  * 자격을 검증한 시험 — **여덟 갈래.** 자원(개체)과 등급 양쪽이 가리킨다: 표준이 그 둘 모두에 이
219
250
  * 참조를 두었기 때문이다(개체는 "이 사람이 통과했다", 등급은 "이 자격은 이 시험을 요구한다").
@@ -221,17 +252,17 @@ export const TWIN_RELATIONS = [
221
252
  * **자리(`locations`)에는 없다** — 표준이 자리를 시험 대상으로 두지 않았고 그 비대칭은 표준의
222
253
  * 판단이다(`contract.ts` 가 "우리도 자리에 넣지 않는다" 고 적었다).
223
254
  */
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 },
255
+ { from: 'persons', field: 'testSpecificationIds[]', target: { kind: 'axis', axis: 'testSpecifications' }, via: 'ops.rel.test', optional: true, tense: 'standing' },
256
+ { from: 'personnelClasses', field: 'testSpecificationIds[]', target: { kind: 'axis', axis: 'testSpecifications' }, via: 'ops.rel.test', optional: true, tense: 'standing' },
257
+ { from: 'equipment', field: 'testSpecificationIds[]', target: { kind: 'axis', axis: 'testSpecifications' }, via: 'ops.rel.test', optional: true, tense: 'standing' },
258
+ { from: 'equipmentClasses', field: 'testSpecificationIds[]', target: { kind: 'axis', axis: 'testSpecifications' }, via: 'ops.rel.test', optional: true, tense: 'standing' },
259
+ { from: 'assets', field: 'testSpecificationIds[]', target: { kind: 'axis', axis: 'testSpecifications' }, via: 'ops.rel.test', optional: true, tense: 'standing' },
260
+ { from: 'assetClasses', field: 'testSpecificationIds[]', target: { kind: 'axis', axis: 'testSpecifications' }, via: 'ops.rel.test', optional: true, tense: 'standing' },
261
+ { from: 'materialDefinitions', field: 'testSpecificationIds[]', target: { kind: 'axis', axis: 'testSpecifications' }, via: 'ops.rel.test', optional: true, tense: 'standing' },
262
+ { from: 'materialClasses', field: 'testSpecificationIds[]', target: { kind: 'axis', axis: 'testSpecifications' }, via: 'ops.rel.test', optional: true, tense: 'standing' },
232
263
  /* 공정이 도는 자리·자원 — 항목이 아니라 **타입**을 가리킨다 */
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 }
264
+ { from: 'operations', field: 'locationType', target: { kind: 'type', role: 'location' }, via: 'ops.rel.at', optional: true, tense: 'standing' },
265
+ { from: 'operations', field: 'resourceType', target: { kind: 'type', role: 'equipment' }, via: 'ops.rel.by', optional: true, tense: 'standing' }
235
266
  ];
236
267
  /**
237
268
  * 오늘 **실제로 짝이 서는 것**만 선언한다. 그럴듯한 속성을 미리 적어 두면 화면이 빈칸을 줄줄이
@@ -136,6 +136,7 @@ __export(index_exports, {
136
136
  ingestMasterData: () => ingestMasterData,
137
137
  ingestOperationalRecords: () => ingestOperationalRecords,
138
138
  isAggregationRecord: () => isAggregationRecord,
139
+ isCacheableTense: () => isCacheableTense,
139
140
  isCommandTerminal: () => isCommandTerminal,
140
141
  isElectricalLocationType: () => isElectricalLocationType,
141
142
  isEnergyBillRecord: () => isEnergyBillRecord,
@@ -1873,26 +1874,29 @@ var TWIN_AXES = [
1873
1874
  systems: ["ems"]
1874
1875
  }
1875
1876
  ];
1877
+ function isCacheableTense(t) {
1878
+ return t === "standing";
1879
+ }
1876
1880
  function tenseOf(r) {
1877
1881
  return r?.tense;
1878
1882
  }
1879
1883
  var TWIN_RELATIONS = [
1880
1884
  /* 자리가 속한 구역 — **board 밖**(호스트의 `TwinArea`). 해소는 호스트가 한다. */
1881
- { from: "locations", field: "parentId", target: { kind: "external", entity: "space.area" }, via: "ops.rel.area", optional: true },
1885
+ { from: "locations", field: "parentId", target: { kind: "external", entity: "space.area" }, via: "ops.rel.area", optional: true, tense: "standing" },
1882
1886
  /* 자원이 사는 자리 */
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 },
1887
+ { from: "equipment", field: "homeLocation", target: { kind: "axis", axis: "locations" }, via: "ops.rel.home", optional: true, tense: "standing" },
1888
+ { from: "persons", field: "homeLocation", target: { kind: "axis", axis: "locations" }, via: "ops.rel.home", optional: true, tense: "standing" },
1889
+ { from: "assets", field: "homeLocation", target: { kind: "axis", axis: "locations" }, via: "ops.rel.home", optional: true, tense: "standing" },
1886
1890
  /* 자원의 등급 — 설비만 `kind`(타입)로 가고 사람·자산은 등급 목록으로 간다. 표준의 비대칭 그대로. */
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" },
1891
+ { from: "persons", field: "personnelClassIds[]", target: { kind: "axis", axis: "personnelClasses" }, via: "ops.rel.class", optional: true, tense: "standing" },
1892
+ { from: "assets", field: "assetClassIds[]", target: { kind: "axis", axis: "assetClasses" }, via: "ops.rel.class", optional: true, tense: "standing" },
1893
+ { from: "equipment", field: "kind", target: { kind: "type", role: "equipment" }, via: "ops.rel.type", tense: "standing" },
1894
+ { from: "locations", field: "type", target: { kind: "type", role: "location" }, via: "ops.rel.type", tense: "standing" },
1891
1895
  /* 생산 — 레시피에서 시작해 라우트·공정·품목으로 퍼진다 */
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" },
1896
+ { from: "recipes", field: "route", target: { kind: "axis", axis: "routes" }, via: "ops.rel.route", optional: true, tense: "standing" },
1897
+ { from: "recipes", field: "inputs[].material", target: { kind: "axis", axis: "materials" }, via: "ops.rel.input", tense: "standing" },
1898
+ { from: "recipes", field: "outputs[].material", target: { kind: "axis", axis: "materials" }, via: "ops.rel.output", tense: "standing" },
1899
+ { from: "routes", field: "steps[]", target: { kind: "axis", axis: "operations" }, via: "ops.rel.step", tense: "standing" },
1896
1900
  /*
1897
1901
  * 일정·실적의 관계 — **관측 관계다.** 선언 관계는 "그렇게 만들기로 했다" 이고, 이것은 "그렇게
1898
1902
  * 일어났다" 다. 관계가 없으면 이 두 축은 지도에서 연결되지 않은 카드가 된다(그러면 걸어 들어갈 수 없다).
@@ -1902,7 +1906,17 @@ var TWIN_RELATIONS = [
1902
1906
  * 것은 `productionSpec.binding` 이고 그 해소는 호스트가 한다 — 자리·구역과 같은 `external` 이다.
1903
1907
  * 축을 직접 가리키게 적으면 없는 필드를 가리키는 선언이 된다(계약을 보고 고쳤다).
1904
1908
  */
1905
- { from: "orders", field: "lines[].gtin", target: { kind: "external", entity: "gs1.itemRef" }, via: "ops.rel.item", optional: true },
1909
+ { from: "orders", field: "lines[].gtin", target: { kind: "external", entity: "gs1.itemRef" }, via: "ops.rel.item", optional: true, tense: "standing" },
1910
+ /*
1911
+ * ── 작업의 관계는 `tense` 를 **선언하지 않는다** ────────────────────────────
1912
+ * 여섯 줄 모두 비어 있는 것이 판단이다. 작업의 시간축은 **그 줄의 상태**가 정한다 — 끝난 작업이
1913
+ * 이 설비를 가리키면 지난 일이고, 아직인 작업이 가리키면 앞의 일이다. 한 선언이 둘 다이다.
1914
+ *
1915
+ * 여기에 `standing` 을 적으면 화면은 「이 설비에 걸린 작업 열」을 시간이 없는 사실로 묶고, **세워도
1916
+ * 되나의 답이 정확히 뒤집힌다.** 채우고 싶은 힘이 있는 자리이므로 적어 둔다: 비어 있음이 답이다.
1917
+ *
1918
+ * 줄마다 다른 것은 선언이 아니라 **읽는 쪽이 상태를 보고 판정한다**(`TaskState.status`).
1919
+ */
1906
1920
  { from: "tasks", field: "orderId", target: { kind: "axis", axis: "orders" }, via: "ops.rel.order", optional: true },
1907
1921
  /* 작업의 `kind` 는 그 공정의 키다(라우트의 단계 이름 그대로 — `mes-kernel` 의 공정 목록이 그 증거). */
1908
1922
  { from: "tasks", field: "kind", target: { kind: "axis", axis: "operations" }, via: "ops.rel.operation", optional: true },
@@ -1924,10 +1938,19 @@ var TWIN_RELATIONS = [
1924
1938
  * 품목(`gtin`)은 오더와 **같은 규율**이다: 자재 키가 아니라 GS1 품목 참조이므로 축을 직접 가리키지
1925
1939
  * 않고 `external` 로 둔다. 축을 가리키게 적으면 없는 필드를 가리키는 선언이 된다.
1926
1940
  */
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 },
1941
+ /*
1942
+ * ── 물품의 지금 자리는 `current` (2026-09-06) ───────────────────────────
1943
+ * `location`·`parent`·`carriedBy` 지난 일도 앞의 일도 아니고 `standing` 아니다 **지금
1944
+ * 그렇고 바뀐다.** 붙박인 자리(`homeLocation`)와 정반대 성질이고, 줄이 같은 낱말을 받으면
1945
+ * 화면이 「이 상자는 A 에 있다」를 등급처럼 담아 둔다(캐시해도 되는 사실로 읽는다).
1946
+ *
1947
+ * 하루 비워 두었다. 낱말이 없어서였고, 소비처가 실제로 그 구별에 걸릴 때 늘리기로 했다 — plant 가
1948
+ * `MaintenanceOrderStatus.IN_PROGRESS` 로 같은 자리를 만나 걸렸고, 그때 넣었다.
1949
+ */
1950
+ { from: "items", field: "location", target: { kind: "axis", axis: "locations" }, via: "ops.rel.at", tense: "current" },
1951
+ { from: "items", field: "parent", target: { kind: "axis", axis: "items" }, via: "ops.rel.parent", optional: true, tense: "current" },
1952
+ { from: "items", field: "carriedBy", target: { kind: "axis", axis: "assets" }, via: "ops.rel.asset", optional: true, tense: "current" },
1953
+ { from: "items", field: "gtin", target: { kind: "external", entity: "gs1.itemRef" }, via: "ops.rel.item", optional: true, tense: "standing" },
1931
1954
  /*
1932
1955
  * 자격을 검증한 시험 — **여덟 갈래.** 자원(개체)과 등급 양쪽이 가리킨다: 표준이 그 둘 모두에 이
1933
1956
  * 참조를 두었기 때문이다(개체는 "이 사람이 통과했다", 등급은 "이 자격은 이 시험을 요구한다").
@@ -1935,17 +1958,17 @@ var TWIN_RELATIONS = [
1935
1958
  * **자리(`locations`)에는 없다** — 표준이 자리를 시험 대상으로 두지 않았고 그 비대칭은 표준의
1936
1959
  * 판단이다(`contract.ts` 가 "우리도 자리에 넣지 않는다" 고 적었다).
1937
1960
  */
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 },
1961
+ { from: "persons", field: "testSpecificationIds[]", target: { kind: "axis", axis: "testSpecifications" }, via: "ops.rel.test", optional: true, tense: "standing" },
1962
+ { from: "personnelClasses", field: "testSpecificationIds[]", target: { kind: "axis", axis: "testSpecifications" }, via: "ops.rel.test", optional: true, tense: "standing" },
1963
+ { from: "equipment", field: "testSpecificationIds[]", target: { kind: "axis", axis: "testSpecifications" }, via: "ops.rel.test", optional: true, tense: "standing" },
1964
+ { from: "equipmentClasses", field: "testSpecificationIds[]", target: { kind: "axis", axis: "testSpecifications" }, via: "ops.rel.test", optional: true, tense: "standing" },
1965
+ { from: "assets", field: "testSpecificationIds[]", target: { kind: "axis", axis: "testSpecifications" }, via: "ops.rel.test", optional: true, tense: "standing" },
1966
+ { from: "assetClasses", field: "testSpecificationIds[]", target: { kind: "axis", axis: "testSpecifications" }, via: "ops.rel.test", optional: true, tense: "standing" },
1967
+ { from: "materialDefinitions", field: "testSpecificationIds[]", target: { kind: "axis", axis: "testSpecifications" }, via: "ops.rel.test", optional: true, tense: "standing" },
1968
+ { from: "materialClasses", field: "testSpecificationIds[]", target: { kind: "axis", axis: "testSpecifications" }, via: "ops.rel.test", optional: true, tense: "standing" },
1946
1969
  /* 공정이 도는 자리·자원 — 항목이 아니라 **타입**을 가리킨다 */
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 }
1970
+ { from: "operations", field: "locationType", target: { kind: "type", role: "location" }, via: "ops.rel.at", optional: true, tense: "standing" },
1971
+ { from: "operations", field: "resourceType", target: { kind: "type", role: "equipment" }, via: "ops.rel.by", optional: true, tense: "standing" }
1949
1972
  ];
1950
1973
  var TWIN_PROPERTIES = [
1951
1974
  /* 공정 — 선언(ISO 8601 기간)과 관측(저널에서 계산한 분포)이 둘 다 있는 유일한 짝. */
@@ -4431,6 +4454,7 @@ function commandSpecGaps(specs, command) {
4431
4454
  ingestMasterData,
4432
4455
  ingestOperationalRecords,
4433
4456
  isAggregationRecord,
4457
+ isCacheableTense,
4434
4458
  isCommandTerminal,
4435
4459
  isElectricalLocationType,
4436
4460
  isEnergyBillRecord,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@operato/ops-contract",
3
- "version": "0.9.6",
3
+ "version": "0.9.8",
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",