@operato/twin-kernel 0.7.50 → 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.
- package/dist/contract.d.ts +53 -0
- package/dist/contract.js +74 -0
- package/dist/flow-engine.d.ts +37 -1
- package/dist/flow-engine.js +108 -12
- package/dist-cjs/index.cjs +78 -12
- package/package.json +1 -1
package/dist/contract.d.ts
CHANGED
|
@@ -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
|
@@ -1,7 +1,81 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* Face 1 — 3채널 계약 (walking skeleton 범위).
|
|
3
3
|
* 설계 SoT: operato-twin/design/integration/face1-contract.md
|
|
4
|
+
*
|
|
5
|
+
* ── 이 계약을 넓히려는 사람이 먼저 읽을 것 (2026-08-23) ──────────────────────
|
|
6
|
+
*
|
|
7
|
+
* 사실은 두 종류이고, 종류가 그 자리를 정한다.
|
|
8
|
+
*
|
|
9
|
+
* **파생** — 시각만으로 넘어가는 판정. 근무 밖 · 교대 · 능력 · 자리 상태 · 납기 대비 · 진척.
|
|
10
|
+
* · **저장하지 않는다.** 저장하면 장부가 둘이 되고, 시각이 지나도 옛 값이 남는다.
|
|
11
|
+
* · **입력으로 받지 않는다.** 원본이 단정하면 답이 둘이 되고 커널이 어느 쪽을 믿을지 정해야 한다.
|
|
12
|
+
* · 계산할 수 없으면 **답하지 않는다.** 모르는 값을 숫자로 만들면 화면이 그것을 사실로 그린다
|
|
13
|
+
* (`dueStatusOf` 가 그 규율을 먼저 적었다: 납기가 없으면 「늦지 않았다」가 아니라 「판단할 수 없다」).
|
|
14
|
+
* · 파생이 **불가능할 때만** 원본의 단정을 쓴다. 그 순서가 선언돼 있어야 한다(`offShift`·`progress`).
|
|
15
|
+
*
|
|
16
|
+
* **누적** — 열린 구간에 쌓이는 값. 가동·준비·고장 시간 · 양품/불량 수 · 조건이 성립한 시각.
|
|
17
|
+
* · **다시 계산할 수 없다.** 원천은 「이번 창에서 지금까지 얼마」를 모른다.
|
|
18
|
+
* · 그래서 **재기동을 넘어 이어받는다.** 잃으면 오류 없이 값이 작아진다 — 가장 비싼 종류의 침묵이다.
|
|
19
|
+
*
|
|
20
|
+
* 그리고 **자리를 늘리는 조건은 하나다: 전이로 표현할 수 없는 사실.**
|
|
21
|
+
*
|
|
22
|
+
* 「자리가 없다」와 「담을 수 없다」는 다르다. 전자는 대개 이미 있는 축을 못 찾은 것이다 — 작업의
|
|
23
|
+
* 실제 착수·완료·대기는 **전이의 `eventTime`** 이 말하고(`task-fold`), 대기는 그것으로 계산된다
|
|
24
|
+
* (`kpi-fold`: `waitMs = startedMs − createdMs`). 원본이 전이를 놓쳐 보내면 **놓친 전이를 그 시각으로
|
|
25
|
+
* 함께 내는 것**이 답이고, 속성을 새로 여는 것이 아니다.
|
|
26
|
+
*
|
|
27
|
+
* 그러므로 원본을 붙이는 사람이 물어야 하는 것은 「이 컬럼을 실을 자리가 있나」가 아니라
|
|
28
|
+
* **「원본이 말하는 이 사실을 커널의 어휘로 어떻게 옮기나」**다. 컬럼 단위로 물으면 원본이 열이면
|
|
29
|
+
* 축도 열이 된다 — 2026-08-23 에 축 다섯을 열었다가 그 이유로 되돌렸다.
|
|
30
|
+
*
|
|
31
|
+
* 열지 않기로 한 것과 그 조건은 `design/04-decisions.md` ADR-0039 에 있다. 넓히기 전에 그 표를 본다.
|
|
32
|
+
* 지키는 하네스: `derived-not-input` · `actuals-come-from-transitions` · `accumulators-survive-restart` ·
|
|
33
|
+
* `purpose-conformance`.
|
|
4
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
|
+
}
|
|
5
79
|
/**
|
|
6
80
|
* 자리의 상태 — **포화도에서 파생한다.** 저장하는 값이 아니다.
|
|
7
81
|
*
|
package/dist/flow-engine.d.ts
CHANGED
|
@@ -78,6 +78,15 @@ export interface FlowPerson extends EffectivePeriod {
|
|
|
78
78
|
};
|
|
79
79
|
/** 근무 캘린더 — 표준 `WorkCalendarEntry`(복수). 있으면 `window` 보다 이것을 본다. */
|
|
80
80
|
workCalendar?: WorkCalendarEntry[];
|
|
81
|
+
/**
|
|
82
|
+
* **원본이 말한 근무 밖 표시** — 캘린더가 없을 때만 쓰는 값이다.
|
|
83
|
+
*
|
|
84
|
+
* 캘린더가 있으면 시각으로 판정한다(`offCalendar`) — 저장값을 함께 보면 교대 안으로 돌아왔을 때 옛
|
|
85
|
+
* `true` 가 남는다. 그런데 근무 캘린더를 주지 않는 원본이 실 시스템에 흔하고, 그때 이것이 **유일한
|
|
86
|
+
* 답**이다. 리듀서가 이미 그 규율로 이 값을 지키고 있었는데(§`observed-reducer` offShift) 커널 상태에
|
|
87
|
+
* 자리가 없어 옮기는 자리에서 잃고 있었다.
|
|
88
|
+
*/
|
|
89
|
+
offShift?: boolean;
|
|
81
90
|
/** 지금 어디에 있나 — 표준 `Person.OperationalLocation`. 커널은 사람을 움직이지 않는다(마스터·관측이 말한다). */
|
|
82
91
|
location?: string;
|
|
83
92
|
/** 자원 속성 — 표준 `PersonProperty`. */
|
|
@@ -165,6 +174,15 @@ export interface FlowTask {
|
|
|
165
174
|
/** 예정 창 — 표준 `JobOrder.StartTime`/`EndTime`. 실제 착수와 다른 축. */
|
|
166
175
|
startTime?: string;
|
|
167
176
|
endTime?: string;
|
|
177
|
+
/**
|
|
178
|
+
* **원본이 말한 진척** — 커널이 계산할 수 없을 때만 쓰는 값이다(§`progressOf`).
|
|
179
|
+
*
|
|
180
|
+
* 파생이 가능하면(소요를 알면) 파생이 이긴다. 이 자리는 파생을 대신하는 것이 아니라 **파생이
|
|
181
|
+
* 불가능할 때 침묵하지 않기 위한 것**이다 — 사람 교대(`offShift`)가 세워 둔 규율과 같다.
|
|
182
|
+
*
|
|
183
|
+
* 관측에서만 채운다. 시뮬은 자기 소요를 알므로 이 값을 쓸 일이 없다.
|
|
184
|
+
*/
|
|
185
|
+
progress?: number;
|
|
168
186
|
/** 이 작업에 투입된 사람들 — 설비와 별개 축(설비 1대 + 작업자 2명이 동시에 잡힌다). */
|
|
169
187
|
personnel?: string[];
|
|
170
188
|
/** 이 작업에 투입된 물리 자산(팔레트 등). */
|
|
@@ -1559,7 +1577,25 @@ export declare abstract class FlowEngine implements TwinKernel {
|
|
|
1559
1577
|
* 로트는 LGTIN 이면 식별자 안에 있고, 직렬 개체는 마스터데이터(`ilmd`)에 실려 온다.
|
|
1560
1578
|
*/
|
|
1561
1579
|
protected itemState(i: FlowItem): ItemState;
|
|
1562
|
-
|
|
1580
|
+
/**
|
|
1581
|
+
* 진척 — **모르면 답하지 않는다.**
|
|
1582
|
+
*
|
|
1583
|
+
* ── 무엇이 틀려 있었나 (2026-08-23 실측) ───────────────────────────────────
|
|
1584
|
+
* 예전에는 `durationMs <= 0` 이면 `1` 을 돌려줬다. 시뮬에서는 그것이 맞다(소요 0 = 즉시 끝나는 일).
|
|
1585
|
+
* 그런데 **미러에서 `durationMs` 0 은 「즉시」가 아니라 「모른다」다** — 원본이 소요를 말해 주지
|
|
1586
|
+
* 않으면 씨앗 경로가 0 으로 메우고, 그러면 이 함수가 **진행 중인 작업을 100% 라고 말한다.**
|
|
1587
|
+
*
|
|
1588
|
+
* 실측: 소요를 주지 않는 원본의 진행 중 작업이 `progress: 1` 로 나왔다. 「모름」을 「완료」로 말하는
|
|
1589
|
+
* 것이고, 화면은 그것을 사실로 그린다.
|
|
1590
|
+
*
|
|
1591
|
+
* 시뮬에서는 이 반환이 바뀌지 않는다: 소요 0 인 작업은 배정된 tick 안에서 끝나므로 **진행 중으로
|
|
1592
|
+
* 관측되지 않는다**(`status = 'in-progress'; remainingMs = durationMs` → 다음 진행에서 즉시 완료).
|
|
1593
|
+
* 즉 `durationMs <= 0` 이면서 진행 중인 작업은 미러 경로에서만 나온다.
|
|
1594
|
+
*
|
|
1595
|
+
* 모를 때 원본이 진척을 말해 주었다면 그 값을 쓴다 — 그 규칙은 부르는 쪽에 있다(§`getSnapshot`).
|
|
1596
|
+
* 이 함수는 **커널이 계산할 수 있나**만 답한다.
|
|
1597
|
+
*/
|
|
1598
|
+
protected progressOf(t: FlowTask): number | undefined;
|
|
1563
1599
|
private generate;
|
|
1564
1600
|
private processOrders;
|
|
1565
1601
|
/**
|
package/dist/flow-engine.js
CHANGED
|
@@ -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
|
-
|
|
220
|
+
/* 끝난 오더는 지연 신호의 대상이 아니다 — 판정은 한 곳에 있다(§`isOrderTerminal`).
|
|
221
|
+
예전에는 여기서 낱말 둘을 견줬고, 그러면 양으로만 끝난 오더(시뮬이 그렇다)가 영원히 늦었다고 말한다. */
|
|
222
|
+
if (isOrderTerminal(o))
|
|
221
223
|
continue;
|
|
222
224
|
if (dueStatusOf(o, nowIso) !== 'late')
|
|
223
225
|
continue;
|
|
@@ -946,6 +948,15 @@ export class FlowEngine {
|
|
|
946
948
|
id: p.id, personnelClassIds: p.personnelClassIds ?? prev?.personnelClassIds,
|
|
947
949
|
status: purpose === 'observe' ? (p.status ?? 'idle') : 'idle',
|
|
948
950
|
taskId: purpose === 'observe' ? (p.taskId ?? null) : null,
|
|
951
|
+
/*
|
|
952
|
+
* 원본이 말한 근무 밖 표시 — **관측일 때만, 그리고 선언이 없을 때만 쓰인다.**
|
|
953
|
+
*
|
|
954
|
+
* 이 규율의 원본 자리는 리듀서다(§`offShift`: 「선언이 없을 때만 받는다 — 선언이 있으면
|
|
955
|
+
* `snapshot()` 이 시각으로 판정하고, 저장값을 함께 두면 교대 안으로 돌아왔을 때 옛 `true` 가
|
|
956
|
+
* 남는다」). 그런데 이 자리가 그 값을 **떨어뜨리고 있었다** — 리듀서가 지킨 것을 여기서 잃었다.
|
|
957
|
+
* 근무 캘린더가 없는 원본(실 시스템에 흔하다)에서는 그것이 유일한 답이다.
|
|
958
|
+
*/
|
|
959
|
+
...(purpose === 'observe' && p.offShift ? { offShift: true } : {}),
|
|
949
960
|
...(p.location ? { location: p.location } : {}),
|
|
950
961
|
...(p.properties ? { properties: p.properties } : {}),
|
|
951
962
|
...(p.testSpecificationIds ? { testSpecificationIds: p.testSpecificationIds } : {}),
|
|
@@ -1026,11 +1037,35 @@ export class FlowEngine {
|
|
|
1026
1037
|
const remaining = lines.length
|
|
1027
1038
|
? lines.reduce((s, l) => s + l.requested, 0)
|
|
1028
1039
|
: Math.max(0, (o.requested ?? 0) - (o.fulfilled ?? 0));
|
|
1029
|
-
|
|
1030
|
-
|
|
1040
|
+
/*
|
|
1041
|
+
* ── **씨앗은 남은 데맨드로, 미러는 들은 그대로** (2026-08-23) ──────────────
|
|
1042
|
+
*
|
|
1043
|
+
* 아래 세 값(`status`·`requested`·`fulfilled`)은 씨앗의 규약이다: 씨앗은 「무엇이 남았나」로
|
|
1044
|
+
* 미래를 굴리므로 요청 10·이행 4 를 **요청 6·이행 0·상태 created** 로 다시 표현한다. 그리고 이미
|
|
1045
|
+
* 이행된 오더는 예측 대상이 아니므로 건너뛴다.
|
|
1046
|
+
*
|
|
1047
|
+
* **미러에는 전부 틀렸다.** 미러는 굴리지 않고 들은 것을 말한다. 실측(포천 미러):
|
|
1048
|
+
*
|
|
1049
|
+
* 원본이 말한 것 picking · 요청 10 · 이행 4 · 진척 0.4
|
|
1050
|
+
* 커널이 낸 것 created · 요청 6 · 이행 0 · 진척 0 ← 40% 된 오더가 0% 로 보였다
|
|
1051
|
+
*
|
|
1052
|
+
* 그리고 이행 완료된 오더는 **상태에서 아예 사라졌다** — 원본이 그 오더가 있다고 말하는데
|
|
1053
|
+
* 트윈은 없다고 말한다. 오더의 상태 낱말은 계약이 도메인에 넘긴 것인데(`picking`·`shipped`…)
|
|
1054
|
+
* 씨앗 규약이 그것을 커널 낱말로 덮고 있었다.
|
|
1055
|
+
*
|
|
1056
|
+
* 이것은 작업에서 고친 것과 **같은 결함의 세 번째 사례**다(§`purpose`): 한 함수가 두 목적을 겸하고
|
|
1057
|
+
* 씨앗 규약이 관측에 새어 나온다.
|
|
1058
|
+
*/
|
|
1059
|
+
const observing = purpose === 'observe';
|
|
1060
|
+
if (!observing && remaining <= 0)
|
|
1061
|
+
continue; // 씨앗: 이미 이행 완료 → 예측 대상 아님
|
|
1031
1062
|
const resolved = this.resolvedAllocated(o.allocated);
|
|
1032
1063
|
this.orders.set(o.orderId, {
|
|
1033
|
-
id: o.orderId,
|
|
1064
|
+
id: o.orderId,
|
|
1065
|
+
kind: o.kind,
|
|
1066
|
+
status: observing ? (o.status ?? 'created') : 'created',
|
|
1067
|
+
requested: observing ? (o.requested ?? 0) : remaining,
|
|
1068
|
+
fulfilled: observing ? (o.fulfilled ?? 0) : 0,
|
|
1034
1069
|
/* **확보해 둔 것과 거래번호를 이어받는다.** 비우면 되살아난 오더가 아무것도 안 잡은 것처럼
|
|
1035
1070
|
보이고, 그 오더의 진행 중 작업이 완료될 때 계보가 입력 없이 나간다. 상태가 말해 주지
|
|
1036
1071
|
않으면(옛 저널) 그때는 비는 것이 사실이다 — 없는 것을 지어내지 않는다. */
|
|
@@ -1039,7 +1074,9 @@ export class FlowEngine {
|
|
|
1039
1074
|
/* 레시피도 함께 — 품목만 이어받으면 되살아난 오더가 「무엇으로」를 모르고, 그 오더의
|
|
1040
1075
|
남은 공정·소요가 다른 레시피 기준으로 계산된다(같은 품목에 대체 레시피가 있다). */
|
|
1041
1076
|
...(o.recipeKey ? { recipeKey: o.recipeKey } : {}),
|
|
1042
|
-
bizTransaction: o.bizTransaction ?? '', allocated: resolved.kept, picked: [], shipmentEpc: null,
|
|
1077
|
+
bizTransaction: o.bizTransaction ?? '', allocated: resolved.kept, picked: [], shipmentEpc: null,
|
|
1078
|
+
/* 라인도 같다 — 씨앗은 남은 양만 남기고, 미러는 원본이 말한 요청·이행을 그대로 든다. */
|
|
1079
|
+
lines: observing ? (o.lines ?? []).map(l => ({ ...l })) : lines,
|
|
1043
1080
|
...(resolved.dropped ? { seedIncomplete: true } : {}),
|
|
1044
1081
|
/* **보류를 이어받는다** — 잃으면 씨앗이 사람이 일부러 멈춘 오더를 다시 계획해 내보낸다.
|
|
1045
1082
|
씨앗 왕복 대조가 이것을 잡았다(그 전에는 사람이 코드를 읽어야만 알 수 있었다). */
|
|
@@ -1112,7 +1149,12 @@ export class FlowEngine {
|
|
|
1112
1149
|
startedAtSimMs: t.startedAtSimMs,
|
|
1113
1150
|
durationMs: t.durationMs ?? (known ? t.remainingMs : 0),
|
|
1114
1151
|
orderId: t.orderId,
|
|
1115
|
-
intent: t.intent
|
|
1152
|
+
intent: t.intent,
|
|
1153
|
+
/*
|
|
1154
|
+
* 원본이 말한 진척 — **관측일 때만, 그리고 커널이 계산할 수 없을 때 쓰인다**(§`progressOf`).
|
|
1155
|
+
* 씨앗 경로에서는 싣지 않는다: 씨앗은 굴러가면서 자기 소요로 진척을 만든다.
|
|
1156
|
+
*/
|
|
1157
|
+
...(purpose === 'observe' && typeof t.progress === 'number' ? { progress: t.progress } : {})
|
|
1116
1158
|
});
|
|
1117
1159
|
/*
|
|
1118
1160
|
* 투입 인원은 **작업이 들고 있는 사실**이므로 관측에서도 그대로 옮긴다(위 `set` 에서 빠져 있어
|
|
@@ -1425,8 +1467,13 @@ export class FlowEngine {
|
|
|
1425
1467
|
equipment: [...this.equipment.values()].map(m => {
|
|
1426
1468
|
const s = { id: m.id, kind: m.kind, location: m.location, ...(m.homeLocation ? { homeLocation: m.homeLocation } : {}), status: m.status, taskId: m.taskId ?? undefined, oee: this.oeeOf(m), held: m.held, ...(m.properties ? { properties: m.properties } : {}), ...(m.testSpecificationIds ? { testSpecificationIds: m.testSpecificationIds } : {}), ...(m.testResults ? { testResults: m.testResults } : {}), capability: this.capabilityOfResource(m, m.kind ? [m.kind] : [], this.classDefs.equipment), origin: 'master', ...(this.offShift(m) ? { offShift: true, ...this.offReason(m) } : {}), ...this.effectivePart(m), ...(this.shiftOf(m) ? { shift: this.shiftOf(m) } : {}), ...energyFieldsOf(m) };
|
|
1427
1469
|
const t = m.taskId ? this.tasks.get(m.taskId) : undefined;
|
|
1428
|
-
|
|
1429
|
-
|
|
1470
|
+
/*
|
|
1471
|
+
* 이동 구간은 **소요를 알 때만** 낸다. 모르면 아래 산수가 전부 0 이나 음수가 되고, 화면은 「제자리에서
|
|
1472
|
+
* 다 끝난 이동」을 그린다 — 원본이 소요를 주지 않는 미러에서 실제로 그랬다(§`progressOf`).
|
|
1473
|
+
*/
|
|
1474
|
+
const motionProgress = t && t.status === 'in-progress' && t.intent !== 'process' ? this.progressOf(t) : undefined;
|
|
1475
|
+
if (t && motionProgress !== undefined)
|
|
1476
|
+
s.motion = { fromNode: t.fromNode, toNode: t.toNode, startedAtSimMs: this.clockMs - (t.durationMs - t.remainingMs), durationMs: t.durationMs, progress: motionProgress, elapsedMs: t.durationMs - t.remainingMs };
|
|
1430
1477
|
return s;
|
|
1431
1478
|
}),
|
|
1432
1479
|
assets: [...this.assets.values()].map(a => {
|
|
@@ -1455,10 +1502,17 @@ export class FlowEngine {
|
|
|
1455
1502
|
물을 수 없다(사실은 자기 집에서 나온다). 판정은 아래 `capability` 가 이미 답한다. */
|
|
1456
1503
|
if (p.testResults)
|
|
1457
1504
|
st.testResults = p.testResults;
|
|
1505
|
+
/*
|
|
1506
|
+
* 근무 밖 판정 — **선언이 있으면 선언이 이기고, 없을 때만 원본이 말한 값을 쓴다.**
|
|
1507
|
+
* 진척(`progressOf`)과 같은 순서다. 선언이 있는데 저장값을 함께 보면 교대 안으로 돌아왔을 때
|
|
1508
|
+
* 옛 `true` 가 남는다(리듀서가 그 이유를 적어 두었다).
|
|
1509
|
+
*/
|
|
1458
1510
|
if (this.personOffShift(p)) {
|
|
1459
1511
|
st.offShift = true;
|
|
1460
1512
|
Object.assign(st, this.offReason(p));
|
|
1461
1513
|
}
|
|
1514
|
+
else if (p.offShift && !p.workCalendar?.length)
|
|
1515
|
+
st.offShift = true;
|
|
1462
1516
|
const sh = this.shiftOf(p);
|
|
1463
1517
|
if (sh)
|
|
1464
1518
|
st.shift = sh;
|
|
@@ -1479,7 +1533,26 @@ export class FlowEngine {
|
|
|
1479
1533
|
fromNode: t.fromNode, toNode: t.toNode, resourceRef: t.resource ?? undefined, orderId: t.orderId,
|
|
1480
1534
|
...(t.intent ? { intent: t.intent } : {}),
|
|
1481
1535
|
...(t.durationMs ? { durationMs: t.durationMs } : {}),
|
|
1482
|
-
|
|
1536
|
+
/*
|
|
1537
|
+
* 진척은 **커널이 계산한 값이 먼저, 못 하면 원본이 말한 값**이다(§`progressOf`).
|
|
1538
|
+
*
|
|
1539
|
+
* 이 순서는 사람 교대(`offShift`)가 세워 둔 규율과 같다: 파생이 가능하면 파생이 이기고, 파생이
|
|
1540
|
+
* 불가능할 때만 원본의 단정을 쓴다. 둘 다 없으면 **싣지 않는다** — 「모른다」를 숫자로 만들지 않는다.
|
|
1541
|
+
*
|
|
1542
|
+
* 예전에는 원본이 진척을 말해도 조용히 버렸다(문이 받아 놓고 파생이 덮었다). 넣었는데 사라지는
|
|
1543
|
+
* 것이 이 시스템에서 가장 비싼 침묵이다.
|
|
1544
|
+
*/
|
|
1545
|
+
...(t.status === 'in-progress'
|
|
1546
|
+
? {
|
|
1547
|
+
remainingMs: t.remainingMs,
|
|
1548
|
+
startedAtSimMs: t.startedAtSimMs,
|
|
1549
|
+
...(() => {
|
|
1550
|
+
const derived = this.progressOf(t);
|
|
1551
|
+
const p = derived ?? t.progress;
|
|
1552
|
+
return p === undefined ? {} : { progress: p };
|
|
1553
|
+
})()
|
|
1554
|
+
}
|
|
1555
|
+
: {}),
|
|
1483
1556
|
...(t.personnel?.length ? { personnel: t.personnel.slice() } : {}),
|
|
1484
1557
|
...(t.assets?.length ? { assets: t.assets.slice() } : {}),
|
|
1485
1558
|
...(t.resources?.length ? { resources: t.resources.slice() } : {}),
|
|
@@ -2042,7 +2115,8 @@ export class FlowEngine {
|
|
|
2042
2115
|
committedDemands() {
|
|
2043
2116
|
const out = [];
|
|
2044
2117
|
for (const o of this.orders.values()) {
|
|
2045
|
-
|
|
2118
|
+
/* 같은 판정을 쓴다 — 남은 수요를 세는 자리와 지연을 세는 자리가 다른 규칙을 쓰면 어긋난다. */
|
|
2119
|
+
if (isOrderTerminal(o))
|
|
2046
2120
|
continue;
|
|
2047
2121
|
const remaining = Math.max(0, (o.requested ?? 0) - (o.fulfilled ?? 0));
|
|
2048
2122
|
if (!remaining)
|
|
@@ -3418,7 +3492,29 @@ export class FlowEngine {
|
|
|
3418
3492
|
...(i.ilmd ? { ilmd: i.ilmd } : {})
|
|
3419
3493
|
};
|
|
3420
3494
|
}
|
|
3421
|
-
|
|
3495
|
+
/**
|
|
3496
|
+
* 진척 — **모르면 답하지 않는다.**
|
|
3497
|
+
*
|
|
3498
|
+
* ── 무엇이 틀려 있었나 (2026-08-23 실측) ───────────────────────────────────
|
|
3499
|
+
* 예전에는 `durationMs <= 0` 이면 `1` 을 돌려줬다. 시뮬에서는 그것이 맞다(소요 0 = 즉시 끝나는 일).
|
|
3500
|
+
* 그런데 **미러에서 `durationMs` 0 은 「즉시」가 아니라 「모른다」다** — 원본이 소요를 말해 주지
|
|
3501
|
+
* 않으면 씨앗 경로가 0 으로 메우고, 그러면 이 함수가 **진행 중인 작업을 100% 라고 말한다.**
|
|
3502
|
+
*
|
|
3503
|
+
* 실측: 소요를 주지 않는 원본의 진행 중 작업이 `progress: 1` 로 나왔다. 「모름」을 「완료」로 말하는
|
|
3504
|
+
* 것이고, 화면은 그것을 사실로 그린다.
|
|
3505
|
+
*
|
|
3506
|
+
* 시뮬에서는 이 반환이 바뀌지 않는다: 소요 0 인 작업은 배정된 tick 안에서 끝나므로 **진행 중으로
|
|
3507
|
+
* 관측되지 않는다**(`status = 'in-progress'; remainingMs = durationMs` → 다음 진행에서 즉시 완료).
|
|
3508
|
+
* 즉 `durationMs <= 0` 이면서 진행 중인 작업은 미러 경로에서만 나온다.
|
|
3509
|
+
*
|
|
3510
|
+
* 모를 때 원본이 진척을 말해 주었다면 그 값을 쓴다 — 그 규칙은 부르는 쪽에 있다(§`getSnapshot`).
|
|
3511
|
+
* 이 함수는 **커널이 계산할 수 있나**만 답한다.
|
|
3512
|
+
*/
|
|
3513
|
+
progressOf(t) {
|
|
3514
|
+
if (!(t.durationMs > 0))
|
|
3515
|
+
return undefined;
|
|
3516
|
+
return Math.min(1, Math.max(0, (t.durationMs - t.remainingMs) / t.durationMs));
|
|
3517
|
+
}
|
|
3422
3518
|
generate() {
|
|
3423
3519
|
for (const g of this.gens) {
|
|
3424
3520
|
while (this.clockMs >= g.nextMs) {
|
package/dist-cjs/index.cjs
CHANGED
|
@@ -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
|
|
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({
|
|
@@ -4043,6 +4054,15 @@ var FlowEngine = class {
|
|
|
4043
4054
|
personnelClassIds: p.personnelClassIds ?? prev?.personnelClassIds,
|
|
4044
4055
|
status: purpose === "observe" ? p.status ?? "idle" : "idle",
|
|
4045
4056
|
taskId: purpose === "observe" ? p.taskId ?? null : null,
|
|
4057
|
+
/*
|
|
4058
|
+
* 원본이 말한 근무 밖 표시 — **관측일 때만, 그리고 선언이 없을 때만 쓰인다.**
|
|
4059
|
+
*
|
|
4060
|
+
* 이 규율의 원본 자리는 리듀서다(§`offShift`: 「선언이 없을 때만 받는다 — 선언이 있으면
|
|
4061
|
+
* `snapshot()` 이 시각으로 판정하고, 저장값을 함께 두면 교대 안으로 돌아왔을 때 옛 `true` 가
|
|
4062
|
+
* 남는다」). 그런데 이 자리가 그 값을 **떨어뜨리고 있었다** — 리듀서가 지킨 것을 여기서 잃었다.
|
|
4063
|
+
* 근무 캘린더가 없는 원본(실 시스템에 흔하다)에서는 그것이 유일한 답이다.
|
|
4064
|
+
*/
|
|
4065
|
+
...purpose === "observe" && p.offShift ? { offShift: true } : {},
|
|
4046
4066
|
...p.location ? { location: p.location } : {},
|
|
4047
4067
|
...p.properties ? { properties: p.properties } : {},
|
|
4048
4068
|
...p.testSpecificationIds ? { testSpecificationIds: p.testSpecificationIds } : {},
|
|
@@ -4094,14 +4114,15 @@ var FlowEngine = class {
|
|
|
4094
4114
|
for (const o of observedOrders) {
|
|
4095
4115
|
const lines = (o.lines ?? []).map((l) => ({ gtin: l.gtin, requested: l.requested - (l.fulfilled ?? 0) })).filter((l) => l.requested > 0);
|
|
4096
4116
|
const remaining = lines.length ? lines.reduce((s, l) => s + l.requested, 0) : Math.max(0, (o.requested ?? 0) - (o.fulfilled ?? 0));
|
|
4097
|
-
|
|
4117
|
+
const observing = purpose === "observe";
|
|
4118
|
+
if (!observing && remaining <= 0) continue;
|
|
4098
4119
|
const resolved = this.resolvedAllocated(o.allocated);
|
|
4099
4120
|
this.orders.set(o.orderId, {
|
|
4100
4121
|
id: o.orderId,
|
|
4101
4122
|
kind: o.kind,
|
|
4102
|
-
status: "created",
|
|
4103
|
-
requested: remaining,
|
|
4104
|
-
fulfilled: 0,
|
|
4123
|
+
status: observing ? o.status ?? "created" : "created",
|
|
4124
|
+
requested: observing ? o.requested ?? 0 : remaining,
|
|
4125
|
+
fulfilled: observing ? o.fulfilled ?? 0 : 0,
|
|
4105
4126
|
/* **확보해 둔 것과 거래번호를 이어받는다.** 비우면 되살아난 오더가 아무것도 안 잡은 것처럼
|
|
4106
4127
|
보이고, 그 오더의 진행 중 작업이 완료될 때 계보가 입력 없이 나간다. 상태가 말해 주지
|
|
4107
4128
|
않으면(옛 저널) 그때는 비는 것이 사실이다 — 없는 것을 지어내지 않는다. */
|
|
@@ -4114,7 +4135,8 @@ var FlowEngine = class {
|
|
|
4114
4135
|
allocated: resolved.kept,
|
|
4115
4136
|
picked: [],
|
|
4116
4137
|
shipmentEpc: null,
|
|
4117
|
-
|
|
4138
|
+
/* 라인도 같다 — 씨앗은 남은 양만 남기고, 미러는 원본이 말한 요청·이행을 그대로 든다. */
|
|
4139
|
+
lines: observing ? (o.lines ?? []).map((l) => ({ ...l })) : lines,
|
|
4118
4140
|
...resolved.dropped ? { seedIncomplete: true } : {},
|
|
4119
4141
|
/* **보류를 이어받는다** — 잃으면 씨앗이 사람이 일부러 멈춘 오더를 다시 계획해 내보낸다.
|
|
4120
4142
|
씨앗 왕복 대조가 이것을 잡았다(그 전에는 사람이 코드를 읽어야만 알 수 있었다). */
|
|
@@ -4175,7 +4197,12 @@ var FlowEngine = class {
|
|
|
4175
4197
|
startedAtSimMs: t.startedAtSimMs,
|
|
4176
4198
|
durationMs: t.durationMs ?? (known ? t.remainingMs : 0),
|
|
4177
4199
|
orderId: t.orderId,
|
|
4178
|
-
intent: t.intent
|
|
4200
|
+
intent: t.intent,
|
|
4201
|
+
/*
|
|
4202
|
+
* 원본이 말한 진척 — **관측일 때만, 그리고 커널이 계산할 수 없을 때 쓰인다**(§`progressOf`).
|
|
4203
|
+
* 씨앗 경로에서는 싣지 않는다: 씨앗은 굴러가면서 자기 소요로 진척을 만든다.
|
|
4204
|
+
*/
|
|
4205
|
+
...purpose === "observe" && typeof t.progress === "number" ? { progress: t.progress } : {}
|
|
4179
4206
|
});
|
|
4180
4207
|
if (purpose === "observe" && t.personnel?.length) {
|
|
4181
4208
|
const restored = this.tasks.get(t.id);
|
|
@@ -4436,7 +4463,8 @@ var FlowEngine = class {
|
|
|
4436
4463
|
equipment: [...this.equipment.values()].map((m) => {
|
|
4437
4464
|
const s = { id: m.id, kind: m.kind, location: m.location, ...m.homeLocation ? { homeLocation: m.homeLocation } : {}, status: m.status, taskId: m.taskId ?? void 0, oee: this.oeeOf(m), held: m.held, ...m.properties ? { properties: m.properties } : {}, ...m.testSpecificationIds ? { testSpecificationIds: m.testSpecificationIds } : {}, ...m.testResults ? { testResults: m.testResults } : {}, capability: this.capabilityOfResource(m, m.kind ? [m.kind] : [], this.classDefs.equipment), origin: "master", ...this.offShift(m) ? { offShift: true, ...this.offReason(m) } : {}, ...this.effectivePart(m), ...this.shiftOf(m) ? { shift: this.shiftOf(m) } : {}, ...energyFieldsOf(m) };
|
|
4438
4465
|
const t = m.taskId ? this.tasks.get(m.taskId) : void 0;
|
|
4439
|
-
|
|
4466
|
+
const motionProgress = t && t.status === "in-progress" && t.intent !== "process" ? this.progressOf(t) : void 0;
|
|
4467
|
+
if (t && motionProgress !== void 0) s.motion = { fromNode: t.fromNode, toNode: t.toNode, startedAtSimMs: this.clockMs - (t.durationMs - t.remainingMs), durationMs: t.durationMs, progress: motionProgress, elapsedMs: t.durationMs - t.remainingMs };
|
|
4440
4468
|
return s;
|
|
4441
4469
|
}),
|
|
4442
4470
|
assets: [...this.assets.values()].map((a) => {
|
|
@@ -4458,7 +4486,7 @@ var FlowEngine = class {
|
|
|
4458
4486
|
if (this.personOffShift(p)) {
|
|
4459
4487
|
st.offShift = true;
|
|
4460
4488
|
Object.assign(st, this.offReason(p));
|
|
4461
|
-
}
|
|
4489
|
+
} else if (p.offShift && !p.workCalendar?.length) st.offShift = true;
|
|
4462
4490
|
const sh = this.shiftOf(p);
|
|
4463
4491
|
if (sh) st.shift = sh;
|
|
4464
4492
|
Object.assign(st, this.effectivePart(p));
|
|
@@ -4484,7 +4512,24 @@ var FlowEngine = class {
|
|
|
4484
4512
|
orderId: t.orderId,
|
|
4485
4513
|
...t.intent ? { intent: t.intent } : {},
|
|
4486
4514
|
...t.durationMs ? { durationMs: t.durationMs } : {},
|
|
4487
|
-
|
|
4515
|
+
/*
|
|
4516
|
+
* 진척은 **커널이 계산한 값이 먼저, 못 하면 원본이 말한 값**이다(§`progressOf`).
|
|
4517
|
+
*
|
|
4518
|
+
* 이 순서는 사람 교대(`offShift`)가 세워 둔 규율과 같다: 파생이 가능하면 파생이 이기고, 파생이
|
|
4519
|
+
* 불가능할 때만 원본의 단정을 쓴다. 둘 다 없으면 **싣지 않는다** — 「모른다」를 숫자로 만들지 않는다.
|
|
4520
|
+
*
|
|
4521
|
+
* 예전에는 원본이 진척을 말해도 조용히 버렸다(문이 받아 놓고 파생이 덮었다). 넣었는데 사라지는
|
|
4522
|
+
* 것이 이 시스템에서 가장 비싼 침묵이다.
|
|
4523
|
+
*/
|
|
4524
|
+
...t.status === "in-progress" ? {
|
|
4525
|
+
remainingMs: t.remainingMs,
|
|
4526
|
+
startedAtSimMs: t.startedAtSimMs,
|
|
4527
|
+
...(() => {
|
|
4528
|
+
const derived = this.progressOf(t);
|
|
4529
|
+
const p = derived ?? t.progress;
|
|
4530
|
+
return p === void 0 ? {} : { progress: p };
|
|
4531
|
+
})()
|
|
4532
|
+
} : {},
|
|
4488
4533
|
...t.personnel?.length ? { personnel: t.personnel.slice() } : {},
|
|
4489
4534
|
...t.assets?.length ? { assets: t.assets.slice() } : {},
|
|
4490
4535
|
...t.resources?.length ? { resources: t.resources.slice() } : {},
|
|
@@ -4999,7 +5044,7 @@ var FlowEngine = class {
|
|
|
4999
5044
|
committedDemands() {
|
|
5000
5045
|
const out = [];
|
|
5001
5046
|
for (const o of this.orders.values()) {
|
|
5002
|
-
if (o
|
|
5047
|
+
if (isOrderTerminal(o)) continue;
|
|
5003
5048
|
const remaining = Math.max(0, (o.requested ?? 0) - (o.fulfilled ?? 0));
|
|
5004
5049
|
if (!remaining) continue;
|
|
5005
5050
|
out.push({ remaining, ...o.endTime ? { dueTime: o.endTime } : {} });
|
|
@@ -6186,8 +6231,27 @@ var FlowEngine = class {
|
|
|
6186
6231
|
...i.ilmd ? { ilmd: i.ilmd } : {}
|
|
6187
6232
|
};
|
|
6188
6233
|
}
|
|
6234
|
+
/**
|
|
6235
|
+
* 진척 — **모르면 답하지 않는다.**
|
|
6236
|
+
*
|
|
6237
|
+
* ── 무엇이 틀려 있었나 (2026-08-23 실측) ───────────────────────────────────
|
|
6238
|
+
* 예전에는 `durationMs <= 0` 이면 `1` 을 돌려줬다. 시뮬에서는 그것이 맞다(소요 0 = 즉시 끝나는 일).
|
|
6239
|
+
* 그런데 **미러에서 `durationMs` 0 은 「즉시」가 아니라 「모른다」다** — 원본이 소요를 말해 주지
|
|
6240
|
+
* 않으면 씨앗 경로가 0 으로 메우고, 그러면 이 함수가 **진행 중인 작업을 100% 라고 말한다.**
|
|
6241
|
+
*
|
|
6242
|
+
* 실측: 소요를 주지 않는 원본의 진행 중 작업이 `progress: 1` 로 나왔다. 「모름」을 「완료」로 말하는
|
|
6243
|
+
* 것이고, 화면은 그것을 사실로 그린다.
|
|
6244
|
+
*
|
|
6245
|
+
* 시뮬에서는 이 반환이 바뀌지 않는다: 소요 0 인 작업은 배정된 tick 안에서 끝나므로 **진행 중으로
|
|
6246
|
+
* 관측되지 않는다**(`status = 'in-progress'; remainingMs = durationMs` → 다음 진행에서 즉시 완료).
|
|
6247
|
+
* 즉 `durationMs <= 0` 이면서 진행 중인 작업은 미러 경로에서만 나온다.
|
|
6248
|
+
*
|
|
6249
|
+
* 모를 때 원본이 진척을 말해 주었다면 그 값을 쓴다 — 그 규칙은 부르는 쪽에 있다(§`getSnapshot`).
|
|
6250
|
+
* 이 함수는 **커널이 계산할 수 있나**만 답한다.
|
|
6251
|
+
*/
|
|
6189
6252
|
progressOf(t) {
|
|
6190
|
-
|
|
6253
|
+
if (!(t.durationMs > 0)) return void 0;
|
|
6254
|
+
return Math.min(1, Math.max(0, (t.durationMs - t.remainingMs) / t.durationMs));
|
|
6191
6255
|
}
|
|
6192
6256
|
generate() {
|
|
6193
6257
|
for (const g of this.gens) {
|
|
@@ -8962,6 +9026,7 @@ function retiredVocabularyIn(line) {
|
|
|
8962
9026
|
OPERATION_PROPERTY,
|
|
8963
9027
|
OP_EVENT,
|
|
8964
9028
|
OP_PARAM,
|
|
9029
|
+
ORDER_TERMINAL_STATUS,
|
|
8965
9030
|
ObservedReducer,
|
|
8966
9031
|
PRIORITY_UNSET,
|
|
8967
9032
|
RETIRED_VOCABULARY,
|
|
@@ -9032,6 +9097,7 @@ function retiredVocabularyIn(line) {
|
|
|
9032
9097
|
isEnergyRecord,
|
|
9033
9098
|
isEquipmentLevel,
|
|
9034
9099
|
isOperationalRecord,
|
|
9100
|
+
isOrderTerminal,
|
|
9035
9101
|
isTransformationRecord,
|
|
9036
9102
|
isoDurationHours,
|
|
9037
9103
|
itemKeyOf,
|
package/package.json
CHANGED