@operato/twin-kernel 0.7.51 → 0.7.52

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.
@@ -7,7 +7,60 @@ export interface CanonicalEnvelope<T = unknown> {
7
7
  correlationId?: string;
8
8
  data: T;
9
9
  }
10
+ /**
11
+ * 작업의 상태 — **ISA-95 근거.** 1차 출처: `JobOrderType.DispatchStatus`(지시 쪽) +
12
+ * `JobResponseType.JobState`(실적 쪽). 대조 기록: `design/plans/isa95-coverage.md` §3-2
13
+ * (「`tasks.status` ↔ `DispatchStatus`+`JobState`」).
14
+ *
15
+ * ── 왜 우리 이름을 쓰나 ─────────────────────────────────────────────────────
16
+ * 표준은 **지시(`JobOrder`)와 실적(`JobResponse`)을 나눈다.** 우리 `TaskState` 는 그 둘을 한 몸에
17
+ * 든다 — 상태 전이와 배정 결과를 함께 들고 진행·완료까지 같은 개체가 표현한다. 그래서 `jobOrders`
18
+ * 로 부르면 **표준 이름을 쓰면서 다른 뜻으로 쓰는 것**이 된다. 대조 문서가 그 판정을 적어 두었다.
19
+ *
20
+ * ── 왜 **닫혀** 있나 ────────────────────────────────────────────────────────
21
+ * 커널이 이 넷으로 접는다: 성과 폴드가 착수·완료로 갈리고(`foldTaskRecords`), 주목 계산이
22
+ * `created`·`assigned` 를 대기로 센다, 배정 루프는 `created` 만 집는다. 그러니 낱말이 하나 늘면
23
+ * 그 셋이 조용히 달라진다 — 유입 문이 이 넷만 받는 이유다(다른 낱말은 오류 없이 사라진다).
24
+ */
10
25
  export type TaskStatus = 'created' | 'assigned' | 'in-progress' | 'completed';
26
+ /** 오더가 **종결 상태**로 인정되는 낱말 — 이 밖은 진행 중이다(§`isOrderTerminal`). */
27
+ export declare const ORDER_TERMINAL_STATUS: readonly ["completed", "cancelled"];
28
+ /**
29
+ * 이 오더는 **끝났나** — 판정을 한 곳에 둔다(`locationStatusOf`·`dueStatusOf` 와 같은 규율).
30
+ *
31
+ * ── 왜 함수여야 하나 (2026-08-23) ───────────────────────────────────────────
32
+ * 이 판정이 없던 동안 호스트의 성과 폴드가 **낱말을 동의어 목록으로 추론했다**
33
+ * (`completed`·`fulfilled`·`done`·`finished`). 그 방식이 낸 대가가 둘이다.
34
+ *
35
+ * ① **원본의 낱말이 목록에 없으면 완료가 조용히 0 이 된다.** 실제로 그랬다 — 첫 실 시스템의
36
+ * `FINISHED` 가 빠져 있었고, 사람이 화면을 보고 의심해서야 드러났다. 그리고 그때 목록에 낱말을
37
+ * 더한 것은 진짜 빈 자리(커넥터가 자기 상태 표를 안 쓰고 있었다)를 가린 땜빵이었다.
38
+ * ② **시뮬 트윈에서는 언제나 0 이었다.** 시뮬은 오더를 이행하면서 `fulfilled` 만 올리고 `status` 는
39
+ * 바꾸지 않는다(그런 대입이 코드에 없다). 그래서 낱말로 묻는 판정은 시뮬의 완료를 **한 건도**
40
+ * 세지 못했다. 낱말이 틀린 것이 아니라 **묻는 축이 틀렸다.**
41
+ *
42
+ * ── 표준이 그 축을 이미 갈라 두었다 ─────────────────────────────────────────
43
+ * `orders` 는 `OperationsRequest` 쪽이고 그 상태는 `RequestState` 다 — **표준도 그 값을 열거하지
44
+ * 않는다.** 그래서 이 축이 열려 있는 것은 방언이 아니라 표준 정합이다. 대신 표준은 「됐나」를 **실적**
45
+ * 에서 읽는다(`JobResponse`·`SegmentResponse`, 그리고 요구는 `SegmentRequirement.Quantity`).
46
+ *
47
+ * 그러므로 이 판정의 1차 근거는 **양**이다: 요구한 만큼 이행됐으면 끝난 것이고, 그 사실은 어느 원본의
48
+ * 어느 낱말과도 무관하다. 낱말은 **양으로 말할 수 없는 종결**을 위해 함께 본다 — 취소, 그리고 요구량을
49
+ * 채우지 못한 채 닫힌 오더.
50
+ *
51
+ * ── 도메인이 상태에 다른 것을 적는다 ────────────────────────────────────────
52
+ * MES 커널은 이 자리에 **진행 단계**를 적는다(`op-<공정키>`, `blocked-seed-incomplete`). 그 값들은
53
+ * 종결이 아니므로 이 판정이 옳게 「아니다」로 답한다. 다만 진행 단계를 상태 문자열에 적는 것 자체가
54
+ * 표준의 모양은 아니다(그것은 `SegmentResponse` 의 일이다) — 그 빚은 ADR-0039 에 조건과 함께 적었다.
55
+ *
56
+ * 모르면 `false` 다 — 「끝나지 않았다」가 아니라 **「끝났다고 말할 근거가 없다」**이고, 성과는 근거가
57
+ * 있는 것만 센다(없는 완료를 세면 처리량이 조용히 부풀려진다).
58
+ */
59
+ export declare function isOrderTerminal(o: {
60
+ status?: string;
61
+ requested?: number;
62
+ fulfilled?: number;
63
+ }): boolean;
11
64
  /**
12
65
  * 자리의 상태 — **포화도에서 파생한다.** 저장하는 값이 아니다.
13
66
  *
package/dist/contract.js CHANGED
@@ -32,6 +32,50 @@
32
32
  * 지키는 하네스: `derived-not-input` · `actuals-come-from-transitions` · `accumulators-survive-restart` ·
33
33
  * `purpose-conformance`.
34
34
  */
