@operato/ops-contract 0.9.16 → 0.9.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -14,6 +14,64 @@ IEC 61850 · ISO 50001 전기 계통과 성과지표
14
14
 
15
15
  여기 없는 것은 **정책과 엔진**입니다. 그 사실로 무엇을 할지는 `@operato/twin-kernel` 이 정합니다.
16
16
 
17
+ ## 제품 간 AI 분석 입력 — `OperationalAttentionV1`
18
+
19
+ `OperationalAttentionV1`은 Plant·WMS·YMS·EMS가 자신이 관측한 운영 주의 사실을 Twin 또는 AI 분석기에
20
+ 보낼 때 쓰는 공통 봉투입니다. 국제 표준의 알람 포맷을 새로 만드는 것이 아닙니다. EPCIS/ISA-95 등의
21
+ 원본 사실을 **어떤 제품이, 어떤 근거로, 무엇을 분석 대상으로 열었거나 닫았는지** 안전하게 전달하는
22
+ Operato의 제품 간 계약입니다.
23
+
24
+ ```ts
25
+ import {
26
+ OPERATIONAL_ATTENTION_CONTRACT,
27
+ assertOperationalAttentionV1,
28
+ type OperationalAttentionV1
29
+ } from '@operato/ops-contract'
30
+
31
+ const attention: OperationalAttentionV1 = {
32
+ contract: OPERATIONAL_ATTENTION_CONTRACT,
33
+ domainId: 'factory-a',
34
+ attentionId: 'schedule-overdue:line-a:101',
35
+ dedupeKey: 'schedule-overdue:line-a:101',
36
+ kind: 'plant.schedule-overdue',
37
+ subjectLabel: 'Line A schedule 101',
38
+ transition: 'opened',
39
+ impact: 'high',
40
+ observedAt: '2026-09-12T01:00:00.000Z',
41
+ evidenceFingerprint: 'sha256:...',
42
+ facts: [{ system: 'operato-plant', type: 'schedule-entry', id: '101' }]
43
+ }
44
+
45
+ assertOperationalAttentionV1(attention)
46
+ ```
47
+
48
+ ### 공통으로 고정한 것
49
+
50
+ - 식별·재전송: `attentionId`, `dedupeKey`
51
+ - 분석 수명주기: `opened`, `changed`, `resolved`, `reopened`
52
+ - 분석 우선순위: `medium`, `high`
53
+ - 근거: 최소 하나 이상의 안정된 `{ system, type, id }` 원본 사실 참조와 `evidenceFingerprint`
54
+
55
+ 사람이 읽는 제목만 있고 원본 사실 ID가 없는 입력은 거부합니다. AI가 그럴듯한 설명을 만들더라도
56
+ 근거를 다시 열어 볼 수 없으면 운영 분석의 입력이 될 수 없기 때문입니다.
57
+
58
+ ### 제품별로 남기는 것
59
+
60
+ `kind`와 `facts[].type`은 생산자가 소유합니다. 예를 들어 Plant는 `plant.schedule-overdue`, WMS는
61
+ `wms.short-pick`을 쓰며 서로의 업무 모델을 흉내 내지 않습니다. 또한 `acknowledged`, `suppressed`,
62
+ 승인, 실제 조치 결과는 생산 제품의 업무 흐름에 남습니다. 이 계약은 분석에 필요한 **사실 변화**만
63
+ 전달하며, 수신자가 현장을 조치할 권한을 주지 않습니다.
64
+
65
+ 새 제품이 붙을 때는 공통 봉투를 넓히기 전에 그 제품의 adapter에서 자기 `kind`·사실 유형·판정 기준을
66
+ 검증합니다. 모든 제품이 실제로 공유하는 필드만 이 계약에 추가합니다.
67
+
68
+ ### 기존 webhook을 통한 전송
69
+
70
+ 새 URL은 만들지 않는다. attention도 기존 `POST /domain/{domain}/twin/hook/{source}/{instanceId}`로 보내며,
71
+ 봉투의 `lane: 'ATTENTIONS'`가 Twin의 AI inbox 경로를 고른다. `scope`는 계속 원천의 순번 흐름 이름이며
72
+ `lane`과 합치지 않는다. `lane`을 생략한 기존 커넥터는 `FACTS`로 읽혀 기존 canonical ingest 경로를 그대로
73
+ 쓴다. 따라서 주소·서명·재시도·순번 규약은 하나이고, 사실과 attention의 저장·처리만 Twin 내부에서 갈린다.
74
+
17
75
  ## 왜 나뉘어 있나
