@operato/ops-contract 0.9.17 → 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 · 커넥터)은 계약만 필요하고 엔진은 필요 없습니다. 한 패키지에 두면 레코드
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.17",
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",