35
+ /** 오더가 **종결 상태**로 인정되는 낱말 — 이 밖은 진행 중이다(§`isOrderTerminal`). */
36
+ export const ORDER_TERMINAL_STATUS = ['completed', 'cancelled'];
37
+ /**
38
+ * 이 오더는 **끝났나** — 판정을 한 곳에 둔다(`locationStatusOf`·`dueStatusOf` 와 같은 규율).
39
+ *
40
+ * ── 왜 함수여야 하나 (2026-08-23) ───────────────────────────────────────────
41
+ * 이 판정이 없던 동안 호스트의 성과 폴드가 **낱말을 동의어 목록으로 추론했다**
42
+ * (`completed`·`fulfilled`·`done`·`finished`). 그 방식이 낸 대가가 둘이다.
43
+ *
44
+ * ① **원본의 낱말이 목록에 없으면 완료가 조용히 0 이 된다.** 실제로 그랬다 — 첫 실 시스템의
45
+ * `FINISHED` 가 빠져 있었고, 사람이 화면을 보고 의심해서야 드러났다. 그리고 그때 목록에 낱말을
46
+ * 더한 것은 진짜 빈 자리(커넥터가 자기 상태 표를 안 쓰고 있었다)를 가린 땜빵이었다.
47
+ * ② **시뮬 트윈에서는 언제나 0 이었다.** 시뮬은 오더를 이행하면서 `fulfilled` 만 올리고 `status` 는
48
+ * 바꾸지 않는다(그런 대입이 코드에 없다). 그래서 낱말로 묻는 판정은 시뮬의 완료를 **한 건도**
49
+ * 세지 못했다. 낱말이 틀린 것이 아니라 **묻는 축이 틀렸다.**
50
+ *
51
+ * ── 표준이 그 축을 이미 갈라 두었다 ─────────────────────────────────────────
52
+ * `orders` 는 `OperationsRequest` 쪽이고 그 상태는 `RequestState` 다 — **표준도 그 값을 열거하지
53
+ * 않는다.** 그래서 이 축이 열려 있는 것은 방언이 아니라 표준 정합이다. 대신 표준은 「됐나」를 **실적**
54
+ * 에서 읽는다(`JobResponse`·`SegmentResponse`, 그리고 요구는 `SegmentRequirement.Quantity`).
55
+ *
56
+ * 그러므로 이 판정의 1차 근거는 **양**이다: 요구한 만큼 이행됐으면 끝난 것이고, 그 사실은 어느 원본의
57
+ * 어느 낱말과도 무관하다. 낱말은 **양으로 말할 수 없는 종결**을 위해 함께 본다 — 취소, 그리고 요구량을
58
+ * 채우지 못한 채 닫힌 오더.
59
+ *
60
+ * ── 도메인이 상태에 다른 것을 적는다 ────────────────────────────────────────
61
+ * MES 커널은 이 자리에 **진행 단계**를 적는다(`op-<공정키>`, `blocked-seed-incomplete`). 그 값들은
62
+ * 종결이 아니므로 이 판정이 옳게 「아니다」로 답한다. 다만 진행 단계를 상태 문자열에 적는 것 자체가
63
+ * 표준의 모양은 아니다(그것은 `SegmentResponse` 의 일이다) — 그 빚은 ADR-0039 에 조건과 함께 적었다.
64
+ *
65
+ * 모르면 `false` 다 — 「끝나지 않았다」가 아니라 **「끝났다고 말할 근거가 없다」**이고, 성과는 근거가
66
+ * 있는 것만 센다(없는 완료를 세면 처리량이 조용히 부풀려진다).
67
+ */
68
+ export function isOrderTerminal(o) {
69
+ const status = String(o?.status ?? '').trim().toLowerCase();
70
+ if (ORDER_TERMINAL_STATUS.includes(status))
71
+ return true;
72
+ /* 양으로 판정 — 요구량이 없으면 판정하지 않는다(0 을 「다 됐다」로 읽지 않는다). */
73
+ const requested = o?.requested;
74
+ const fulfilled = o?.fulfilled;
75
+ if (typeof requested !== 'number' || !(requested > 0))
76
+ return false;
77
+ return typeof fulfilled === 'number' && fulfilled >= requested;
78
+ }
35
79
  /**
36
80
  * 자리의 상태 — **포화도에서 파생한다.** 저장하는 값이 아니다.
37
81
  *
@@ -9,7 +9,7 @@
9
9
  * 통합 타입은 도메인 필드를 옵셔널로 넓혀(FlowItem.gtin?, FlowOrder.shipmentEpc? 등) 두 도메인을 담는다.
10
10
  * (roadmap Phase5 발견 → 추출. [[project_flow_single_base_vision]] FlowLocation 단일 base 방향과 정합.)
11
11
  */