18
76
 
19
77
  사실을 **만드는 쪽**(MES · 커넥터)은 계약만 필요하고 엔진은 필요 없습니다. 한 패키지에 두면 레코드
@@ -32,38 +32,72 @@ export interface CanonicalEnvelope<T = unknown> {
32
32
  */
33
33
  description?: string;
34
34
  /**
35
- * **이 사실이 실제로 일어난 것인가**보내는 쪽만 답할 있다.
35
+ * **Whether this fact actually happened**only the sending side can answer.
36
+ *
37
+ * ── Why this exists (measured 2026-09-09) ─────────────────────────────────
38
+ * The twin's journal held 1,134 rows, every one of them produced by a generator, and nothing
39
+ * in it said so. The screens compute yield and utilisation from those rows and had no way to
40
+ * mention it.
41
+ *
42
+ * The only signal was the sender's naming convention — `SIM-`, `WO-SIM-` — and the column that
43
+ * convention reaches was empty on half the rows: **558 of 1,134 were recognisable.** A
44
+ * convention belongs to whoever writes it, and a receiver that depends on one is wrong the day
45
+ * it changes.
46
+ *
47
+ * ── Why the twin does not judge this itself ───────────────────────────────
48
+ * **It has nothing to judge from.** A generator arrives through the same door, in the same
49
+ * shape, as a real system. What the twin knows is which connection a fact came through;
50
+ * whether that connection reads a factory or invents one is knowable only at that end.
51
+ *
52
+ * So this is a declaration: the side that invents says it invents.
53
+ *
54
+ * ── Absent is not `actual` ────────────────────────────────────────────────
55
+ * Empty means **not declared**, which is not "it happened". Reading the two as the same thing
56
+ * is the defect: a generator that omits the field has its output counted as production, and
57
+ * nothing says so.
58
+ *
59
+ * Nor is every real source required to declare `actual` — that adds a line to each connector
60
+ * and makes the ones that forget look fabricated. **Only the exception is declared**, and a
61
+ * reader distinguishes three states: declared simulated, declared actual, not declared.
62
+ *
63
+ * ── The kernel does not decide anything on this ───────────────────────────
64
+ * It carries it. Whether performance figures exclude simulated facts is **the reader's**
65
+ * decision; excluding them in the kernel would erase the answer of whoever ran a simulation
66
+ * to predict something.
67
+ *
68
+ * ── Why the envelope ──────────────────────────────────────────────────────
69
+ * The same reason as `description` — it is a fact **about a past event**, not about current
70
+ * state. And it does not go inside the EPCIS event: the standard has no such field, and
71
+ * inventing a name in there would take that event outside the standard. The envelope is ours,
72
+ * and the journal keeps the envelope as it arrived.
73
+ *
74
+ * ── ⚠ Do not put lineage in this field ───────────────────────────────────
75
+ * `provenance` normally means a chain. In art it is the record of ownership; in computing it
76
+ * is the record of **who, which system, and which activity produced a piece of data**
77
+ * (W3C PROV). The word was chosen because its purpose matches this one: telling the genuine
78
+ * from the forged.
79
+ *
80
+ * **But this field is one bit of that chain** — it answers "did it happen" and nothing else.
81
+ * The name promises a chain and hands over a flag, which leaves exactly one risk: that someone
82
+ * later reasons "it is called provenance, so the lineage goes here". Then this field becomes
83
+ * the home of a different concept, and code already splitting performance figures on it
84
+ * receives a value whose shape changed (ADR-0041 — a name fitting is not the same as a place
85
+ * fitting).
86
+ *
87
+ * So do not widen it. **Open a new place.**
36
88
  *
37
- * ── 왜 필요한가 (2026-09-09 실측) ─────────────────────────────────────────
38
- * 트윈 저널 1,134건이 전부 발생기가 만든 실적이었고, **저널에 그렇다고 말하는 자리가 없었다.**
39
- * 화면의 성과·수율·가동률이 수로 계산되는데, 화면이 사실을 말할 방법이 없다.
40
- *
41
- * 가릴 있는 단서가 이름 규약뿐이었다발생기가 `SIM-`·`WO-SIM-` 붙인다. 그런데 그 규약이
42
- * 닿는 칸(`order_id`)이 절반은 비어 있어서 **1,134 중 558 만 갈렸다.** 규약은 보내는 쪽의 것이고,
43
- * 받는 쪽이 그것에 기대면 규약이 바뀌는 조용히 틀린다.
44
- *
45
- * ── 왜 트윈이 스스로 판정하지 않나 ────────────────────────────────────────
46
- * **판정할 근거가 없다.** 발생기는 실 시스템과 같은 문으로, 같은 모양으로 보낸다. 트윈이 아는 것은
47
- * 「어느 연결로 들어왔나」까지이고, 그 연결이 실 공장을 읽는지 지어내는지는 그쪽만 안다.
48
- *
49
- * 그래서 이 값은 **선언**이다. 지어내는 쪽이 지어낸다고 말한다.
50
- *
51
- * ── 없음은 `actual` 이 아니다 ─────────────────────────────────────────────
52
- * 비어 있으면 **선언되지 않았다**는 뜻이고, 「실제로 일어났다」가 아니다. 둘을 같게 읽으면 발생기가
53
- * 이 칸을 잊는 순간 그 수가 실적으로 계산되고, 아무것도 그렇다고 말하지 않는다.
54
- *
55
- * 모든 실 원본에 `actual` 을 요구하지도 않는다 — 그러면 붙는 쪽마다 한 줄이 늘고, 잊은 곳이
56
- * 「지어낸 것」으로 보인다. **선언하는 것은 예외뿐**이고, 읽는 쪽은 셋을 구별해 말한다:
57
- * 지어낸 것 · 실제라고 선언된 것 · 선언되지 않은 것.
58
- *
59
- * ── 커널은 이 값으로 판단하지 않는다 ──────────────────────────────────────
60
- * 나르기만 한다. 성과 계산에서 지어낸 것을 뺄지는 **읽는 쪽의 결정**이고, 커널이 미리 빼면
61
- * 시뮬레이션으로 무엇을 예측하려던 사람의 답이 사라진다.
89
+ * ```
90
+ * did it happen this field
91
+ * which connection did it arrive on a separate place none yet; the twin knows this, it is
92
+ * not a declaration
93
+ * when did we learn it (late?) a separate axis that is backfill, and a backfilled
94
+ * fact is not simulated
95
+ * is the value correct a separate axis `rejected` and `errors` answer it
96
+ * ```
62
97
  *
63
- * ── 봉투인가 ───────────────────────────────────────────────────────────
64
- * `description` 같은 이유다**지난 사건에 대한 사실**이고 지금 상태에 대한 사실이 아니다.
65
- * 그리고 EPCIS 사건 안에 넣지 않는다: 표준에 그 칸이 없고, 이름을 지어 넣으면 그 사건이 표준을
66
- * 벗어난다. 봉투는 우리 것이고, 지난 기록에는 봉투가 그대로 남는다.
98
+ * Adding a value follows the same discipline. If a third one seems necessary, ask first
99
+ * whether it answers "did it happen" usually it does not, and then it is one of the rows
100
+ * above.
67
101
  */
68
102
  provenance?: 'actual' | 'simulated';
69
103
  data: T;
package/dist/index.d.ts CHANGED
@@ -25,3 +25,4 @@ export * from './yield.ts';
25
25
  export * from './reliability.ts';
26
26
  export * from './erp.ts';
27
27
  export * from './actuation.ts';
28
+ export * from './operational-attention.ts';
package/dist/index.js CHANGED
@@ -48,3 +48,4 @@ export * from "./yield.js";
48
48
  export * from "./reliability.js";
49
49
  export * from "./erp.js";
50
50
  export * from "./actuation.js";
51
+ export * from "./operational-attention.js";
@@ -0,0 +1,43 @@
1
+ import type { ISOTime } from './contract.ts';
2
+ /**
3
+ * Product-neutral evidence for an operational concern that another product's
4
+ * Twin/AI may analyse. It is not an alarm protocol and never authorises work.
5
+ *
6
+ * A producer owns how it detects and manages an attention. Consumers receive
7
+ * only its evidence-backed lifecycle change; acknowledgement, suppression,
8
+ * approval, and outcome remain in the producer's workflow.
9
+ */
10
+ export declare const OPERATIONAL_ATTENTION_CONTRACT: "operational.attention.v1";
11
+ export declare const OPERATIONAL_ATTENTION_TRANSITIONS: readonly ["opened", "changed", "resolved", "reopened"];
12
+ export declare const OPERATIONAL_ATTENTION_IMPACTS: readonly ["medium", "high"];
13
+ export type OperationalAttentionTransition = (typeof OPERATIONAL_ATTENTION_TRANSITIONS)[number];
14
+ export type OperationalAttentionImpact = (typeof OPERATIONAL_ATTENTION_IMPACTS)[number];
15
+ /** Stable reference to a fact held by the producing product. */
16
+ export interface OperationalFactReference {
17
+ /** Product/system that owns the source fact, e.g. operato-plant or operato-wms. */
18
+ system: string;
19
+ /** Producer-owned entity vocabulary, e.g. schedule-entry or inventory. */
20
+ type: string;
21
+ id: string;
22
+ observedAt?: ISOTime;
23
+ label?: string;
24
+ }
25
+ export interface OperationalAttentionV1 {
26
+ contract: typeof OPERATIONAL_ATTENTION_CONTRACT;
27
+ domainId: string;
28
+ /** Stable producer identifier; unchanged across lifecycle updates. */
29
+ attentionId: string;
30
+ dedupeKey: string;
31
+ /** Producer-owned, namespaced reason code (for example plant.schedule-overdue). */
32
+ kind: string;
33
+ subjectLabel: string;
34
+ transition: OperationalAttentionTransition;
35
+ impact: OperationalAttentionImpact;
36
+ observedAt: ISOTime;
37
+ /** Deterministic hash of the evidence used for this particular observation. */
38
+ evidenceFingerprint: string;
39
+ /** At least one stable producer fact is mandatory; labels alone are not evidence. */
40
+ facts: OperationalFactReference[];
41
+ }
42
+ export declare function isOperationalAttentionV1(value: unknown): value is OperationalAttentionV1;
43
+ export declare function assertOperationalAttentionV1(value: unknown): asserts value is OperationalAttentionV1;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Product-neutral evidence for an operational concern that another product's
3
+ * Twin/AI may analyse. It is not an alarm protocol and never authorises work.
4
+ *
5
+ * A producer owns how it detects and manages an attention. Consumers receive
6
+ * only its evidence-backed lifecycle change; acknowledgement, suppression,
7
+ * approval, and outcome remain in the producer's workflow.
8
+ */
9
+ export const OPERATIONAL_ATTENTION_CONTRACT = 'operational.attention.v1';
10
+ export const OPERATIONAL_ATTENTION_TRANSITIONS = ['opened', 'changed', 'resolved', 'reopened'];
11
+ export const OPERATIONAL_ATTENTION_IMPACTS = ['medium', 'high'];
12
+ export function isOperationalAttentionV1(value) {
13
+ const one = value;
14
+ return !!one && typeof one === 'object' &&
15
+ one.contract === OPERATIONAL_ATTENTION_CONTRACT &&
16
+ nonEmpty(one.domainId) && nonEmpty(one.attentionId) && nonEmpty(one.dedupeKey) &&
17
+ nonEmpty(one.kind) && nonEmpty(one.subjectLabel) && nonEmpty(one.observedAt) && nonEmpty(one.evidenceFingerprint) &&
18
+ OPERATIONAL_ATTENTION_TRANSITIONS.includes(one.transition) &&
19
+ OPERATIONAL_ATTENTION_IMPACTS.includes(one.impact) &&
20
+ Array.isArray(one.facts) && one.facts.length > 0 && one.facts.every(isOperationalFactReference);
21
+ }
22
+ export function assertOperationalAttentionV1(value) {
23
+ if (!isOperationalAttentionV1(value)) {
24
+ throw new Error('invalid operational.attention.v1: lifecycle fields and stable source-fact references are required');
25
+ }
26
+ }
27
+ function isOperationalFactReference(value) {
28
+ const one = value;
29
+ return !!one && typeof one === 'object' && nonEmpty(one.system) && nonEmpty(one.type) && nonEmpty(one.id) &&
30
+ (one.observedAt === undefined || nonEmpty(one.observedAt)) &&
31
+ (one.label === undefined || nonEmpty(one.label));
32
+ }
33
+ function nonEmpty(value) {
34
+ return typeof value === 'string' && value.trim().length > 0;
35
+ }
package/dist/webhook.d.ts CHANGED
@@ -30,6 +30,18 @@ export declare const WEBHOOK_STATUS: {
30
30
  readonly notLive: 503;
31
31
  };
32
32
  export type WebhookStatus = (typeof WEBHOOK_STATUS)[keyof typeof WEBHOOK_STATUS];
33
+ /**
34
+ * A webhook lane says what the envelope is for; it is deliberately separate
35
+ * from `scope`, which names the producer's sequence stream.
36
+ *
37
+ * Omitting it means FACTS so every existing connector remains compatible.
38
+ */
39
+ export declare const WEBHOOK_LANE: {
40
+ readonly facts: "FACTS";
41
+ readonly attentions: "ATTENTIONS";
42
+ };
43
+ export type WebhookLane = (typeof WEBHOOK_LANE)[keyof typeof WEBHOOK_LANE];
44
+ export declare function webhookLaneOf(value: unknown): WebhookLane | undefined;
33
45
  /**
34
46
  * 그 응답을 받은 뒤 보내는 쪽이 할 일.
35
47
  *
@@ -217,6 +229,11 @@ export interface WebhookEnvelope {
217
229
  seq: number;
218
230
  /** 번호를 매기는 단위의 이름. 보내는 쪽이 정하고 받는 쪽은 열쇠로만 다룬다. */
219
231
  scope: string;
232
+ /**
233
+ * Delivery purpose. `scope` remains the source-owned numbering stream;
234
+ * `lane` selects the receiver path. Missing means `FACTS` for compatibility.
235
+ */
236
+ lane?: WebhookLane;
220
237
  /** 멱등 키. 같은 값이 두 번 와도 결과가 같아야 한다. */
221
238
  eventId?: string;
222
239
  /** 사실이 일어난 시각. */
package/dist/webhook.js CHANGED
@@ -45,6 +45,23 @@ export const WEBHOOK_STATUS = {
45
45
  unsupported: 501,
46
46
  notLive: 503
47
47
  };
48
+ /**
49
+ * A webhook lane says what the envelope is for; it is deliberately separate
50
+ * from `scope`, which names the producer's sequence stream.
51
+ *
52
+ * Omitting it means FACTS so every existing connector remains compatible.
53
+ */
54
+ export const WEBHOOK_LANE = {
55
+ facts: 'FACTS',
56
+ attentions: 'ATTENTIONS'
57
+ };
58
+ export function webhookLaneOf(value) {
59
+ return value === undefined || value === WEBHOOK_LANE.facts
60
+ ? WEBHOOK_LANE.facts
61
+ : value === WEBHOOK_LANE.attentions
62
+ ? WEBHOOK_LANE.attentions
63
+ : undefined;
64
+ }
48
65
  /**
49
66
  * 응답 코드 → 보내는 쪽의 행동. **모르는 코드도 답을 낸다.**
50
67
  *
@@ -58,6 +58,9 @@ __export(index_exports, {
58
58
  MES_TYPES: () => MES_TYPES,
59
59
  METER_DIRECTION: () => METER_DIRECTION,
60
60
  OBSERVATION_BASIS: () => OBSERVATION_BASIS,
61
+ OPERATIONAL_ATTENTION_CONTRACT: () => OPERATIONAL_ATTENTION_CONTRACT,
62
+ OPERATIONAL_ATTENTION_IMPACTS: () => OPERATIONAL_ATTENTION_IMPACTS,
63
+ OPERATIONAL_ATTENTION_TRANSITIONS: () => OPERATIONAL_ATTENTION_TRANSITIONS,
61
64
  OP_EVENT: () => OP_EVENT,
62
65
  OP_PARAM: () => OP_PARAM,
63
66
  ORDER_TERMINAL_STATUS: () => ORDER_TERMINAL_STATUS,
@@ -72,6 +75,7 @@ __export(index_exports, {
72
75
  UTC_OFFSET: () => UTC_OFFSET,
73
76
  VOCABULARY_EXCEPTIONS: () => VOCABULARY_EXCEPTIONS,
74
77
  VOCABULARY_TYPE: () => VOCABULARY_TYPE,
78
+ WEBHOOK_LANE: () => WEBHOOK_LANE,
75
79
  WEBHOOK_SECRET_BASE: () => WEBHOOK_SECRET_BASE,
76
80
  WEBHOOK_SECRET_NAME_SHAPE: () => WEBHOOK_SECRET_NAME_SHAPE,
77
81
  WEBHOOK_STATUS: () => WEBHOOK_STATUS,
@@ -88,6 +92,7 @@ __export(index_exports, {
88
92
  analyzeCapacity: () => analyzeCapacity,
89
93
  approveCommand: () => approveCommand,
90
94
  asApproved: () => asApproved,
95
+ assertOperationalAttentionV1: () => assertOperationalAttentionV1,
91
96
  axesOfSystem: () => axesOfSystem,
92
97
  axisAppliesTo: () => axisAppliesTo,
93
98
  axisInfo: () => axisInfo,
@@ -154,6 +159,7 @@ __export(index_exports, {
154
159
  isEquipmentStatus: () => isEquipmentStatus,
155
160
  isFailureStatus: () => isFailureStatus,
156
161
  isMasterDataRecord: () => isMasterDataRecord,
162
+ isOperationalAttentionV1: () => isOperationalAttentionV1,
157
163
  isOperationalRecord: () => isOperationalRecord,
158
164
  isOrderTerminal: () => isOrderTerminal,
159
165
  isPlannedStopStatus: () => isPlannedStopStatus,
@@ -222,6 +228,7 @@ __export(index_exports, {
222
228
  validateEpcisEvent: () => validateEpcisEvent,
223
229
  validatePerformance: () => validatePerformance,
224
230
  validateScenario: () => validateScenario,
231
+ webhookLaneOf: () => webhookLaneOf,
225
232
  webhookSecretCandidates: () => webhookSecretCandidates,
226
233
  webhookSecretEnvName: () => webhookSecretEnvName,
227
234
  webhookSecretFrom: () => webhookSecretFrom,
@@ -4050,6 +4057,13 @@ var WEBHOOK_STATUS = {
4050
4057
  unsupported: 501,
4051
4058
  notLive: 503
4052
4059
  };
4060
+ var WEBHOOK_LANE = {
4061
+ facts: "FACTS",
4062
+ attentions: "ATTENTIONS"
4063
+ };
4064
+ function webhookLaneOf(value) {
4065
+ return value === void 0 || value === WEBHOOK_LANE.facts ? WEBHOOK_LANE.facts : value === WEBHOOK_LANE.attentions ? WEBHOOK_LANE.attentions : void 0;
4066
+ }
4053
4067
  function webhookSenderAction(status) {
4054
4068
  switch (status) {
4055
4069
  case WEBHOOK_STATUS.ok:
@@ -4453,6 +4467,27 @@ function commandSpecGaps(specs, command) {
4453
4467
  }
4454
4468
  return gaps;
4455
4469
  }
4470
+
4471
+ // src/operational-attention.ts
4472
+ var OPERATIONAL_ATTENTION_CONTRACT = "operational.attention.v1";
4473
+ var OPERATIONAL_ATTENTION_TRANSITIONS = ["opened", "changed", "resolved", "reopened"];
4474
+ var OPERATIONAL_ATTENTION_IMPACTS = ["medium", "high"];
4475
+ function isOperationalAttentionV1(value) {
4476
+ const one = value;
4477
+ return !!one && typeof one === "object" && one.contract === OPERATIONAL_ATTENTION_CONTRACT && nonEmpty(one.domainId) && nonEmpty(one.attentionId) && nonEmpty(one.dedupeKey) && nonEmpty(one.kind) && nonEmpty(one.subjectLabel) && nonEmpty(one.observedAt) && nonEmpty(one.evidenceFingerprint) && OPERATIONAL_ATTENTION_TRANSITIONS.includes(one.transition) && OPERATIONAL_ATTENTION_IMPACTS.includes(one.impact) && Array.isArray(one.facts) && one.facts.length > 0 && one.facts.every(isOperationalFactReference);
4478
+ }
4479
+ function assertOperationalAttentionV1(value) {
4480
+ if (!isOperationalAttentionV1(value)) {
4481
+ throw new Error("invalid operational.attention.v1: lifecycle fields and stable source-fact references are required");
4482
+ }
4483
+ }
4484
+ function isOperationalFactReference(value) {
4485
+ const one = value;
4486
+ return !!one && typeof one === "object" && nonEmpty(one.system) && nonEmpty(one.type) && nonEmpty(one.id) && (one.observedAt === void 0 || nonEmpty(one.observedAt)) && (one.label === void 0 || nonEmpty(one.label));
4487
+ }
4488
+ function nonEmpty(value) {
4489
+ return typeof value === "string" && value.trim().length > 0;
4490
+ }
4456
4491
  // Annotate the CommonJS export names for ESM import in node:
4457
4492
  0 && (module.exports = {
4458
4493
  BIZSTEP,
@@ -4494,6 +4529,9 @@ function commandSpecGaps(specs, command) {
4494
4529
  MES_TYPES,
4495
4530
  METER_DIRECTION,
4496
4531
  OBSERVATION_BASIS,
4532
+ OPERATIONAL_ATTENTION_CONTRACT,
4533
+ OPERATIONAL_ATTENTION_IMPACTS,
4534
+ OPERATIONAL_ATTENTION_TRANSITIONS,
4497
4535
  OP_EVENT,
4498
4536
  OP_PARAM,
4499
4537
  ORDER_TERMINAL_STATUS,
@@ -4508,6 +4546,7 @@ function commandSpecGaps(specs, command) {
4508
4546
  UTC_OFFSET,
4509
4547
  VOCABULARY_EXCEPTIONS,
4510
4548
  VOCABULARY_TYPE,
4549
+ WEBHOOK_LANE,
4511
4550
  WEBHOOK_SECRET_BASE,
4512
4551
  WEBHOOK_SECRET_NAME_SHAPE,
4513
4552
  WEBHOOK_STATUS,
@@ -4524,6 +4563,7 @@ function commandSpecGaps(specs, command) {
4524
4563
  analyzeCapacity,
4525
4564
  approveCommand,
4526
4565
  asApproved,
4566
+ assertOperationalAttentionV1,
4527
4567
  axesOfSystem,
4528
4568
  axisAppliesTo,
4529
4569
  axisInfo,
@@ -4590,6 +4630,7 @@ function commandSpecGaps(specs, command) {
4590
4630
  isEquipmentStatus,
4591
4631
  isFailureStatus,
4592
4632
  isMasterDataRecord,
4633
+ isOperationalAttentionV1,
4593
4634
  isOperationalRecord,
4594
4635
  isOrderTerminal,
4595
4636
  isPlannedStopStatus,
@@ -4658,6 +4699,7 @@ function commandSpecGaps(specs, command) {
4658
4699
  validateEpcisEvent,
4659
4700
  validatePerformance,
4660
4701
  validateScenario,
4702
+ webhookLaneOf,
4661
4703
  webhookSecretCandidates,
4662
4704
  webhookSecretEnvName,
4663
4705
  webhookSecretFrom,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@operato/ops-contract",
3
- "version": "0.9.16",
3
+ "version": "0.9.19",
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",