@operato/twin-kernel 0.7.50 → 0.7.51
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.js +30 -0
- package/dist/flow-engine.d.ts +37 -1
- package/dist/flow-engine.js +102 -9
- package/dist-cjs/index.cjs +63 -10
- package/package.json +1 -1
package/dist/contract.js
CHANGED
|
@@ -1,6 +1,36 @@
|
|
|
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
|
*/
|
|
5
35
|
/**
|
|
6
36
|
* 자리의 상태 — **포화도에서 파생한다.** 저장하는 값이 아니다.
|
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
|
@@ -946,6 +946,15 @@ export class FlowEngine {
|
|
|
946
946
|
id: p.id, personnelClassIds: p.personnelClassIds ?? prev?.personnelClassIds,
|
|
947
947
|
status: purpose === 'observe' ? (p.status ?? 'idle') : 'idle',
|
|
948
948
|
taskId: purpose === 'observe' ? (p.taskId ?? null) : null,
|
|
949
|
+
/*
|
|
950
|
+
* 원본이 말한 근무 밖 표시 — **관측일 때만, 그리고 선언이 없을 때만 쓰인다.**
|
|
951
|
+
*
|
|
952
|
+
* 이 규율의 원본 자리는 리듀서다(§`offShift`: 「선언이 없을 때만 받는다 — 선언이 있으면
|
|
953
|
+
* `snapshot()` 이 시각으로 판정하고, 저장값을 함께 두면 교대 안으로 돌아왔을 때 옛 `true` 가
|
|
954
|
+
* 남는다」). 그런데 이 자리가 그 값을 **떨어뜨리고 있었다** — 리듀서가 지킨 것을 여기서 잃었다.
|
|
955
|
+
* 근무 캘린더가 없는 원본(실 시스템에 흔하다)에서는 그것이 유일한 답이다.
|
|
956
|
+
*/
|
|
957
|
+
...(purpose === 'observe' && p.offShift ? { offShift: true } : {}),
|
|
949
958
|
...(p.location ? { location: p.location } : {}),
|
|
950
959
|
...(p.properties ? { properties: p.properties } : {}),
|
|
951
960
|
...(p.testSpecificationIds ? { testSpecificationIds: p.testSpecificationIds } : {}),
|
|
@@ -1026,11 +1035,35 @@ export class FlowEngine {
|
|
|
1026
1035
|
const remaining = lines.length
|
|
1027
1036
|
? lines.reduce((s, l) => s + l.requested, 0)
|
|
1028
1037
|
: Math.max(0, (o.requested ?? 0) - (o.fulfilled ?? 0));
|
|
1029
|
-
|
|
1030
|
-
|
|
1038
|
+
/*
|
|
1039
|
+
* ── **씨앗은 남은 데맨드로, 미러는 들은 그대로** (2026-08-23) ──────────────
|
|
1040
|
+
*
|
|
1041
|
+
* 아래 세 값(`status`·`requested`·`fulfilled`)은 씨앗의 규약이다: 씨앗은 「무엇이 남았나」로
|
|
1042
|
+
* 미래를 굴리므로 요청 10·이행 4 를 **요청 6·이행 0·상태 created** 로 다시 표현한다. 그리고 이미
|
|
1043
|
+
* 이행된 오더는 예측 대상이 아니므로 건너뛴다.
|
|
1044
|
+
*
|
|
1045
|
+
* **미러에는 전부 틀렸다.** 미러는 굴리지 않고 들은 것을 말한다. 실측(포천 미러):
|
|
1046
|
+
*
|
|
1047
|
+
* 원본이 말한 것 picking · 요청 10 · 이행 4 · 진척 0.4
|
|
1048
|
+
* 커널이 낸 것 created · 요청 6 · 이행 0 · 진척 0 ← 40% 된 오더가 0% 로 보였다
|
|
1049
|
+
*
|
|
1050
|
+
* 그리고 이행 완료된 오더는 **상태에서 아예 사라졌다** — 원본이 그 오더가 있다고 말하는데
|
|
1051
|
+
* 트윈은 없다고 말한다. 오더의 상태 낱말은 계약이 도메인에 넘긴 것인데(`picking`·`shipped`…)
|
|
1052
|
+
* 씨앗 규약이 그것을 커널 낱말로 덮고 있었다.
|
|
1053
|
+
*
|
|
1054
|
+
* 이것은 작업에서 고친 것과 **같은 결함의 세 번째 사례**다(§`purpose`): 한 함수가 두 목적을 겸하고
|
|
1055
|
+
* 씨앗 규약이 관측에 새어 나온다.
|
|
1056
|
+
*/
|
|
1057
|
+
const observing = purpose === 'observe';
|
|
1058
|
+
if (!observing && remaining <= 0)
|
|
1059
|
+
continue; // 씨앗: 이미 이행 완료 → 예측 대상 아님
|
|
1031
1060
|
const resolved = this.resolvedAllocated(o.allocated);
|
|
1032
1061
|
this.orders.set(o.orderId, {
|
|
1033
|
-
id: o.orderId,
|
|
1062
|
+
id: o.orderId,
|
|
1063
|
+
kind: o.kind,
|
|
1064
|
+
status: observing ? (o.status ?? 'created') : 'created',
|
|
1065
|
+
requested: observing ? (o.requested ?? 0) : remaining,
|
|
1066
|
+
fulfilled: observing ? (o.fulfilled ?? 0) : 0,
|
|
1034
1067
|
/* **확보해 둔 것과 거래번호를 이어받는다.** 비우면 되살아난 오더가 아무것도 안 잡은 것처럼
|
|
1035
1068
|
보이고, 그 오더의 진행 중 작업이 완료될 때 계보가 입력 없이 나간다. 상태가 말해 주지
|
|
1036
1069
|
않으면(옛 저널) 그때는 비는 것이 사실이다 — 없는 것을 지어내지 않는다. */
|
|
@@ -1039,7 +1072,9 @@ export class FlowEngine {
|
|
|
1039
1072
|
/* 레시피도 함께 — 품목만 이어받으면 되살아난 오더가 「무엇으로」를 모르고, 그 오더의
|
|
1040
1073
|
남은 공정·소요가 다른 레시피 기준으로 계산된다(같은 품목에 대체 레시피가 있다). */
|
|
1041
1074
|
...(o.recipeKey ? { recipeKey: o.recipeKey } : {}),
|
|
1042
|
-
bizTransaction: o.bizTransaction ?? '', allocated: resolved.kept, picked: [], shipmentEpc: null,
|
|
1075
|
+
bizTransaction: o.bizTransaction ?? '', allocated: resolved.kept, picked: [], shipmentEpc: null,
|
|
1076
|
+
/* 라인도 같다 — 씨앗은 남은 양만 남기고, 미러는 원본이 말한 요청·이행을 그대로 든다. */
|
|
1077
|
+
lines: observing ? (o.lines ?? []).map(l => ({ ...l })) : lines,
|
|
1043
1078
|
...(resolved.dropped ? { seedIncomplete: true } : {}),
|
|
1044
1079
|
/* **보류를 이어받는다** — 잃으면 씨앗이 사람이 일부러 멈춘 오더를 다시 계획해 내보낸다.
|
|
1045
1080
|
씨앗 왕복 대조가 이것을 잡았다(그 전에는 사람이 코드를 읽어야만 알 수 있었다). */
|
|
@@ -1112,7 +1147,12 @@ export class FlowEngine {
|
|
|
1112
1147
|
startedAtSimMs: t.startedAtSimMs,
|
|
1113
1148
|
durationMs: t.durationMs ?? (known ? t.remainingMs : 0),
|
|
1114
1149
|
orderId: t.orderId,
|
|
1115
|
-
intent: t.intent
|
|
1150
|
+
intent: t.intent,
|
|
1151
|
+
/*
|
|
1152
|
+
* 원본이 말한 진척 — **관측일 때만, 그리고 커널이 계산할 수 없을 때 쓰인다**(§`progressOf`).
|
|
1153
|
+
* 씨앗 경로에서는 싣지 않는다: 씨앗은 굴러가면서 자기 소요로 진척을 만든다.
|
|
1154
|
+
*/
|
|
1155
|
+
...(purpose === 'observe' && typeof t.progress === 'number' ? { progress: t.progress } : {})
|
|
1116
1156
|
});
|
|
1117
1157
|
/*
|
|
1118
1158
|
* 투입 인원은 **작업이 들고 있는 사실**이므로 관측에서도 그대로 옮긴다(위 `set` 에서 빠져 있어
|
|
@@ -1425,8 +1465,13 @@ export class FlowEngine {
|
|
|
1425
1465
|
equipment: [...this.equipment.values()].map(m => {
|
|
1426
1466
|
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
1467
|
const t = m.taskId ? this.tasks.get(m.taskId) : undefined;
|
|
1428
|
-
|
|
1429
|
-
|
|
1468
|
+
/*
|
|
1469
|
+
* 이동 구간은 **소요를 알 때만** 낸다. 모르면 아래 산수가 전부 0 이나 음수가 되고, 화면은 「제자리에서
|
|
1470
|
+
* 다 끝난 이동」을 그린다 — 원본이 소요를 주지 않는 미러에서 실제로 그랬다(§`progressOf`).
|
|
1471
|
+
*/
|
|
1472
|
+
const motionProgress = t && t.status === 'in-progress' && t.intent !== 'process' ? this.progressOf(t) : undefined;
|
|
1473
|
+
if (t && motionProgress !== undefined)
|
|
1474
|
+
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
1475
|
return s;
|
|
1431
1476
|
}),
|
|
1432
1477
|
assets: [...this.assets.values()].map(a => {
|
|
@@ -1455,10 +1500,17 @@ export class FlowEngine {
|
|
|
1455
1500
|
물을 수 없다(사실은 자기 집에서 나온다). 판정은 아래 `capability` 가 이미 답한다. */
|
|
1456
1501
|
if (p.testResults)
|
|
1457
1502
|
st.testResults = p.testResults;
|
|
1503
|
+
/*
|
|
1504
|
+
* 근무 밖 판정 — **선언이 있으면 선언이 이기고, 없을 때만 원본이 말한 값을 쓴다.**
|
|
1505
|
+
* 진척(`progressOf`)과 같은 순서다. 선언이 있는데 저장값을 함께 보면 교대 안으로 돌아왔을 때
|
|
1506
|
+
* 옛 `true` 가 남는다(리듀서가 그 이유를 적어 두었다).
|
|
1507
|
+
*/
|
|
1458
1508
|
if (this.personOffShift(p)) {
|
|
1459
1509
|
st.offShift = true;
|
|
1460
1510
|
Object.assign(st, this.offReason(p));
|
|
1461
1511
|
}
|
|
1512
|
+
else if (p.offShift && !p.workCalendar?.length)
|
|
1513
|
+
st.offShift = true;
|
|
1462
1514
|
const sh = this.shiftOf(p);
|
|
1463
1515
|
if (sh)
|
|
1464
1516
|
st.shift = sh;
|
|
@@ -1479,7 +1531,26 @@ export class FlowEngine {
|
|
|
1479
1531
|
fromNode: t.fromNode, toNode: t.toNode, resourceRef: t.resource ?? undefined, orderId: t.orderId,
|
|
1480
1532
|
...(t.intent ? { intent: t.intent } : {}),
|
|
1481
1533
|
...(t.durationMs ? { durationMs: t.durationMs } : {}),
|
|
1482
|
-
|
|
1534
|
+
/*
|
|
1535
|
+
* 진척은 **커널이 계산한 값이 먼저, 못 하면 원본이 말한 값**이다(§`progressOf`).
|
|
1536
|
+
*
|
|
1537
|
+
* 이 순서는 사람 교대(`offShift`)가 세워 둔 규율과 같다: 파생이 가능하면 파생이 이기고, 파생이
|
|
1538
|
+
* 불가능할 때만 원본의 단정을 쓴다. 둘 다 없으면 **싣지 않는다** — 「모른다」를 숫자로 만들지 않는다.
|
|
1539
|
+
*
|
|
1540
|
+
* 예전에는 원본이 진척을 말해도 조용히 버렸다(문이 받아 놓고 파생이 덮었다). 넣었는데 사라지는
|
|
1541
|
+
* 것이 이 시스템에서 가장 비싼 침묵이다.
|
|
1542
|
+
*/
|
|
1543
|
+
...(t.status === 'in-progress'
|
|
1544
|
+
? {
|
|
1545
|
+
remainingMs: t.remainingMs,
|
|
1546
|
+
startedAtSimMs: t.startedAtSimMs,
|
|
1547
|
+
...(() => {
|
|
1548
|
+
const derived = this.progressOf(t);
|
|
1549
|
+
const p = derived ?? t.progress;
|
|
1550
|
+
return p === undefined ? {} : { progress: p };
|
|
1551
|
+
})()
|
|
1552
|
+
}
|
|
1553
|
+
: {}),
|
|
1483
1554
|
...(t.personnel?.length ? { personnel: t.personnel.slice() } : {}),
|
|
1484
1555
|
...(t.assets?.length ? { assets: t.assets.slice() } : {}),
|
|
1485
1556
|
...(t.resources?.length ? { resources: t.resources.slice() } : {}),
|
|
@@ -3418,7 +3489,29 @@ export class FlowEngine {
|
|
|
3418
3489
|
...(i.ilmd ? { ilmd: i.ilmd } : {})
|
|
3419
3490
|
};
|
|
3420
3491
|
}
|
|
3421
|
-
|
|
3492
|
+
/**
|
|
3493
|
+
* 진척 — **모르면 답하지 않는다.**
|
|
3494
|
+
*
|
|
3495
|
+
* ── 무엇이 틀려 있었나 (2026-08-23 실측) ───────────────────────────────────
|
|
3496
|
+
* 예전에는 `durationMs <= 0` 이면 `1` 을 돌려줬다. 시뮬에서는 그것이 맞다(소요 0 = 즉시 끝나는 일).
|
|
3497
|
+
* 그런데 **미러에서 `durationMs` 0 은 「즉시」가 아니라 「모른다」다** — 원본이 소요를 말해 주지
|
|
3498
|
+
* 않으면 씨앗 경로가 0 으로 메우고, 그러면 이 함수가 **진행 중인 작업을 100% 라고 말한다.**
|
|
3499
|
+
*
|
|
3500
|
+
* 실측: 소요를 주지 않는 원본의 진행 중 작업이 `progress: 1` 로 나왔다. 「모름」을 「완료」로 말하는
|
|
3501
|
+
* 것이고, 화면은 그것을 사실로 그린다.
|
|
3502
|
+
*
|
|
3503
|
+
* 시뮬에서는 이 반환이 바뀌지 않는다: 소요 0 인 작업은 배정된 tick 안에서 끝나므로 **진행 중으로
|
|
3504
|
+
* 관측되지 않는다**(`status = 'in-progress'; remainingMs = durationMs` → 다음 진행에서 즉시 완료).
|
|
3505
|
+
* 즉 `durationMs <= 0` 이면서 진행 중인 작업은 미러 경로에서만 나온다.
|
|
3506
|
+
*
|
|
3507
|
+
* 모를 때 원본이 진척을 말해 주었다면 그 값을 쓴다 — 그 규칙은 부르는 쪽에 있다(§`getSnapshot`).
|
|
3508
|
+
* 이 함수는 **커널이 계산할 수 있나**만 답한다.
|
|
3509
|
+
*/
|
|
3510
|
+
progressOf(t) {
|
|
3511
|
+
if (!(t.durationMs > 0))
|
|
3512
|
+
return undefined;
|
|
3513
|
+
return Math.min(1, Math.max(0, (t.durationMs - t.remainingMs) / t.durationMs));
|
|
3514
|
+
}
|
|
3422
3515
|
generate() {
|
|
3423
3516
|
for (const g of this.gens) {
|
|
3424
3517
|
while (this.clockMs >= g.nextMs) {
|
package/dist-cjs/index.cjs
CHANGED
|
@@ -4043,6 +4043,15 @@ var FlowEngine = class {
|
|
|
4043
4043
|
personnelClassIds: p.personnelClassIds ?? prev?.personnelClassIds,
|
|
4044
4044
|
status: purpose === "observe" ? p.status ?? "idle" : "idle",
|
|
4045
4045
|
taskId: purpose === "observe" ? p.taskId ?? null : null,
|
|
4046
|
+
/*
|
|
4047
|
+
* 원본이 말한 근무 밖 표시 — **관측일 때만, 그리고 선언이 없을 때만 쓰인다.**
|
|
4048
|
+
*
|
|
4049
|
+
* 이 규율의 원본 자리는 리듀서다(§`offShift`: 「선언이 없을 때만 받는다 — 선언이 있으면
|
|
4050
|
+
* `snapshot()` 이 시각으로 판정하고, 저장값을 함께 두면 교대 안으로 돌아왔을 때 옛 `true` 가
|
|
4051
|
+
* 남는다」). 그런데 이 자리가 그 값을 **떨어뜨리고 있었다** — 리듀서가 지킨 것을 여기서 잃었다.
|
|
4052
|
+
* 근무 캘린더가 없는 원본(실 시스템에 흔하다)에서는 그것이 유일한 답이다.
|
|
4053
|
+
*/
|
|
4054
|
+
...purpose === "observe" && p.offShift ? { offShift: true } : {},
|
|
4046
4055
|
...p.location ? { location: p.location } : {},
|
|
4047
4056
|
...p.properties ? { properties: p.properties } : {},
|
|
4048
4057
|
...p.testSpecificationIds ? { testSpecificationIds: p.testSpecificationIds } : {},
|
|
@@ -4094,14 +4103,15 @@ var FlowEngine = class {
|
|
|
4094
4103
|
for (const o of observedOrders) {
|
|
4095
4104
|
const lines = (o.lines ?? []).map((l) => ({ gtin: l.gtin, requested: l.requested - (l.fulfilled ?? 0) })).filter((l) => l.requested > 0);
|
|
4096
4105
|
const remaining = lines.length ? lines.reduce((s, l) => s + l.requested, 0) : Math.max(0, (o.requested ?? 0) - (o.fulfilled ?? 0));
|
|
4097
|
-
|
|
4106
|
+
const observing = purpose === "observe";
|
|
4107
|
+
if (!observing && remaining <= 0) continue;
|
|
4098
4108
|
const resolved = this.resolvedAllocated(o.allocated);
|
|
4099
4109
|
this.orders.set(o.orderId, {
|
|
4100
4110
|
id: o.orderId,
|
|
4101
4111
|
kind: o.kind,
|
|
4102
|
-
status: "created",
|
|
4103
|
-
requested: remaining,
|
|
4104
|
-
fulfilled: 0,
|
|
4112
|
+
status: observing ? o.status ?? "created" : "created",
|
|
4113
|
+
requested: observing ? o.requested ?? 0 : remaining,
|
|
4114
|
+
fulfilled: observing ? o.fulfilled ?? 0 : 0,
|
|
4105
4115
|
/* **확보해 둔 것과 거래번호를 이어받는다.** 비우면 되살아난 오더가 아무것도 안 잡은 것처럼
|
|
4106
4116
|
보이고, 그 오더의 진행 중 작업이 완료될 때 계보가 입력 없이 나간다. 상태가 말해 주지
|
|
4107
4117
|
않으면(옛 저널) 그때는 비는 것이 사실이다 — 없는 것을 지어내지 않는다. */
|
|
@@ -4114,7 +4124,8 @@ var FlowEngine = class {
|
|
|
4114
4124
|
allocated: resolved.kept,
|
|
4115
4125
|
picked: [],
|
|
4116
4126
|
shipmentEpc: null,
|
|
4117
|
-
|
|
4127
|
+
/* 라인도 같다 — 씨앗은 남은 양만 남기고, 미러는 원본이 말한 요청·이행을 그대로 든다. */
|
|
4128
|
+
lines: observing ? (o.lines ?? []).map((l) => ({ ...l })) : lines,
|
|
4118
4129
|
...resolved.dropped ? { seedIncomplete: true } : {},
|
|
4119
4130
|
/* **보류를 이어받는다** — 잃으면 씨앗이 사람이 일부러 멈춘 오더를 다시 계획해 내보낸다.
|
|
4120
4131
|
씨앗 왕복 대조가 이것을 잡았다(그 전에는 사람이 코드를 읽어야만 알 수 있었다). */
|
|
@@ -4175,7 +4186,12 @@ var FlowEngine = class {
|
|
|
4175
4186
|
startedAtSimMs: t.startedAtSimMs,
|
|
4176
4187
|
durationMs: t.durationMs ?? (known ? t.remainingMs : 0),
|
|
4177
4188
|
orderId: t.orderId,
|
|
4178
|
-
intent: t.intent
|
|
4189
|
+
intent: t.intent,
|
|
4190
|
+
/*
|
|
4191
|
+
* 원본이 말한 진척 — **관측일 때만, 그리고 커널이 계산할 수 없을 때 쓰인다**(§`progressOf`).
|
|
4192
|
+
* 씨앗 경로에서는 싣지 않는다: 씨앗은 굴러가면서 자기 소요로 진척을 만든다.
|
|
4193
|
+
*/
|
|
4194
|
+
...purpose === "observe" && typeof t.progress === "number" ? { progress: t.progress } : {}
|
|
4179
4195
|
});
|
|
4180
4196
|
if (purpose === "observe" && t.personnel?.length) {
|
|
4181
4197
|
const restored = this.tasks.get(t.id);
|
|
@@ -4436,7 +4452,8 @@ var FlowEngine = class {
|
|
|
4436
4452
|
equipment: [...this.equipment.values()].map((m) => {
|
|
4437
4453
|
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
4454
|
const t = m.taskId ? this.tasks.get(m.taskId) : void 0;
|
|
4439
|
-
|
|
4455
|
+
const motionProgress = t && t.status === "in-progress" && t.intent !== "process" ? this.progressOf(t) : void 0;
|
|
4456
|
+
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
4457
|
return s;
|
|
4441
4458
|
}),
|
|
4442
4459
|
assets: [...this.assets.values()].map((a) => {
|
|
@@ -4458,7 +4475,7 @@ var FlowEngine = class {
|
|
|
4458
4475
|
if (this.personOffShift(p)) {
|
|
4459
4476
|
st.offShift = true;
|
|
4460
4477
|
Object.assign(st, this.offReason(p));
|
|
4461
|
-
}
|
|
4478
|
+
} else if (p.offShift && !p.workCalendar?.length) st.offShift = true;
|
|
4462
4479
|
const sh = this.shiftOf(p);
|
|
4463
4480
|
if (sh) st.shift = sh;
|
|
4464
4481
|
Object.assign(st, this.effectivePart(p));
|
|
@@ -4484,7 +4501,24 @@ var FlowEngine = class {
|
|
|
4484
4501
|
orderId: t.orderId,
|
|
4485
4502
|
...t.intent ? { intent: t.intent } : {},
|
|
4486
4503
|
...t.durationMs ? { durationMs: t.durationMs } : {},
|
|
4487
|
-
|
|
4504
|
+
/*
|
|
4505
|
+
* 진척은 **커널이 계산한 값이 먼저, 못 하면 원본이 말한 값**이다(§`progressOf`).
|
|
4506
|
+
*
|
|
4507
|
+
* 이 순서는 사람 교대(`offShift`)가 세워 둔 규율과 같다: 파생이 가능하면 파생이 이기고, 파생이
|
|
4508
|
+
* 불가능할 때만 원본의 단정을 쓴다. 둘 다 없으면 **싣지 않는다** — 「모른다」를 숫자로 만들지 않는다.
|
|
4509
|
+
*
|
|
4510
|
+
* 예전에는 원본이 진척을 말해도 조용히 버렸다(문이 받아 놓고 파생이 덮었다). 넣었는데 사라지는
|
|
4511
|
+
* 것이 이 시스템에서 가장 비싼 침묵이다.
|
|
4512
|
+
*/
|
|
4513
|
+
...t.status === "in-progress" ? {
|
|
4514
|
+
remainingMs: t.remainingMs,
|
|
4515
|
+
startedAtSimMs: t.startedAtSimMs,
|
|
4516
|
+
...(() => {
|
|
4517
|
+
const derived = this.progressOf(t);
|
|
4518
|
+
const p = derived ?? t.progress;
|
|
4519
|
+
return p === void 0 ? {} : { progress: p };
|
|
4520
|
+
})()
|
|
4521
|
+
} : {},
|
|
4488
4522
|
...t.personnel?.length ? { personnel: t.personnel.slice() } : {},
|
|
4489
4523
|
...t.assets?.length ? { assets: t.assets.slice() } : {},
|
|
4490
4524
|
...t.resources?.length ? { resources: t.resources.slice() } : {},
|
|
@@ -6186,8 +6220,27 @@ var FlowEngine = class {
|
|
|
6186
6220
|
...i.ilmd ? { ilmd: i.ilmd } : {}
|
|
6187
6221
|
};
|
|
6188
6222
|
}
|
|
6223
|
+
/**
|
|
6224
|
+
* 진척 — **모르면 답하지 않는다.**
|
|
6225
|
+
*
|
|
6226
|
+
* ── 무엇이 틀려 있었나 (2026-08-23 실측) ───────────────────────────────────
|
|
6227
|
+
* 예전에는 `durationMs <= 0` 이면 `1` 을 돌려줬다. 시뮬에서는 그것이 맞다(소요 0 = 즉시 끝나는 일).
|
|
6228
|
+
* 그런데 **미러에서 `durationMs` 0 은 「즉시」가 아니라 「모른다」다** — 원본이 소요를 말해 주지
|
|
6229
|
+
* 않으면 씨앗 경로가 0 으로 메우고, 그러면 이 함수가 **진행 중인 작업을 100% 라고 말한다.**
|
|
6230
|
+
*
|
|
6231
|
+
* 실측: 소요를 주지 않는 원본의 진행 중 작업이 `progress: 1` 로 나왔다. 「모름」을 「완료」로 말하는
|
|
6232
|
+
* 것이고, 화면은 그것을 사실로 그린다.
|
|
6233
|
+
*
|
|
6234
|
+
* 시뮬에서는 이 반환이 바뀌지 않는다: 소요 0 인 작업은 배정된 tick 안에서 끝나므로 **진행 중으로
|
|
6235
|
+
* 관측되지 않는다**(`status = 'in-progress'; remainingMs = durationMs` → 다음 진행에서 즉시 완료).
|
|
6236
|
+
* 즉 `durationMs <= 0` 이면서 진행 중인 작업은 미러 경로에서만 나온다.
|
|
6237
|
+
*
|
|
6238
|
+
* 모를 때 원본이 진척을 말해 주었다면 그 값을 쓴다 — 그 규칙은 부르는 쪽에 있다(§`getSnapshot`).
|
|
6239
|
+
* 이 함수는 **커널이 계산할 수 있나**만 답한다.
|
|
6240
|
+
*/
|
|
6189
6241
|
progressOf(t) {
|
|
6190
|
-
|
|
6242
|
+
if (!(t.durationMs > 0)) return void 0;
|
|
6243
|
+
return Math.min(1, Math.max(0, (t.durationMs - t.remainingMs) / t.durationMs));
|
|
6191
6244
|
}
|
|
6192
6245
|
generate() {
|
|
6193
6246
|
for (const g of this.gens) {
|
package/package.json
CHANGED