12
- import { OP_EVENT, CMD, locationStatusOf, readBoardEquipment, readBoardLocations, readBoardAssets, classClosure, capabilityOf, requiredTestsFor, priorityRank, dueStatusOf, effectivityAt, offCalendarAt, offCalendarReasonAt, minuteOfDayAt, activeShiftAt, subLotIdOf, itemKeyOf, identityGroundingOf } from "./contract.js";
12
+ import { OP_EVENT, CMD, locationStatusOf, readBoardEquipment, readBoardLocations, readBoardAssets, classClosure, capabilityOf, requiredTestsFor, priorityRank, dueStatusOf, isOrderTerminal, effectivityAt, offCalendarAt, offCalendarReasonAt, minuteOfDayAt, activeShiftAt, subLotIdOf, itemKeyOf, identityGroundingOf } from "./contract.js";
13
13
  import { ObservedReducer } from "./observed-reducer.js";
14
14
  import { transformationEvent, aggregationEvent, objectEvent, parseEpc, DISP, ILMD_ATTR, CBV_BIZSTEP, objectUri, bizTransactionUri, gdtiUri } from "./epcis.js";
15
15
  import { OP_PARAM } from "./domain-definition.js";
@@ -217,7 +217,9 @@ thresholds) {
217
217
  지연으로도 정시로도 말하지 않는다). 이미 끝난 오더는 대상이 아니다.
218
218
  심각도는 얼마나 늦었는지로 구분한다 — 방금 넘긴 것과 하루 넘긴 것을 같게 부르면 신호가 무의미해진다. */
