@operato/twin-kernel 0.7.27 → 0.7.29
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/flow-engine.d.ts +93 -4
- package/dist/flow-engine.js +149 -11
- package/dist-cjs/index.cjs +128 -10
- package/package.json +1 -1
package/dist/flow-engine.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { TestResult, ISOTime, MaterialQuantity, WorkCalendarEntry, Effectiv
|
|
|
2
2
|
import type { EpcisEvent, BizTransactionElement } from './epcis.ts';
|
|
3
3
|
import type { AllocationPolicy, SlotView } from './allocation-policy.ts';
|
|
4
4
|
import type { DurationEstimator, DurationContext } from './duration-estimator.ts';
|
|
5
|
-
import type { OperationDef } from './domain-definition.ts';
|
|
5
|
+
import type { OperationDef, IsoDuration } from './domain-definition.ts';
|
|
6
6
|
import { type CapacityAnalysis } from './capacity.ts';
|
|
7
7
|
import { type CommittedDemand, type OperationsCapabilityReport } from './operations-capability.ts';
|
|
8
8
|
export interface FlowLocation {
|
|
@@ -210,6 +210,51 @@ interface Rng {
|
|
|
210
210
|
* FlowEngine.computeAttentions(sim) 와 live projector 미러가 **공유**하는 계산 층(face2-inbound-live §1.1):
|
|
211
211
|
* sim/live 가 같은 임계값·규칙을 쓴다. good/scrap 미제공(관측 상태에 없음)이면 scrap-high 는 자연 스킵(metric 층 갭).
|
|
212
212
|
*/
|
|
213
|
+
/**
|
|
214
|
+
* 주의 판정의 **기준값 어휘** — 현장이 정하는 수다.
|
|
215
|
+
*
|
|
216
|
+
* ── 왜 선언으로 받나 (2026-08-18) ───────────────────────────────────────────
|
|
217
|
+
* 혼잡 90%·불량 15%는 **우리가 정한 수**였다(코드 상수). 그런데 통로를 95%로 채워 쓰는 창고는 그 기준에서
|
|
218
|
+
* 영구히 「병목」이고, 초기 라인(불량 20% 예상)은 계속 「불량 과다」로 운다. 경보가 소음이 되면 사람이
|
|
219
|
+
* 경보 자체를 읽지 않는다 — 그때 진짜 경보도 함께 묻힌다.
|
|
220
|
+
*
|
|
221
|
+
* 그래서 기준을 현장이 넣을 수 있게 하고, **넣지 않으면 기본값을 쓰되 그 사실을 밝힌다**(`basis`).
|
|
222
|
+
* 조용히 기본값을 진실처럼 두지 않는다.
|
|
223
|
+
*
|
|
224
|
+
* 퍼센트다: 90 이 90%이고 0.9 는 0.9%다(SOC 에서 한 번 겪은 함정 — 명세가 그 말을 한다).
|
|
225
|
+
*/
|
|
226
|
+
export declare const ATTENTION_PROPERTY: {
|
|
227
|
+
/** 자리의 혼잡 판정 기준(%) — 점유/용량이 이 값을 넘으면 병목으로 본다. */
|
|
228
|
+
readonly congestionRatio: "attention.congestionRatio";
|
|
229
|
+
/** 설비의 불량률 판정 기준(%) — 이 값을 넘으면 불량 과다로 본다. */
|
|
230
|
+
readonly scrapRate: "attention.scrapRate";
|
|
231
|
+
/** 불량률을 재기 시작할 최소 표본 수 — 적은 표본의 비율은 판정 근거가 못 된다. */
|
|
232
|
+
readonly scrapMinSamples: "attention.scrapMinSamples";
|
|
233
|
+
};
|
|
234
|
+
/** 선언이 없을 때 쓰는 기본값 — **드러내 둔다**(코드 안에 숨은 상수가 아니라 계약의 일부다). */
|
|
235
|
+
export declare const ATTENTION_DEFAULTS: {
|
|
236
|
+
readonly congestionPct: 90;
|
|
237
|
+
readonly scrapPct: 15;
|
|
238
|
+
readonly scrapMinSamples: 10;
|
|
239
|
+
};
|
|
240
|
+
/**
|
|
241
|
+
* 공정에 선언할 수 있는 것의 **어휘** — 지금은 소요시간 하나다(`declareDurations` 가 읽는다).
|
|
242
|
+
*
|
|
243
|
+
* 자리·설비의 속성처럼 「속성 id」로 부른다: 선언을 받는 문·화면·AI 가 같은 이름을 써야 하고, 그 이름이
|
|
244
|
+
* 두 곳에 적히면 한쪽만 바뀌는 순간 값이 조용히 버려진다.
|
|
245
|
+
*
|
|
246
|
+
* 이 값이 얹히는 대상은 **자리·설비가 아니라 공정 종류**다(`FlowTask.kind` = `OperationDef.key`).
|
|
247
|
+
*/
|
|
248
|
+
export declare const OPERATION_PROPERTY: {
|
|
249
|
+
/** 그 현장에서 이 공정이 걸리는 시간 — 실측이 없을 때 상수를 대신한다. */
|
|
250
|
+
readonly duration: "operation.duration";
|
|
251
|
+
};
|
|
252
|
+
/** 판정에 쓸 기준 — 대상별 선언이 있으면 그것, 없으면 기본값(그 사실을 함께 든다). */
|
|
253
|
+
export interface AttentionThresholds {
|
|
254
|
+
congestionPctOf?: (locationId: string) => number | undefined;
|
|
255
|
+
scrapPctOf?: (equipmentId: string) => number | undefined;
|
|
256
|
+
scrapMinSamplesOf?: (equipmentId: string) => number | undefined;
|
|
257
|
+
}
|
|
213
258
|
export declare function deriveAttentions(view: {
|
|
214
259
|
equipment: {
|
|
215
260
|
id: string;
|
|
@@ -243,7 +288,13 @@ export declare function deriveAttentions(view: {
|
|
|
243
288
|
}[];
|
|
244
289
|
}, acked?: ReadonlySet<string>,
|
|
245
290
|
/** 지금(ISO) — 납기 판정에 필요하다. **주지 않으면 지연을 판정하지 않는다**(모르면 판단하지 않는다). */
|
|
246
|
-
nowIso?: ISOTime
|
|
291
|
+
nowIso?: ISOTime,
|
|
292
|
+
/**
|
|
293
|
+
* 현장이 선언한 판정 기준 — 주지 않으면 기본값으로 판정하고 그 사실을 `params.basis` 로 밝힌다.
|
|
294
|
+
*
|
|
295
|
+
* 부르는 쪽이 모델에서 뽑아 넘긴다(이 함수는 순수하게 남는다 — 모델을 읽는 규칙이 두 곳이 되지 않게).
|
|
296
|
+
*/
|
|
297
|
+
thresholds?: AttentionThresholds): Attention[];
|
|
247
298
|
/** OEE 계측 카운터 — sim 은 tick 으로 누적, live 는 실 텔레메트리 또는 이벤트 누적기가 채운다(face2-inbound-live §1.1). */
|
|
248
299
|
export interface OeeCounters {
|
|
249
300
|
runMs: number;
|
|
@@ -308,6 +359,13 @@ export declare abstract class FlowEngine implements TwinKernel {
|
|
|
308
359
|
* 도메인 정의에서 실어 온다(`loadOperations`). 없으면 커널 기본값을 쓰고 그 사실을 `specCoverage()` 가 밝힌다.
|
|
309
360
|
*/
|
|
310
361
|
protected operationSpecs: Map<string, OperationDef>;
|
|
362
|
+
/**
|
|
363
|
+
* 현장이 선언한 소요시간(작업 종류 → ms) — `declareDurations` 로 들어온다.
|
|
364
|
+
*
|
|
365
|
+
* 명세 행과 **따로** 두는 이유: 행이 없는 종류에도 시간을 줄 수 있어야 하고(창고·야드 트윈에는 행이
|
|
366
|
+
* 없다), 행을 지어 만들면 지어낸 `intent` 가 능력 계산까지 오염시킨다.
|
|
367
|
+
*/
|
|
368
|
+
protected localDurations: Map<string, number>;
|
|
311
369
|
/** 관측 구동(P0) — 이벤트를 접는 투영기와 그 사실. tick 과 섞이지 않게 명시적으로 들고 있다. */
|
|
312
370
|
private observer?;
|
|
313
371
|
/** 관측분이 아직 커널 상태로 옮겨지지 않았다 — 스냅샷·fork 직전에 한 번만 옮긴다. */
|
|
@@ -491,6 +549,14 @@ export declare abstract class FlowEngine implements TwinKernel {
|
|
|
491
549
|
getSnapshot(): StateSnapshot;
|
|
492
550
|
protected computeAttentions(): Attention[];
|
|
493
551
|
protected collectAttentions(): Attention[];
|
|
552
|
+
/**
|
|
553
|
+
* 현장이 선언한 판정 기준을 모델에서 뽑는다 — **규칙은 한 곳**(순수 함수는 모델을 읽지 않는다).
|
|
554
|
+
*
|
|
555
|
+
* 자리·설비의 속성에서 읽고, 없으면 `undefined` 를 돌려 기본값이 쓰이게 한다(그 사실은 판정 층이
|
|
556
|
+
* `basis` 로 밝힌다). 값이 수가 아니거나 범위를 벗어나면 **없는 것으로 본다** — 사람이 손으로 넣는
|
|
557
|
+
* 값이라 잘못 들어올 수 있고, 잘못된 기준으로 판정하는 것은 기본값보다 나쁘다.
|
|
558
|
+
*/
|
|
559
|
+
protected attentionThresholds(): AttentionThresholds;
|
|
494
560
|
/**
|
|
495
561
|
* fork — 현재 상태를 정확히 복제한 새 엔진 (디지털트윈 본연: "현재로부터 예측").
|
|
496
562
|
* 원본(live/sim)은 계속 진행, fork 는 what-if 를 앞으로 시뮬레이션해 forecast·발산(predicted vs actual) 검사에 쓴다.
|
|
@@ -626,6 +692,27 @@ export declare abstract class FlowEngine implements TwinKernel {
|
|
|
626
692
|
*/
|
|
627
693
|
protected declaredOperations(): readonly OperationDef[];
|
|
628
694
|
loadOperations(ops?: OperationDef[]): void;
|
|
695
|
+
/**
|
|
696
|
+
* **현장이 정한 소요시간** — 종류별로 시간만 받는다(2026-08-18).
|
|
697
|
+
*
|
|
698
|
+
* ── 왜 명세 행과 따로 받나 ──────────────────────────────────────────────────
|
|
699
|
+
* 창고·야드 트윈에는 오퍼레이션 명세 행이 **아예 없다**(실측: 35개 트윈 중 WMS·YMS 전부 0개). 그런데
|
|
700
|
+
* 그 트윈들도 `putaway`·`pick`·`spot`·`dwell` 을 돌리고, 시간은 커널 상수에서 온다 — 그 현장이 실제로
|
|
701
|
+
* 몇 분 걸리는지 말할 문이 없었다.
|
|
702
|
+
*
|
|
703
|
+
* 명세 행을 지어 만들 수는 없다: `OperationDef` 는 `label`·`intent` 를 요구하고(ISA-95
|
|
704
|
+
* `OperationsSegment`), 그 둘은 **지어내면 다른 답까지 오염시킨다**(`capacity()`·능력 보고가 의도별로
|
|
705
|
+
* 자원을 센다). 「몇 분 걸리나」를 말하려고 「무슨 종류의 작업인가」를 발명하지 않는다.
|
|
706
|
+
*
|
|
707
|
+
* ── 순서: 실측 > 현장 선언 > 원천 명세 > 상수 ──────────────────────────────
|
|
708
|
+
* 이력에서 배운 값이 가장 강하고(그 현장이 실제로 그랬다), 그다음이 **이 선언**이다 — 원천 명세보다
|
|
709
|
+
* 이긴다(원천 사본의 값이 낡았을 때 고칠 자리가 여기다). 무엇을 썼는지는 `specCoverage()` 가 밝힌다.
|
|
710
|
+
*
|
|
711
|
+
* ── 조용히 버리지 않는다 ────────────────────────────────────────────────────
|
|
712
|
+
* 읽을 수 없는 값은 던진다. 받아 두고 무시하면 화면은 「넣었습니다」라고 말하고 시뮬은 상수로 도는데,
|
|
713
|
+
* 그 어긋남을 아무도 볼 수 없다(이 시스템에서 가장 비싼 종류의 침묵이다).
|
|
714
|
+
*/
|
|
715
|
+
declareDurations(durations: Record<string, IsoDuration> | undefined | null): void;
|
|
629
716
|
/**
|
|
630
717
|
* 라우트(공정 순서) — 수율을 거슬러 올릴 때 필요하다. 기본은 모른다(선언 순서를 쓴다).
|
|
631
718
|
* 생산 정의를 가진 커널이 override 해서 자기 라우트를 답한다.
|
|
@@ -680,10 +767,12 @@ export declare abstract class FlowEngine implements TwinKernel {
|
|
|
680
767
|
*/
|
|
681
768
|
protected committedDemands(): CommittedDemand[];
|
|
682
769
|
/**
|
|
683
|
-
* task 소요 산출 — 우선순위: **① 추정기(이력 보정) → ②
|
|
770
|
+
* task 소요 산출 — 우선순위: **① 추정기(이력 보정) → ② 현장 선언(`declareDurations`) →
|
|
771
|
+
* ③ 명세(ISA-95 Duration + 변동) → ④ 도메인 상수.**
|
|
684
772
|
*
|
|
685
773
|
* 이 순서인 이유: 실측에서 배운 값이 선언값을 이기고, 선언값이 우리가 코드에 고정한 상수를 이긴다.
|
|
686
|
-
*
|
|
774
|
+
* 선언이 둘로 갈리는 이유는 권위다 — **현장이 정한 값**이 원천이 그려 준 명세를 이긴다(ADR-0034).
|
|
775
|
+
* 무엇을 썼는지는 `specCoverage()` 로 드러낸다 — 상수를 쓴 것이 조용히 넘어가지 않게.
|
|
687
776
|
* "얼마"만 소비하고 "경로"는 씬이 소유한다(좌표-free 유지).
|
|
688
777
|
*/
|
|
689
778
|
protected durationOf(ctx: DurationContext, fallbackMs: number): number;
|
package/dist/flow-engine.js
CHANGED
|
@@ -44,9 +44,50 @@ function mulberry32(seed) {
|
|
|
44
44
|
* FlowEngine.computeAttentions(sim) 와 live projector 미러가 **공유**하는 계산 층(face2-inbound-live §1.1):
|
|
45
45
|
* sim/live 가 같은 임계값·규칙을 쓴다. good/scrap 미제공(관측 상태에 없음)이면 scrap-high 는 자연 스킵(metric 층 갭).
|
|
46
46
|
*/
|
|
47
|
+
/**
|
|
48
|
+
* 주의 판정의 **기준값 어휘** — 현장이 정하는 수다.
|
|
49
|
+
*
|
|
50
|
+
* ── 왜 선언으로 받나 (2026-08-18) ───────────────────────────────────────────
|
|
51
|
+
* 혼잡 90%·불량 15%는 **우리가 정한 수**였다(코드 상수). 그런데 통로를 95%로 채워 쓰는 창고는 그 기준에서
|
|
52
|
+
* 영구히 「병목」이고, 초기 라인(불량 20% 예상)은 계속 「불량 과다」로 운다. 경보가 소음이 되면 사람이
|
|
53
|
+
* 경보 자체를 읽지 않는다 — 그때 진짜 경보도 함께 묻힌다.
|
|
54
|
+
*
|
|
55
|
+
* 그래서 기준을 현장이 넣을 수 있게 하고, **넣지 않으면 기본값을 쓰되 그 사실을 밝힌다**(`basis`).
|
|
56
|
+
* 조용히 기본값을 진실처럼 두지 않는다.
|
|
57
|
+
*
|
|
58
|
+
* 퍼센트다: 90 이 90%이고 0.9 는 0.9%다(SOC 에서 한 번 겪은 함정 — 명세가 그 말을 한다).
|
|
59
|
+
*/
|
|
60
|
+
export const ATTENTION_PROPERTY = {
|
|
61
|
+
/** 자리의 혼잡 판정 기준(%) — 점유/용량이 이 값을 넘으면 병목으로 본다. */
|
|
62
|
+
congestionRatio: 'attention.congestionRatio',
|
|
63
|
+
/** 설비의 불량률 판정 기준(%) — 이 값을 넘으면 불량 과다로 본다. */
|
|
64
|
+
scrapRate: 'attention.scrapRate',
|
|
65
|
+
/** 불량률을 재기 시작할 최소 표본 수 — 적은 표본의 비율은 판정 근거가 못 된다. */
|
|
66
|
+
scrapMinSamples: 'attention.scrapMinSamples'
|
|
67
|
+
};
|
|
68
|
+
/** 선언이 없을 때 쓰는 기본값 — **드러내 둔다**(코드 안에 숨은 상수가 아니라 계약의 일부다). */
|
|
69
|
+
export const ATTENTION_DEFAULTS = { congestionPct: 90, scrapPct: 15, scrapMinSamples: 10 };
|
|
70
|
+
/**
|
|
71
|
+
* 공정에 선언할 수 있는 것의 **어휘** — 지금은 소요시간 하나다(`declareDurations` 가 읽는다).
|
|
72
|
+
*
|
|
73
|
+
* 자리·설비의 속성처럼 「속성 id」로 부른다: 선언을 받는 문·화면·AI 가 같은 이름을 써야 하고, 그 이름이
|
|
74
|
+
* 두 곳에 적히면 한쪽만 바뀌는 순간 값이 조용히 버려진다.
|
|
75
|
+
*
|
|
76
|
+
* 이 값이 얹히는 대상은 **자리·설비가 아니라 공정 종류**다(`FlowTask.kind` = `OperationDef.key`).
|
|
77
|
+
*/
|
|
78
|
+
export const OPERATION_PROPERTY = {
|
|
79
|
+
/** 그 현장에서 이 공정이 걸리는 시간 — 실측이 없을 때 상수를 대신한다. */
|
|
80
|
+
duration: 'operation.duration'
|
|
81
|
+
};
|
|
47
82
|
export function deriveAttentions(view, acked,
|
|
48
83
|
/** 지금(ISO) — 납기 판정에 필요하다. **주지 않으면 지연을 판정하지 않는다**(모르면 판단하지 않는다). */
|
|
49
|
-
nowIso
|
|
84
|
+
nowIso,
|
|
85
|
+
/**
|
|
86
|
+
* 현장이 선언한 판정 기준 — 주지 않으면 기본값으로 판정하고 그 사실을 `params.basis` 로 밝힌다.
|
|
87
|
+
*
|
|
88
|
+
* 부르는 쪽이 모델에서 뽑아 넘긴다(이 함수는 순수하게 남는다 — 모델을 읽는 규칙이 두 곳이 되지 않게).
|
|
89
|
+
*/
|
|
90
|
+
thresholds) {
|
|
50
91
|
// 언어 중립: kind + params(원시값) + 조치 code 만 방출. 사람이 읽는 title/detail/rationale/라벨은
|
|
51
92
|
// 표현계층(클라 i18next)이 kind/code 로 렌더한다(무방언·다국어 — design/plans/i18n.md L3).
|
|
52
93
|
const out = [];
|
|
@@ -67,12 +108,19 @@ nowIso) {
|
|
|
67
108
|
for (const n of view.locations) {
|
|
68
109
|
if ((n.capacity ?? 0) > 0) {
|
|
69
110
|
const r = (n.occupancy ?? 0) / n.capacity;
|
|
70
|
-
|
|
111
|
+
/* 기준은 현장이 정한다 — 없으면 기본값이고, 어느 쪽인지 함께 낸다(조용히 기본값을 진실로 두지 않는다). */
|
|
112
|
+
const declaredPct = thresholds?.congestionPctOf?.(n.id);
|
|
113
|
+
const thresholdPct = declaredPct ?? ATTENTION_DEFAULTS.congestionPct;
|
|
114
|
+
if (r * 100 >= thresholdPct) {
|
|
71
115
|
const saturated = r >= 1;
|
|
72
116
|
out.push({
|
|
73
117
|
id: `bottleneck:${n.id}`, kind: 'bottleneck', severity: saturated ? 'high' : 'medium',
|
|
74
118
|
anchor: { locationId: n.id },
|
|
75
|
-
params: {
|
|
119
|
+
params: {
|
|
120
|
+
locationId: n.id, occupancy: n.occupancy ?? 0, capacity: n.capacity ?? 0, ratioPct: Math.round(r * 100),
|
|
121
|
+
saturated: saturated ? 1 : 0,
|
|
122
|
+
thresholdPct, basis: declaredPct === undefined ? 'default' : 'declared'
|
|
123
|
+
},
|
|
76
124
|
recommendedActions: [{ code: 'advice.add-resource' }, { code: 'advice.downstream-priority' }]
|
|
77
125
|
// 권고만(command 없음) — 표현계층이 advice.* 코드로 렌더
|
|
78
126
|
});
|
|
@@ -130,13 +178,21 @@ nowIso) {
|
|
|
130
178
|
}
|
|
131
179
|
for (const m of view.equipment) {
|
|
132
180
|
const total = (m.goodCount ?? 0) + (m.scrapCount ?? 0);
|
|
133
|
-
|
|
181
|
+
const declaredMin = thresholds?.scrapMinSamplesOf?.(m.id);
|
|
182
|
+
const minSamples = declaredMin ?? ATTENTION_DEFAULTS.scrapMinSamples;
|
|
183
|
+
if (total >= minSamples) {
|
|
134
184
|
const rate = (m.scrapCount ?? 0) / total;
|
|
135
|
-
|
|
185
|
+
const declaredPct = thresholds?.scrapPctOf?.(m.id);
|
|
186
|
+
const thresholdPct = declaredPct ?? ATTENTION_DEFAULTS.scrapPct;
|
|
187
|
+
/* 「높음」은 기준의 두 배에서 — 기준이 현장의 것이면 그 두 배도 현장의 것이다(상수 두 벌을 두지 않는다). */
|
|
188
|
+
if (rate * 100 >= thresholdPct)
|
|
136
189
|
out.push({
|
|
137
|
-
id: `scrap:${m.id}`, kind: 'scrap-high', severity: rate >=
|
|
190
|
+
id: `scrap:${m.id}`, kind: 'scrap-high', severity: rate * 100 >= thresholdPct * 2 ? 'high' : 'medium',
|
|
138
191
|
anchor: { moverId: m.id, locationId: m.location },
|
|
139
|
-
params: {
|
|
192
|
+
params: {
|
|
193
|
+
moverId: m.id, goodCount: m.goodCount ?? 0, scrapCount: m.scrapCount ?? 0, ratePct: Math.round(rate * 100),
|
|
194
|
+
thresholdPct, basis: declaredPct === undefined ? 'default' : 'declared'
|
|
195
|
+
},
|
|
140
196
|
recommendedActions: [
|
|
141
197
|
{ code: 'act.hold-for-inspection', command: CMD.resourceHold, args: { resourceId: m.id } },
|
|
142
198
|
{ code: 'act.reset-metrics', command: CMD.resourceResetMetrics, args: { resourceId: m.id } }
|
|
@@ -267,6 +323,13 @@ export class FlowEngine {
|
|
|
267
323
|
* 도메인 정의에서 실어 온다(`loadOperations`). 없으면 커널 기본값을 쓰고 그 사실을 `specCoverage()` 가 밝힌다.
|
|
268
324
|
*/
|
|
269
325
|
operationSpecs = new Map();
|
|
326
|
+
/**
|
|
327
|
+
* 현장이 선언한 소요시간(작업 종류 → ms) — `declareDurations` 로 들어온다.
|
|
328
|
+
*
|
|
329
|
+
* 명세 행과 **따로** 두는 이유: 행이 없는 종류에도 시간을 줄 수 있어야 하고(창고·야드 트윈에는 행이
|
|
330
|
+
* 없다), 행을 지어 만들면 지어낸 `intent` 가 능력 계산까지 오염시킨다.
|
|
331
|
+
*/
|
|
332
|
+
localDurations = new Map();
|
|
270
333
|
/** 관측 구동(P0) — 이벤트를 접는 투영기와 그 사실. tick 과 섞이지 않게 명시적으로 들고 있다. */
|
|
271
334
|
observer;
|
|
272
335
|
/** 관측분이 아직 커널 상태로 옮겨지지 않았다 — 스냅샷·fork 직전에 한 번만 옮긴다. */
|
|
@@ -1105,10 +1168,40 @@ export class FlowEngine {
|
|
|
1105
1168
|
locations: [...this.locations.values()],
|
|
1106
1169
|
orders: [...this.orders.values()],
|
|
1107
1170
|
tasks: [...this.tasks.values()]
|
|
1108
|
-
}, this._acked, now // 지연 판정의 "지금" — 관측 중이면 마지막으로 들은 시각이다(§nowMs)
|
|
1109
|
-
);
|
|
1171
|
+
}, this._acked, now, // 지연 판정의 "지금" — 관측 중이면 마지막으로 들은 시각이다(§nowMs)
|
|
1172
|
+
this.attentionThresholds());
|
|
1110
1173
|
return out;
|
|
1111
1174
|
}
|
|
1175
|
+
/**
|
|
1176
|
+
* 현장이 선언한 판정 기준을 모델에서 뽑는다 — **규칙은 한 곳**(순수 함수는 모델을 읽지 않는다).
|
|
1177
|
+
*
|
|
1178
|
+
* 자리·설비의 속성에서 읽고, 없으면 `undefined` 를 돌려 기본값이 쓰이게 한다(그 사실은 판정 층이
|
|
1179
|
+
* `basis` 로 밝힌다). 값이 수가 아니거나 범위를 벗어나면 **없는 것으로 본다** — 사람이 손으로 넣는
|
|
1180
|
+
* 값이라 잘못 들어올 수 있고, 잘못된 기준으로 판정하는 것은 기본값보다 나쁘다.
|
|
1181
|
+
*/
|
|
1182
|
+
attentionThresholds() {
|
|
1183
|
+
const pct = (props, id) => {
|
|
1184
|
+
const raw = props?.find(p => p.id === id)?.value;
|
|
1185
|
+
if (raw === undefined)
|
|
1186
|
+
return undefined;
|
|
1187
|
+
const n = Number(raw);
|
|
1188
|
+
return Number.isFinite(n) && n > 0 && n <= 100 ? n : undefined;
|
|
1189
|
+
};
|
|
1190
|
+
const count = (props, id) => {
|
|
1191
|
+
const raw = props?.find(p => p.id === id)?.value;
|
|
1192
|
+
if (raw === undefined)
|
|
1193
|
+
return undefined;
|
|
1194
|
+
const n = Number(raw);
|
|
1195
|
+
return Number.isFinite(n) && n >= 1 ? Math.floor(n) : undefined;
|
|
1196
|
+
};
|
|
1197
|
+
const locProps = new Map((this.boardDef?.locations ?? []).map(l => [l.id, l.properties]));
|
|
1198
|
+
const eqProps = new Map(readBoardEquipment(this.boardDef ?? {}).map(e => [e.id, e.properties]));
|
|
1199
|
+
return {
|
|
1200
|
+
congestionPctOf: id => pct(locProps.get(id), ATTENTION_PROPERTY.congestionRatio),
|
|
1201
|
+
scrapPctOf: id => pct(eqProps.get(id), ATTENTION_PROPERTY.scrapRate),
|
|
1202
|
+
scrapMinSamplesOf: id => count(eqProps.get(id), ATTENTION_PROPERTY.scrapMinSamples)
|
|
1203
|
+
};
|
|
1204
|
+
}
|
|
1112
1205
|
/**
|
|
1113
1206
|
* fork — 현재 상태를 정확히 복제한 새 엔진 (디지털트윈 본연: "현재로부터 예측").
|
|
1114
1207
|
* 원본(live/sim)은 계속 진행, fork 는 what-if 를 앞으로 시뮬레이션해 forecast·발산(predicted vs actual) 검사에 쓴다.
|
|
@@ -1351,6 +1444,39 @@ export class FlowEngine {
|
|
|
1351
1444
|
if (o?.key)
|
|
1352
1445
|
this.operationSpecs.set(o.key, o);
|
|
1353
1446
|
}
|
|
1447
|
+
/**
|
|
1448
|
+
* **현장이 정한 소요시간** — 종류별로 시간만 받는다(2026-08-18).
|
|
1449
|
+
*
|
|
1450
|
+
* ── 왜 명세 행과 따로 받나 ──────────────────────────────────────────────────
|
|
1451
|
+
* 창고·야드 트윈에는 오퍼레이션 명세 행이 **아예 없다**(실측: 35개 트윈 중 WMS·YMS 전부 0개). 그런데
|
|
1452
|
+
* 그 트윈들도 `putaway`·`pick`·`spot`·`dwell` 을 돌리고, 시간은 커널 상수에서 온다 — 그 현장이 실제로
|
|
1453
|
+
* 몇 분 걸리는지 말할 문이 없었다.
|
|
1454
|
+
*
|
|
1455
|
+
* 명세 행을 지어 만들 수는 없다: `OperationDef` 는 `label`·`intent` 를 요구하고(ISA-95
|
|
1456
|
+
* `OperationsSegment`), 그 둘은 **지어내면 다른 답까지 오염시킨다**(`capacity()`·능력 보고가 의도별로
|
|
1457
|
+
* 자원을 센다). 「몇 분 걸리나」를 말하려고 「무슨 종류의 작업인가」를 발명하지 않는다.
|
|
1458
|
+
*
|
|
1459
|
+
* ── 순서: 실측 > 현장 선언 > 원천 명세 > 상수 ──────────────────────────────
|
|
1460
|
+
* 이력에서 배운 값이 가장 강하고(그 현장이 실제로 그랬다), 그다음이 **이 선언**이다 — 원천 명세보다
|
|
1461
|
+
* 이긴다(원천 사본의 값이 낡았을 때 고칠 자리가 여기다). 무엇을 썼는지는 `specCoverage()` 가 밝힌다.
|
|
1462
|
+
*
|
|
1463
|
+
* ── 조용히 버리지 않는다 ────────────────────────────────────────────────────
|
|
1464
|
+
* 읽을 수 없는 값은 던진다. 받아 두고 무시하면 화면은 「넣었습니다」라고 말하고 시뮬은 상수로 도는데,
|
|
1465
|
+
* 그 어긋남을 아무도 볼 수 없다(이 시스템에서 가장 비싼 종류의 침묵이다).
|
|
1466
|
+
*/
|
|
1467
|
+
declareDurations(durations) {
|
|
1468
|
+
for (const [kind, text] of Object.entries(durations ?? {})) {
|
|
1469
|
+
const key = String(kind ?? '').trim();
|
|
1470
|
+
if (!key)
|
|
1471
|
+
throw new Error('declareDurations: an operation kind is empty — say which operation the duration belongs to');
|
|
1472
|
+
const ms = parseIsoDuration(text);
|
|
1473
|
+
if (ms === undefined)
|
|
1474
|
+
throw new Error(`declareDurations: "${text}" is not an ISO 8601 duration (operation "${key}") — the value would be dropped without a trace`);
|
|
1475
|
+
if (ms <= 0)
|
|
1476
|
+
throw new Error(`declareDurations: operation "${key}" cannot take ${ms}ms — a task with no duration never finishes`);
|
|
1477
|
+
this.localDurations.set(key, ms);
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1354
1480
|
/**
|
|
1355
1481
|
* 라우트(공정 순서) — 수율을 거슬러 올릴 때 필요하다. 기본은 모른다(선언 순서를 쓴다).
|
|
1356
1482
|
* 생산 정의를 가진 커널이 override 해서 자기 라우트를 답한다.
|
|
@@ -1454,10 +1580,12 @@ export class FlowEngine {
|
|
|
1454
1580
|
return out;
|
|
1455
1581
|
}
|
|
1456
1582
|
/**
|
|
1457
|
-
* task 소요 산출 — 우선순위: **① 추정기(이력 보정) → ②
|
|
1583
|
+
* task 소요 산출 — 우선순위: **① 추정기(이력 보정) → ② 현장 선언(`declareDurations`) →
|
|
1584
|
+
* ③ 명세(ISA-95 Duration + 변동) → ④ 도메인 상수.**
|
|
1458
1585
|
*
|
|
1459
1586
|
* 이 순서인 이유: 실측에서 배운 값이 선언값을 이기고, 선언값이 우리가 코드에 고정한 상수를 이긴다.
|
|
1460
|
-
*
|
|
1587
|
+
* 선언이 둘로 갈리는 이유는 권위다 — **현장이 정한 값**이 원천이 그려 준 명세를 이긴다(ADR-0034).
|
|
1588
|
+
* 무엇을 썼는지는 `specCoverage()` 로 드러낸다 — 상수를 쓴 것이 조용히 넘어가지 않게.
|
|
1461
1589
|
* "얼마"만 소비하고 "경로"는 씬이 소유한다(좌표-free 유지).
|
|
1462
1590
|
*/
|
|
1463
1591
|
durationOf(ctx, fallbackMs) {
|
|
@@ -1474,6 +1602,16 @@ export class FlowEngine {
|
|
|
1474
1602
|
return this.sampleSpread(estimated.meanMs, estimated.spread);
|
|
1475
1603
|
}
|
|
1476
1604
|
const spec = this.operationSpecs.get(ctx.kind);
|
|
1605
|
+
/*
|
|
1606
|
+
* **현장이 정한 시간이 원천 명세를 이긴다** — 원천 사본의 값이 낡았을 때 고칠 자리가 그것뿐이다
|
|
1607
|
+
* (ADR-0034: 우리가 정한 값이 원천이 그려 준 값을 이긴다). 변동은 명세 행이 말한 것을 그대로 쓴다 —
|
|
1608
|
+
* 평균만 우리가 고쳤다고 퍼짐을 0 으로 만들면 줄이 사라진다.
|
|
1609
|
+
*/
|
|
1610
|
+
const local = this.localDurations.get(ctx.kind);
|
|
1611
|
+
if (local !== undefined) {
|
|
1612
|
+
this.noteSpecUse(ctx.kind, 'declared');
|
|
1613
|
+
return this.applyVariability(local, spec?.variability);
|
|
1614
|
+
}
|
|
1477
1615
|
const declared = parseIsoDuration(spec?.duration);
|
|
1478
1616
|
if (declared === undefined) {
|
|
1479
1617
|
this.noteSpecUse(ctx.kind, 'default');
|
package/dist-cjs/index.cjs
CHANGED
|
@@ -19,6 +19,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
19
19
|
// src/index.ts
|
|
20
20
|
var index_exports = {};
|
|
21
21
|
__export(index_exports, {
|
|
22
|
+
ATTENTION_DEFAULTS: () => ATTENTION_DEFAULTS,
|
|
23
|
+
ATTENTION_PROPERTY: () => ATTENTION_PROPERTY,
|
|
22
24
|
BIZSTEP: () => BIZSTEP,
|
|
23
25
|
BTT: () => BTT,
|
|
24
26
|
BTT_DELIVERY: () => BTT_DELIVERY,
|
|
@@ -53,6 +55,7 @@ __export(index_exports, {
|
|
|
53
55
|
MES_PRODUCT_GTINS: () => MES_PRODUCT_GTINS,
|
|
54
56
|
MES_TYPES: () => MES_TYPES,
|
|
55
57
|
MesKernel: () => MesKernel,
|
|
58
|
+
OPERATION_PROPERTY: () => OPERATION_PROPERTY,
|
|
56
59
|
OP_EVENT: () => OP_EVENT,
|
|
57
60
|
OP_PARAM: () => OP_PARAM,
|
|
58
61
|
ObservedReducer: () => ObservedReducer,
|
|
@@ -2952,7 +2955,20 @@ function mulberry32(seed) {
|
|
|
2952
2955
|
} });
|
|
2953
2956
|
return fn;
|
|
2954
2957
|
}
|
|
2955
|
-
|
|
2958
|
+
var ATTENTION_PROPERTY = {
|
|
2959
|
+
/** 자리의 혼잡 판정 기준(%) — 점유/용량이 이 값을 넘으면 병목으로 본다. */
|
|
2960
|
+
congestionRatio: "attention.congestionRatio",
|
|
2961
|
+
/** 설비의 불량률 판정 기준(%) — 이 값을 넘으면 불량 과다로 본다. */
|
|
2962
|
+
scrapRate: "attention.scrapRate",
|
|
2963
|
+
/** 불량률을 재기 시작할 최소 표본 수 — 적은 표본의 비율은 판정 근거가 못 된다. */
|
|
2964
|
+
scrapMinSamples: "attention.scrapMinSamples"
|
|
2965
|
+
};
|
|
2966
|
+
var ATTENTION_DEFAULTS = { congestionPct: 90, scrapPct: 15, scrapMinSamples: 10 };
|
|
2967
|
+
var OPERATION_PROPERTY = {
|
|
2968
|
+
/** 그 현장에서 이 공정이 걸리는 시간 — 실측이 없을 때 상수를 대신한다. */
|
|
2969
|
+
duration: "operation.duration"
|
|
2970
|
+
};
|
|
2971
|
+
function deriveAttentions(view, acked, nowIso, thresholds) {
|
|
2956
2972
|
const out = [];
|
|
2957
2973
|
for (const m of view.equipment) {
|
|
2958
2974
|
if (m.status === "down") {
|
|
@@ -2973,14 +2989,24 @@ function deriveAttentions(view, acked, nowIso) {
|
|
|
2973
2989
|
for (const n of view.locations) {
|
|
2974
2990
|
if ((n.capacity ?? 0) > 0) {
|
|
2975
2991
|
const r = (n.occupancy ?? 0) / n.capacity;
|
|
2976
|
-
|
|
2992
|
+
const declaredPct = thresholds?.congestionPctOf?.(n.id);
|
|
2993
|
+
const thresholdPct = declaredPct ?? ATTENTION_DEFAULTS.congestionPct;
|
|
2994
|
+
if (r * 100 >= thresholdPct) {
|
|
2977
2995
|
const saturated = r >= 1;
|
|
2978
2996
|
out.push({
|
|
2979
2997
|
id: `bottleneck:${n.id}`,
|
|
2980
2998
|
kind: "bottleneck",
|
|
2981
2999
|
severity: saturated ? "high" : "medium",
|
|
2982
3000
|
anchor: { locationId: n.id },
|
|
2983
|
-
params: {
|
|
3001
|
+
params: {
|
|
3002
|
+
locationId: n.id,
|
|
3003
|
+
occupancy: n.occupancy ?? 0,
|
|
3004
|
+
capacity: n.capacity ?? 0,
|
|
3005
|
+
ratioPct: Math.round(r * 100),
|
|
3006
|
+
saturated: saturated ? 1 : 0,
|
|
3007
|
+
thresholdPct,
|
|
3008
|
+
basis: declaredPct === void 0 ? "default" : "declared"
|
|
3009
|
+
},
|
|
2984
3010
|
recommendedActions: [{ code: "advice.add-resource" }, { code: "advice.downstream-priority" }]
|
|
2985
3011
|
// 권고만(command 없음) — 표현계층이 advice.* 코드로 렌더
|
|
2986
3012
|
});
|
|
@@ -3013,14 +3039,25 @@ function deriveAttentions(view, acked, nowIso) {
|
|
|
3013
3039
|
}
|
|
3014
3040
|
for (const m of view.equipment) {
|
|
3015
3041
|
const total = (m.goodCount ?? 0) + (m.scrapCount ?? 0);
|
|
3016
|
-
|
|
3042
|
+
const declaredMin = thresholds?.scrapMinSamplesOf?.(m.id);
|
|
3043
|
+
const minSamples = declaredMin ?? ATTENTION_DEFAULTS.scrapMinSamples;
|
|
3044
|
+
if (total >= minSamples) {
|
|
3017
3045
|
const rate = (m.scrapCount ?? 0) / total;
|
|
3018
|
-
|
|
3046
|
+
const declaredPct = thresholds?.scrapPctOf?.(m.id);
|
|
3047
|
+
const thresholdPct = declaredPct ?? ATTENTION_DEFAULTS.scrapPct;
|
|
3048
|
+
if (rate * 100 >= thresholdPct) out.push({
|
|
3019
3049
|
id: `scrap:${m.id}`,
|
|
3020
3050
|
kind: "scrap-high",
|
|
3021
|
-
severity: rate >=
|
|
3051
|
+
severity: rate * 100 >= thresholdPct * 2 ? "high" : "medium",
|
|
3022
3052
|
anchor: { moverId: m.id, locationId: m.location },
|
|
3023
|
-
params: {
|
|
3053
|
+
params: {
|
|
3054
|
+
moverId: m.id,
|
|
3055
|
+
goodCount: m.goodCount ?? 0,
|
|
3056
|
+
scrapCount: m.scrapCount ?? 0,
|
|
3057
|
+
ratePct: Math.round(rate * 100),
|
|
3058
|
+
thresholdPct,
|
|
3059
|
+
basis: declaredPct === void 0 ? "default" : "declared"
|
|
3060
|
+
},
|
|
3024
3061
|
recommendedActions: [
|
|
3025
3062
|
{ code: "act.hold-for-inspection", command: CMD.resourceHold, args: { resourceId: m.id } },
|
|
3026
3063
|
{ code: "act.reset-metrics", command: CMD.resourceResetMetrics, args: { resourceId: m.id } }
|
|
@@ -3131,6 +3168,13 @@ var FlowEngine = class {
|
|
|
3131
3168
|
* 도메인 정의에서 실어 온다(`loadOperations`). 없으면 커널 기본값을 쓰고 그 사실을 `specCoverage()` 가 밝힌다.
|
|
3132
3169
|
*/
|
|
3133
3170
|
operationSpecs = /* @__PURE__ */ new Map();
|
|
3171
|
+
/**
|
|
3172
|
+
* 현장이 선언한 소요시간(작업 종류 → ms) — `declareDurations` 로 들어온다.
|
|
3173
|
+
*
|
|
3174
|
+
* 명세 행과 **따로** 두는 이유: 행이 없는 종류에도 시간을 줄 수 있어야 하고(창고·야드 트윈에는 행이
|
|
3175
|
+
* 없다), 행을 지어 만들면 지어낸 `intent` 가 능력 계산까지 오염시킨다.
|
|
3176
|
+
*/
|
|
3177
|
+
localDurations = /* @__PURE__ */ new Map();
|
|
3134
3178
|
/** 관측 구동(P0) — 이벤트를 접는 투영기와 그 사실. tick 과 섞이지 않게 명시적으로 들고 있다. */
|
|
3135
3179
|
observer;
|
|
3136
3180
|
/** 관측분이 아직 커널 상태로 옮겨지지 않았다 — 스냅샷·fork 직전에 한 번만 옮긴다. */
|
|
@@ -3885,11 +3929,44 @@ var FlowEngine = class {
|
|
|
3885
3929
|
tasks: [...this.tasks.values()]
|
|
3886
3930
|
},
|
|
3887
3931
|
this._acked,
|
|
3888
|
-
now
|
|
3932
|
+
now,
|
|
3889
3933
|
// 지연 판정의 "지금" — 관측 중이면 마지막으로 들은 시각이다(§nowMs)
|
|
3934
|
+
this.attentionThresholds()
|
|
3890
3935
|
);
|
|
3891
3936
|
return out;
|
|
3892
3937
|
}
|
|
3938
|
+
/**
|
|
3939
|
+
* 현장이 선언한 판정 기준을 모델에서 뽑는다 — **규칙은 한 곳**(순수 함수는 모델을 읽지 않는다).
|
|
3940
|
+
*
|
|
3941
|
+
* 자리·설비의 속성에서 읽고, 없으면 `undefined` 를 돌려 기본값이 쓰이게 한다(그 사실은 판정 층이
|
|
3942
|
+
* `basis` 로 밝힌다). 값이 수가 아니거나 범위를 벗어나면 **없는 것으로 본다** — 사람이 손으로 넣는
|
|
3943
|
+
* 값이라 잘못 들어올 수 있고, 잘못된 기준으로 판정하는 것은 기본값보다 나쁘다.
|
|
3944
|
+
*/
|
|
3945
|
+
attentionThresholds() {
|
|
3946
|
+
const pct = (props, id) => {
|
|
3947
|
+
const raw = props?.find((p) => p.id === id)?.value;
|
|
3948
|
+
if (raw === void 0) return void 0;
|
|
3949
|
+
const n = Number(raw);
|
|
3950
|
+
return Number.isFinite(n) && n > 0 && n <= 100 ? n : void 0;
|
|
3951
|
+
};
|
|
3952
|
+
const count = (props, id) => {
|
|
3953
|
+
const raw = props?.find((p) => p.id === id)?.value;
|
|
3954
|
+
if (raw === void 0) return void 0;
|
|
3955
|
+
const n = Number(raw);
|
|
3956
|
+
return Number.isFinite(n) && n >= 1 ? Math.floor(n) : void 0;
|
|
3957
|
+
};
|
|
3958
|
+
const locProps = new Map(
|
|
3959
|
+
(this.boardDef?.locations ?? []).map((l) => [l.id, l.properties])
|
|
3960
|
+
);
|
|
3961
|
+
const eqProps = new Map(
|
|
3962
|
+
readBoardEquipment(this.boardDef ?? {}).map((e) => [e.id, e.properties])
|
|
3963
|
+
);
|
|
3964
|
+
return {
|
|
3965
|
+
congestionPctOf: (id) => pct(locProps.get(id), ATTENTION_PROPERTY.congestionRatio),
|
|
3966
|
+
scrapPctOf: (id) => pct(eqProps.get(id), ATTENTION_PROPERTY.scrapRate),
|
|
3967
|
+
scrapMinSamplesOf: (id) => count(eqProps.get(id), ATTENTION_PROPERTY.scrapMinSamples)
|
|
3968
|
+
};
|
|
3969
|
+
}
|
|
3893
3970
|
/**
|
|
3894
3971
|
* fork — 현재 상태를 정확히 복제한 새 엔진 (디지털트윈 본연: "현재로부터 예측").
|
|
3895
3972
|
* 원본(live/sim)은 계속 진행, fork 는 what-if 를 앞으로 시뮬레이션해 forecast·발산(predicted vs actual) 검사에 쓴다.
|
|
@@ -4105,6 +4182,37 @@ var FlowEngine = class {
|
|
|
4105
4182
|
loadOperations(ops = []) {
|
|
4106
4183
|
for (const o of ops) if (o?.key) this.operationSpecs.set(o.key, o);
|
|
4107
4184
|
}
|
|
4185
|
+
/**
|
|
4186
|
+
* **현장이 정한 소요시간** — 종류별로 시간만 받는다(2026-08-18).
|
|
4187
|
+
*
|
|
4188
|
+
* ── 왜 명세 행과 따로 받나 ──────────────────────────────────────────────────
|
|
4189
|
+
* 창고·야드 트윈에는 오퍼레이션 명세 행이 **아예 없다**(실측: 35개 트윈 중 WMS·YMS 전부 0개). 그런데
|
|
4190
|
+
* 그 트윈들도 `putaway`·`pick`·`spot`·`dwell` 을 돌리고, 시간은 커널 상수에서 온다 — 그 현장이 실제로
|
|
4191
|
+
* 몇 분 걸리는지 말할 문이 없었다.
|
|
4192
|
+
*
|
|
4193
|
+
* 명세 행을 지어 만들 수는 없다: `OperationDef` 는 `label`·`intent` 를 요구하고(ISA-95
|
|
4194
|
+
* `OperationsSegment`), 그 둘은 **지어내면 다른 답까지 오염시킨다**(`capacity()`·능력 보고가 의도별로
|
|
4195
|
+
* 자원을 센다). 「몇 분 걸리나」를 말하려고 「무슨 종류의 작업인가」를 발명하지 않는다.
|
|
4196
|
+
*
|
|
4197
|
+
* ── 순서: 실측 > 현장 선언 > 원천 명세 > 상수 ──────────────────────────────
|
|
4198
|
+
* 이력에서 배운 값이 가장 강하고(그 현장이 실제로 그랬다), 그다음이 **이 선언**이다 — 원천 명세보다
|
|
4199
|
+
* 이긴다(원천 사본의 값이 낡았을 때 고칠 자리가 여기다). 무엇을 썼는지는 `specCoverage()` 가 밝힌다.
|
|
4200
|
+
*
|
|
4201
|
+
* ── 조용히 버리지 않는다 ────────────────────────────────────────────────────
|
|
4202
|
+
* 읽을 수 없는 값은 던진다. 받아 두고 무시하면 화면은 「넣었습니다」라고 말하고 시뮬은 상수로 도는데,
|
|
4203
|
+
* 그 어긋남을 아무도 볼 수 없다(이 시스템에서 가장 비싼 종류의 침묵이다).
|
|
4204
|
+
*/
|
|
4205
|
+
declareDurations(durations) {
|
|
4206
|
+
for (const [kind, text] of Object.entries(durations ?? {})) {
|
|
4207
|
+
const key = String(kind ?? "").trim();
|
|
4208
|
+
if (!key) throw new Error("declareDurations: an operation kind is empty \u2014 say which operation the duration belongs to");
|
|
4209
|
+
const ms2 = parseIsoDuration(text);
|
|
4210
|
+
if (ms2 === void 0)
|
|
4211
|
+
throw new Error(`declareDurations: "${text}" is not an ISO 8601 duration (operation "${key}") \u2014 the value would be dropped without a trace`);
|
|
4212
|
+
if (ms2 <= 0) throw new Error(`declareDurations: operation "${key}" cannot take ${ms2}ms \u2014 a task with no duration never finishes`);
|
|
4213
|
+
this.localDurations.set(key, ms2);
|
|
4214
|
+
}
|
|
4215
|
+
}
|
|
4108
4216
|
/**
|
|
4109
4217
|
* 라우트(공정 순서) — 수율을 거슬러 올릴 때 필요하다. 기본은 모른다(선언 순서를 쓴다).
|
|
4110
4218
|
* 생산 정의를 가진 커널이 override 해서 자기 라우트를 답한다.
|
|
@@ -4206,10 +4314,12 @@ var FlowEngine = class {
|
|
|
4206
4314
|
return out;
|
|
4207
4315
|
}
|
|
4208
4316
|
/**
|
|
4209
|
-
* task 소요 산출 — 우선순위: **① 추정기(이력 보정) → ②
|
|
4317
|
+
* task 소요 산출 — 우선순위: **① 추정기(이력 보정) → ② 현장 선언(`declareDurations`) →
|
|
4318
|
+
* ③ 명세(ISA-95 Duration + 변동) → ④ 도메인 상수.**
|
|
4210
4319
|
*
|
|
4211
4320
|
* 이 순서인 이유: 실측에서 배운 값이 선언값을 이기고, 선언값이 우리가 코드에 고정한 상수를 이긴다.
|
|
4212
|
-
*
|
|
4321
|
+
* 선언이 둘로 갈리는 이유는 권위다 — **현장이 정한 값**이 원천이 그려 준 명세를 이긴다(ADR-0034).
|
|
4322
|
+
* 무엇을 썼는지는 `specCoverage()` 로 드러낸다 — 상수를 쓴 것이 조용히 넘어가지 않게.
|
|
4213
4323
|
* "얼마"만 소비하고 "경로"는 씬이 소유한다(좌표-free 유지).
|
|
4214
4324
|
*/
|
|
4215
4325
|
durationOf(ctx, fallbackMs) {
|
|
@@ -4223,6 +4333,11 @@ var FlowEngine = class {
|
|
|
4223
4333
|
return this.sampleSpread(estimated.meanMs, estimated.spread);
|
|
4224
4334
|
}
|
|
4225
4335
|
const spec = this.operationSpecs.get(ctx.kind);
|
|
4336
|
+
const local = this.localDurations.get(ctx.kind);
|
|
4337
|
+
if (local !== void 0) {
|
|
4338
|
+
this.noteSpecUse(ctx.kind, "declared");
|
|
4339
|
+
return this.applyVariability(local, spec?.variability);
|
|
4340
|
+
}
|
|
4226
4341
|
const declared = parseIsoDuration(spec?.duration);
|
|
4227
4342
|
if (declared === void 0) {
|
|
4228
4343
|
this.noteSpecUse(ctx.kind, "default");
|
|
@@ -7175,6 +7290,8 @@ function retiredVocabularyIn(line) {
|
|
|
7175
7290
|
}
|
|
7176
7291
|
// Annotate the CommonJS export names for ESM import in node:
|
|
7177
7292
|
0 && (module.exports = {
|
|
7293
|
+
ATTENTION_DEFAULTS,
|
|
7294
|
+
ATTENTION_PROPERTY,
|
|
7178
7295
|
BIZSTEP,
|
|
7179
7296
|
BTT,
|
|
7180
7297
|
BTT_DELIVERY,
|
|
@@ -7209,6 +7326,7 @@ function retiredVocabularyIn(line) {
|
|
|
7209
7326
|
MES_PRODUCT_GTINS,
|
|
7210
7327
|
MES_TYPES,
|
|
7211
7328
|
MesKernel,
|
|
7329
|
+
OPERATION_PROPERTY,
|
|
7212
7330
|
OP_EVENT,
|
|
7213
7331
|
OP_PARAM,
|
|
7214
7332
|
ObservedReducer,
|
package/package.json
CHANGED