@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 +58 -0
- package/dist/contract.d.ts +64 -30
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/operational-attention.d.ts +43 -0
- package/dist/operational-attention.js +35 -0
- package/dist/webhook.d.ts +17 -0
- package/dist/webhook.js +17 -0
- package/dist-cjs/index.cjs +42 -0
- package/package.json +1 -1
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 · 커넥터)은 계약만 필요하고 엔진은 필요 없습니다. 한 패키지에 두면 레코드
|
package/dist/contract.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
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
|
-
*
|
|
65
|
-
*
|
|
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
package/dist/index.js
CHANGED
|
@@ -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
|
*
|
package/dist-cjs/index.cjs
CHANGED
|
@@ -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.
|
|
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",
|