219
219
  for (const o of view.orders) {
220
- if (o.status === 'completed' || o.status === 'fulfilled')
220
+ /* 끝난 오더는 지연 신호의 대상이 아니다 — 판정은 한 곳에 있다(§`isOrderTerminal`).
221
+ 예전에는 여기서 낱말 둘을 견줬고, 그러면 양으로만 끝난 오더(시뮬이 그렇다)가 영원히 늦었다고 말한다. */
222
+ if (isOrderTerminal(o))
221
223
  continue;
222
224
  if (dueStatusOf(o, nowIso) !== 'late')
223
225
  continue;
@@ -2113,7 +2115,8 @@ export class FlowEngine {
2113
2115
  committedDemands() {
2114
2116
  const out = [];
2115
2117
  for (const o of this.orders.values()) {
2116
- if (o.status === 'completed' || o.status === 'cancelled')
2118
+ /* 같은 판정을 쓴다 남은 수요를 세는 자리와 지연을 세는 자리가 다른 규칙을 쓰면 어긋난다. */
2119
+ if (isOrderTerminal(o))
2117
2120
  continue;
2118
2121
  const remaining = Math.max(0, (o.requested ?? 0) - (o.fulfilled ?? 0));
2119
2122
  if (!remaining)
@@ -56,6 +56,7 @@ __export(index_exports, {
56
56
  OPERATION_PROPERTY: () => OPERATION_PROPERTY,
57
57
  OP_EVENT: () => OP_EVENT,
58
58
  OP_PARAM: () => OP_PARAM,
59
+ ORDER_TERMINAL_STATUS: () => ORDER_TERMINAL_STATUS,
59
60
  ObservedReducer: () => ObservedReducer,
60
61
  PRIORITY_UNSET: () => PRIORITY_UNSET,
61
62
  RETIRED_VOCABULARY: () => RETIRED_VOCABULARY,
@@ -126,6 +127,7 @@ __export(index_exports, {
126
127
  isEnergyRecord: () => isEnergyRecord,
127
128
  isEquipmentLevel: () => isEquipmentLevel,
128
129
  isOperationalRecord: () => isOperationalRecord,
130
+ isOrderTerminal: () => isOrderTerminal,
129
131
  isTransformationRecord: () => isTransformationRecord,
130
132
  isoDurationHours: () => isoDurationHours,
131
133
  itemKeyOf: () => itemKeyOf,
@@ -179,6 +181,15 @@ __export(index_exports, {
179
181
  module.exports = __toCommonJS(index_exports);
180
182
 
181
183
  // src/contract.ts
184
+ var ORDER_TERMINAL_STATUS = ["completed", "cancelled"];
185
+ function isOrderTerminal(o) {
186
+ const status = String(o?.status ?? "").trim().toLowerCase();
187
+ if (ORDER_TERMINAL_STATUS.includes(status)) return true;
188
+ const requested = o?.requested;
189
+ const fulfilled = o?.fulfilled;
190
+ if (typeof requested !== "number" || !(requested > 0)) return false;
191
+ return typeof fulfilled === "number" && fulfilled >= requested;
192
+ }
182
193
  var LOCATION_SATURATION_NEAR = 0.9;
183
194
  function locationStatusOf(n) {
184
195
  const cap = n.capacity;
@@ -3429,7 +3440,7 @@ function deriveAttentions(view, acked, nowIso, thresholds) {
3429
3440
  });
3430
3441
  }
3431
3442
  for (const o of view.orders) {
3432
- if (o.status === "completed" || o.status === "fulfilled") continue;
3443
+ if (isOrderTerminal(o)) continue;
3433
3444
  if (dueStatusOf(o, nowIso) !== "late") continue;
3434
3445
  const overdueMs = Date.parse(nowIso) - Date.parse(o.endTime);
3435
3446
  out.push({
@@ -5033,7 +5044,7 @@ var FlowEngine = class {
5033
5044
  committedDemands() {
5034
5045
  const out = [];
5035
5046
  for (const o of this.orders.values()) {
5036
- if (o.status === "completed" || o.status === "cancelled") continue;
5047
+ if (isOrderTerminal(o)) continue;
5037
5048
  const remaining = Math.max(0, (o.requested ?? 0) - (o.fulfilled ?? 0));
5038
5049
  if (!remaining) continue;
5039
5050
  out.push({ remaining, ...o.endTime ? { dueTime: o.endTime } : {} });
@@ -9015,6 +9026,7 @@ function retiredVocabularyIn(line) {
9015
9026
  OPERATION_PROPERTY,
9016
9027
  OP_EVENT,
9017
9028
  OP_PARAM,
9029
+ ORDER_TERMINAL_STATUS,
9018
9030
  ObservedReducer,
9019
9031
  PRIORITY_UNSET,
9020
9032
  RETIRED_VOCABULARY,
@@ -9085,6 +9097,7 @@ function retiredVocabularyIn(line) {
9085
9097
  isEnergyRecord,
9086
9098
  isEquipmentLevel,
9087
9099
  isOperationalRecord,
9100
+ isOrderTerminal,
9088
9101
  isTransformationRecord,
9089
9102
  isoDurationHours,
9090
9103
  itemKeyOf,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@operato/twin-kernel",
3
- "version": "0.7.51",
3
+ "version": "0.7.52",
4
4
  "type": "module",
5
5
  "description": "Twin Domain Kernel — framework-agnostic, zero-dep (domain + sim + 3-channel contract). WMS/YMS/MES, EPCIS 2.0 · ISA-95.",
6
6
  "publishConfig": {