@operato/ops-contract 0.9.8 → 0.9.10

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.
@@ -34,7 +34,7 @@ export interface OperationalIngestOptions {
34
34
  * 거절도 경고도 없었다. **같은 계약의 ERP 경로는 이것을 잡는다**(§`erp.ts` 의 자재 줄 검사).
35
35
  * 한 계약 안에 검사가 두 벌이고 한쪽만 봤다.
36
36
  *
37
- * 이름을 맞춰 달라고 원본에 청하는 것으로는 다음에 또 조용해진다 다음 원본이 `use: 'out'` 같은
37
+ * 이름을 맞춰 달라고 원본에 요청하는 것으로는 다음에 또 오류보고없이 넘어간다. 다음 원본이 `use: 'out'` 같은
38
38
  * 것을 보내면 같은 일이 난다(실제로 그런 시드가 있었다: 계약에 `out` 이라는 낱말이 없다).
39
39
  */
40
40
  export interface ObjectShape {
@@ -62,6 +62,25 @@ export interface ObjectShape {
62
62
  */
63
63
  export declare function operationalKindOf(record: unknown): OperationalKind | undefined;
64
64
  /** 이 레코드가 운영 사실인가 — 호스트의 라우팅이 묻는 자리. */
65
+ /**
66
+ * 그 종류의 사실을 **무엇이 가리키나** — 선언된 identity 칸의 이름.
67
+ *
68
+ * ── 왜 내보내나 (2026-09-06) ───────────────────────────────────────────────
69
+ * 이 선언은 여기 있는데 밖에서 읽을 수 없었다. 그래서 중복 판정(`ingest-dedupe.ts`)이 같은 목록을
70
+ * **손으로 옮겨** 들고 있었고, 새 종류가 생길 때마다 조용히 뒤처졌다.
71
+ *
72
+ * 실제로 두 번 그랬다.
73
+ *
74
+ * ```
75
+ * subjectId 처분 · 목록에 없어 정체를 아예 못 만들었다 → dedupe 밖
76
+ * testableObjectId 시험 · 목록에 없어 같은 명세·같은 시각의 다른 로트가 뭉칠 수 있었다
77
+ * ```
78
+ *
79
+ * 뒤엣것이 더 나쁘다. 중복은 눈에 보이지만 뭉친 것은 **사실 하나가 사라진 것**이고 아무 흔적이 없다.
80
+ *
81
+ * 모르는 종류에는 `undefined` 를 낸다 — 짐작으로 이름을 내지 않는다.
82
+ */
83
+ export declare function operationalIdentityField(kind: OperationalKind | undefined): string | undefined;
65
84
  export declare function isOperationalRecord(record: unknown): boolean;
66
85
  /**
67
86
  * 운영 레코드들을 봉투로 — 유효한 것만 통과하고 나머지는 **이유와 함께** 남는다.
@@ -401,6 +401,27 @@ export function operationalKindOf(record) {
401
401
  */
402
402
  const MATCH_ORDER = Object.keys(SPECS).sort((a, b) => (SPECS[a].matchOrder ?? 1000) - (SPECS[b].matchOrder ?? 1000));
403
403
  /** 이 레코드가 운영 사실인가 — 호스트의 라우팅이 묻는 자리. */
404
+ /**
405
+ * 그 종류의 사실을 **무엇이 가리키나** — 선언된 identity 칸의 이름.
406
+ *
407
+ * ── 왜 내보내나 (2026-09-06) ───────────────────────────────────────────────
408
+ * 이 선언은 여기 있는데 밖에서 읽을 수 없었다. 그래서 중복 판정(`ingest-dedupe.ts`)이 같은 목록을
409
+ * **손으로 옮겨** 들고 있었고, 새 종류가 생길 때마다 조용히 뒤처졌다.
410
+ *
411
+ * 실제로 두 번 그랬다.
412
+ *
413
+ * ```
414
+ * subjectId 처분 · 목록에 없어 정체를 아예 못 만들었다 → dedupe 밖
415
+ * testableObjectId 시험 · 목록에 없어 같은 명세·같은 시각의 다른 로트가 뭉칠 수 있었다
416
+ * ```
417
+ *
418
+ * 뒤엣것이 더 나쁘다. 중복은 눈에 보이지만 뭉친 것은 **사실 하나가 사라진 것**이고 아무 흔적이 없다.
419
+ *
420
+ * 모르는 종류에는 `undefined` 를 낸다 — 짐작으로 이름을 내지 않는다.
421
+ */
422
+ export function operationalIdentityField(kind) {
423
+ return kind ? SPECS[kind]?.identity : undefined;
424
+ }
404
425
  export function isOperationalRecord(record) {
405
426
  return operationalKindOf(record) !== undefined;
406
427
  }
package/dist/webhook.d.ts CHANGED
@@ -79,7 +79,7 @@ export interface WebhookEnvelope {
79
79
  /**
80
80
  * 번호 판정 — **네 가지이고, 합치지 않는다.**
81
81
  *
82
- * first 이 단위의 첫 봉투다. 받는 쪽에 커서가 없다
82
+ * first 이 단위의 첫 봉투다. 받는 쪽에 커서가 없다 — 그 앞을 몇 개 못 봤는지 함께 낸다
83
83
  * next 바로 다음 번호다
84
84
  * behind 이미 본 번호다. 재전송이다
85
85
  * gap 건너뛰었다. `expectedSeq` 부터 다시 받아야 한다
@@ -94,6 +94,24 @@ export interface WebhookEnvelope {
94
94
  export type SequenceVerdict = {
95
95
  kind: 'first';
96
96
  lastSeq: number;
97
+ /**
98
+ * 이 번호 아래로 몇 개가 있나 — **받은 적이 없는 개수**다(`seq - 1`).
99
+ *
100
+ * ── 왜 필요한가 (2026-09-06 실측, 인티그레이션 레인이 찾음) ──────────────
101
+ * plant 아웃박스의 1~20 이 400 으로 거절돼 그 연결에 커서가 없었다. 그래서 다음에 온 21번이
102
+ * **첫 봉투로 앉으며 커서가 21 로 뛰었다.** 커서가 0 이었다면 같은 21이 `gap` 이었다.
103
+ *
104
+ * 스무 건이 없는데 빈 구간으로도 안 세어지고, 경고도 없고, backfill 대상도 아니었다. backfill 은
105
+ * 커서(48)부터 묻기 때문에 영영 메워지지 않는다.
106
+ *
107
+ * ── 「잃었다」가 아니다 ────────────────────────────────────────────────
108
+ * 이 수가 곧 손실은 아니다. 원본이 오래 돌고 있는데 트윈을 나중에 붙이면 seq 4,712 부터 받는
109
+ * 것이 정상이고, 그 아래 4,711 개는 **애초에 안 받기로 한 것**이다.
110
+ *
111
+ * 계약은 둘을 구별할 수 없다. 그래서 「못 봤다」까지만 말하고 손실인지는 판정하지 않는다.
112
+ * 판정에 필요한 것(원본이 언제부터 보냈나)은 원본만 안다.
113
+ */
114
+ unseenBefore: number;
97
115
  } | {
98
116
  kind: 'next';
99
117
  lastSeq: number;
package/dist/webhook.js CHANGED
@@ -90,7 +90,7 @@ export function checkSequence(lastSeq, seq) {
90
90
  throw new Error(`checkSequence: 번호는 1 이상의 정수여야 한다 — 받은 값 ${JSON.stringify(seq)}`);
91
91
  }
92
92
  if (lastSeq === undefined || lastSeq === null)
93
- return { kind: 'first', lastSeq: seq };
93
+ return { kind: 'first', lastSeq: seq, unseenBefore: seq - 1 };
94
94
  if (seq <= lastSeq)
95
95
  return { kind: 'behind', lastSeq };
96
96
  if (seq === lastSeq + 1)
@@ -179,6 +179,7 @@ __export(index_exports, {
179
179
  observationAt: () => observationAt,
180
180
  offCalendarAt: () => offCalendarAt,
181
181
  offCalendarReasonAt: () => offCalendarReasonAt,
182
+ operationalIdentityField: () => operationalIdentityField,
182
183
  operationalKindOf: () => operationalKindOf,
183
184
  operationsCapabilityOf: () => operationsCapabilityOf,
184
185
  outsideLimit: () => outsideLimit,
@@ -3700,6 +3701,9 @@ function operationalKindOf(record) {
3700
3701
  var MATCH_ORDER = Object.keys(SPECS).sort(
3701
3702
  (a, b) => (SPECS[a].matchOrder ?? 1e3) - (SPECS[b].matchOrder ?? 1e3)
3702
3703
  );
3704
+ function operationalIdentityField(kind) {
3705
+ return kind ? SPECS[kind]?.identity : void 0;
3706
+ }
3703
3707
  function isOperationalRecord(record) {
3704
3708
  return operationalKindOf(record) !== void 0;
3705
3709
  }
@@ -4016,7 +4020,7 @@ function checkSequence(lastSeq, seq) {
4016
4020
  if (!Number.isInteger(seq) || seq < 1) {
4017
4021
  throw new Error(`checkSequence: \uBC88\uD638\uB294 1 \uC774\uC0C1\uC758 \uC815\uC218\uC5EC\uC57C \uD55C\uB2E4 \u2014 \uBC1B\uC740 \uAC12 ${JSON.stringify(seq)}`);
4018
4022
  }
4019
- if (lastSeq === void 0 || lastSeq === null) return { kind: "first", lastSeq: seq };
4023
+ if (lastSeq === void 0 || lastSeq === null) return { kind: "first", lastSeq: seq, unseenBefore: seq - 1 };
4020
4024
  if (seq <= lastSeq) return { kind: "behind", lastSeq };
4021
4025
  if (seq === lastSeq + 1) return { kind: "next", lastSeq: seq };
4022
4026
  return { kind: "gap", lastSeq, expectedSeq: lastSeq + 1 };
@@ -4497,6 +4501,7 @@ function commandSpecGaps(specs, command) {
4497
4501
  observationAt,
4498
4502
  offCalendarAt,
4499
4503
  offCalendarReasonAt,
4504
+ operationalIdentityField,
4500
4505
  operationalKindOf,
4501
4506
  operationsCapabilityOf,
4502
4507
  outsideLimit,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@operato/ops-contract",
3
- "version": "0.9.8",
3
+ "version": "0.9.10",
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",