@operato/twin-kernel 0.7.30 → 0.7.32
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 +11 -0
- package/dist/flow-engine.d.ts +90 -0
- package/dist/flow-engine.js +114 -21
- package/dist/mes-kernel.js +5 -2
- package/dist/task-fold.d.ts +10 -0
- package/dist/task-fold.js +3 -0
- package/dist-cjs/index.cjs +117 -14
- package/package.json +1 -1
package/dist/contract.d.ts
CHANGED
|
@@ -1770,6 +1770,17 @@ export interface TaskStatusDelta {
|
|
|
1770
1770
|
remainingMs?: number;
|
|
1771
1771
|
/** 이 작업의 총 소요 예상(ms) — 진척의 분모. */
|
|
1772
1772
|
durationMs?: number;
|
|
1773
|
+
/**
|
|
1774
|
+
* 품질 판정 결과 — **판정이 있었던 작업만** (2026-08-19).
|
|
1775
|
+
*
|
|
1776
|
+
* 왜 계약에 있나: 양품/불량은 설비 누적 카운터(종류를 모른다)와 EPCIS disposition(종류를 모른다)
|
|
1777
|
+
* 에만 남아서, **「이 공정의 양품률이 얼마였나」를 저널에서 되짚을 수 없었다** — 재기동하면 종류별
|
|
1778
|
+
* 품질 이력이 통째로 사라졌다. 판정이 일어나는 자리(작업 완료)에서 사실을 적는다.
|
|
1779
|
+
*
|
|
1780
|
+
* **없음은 「양품」이 아니라 「판정하지 않았다」**다(이동·체류에는 품질 판정이 없다). 그래서 소비처는
|
|
1781
|
+
* 없는 것을 세지 않는다 — 모름을 양품으로 세면 양품률이 조용히 100% 로 올라간다.
|
|
1782
|
+
*/
|
|
1783
|
+
outcome?: 'good' | 'scrap';
|
|
1773
1784
|
/** 우선순위 — 나가지 않으면 미러가 배정 순서의 근거를 모른다(상태⊆이벤트). */
|
|
1774
1785
|
priority?: number;
|
|
1775
1786
|
startTime?: ISOTime;
|
package/dist/flow-engine.d.ts
CHANGED
|
@@ -174,6 +174,20 @@ export interface FlowTask {
|
|
|
174
174
|
* dwell: 자리에서 시간만 소비(무자원 체류·큐어링 — 즉시 진행, 자원 불요).
|
|
175
175
|
*/
|
|
176
176
|
intent?: 'transport' | 'process' | 'dwell';
|
|
177
|
+
/**
|
|
178
|
+
* 이 작업의 **품질 판정 결과** — 판정이 있었을 때만 채운다 (2026-08-19).
|
|
179
|
+
*
|
|
180
|
+
* ── 왜 작업에 적나 ────────────────────────────────────────────────────────
|
|
181
|
+
* 양품/불량은 지금까지 두 곳에만 남았다: 설비 누적 카운터(`goodCount`·`scrapCount` — 종류를 모른다)와
|
|
182
|
+
* EPCIS disposition(`sellable`/`non_sellable` — 종류를 모른다). 그래서 **「이 공정의 양품률이 얼마였나」**
|
|
183
|
+
* 를 되짚을 방법이 저널에 없었다 — 재기동하면 종류별 품질 이력이 통째로 사라진다.
|
|
184
|
+
*
|
|
185
|
+
* 판정이 일어나는 자리는 작업 완료다(그때 종류·자원·결과를 함께 안다). 그러니 그 자리에서 적는다 —
|
|
186
|
+
* 나중에 자리·업무단계로 종류를 되짚는 추론을 만들지 않는다(설비 둘이 한 자리를 쓰면 조용히 틀린다).
|
|
187
|
+
*
|
|
188
|
+
* **없음은 「양품」이 아니라 「판정하지 않았다」**다(이동·체류에는 품질 판정이 없다).
|
|
189
|
+
*/
|
|
190
|
+
outcome?: 'good' | 'scrap';
|
|
177
191
|
}
|
|
178
192
|
/** 오더 라인(멀티SKU) — 라인별 품목·잔량. 단일 SKU 오더 = 1 라인. */
|
|
179
193
|
export interface OrderLine {
|
|
@@ -245,9 +259,29 @@ export declare const ATTENTION_DEFAULTS: {
|
|
|
245
259
|
*
|
|
246
260
|
* 이 값이 얹히는 대상은 **자리·설비가 아니라 공정 종류**다(`FlowTask.kind` = `OperationDef.key`).
|
|
247
261
|
*/
|
|
262
|
+
/**
|
|
263
|
+
* 이력에서 배운 양품률 — 종류별 비율(0..1). **모르면 `undefined`**(0 을 답하지 않는다: 0 은 「전부 불량」이다).
|
|
264
|
+
*
|
|
265
|
+
* 소요시간 추정기와 같은 모양이다(무상태 시임, fork 가 참조를 공유). 표본이 모자란 종류는 추정기가
|
|
266
|
+
* 스스로 빼야 한다 — 두 건으로 만든 비율은 근거가 아니다.
|
|
267
|
+
*/
|
|
268
|
+
export interface YieldEstimator {
|
|
269
|
+
estimate(ctx: {
|
|
270
|
+
kind: string;
|
|
271
|
+
}): number | undefined;
|
|
272
|
+
}
|
|
248
273
|
export declare const OPERATION_PROPERTY: {
|
|
249
274
|
/** 그 현장에서 이 공정이 걸리는 시간 — 실측이 없을 때 상수를 대신한다. */
|
|
250
275
|
readonly duration: "operation.duration";
|
|
276
|
+
/**
|
|
277
|
+
* 양품률 — 이 공정을 지난 것 중 몇이 양품인가. **커널이 읽는 값은 0..1** 이다(`OP_PARAM.yield`).
|
|
278
|
+
*
|
|
279
|
+
* 기본값이 코드 상수(0.8)였고, 그 상수가 불량 과다 주목 신호까지 만들었다 — 현장이 자기 수를 말할
|
|
280
|
+
* 자리가 필요하다. 사람이 넣는 단위(퍼센트)와 커널이 읽는 값(비율)이 다르므로 환산은 호스트가 한다.
|
|
281
|
+
*/
|
|
282
|
+
readonly yield: "operation.yield";
|
|
283
|
+
/** 셋업·체인지오버 시간 — 가동 앞에 붙는다(`OP_PARAM.setupDuration`, ISO 8601). */
|
|
284
|
+
readonly setupDuration: "operation.setupDuration";
|
|
251
285
|
};
|
|
252
286
|
/** 판정에 쓸 기준 — 대상별 선언이 있으면 그것, 없으면 기본값(그 사실을 함께 든다). */
|
|
253
287
|
export interface AttentionThresholds {
|
|
@@ -354,6 +388,13 @@ export declare abstract class FlowEngine implements TwinKernel {
|
|
|
354
388
|
protected policy: AllocationPolicy;
|
|
355
389
|
/** duration 시임(선택) — 미주입 시 명세, 명세도 없으면 도메인 상수. 이력 보정 추정기가 여기 들어온다. */
|
|
356
390
|
durationEstimator?: DurationEstimator;
|
|
391
|
+
/**
|
|
392
|
+
* 양품률 시임(선택) — **이력에서 배운 수율.** 소요시간과 같은 자리·같은 규율이다 (2026-08-19).
|
|
393
|
+
*
|
|
394
|
+
* 없으면 선언값(현장→원천), 그것도 없으면 도메인 상수. 무엇을 썼는지는 `specCoverage()` 가 밝힌다.
|
|
395
|
+
* 주입은 호스트가 한다(저널을 접는 쪽이 이력을 안다) — 커널은 시임만 든다.
|
|
396
|
+
*/
|
|
397
|
+
yieldEstimator?: YieldEstimator;
|
|
357
398
|
/**
|
|
358
399
|
* 오퍼레이션 명세(선택) — 작업 종류(`FlowTask.kind` = `OperationDef.key`) → 소요·변동·모수.
|
|
359
400
|
* 도메인 정의에서 실어 온다(`loadOperations`). 없으면 커널 기본값을 쓰고 그 사실을 `specCoverage()` 가 밝힌다.
|
|
@@ -366,6 +407,13 @@ export declare abstract class FlowEngine implements TwinKernel {
|
|
|
366
407
|
* 없다), 행을 지어 만들면 지어낸 `intent` 가 능력 계산까지 오염시킨다.
|
|
367
408
|
*/
|
|
368
409
|
protected localDurations: Map<string, number>;
|
|
410
|
+
/**
|
|
411
|
+
* 현장이 선언한 공정 모수(작업 종류 → 모수 id → 값) — `declareParameters` 로 들어온다.
|
|
412
|
+
*
|
|
413
|
+
* 값은 **표준 표기 그대로** 든다(ISA-95 `Parameter.Value.ValueString`): 수율은 0..1, 셋업은 ISO 8601.
|
|
414
|
+
* 읽는 쪽(`paramNumber`·`paramDuration`)이 명세 행보다 이것을 먼저 본다.
|
|
415
|
+
*/
|
|
416
|
+
protected localParams: Map<string, Map<string, string>>;
|
|
369
417
|
/** 관측 구동(P0) — 이벤트를 접는 투영기와 그 사실. tick 과 섞이지 않게 명시적으로 들고 있다. */
|
|
370
418
|
private observer?;
|
|
371
419
|
/** 관측분이 아직 커널 상태로 옮겨지지 않았다 — 스냅샷·fork 직전에 한 번만 옮긴다. */
|
|
@@ -713,6 +761,23 @@ export declare abstract class FlowEngine implements TwinKernel {
|
|
|
713
761
|
* 그 어긋남을 아무도 볼 수 없다(이 시스템에서 가장 비싼 종류의 침묵이다).
|
|
714
762
|
*/
|
|
715
763
|
declareDurations(durations: Record<string, IsoDuration> | undefined | null): void;
|
|
764
|
+
/**
|
|
765
|
+
* **현장이 정한 공정 모수** — 수율·셋업처럼 「얼마나 성공하나·바꾸는 데 얼마나 걸리나」 (2026-08-19).
|
|
766
|
+
*
|
|
767
|
+
* ── 왜 필요한가 ─────────────────────────────────────────────────────────────
|
|
768
|
+
* 양품률의 기본값은 커널 소스의 상수 0.8 이었다. 그 상수는 조용하지 않다: **불량 판정(주목 신호)까지
|
|
769
|
+
* 만든다** — 초기 라인이든 성숙한 라인이든 같은 0.8 로 굴러가고, 화면은 그 결과를 사실처럼 보인다.
|
|
770
|
+
* 셋업도 같다(기본 상수). 현장이 자기 수를 말할 문이 없었다.
|
|
771
|
+
*
|
|
772
|
+
* ── 소요시간과 같은 규율 ────────────────────────────────────────────────────
|
|
773
|
+
* · 값은 **표준 표기 그대로** 받는다(ISA-95 `Parameter.Value.ValueString`): 수율 0..1, 셋업 ISO 8601.
|
|
774
|
+
* 퍼센트·분 같은 사람 단위를 커널이 알 필요는 없다 — 그 환산은 값을 받는 쪽(호스트)이 한다.
|
|
775
|
+
* · **현장 선언이 원천 명세를 이긴다**(ADR-0034) — 원천 사본이 낡았을 때 고칠 자리가 그것뿐이다.
|
|
776
|
+
* · 명세 행이 없는 종류에도 얹힌다 — 행을 지어 만들면 지어낸 `intent` 가 능력 계산을 오염시킨다.
|
|
777
|
+
* · 무엇을 썼는지는 `specCoverage()` 의 `parameters` 가 그대로 밝힌다.
|
|
778
|
+
* · 빈 이름·빈 값은 **던진다**: 받아 두고 무시하면 화면은 「넣었습니다」라고 말하고 시뮬은 상수로 돈다.
|
|
779
|
+
*/
|
|
780
|
+
declareParameters(params: Record<string, Record<string, string | number>> | undefined | null): void;
|
|
716
781
|
/**
|
|
717
782
|
* 라우트(공정 순서) — 수율을 거슬러 올릴 때 필요하다. 기본은 모른다(선언 순서를 쓴다).
|
|
718
783
|
* 생산 정의를 가진 커널이 override 해서 자기 라우트를 답한다.
|
|
@@ -791,7 +856,30 @@ export declare abstract class FlowEngine implements TwinKernel {
|
|
|
791
856
|
protected paramNumber(opKey: string, id: string): number | undefined;
|
|
792
857
|
/** 명세 모수(기간) — ISO 8601 문자열을 밀리초로. 선언 없으면 undefined. */
|
|
793
858
|
protected paramDuration(opKey: string, id: string): number | undefined;
|
|
859
|
+
/**
|
|
860
|
+
* 이 공정의 양품률 — **실측 > 현장 선언 > 원천 명세 > 도메인 상수** (2026-08-19).
|
|
861
|
+
*
|
|
862
|
+
* 소요시간(`durationOf`)과 같은 사다리다. 도메인 커널은 이 창구만 부르고 상수는 스스로 든다
|
|
863
|
+
* (`?? DEFAULT_YIELD`) — 상수를 쓴 사실은 `specCoverage()` 의 `parameterBasis` 가 밝힌다.
|
|
864
|
+
*
|
|
865
|
+
* 범위를 벗어난 실측은 쓰지 않는다: 비율이 아닌 값(0 미만·1 초과)은 이력 계산이 틀린 것이므로,
|
|
866
|
+
* 그것으로 시뮬을 굴리면 조용히 이상한 공장이 된다.
|
|
867
|
+
*/
|
|
868
|
+
protected yieldOf(kind: string): number | undefined;
|
|
869
|
+
/**
|
|
870
|
+
* 모수 값 한 개 — **현장이 정한 것이 원천 명세를 이긴다**(소요시간과 같은 순서: ADR-0034).
|
|
871
|
+
*
|
|
872
|
+
* 두 읽기(수·기간)가 같은 자리에서 값을 얻어야 우선순위가 한 벌로 남는다 — 예전에는 각자 명세 행만
|
|
873
|
+
* 뒤졌고, 그래서 현장 선언을 얹을 자리가 아예 없었다.
|
|
874
|
+
*/
|
|
875
|
+
private paramValue;
|
|
794
876
|
private noteSpecUse;
|
|
877
|
+
/**
|
|
878
|
+
* 모수를 **무엇으로** 읽었나 — id 와 근거를 함께 적는다 (2026-08-19).
|
|
879
|
+
*
|
|
880
|
+
* 예전에는 id 만 세었다. 그래서 「수율을 읽었다」는 알 수 있어도 그것이 이력에서 배운 값인지 선언값인지
|
|
881
|
+
* 코드 상수인지 구별할 수 없었다 — 예측의 자격을 말하려면 그 구별이 필요하다(소요시간은 이미 구별한다).
|
|
882
|
+
*/
|
|
795
883
|
private noteParamUse;
|
|
796
884
|
/**
|
|
797
885
|
* 시뮬 명세 자기보고 — **어디까지 데이터로 말했고 어디부터 우리가 코드에 고정한 상수인가.**
|
|
@@ -805,6 +893,8 @@ export declare abstract class FlowEngine implements TwinKernel {
|
|
|
805
893
|
duration: 'measured' | 'declared' | 'default';
|
|
806
894
|
variability?: string;
|
|
807
895
|
parameters: string[];
|
|
896
|
+
/** 그 모수를 무엇으로 읽었나(`id → measured|declared`) — 없는 id 는 상수를 썼다는 뜻이다. */
|
|
897
|
+
parameterBasis?: Record<string, 'measured' | 'declared'>;
|
|
808
898
|
}[];
|
|
809
899
|
/** 이력·계산에서 온 소요(추정기) — 선언값보다 강한 근거. */
|
|
810
900
|
measuredDurations: number;
|
package/dist/flow-engine.js
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import { OP_EVENT, CMD, locationStatusOf, readBoardEquipment, readBoardLocations, readBoardAssets, classClosure, capabilityOf, requiredTestsFor, priorityRank, dueStatusOf, effectivityAt, offCalendarAt, offCalendarReasonAt, minuteOfDayAt, activeShiftAt, subLotIdOf, itemKeyOf } from "./contract.js";
|
|
13
13
|
import { ObservedReducer } from "./observed-reducer.js";
|
|
14
14
|
import { transformationEvent, aggregationEvent, objectEvent, parseEpc, DISP, ILMD_ATTR, CBV_BIZSTEP } from "./epcis.js";
|
|
15
|
+
import { OP_PARAM } from "./domain-definition.js";
|
|
15
16
|
import { parseIsoDuration } from "./iso-duration.js";
|
|
16
17
|
import { analyzeCapacity } from "./capacity.js";
|
|
17
18
|
import { operationsCapabilityOf } from "./operations-capability.js";
|
|
@@ -67,17 +68,18 @@ export const ATTENTION_PROPERTY = {
|
|
|
67
68
|
};
|
|
68
69
|
/** 선언이 없을 때 쓰는 기본값 — **드러내 둔다**(코드 안에 숨은 상수가 아니라 계약의 일부다). */
|
|
69
70
|
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
71
|
export const OPERATION_PROPERTY = {
|
|
79
72
|
/** 그 현장에서 이 공정이 걸리는 시간 — 실측이 없을 때 상수를 대신한다. */
|
|
80
|
-
duration: 'operation.duration'
|
|
73
|
+
duration: 'operation.duration',
|
|
74
|
+
/**
|
|
75
|
+
* 양품률 — 이 공정을 지난 것 중 몇이 양품인가. **커널이 읽는 값은 0..1** 이다(`OP_PARAM.yield`).
|
|
76
|
+
*
|
|
77
|
+
* 기본값이 코드 상수(0.8)였고, 그 상수가 불량 과다 주목 신호까지 만들었다 — 현장이 자기 수를 말할
|
|
78
|
+
* 자리가 필요하다. 사람이 넣는 단위(퍼센트)와 커널이 읽는 값(비율)이 다르므로 환산은 호스트가 한다.
|
|
79
|
+
*/
|
|
80
|
+
yield: 'operation.yield',
|
|
81
|
+
/** 셋업·체인지오버 시간 — 가동 앞에 붙는다(`OP_PARAM.setupDuration`, ISO 8601). */
|
|
82
|
+
setupDuration: 'operation.setupDuration'
|
|
81
83
|
};
|
|
82
84
|
export function deriveAttentions(view, acked,
|
|
83
85
|
/** 지금(ISO) — 납기 판정에 필요하다. **주지 않으면 지연을 판정하지 않는다**(모르면 판단하지 않는다). */
|
|
@@ -318,6 +320,13 @@ export class FlowEngine {
|
|
|
318
320
|
policy;
|
|
319
321
|
/** duration 시임(선택) — 미주입 시 명세, 명세도 없으면 도메인 상수. 이력 보정 추정기가 여기 들어온다. */
|
|
320
322
|
durationEstimator;
|
|
323
|
+
/**
|
|
324
|
+
* 양품률 시임(선택) — **이력에서 배운 수율.** 소요시간과 같은 자리·같은 규율이다 (2026-08-19).
|
|
325
|
+
*
|
|
326
|
+
* 없으면 선언값(현장→원천), 그것도 없으면 도메인 상수. 무엇을 썼는지는 `specCoverage()` 가 밝힌다.
|
|
327
|
+
* 주입은 호스트가 한다(저널을 접는 쪽이 이력을 안다) — 커널은 시임만 든다.
|
|
328
|
+
*/
|
|
329
|
+
yieldEstimator;
|
|
321
330
|
/**
|
|
322
331
|
* 오퍼레이션 명세(선택) — 작업 종류(`FlowTask.kind` = `OperationDef.key`) → 소요·변동·모수.
|
|
323
332
|
* 도메인 정의에서 실어 온다(`loadOperations`). 없으면 커널 기본값을 쓰고 그 사실을 `specCoverage()` 가 밝힌다.
|
|
@@ -330,6 +339,13 @@ export class FlowEngine {
|
|
|
330
339
|
* 없다), 행을 지어 만들면 지어낸 `intent` 가 능력 계산까지 오염시킨다.
|
|
331
340
|
*/
|
|
332
341
|
localDurations = new Map();
|
|
342
|
+
/**
|
|
343
|
+
* 현장이 선언한 공정 모수(작업 종류 → 모수 id → 값) — `declareParameters` 로 들어온다.
|
|
344
|
+
*
|
|
345
|
+
* 값은 **표준 표기 그대로** 든다(ISA-95 `Parameter.Value.ValueString`): 수율은 0..1, 셋업은 ISO 8601.
|
|
346
|
+
* 읽는 쪽(`paramNumber`·`paramDuration`)이 명세 행보다 이것을 먼저 본다.
|
|
347
|
+
*/
|
|
348
|
+
localParams = new Map();
|
|
333
349
|
/** 관측 구동(P0) — 이벤트를 접는 투영기와 그 사실. tick 과 섞이지 않게 명시적으로 들고 있다. */
|
|
334
350
|
observer;
|
|
335
351
|
/** 관측분이 아직 커널 상태로 옮겨지지 않았다 — 스냅샷·fork 직전에 한 번만 옮긴다. */
|
|
@@ -1229,7 +1245,10 @@ export class FlowEngine {
|
|
|
1229
1245
|
clone.durationEstimator = this.durationEstimator; // 무상태 시임 — 참조 공유(policy 와 동형)
|
|
1230
1246
|
/* 명세는 불변(정의) → 참조 공유. 사용 기록은 fork 자기 것(무엇을 기본값으로 굴렸는지 fork 별로 다르다). */
|
|
1231
1247
|
clone.operationSpecs = this.operationSpecs;
|
|
1232
|
-
clone.specUse = new Map([...this.specUse.entries()].map(([k, u]) => [
|
|
1248
|
+
clone.specUse = new Map([...this.specUse.entries()].map(([k, u]) => [
|
|
1249
|
+
k,
|
|
1250
|
+
{ duration: u.duration, variability: u.variability, params: new Set(u.params), paramBasis: new Map(u.paramBasis) }
|
|
1251
|
+
]));
|
|
1233
1252
|
return clone;
|
|
1234
1253
|
}
|
|
1235
1254
|
/**
|
|
@@ -1477,6 +1496,40 @@ export class FlowEngine {
|
|
|
1477
1496
|
this.localDurations.set(key, ms);
|
|
1478
1497
|
}
|
|
1479
1498
|
}
|
|
1499
|
+
/**
|
|
1500
|
+
* **현장이 정한 공정 모수** — 수율·셋업처럼 「얼마나 성공하나·바꾸는 데 얼마나 걸리나」 (2026-08-19).
|
|
1501
|
+
*
|
|
1502
|
+
* ── 왜 필요한가 ─────────────────────────────────────────────────────────────
|
|
1503
|
+
* 양품률의 기본값은 커널 소스의 상수 0.8 이었다. 그 상수는 조용하지 않다: **불량 판정(주목 신호)까지
|
|
1504
|
+
* 만든다** — 초기 라인이든 성숙한 라인이든 같은 0.8 로 굴러가고, 화면은 그 결과를 사실처럼 보인다.
|
|
1505
|
+
* 셋업도 같다(기본 상수). 현장이 자기 수를 말할 문이 없었다.
|
|
1506
|
+
*
|
|
1507
|
+
* ── 소요시간과 같은 규율 ────────────────────────────────────────────────────
|
|
1508
|
+
* · 값은 **표준 표기 그대로** 받는다(ISA-95 `Parameter.Value.ValueString`): 수율 0..1, 셋업 ISO 8601.
|
|
1509
|
+
* 퍼센트·분 같은 사람 단위를 커널이 알 필요는 없다 — 그 환산은 값을 받는 쪽(호스트)이 한다.
|
|
1510
|
+
* · **현장 선언이 원천 명세를 이긴다**(ADR-0034) — 원천 사본이 낡았을 때 고칠 자리가 그것뿐이다.
|
|
1511
|
+
* · 명세 행이 없는 종류에도 얹힌다 — 행을 지어 만들면 지어낸 `intent` 가 능력 계산을 오염시킨다.
|
|
1512
|
+
* · 무엇을 썼는지는 `specCoverage()` 의 `parameters` 가 그대로 밝힌다.
|
|
1513
|
+
* · 빈 이름·빈 값은 **던진다**: 받아 두고 무시하면 화면은 「넣었습니다」라고 말하고 시뮬은 상수로 돈다.
|
|
1514
|
+
*/
|
|
1515
|
+
declareParameters(params) {
|
|
1516
|
+
for (const [kind, entries] of Object.entries(params ?? {})) {
|
|
1517
|
+
const key = String(kind ?? '').trim();
|
|
1518
|
+
if (!key)
|
|
1519
|
+
throw new Error('declareParameters: an operation kind is empty — say which operation the parameter belongs to');
|
|
1520
|
+
for (const [id, value] of Object.entries(entries ?? {})) {
|
|
1521
|
+
const paramId = String(id ?? '').trim();
|
|
1522
|
+
if (!paramId)
|
|
1523
|
+
throw new Error(`declareParameters: operation "${key}" has a parameter with no id — the value would never be read`);
|
|
1524
|
+
const text = String(value ?? '').trim();
|
|
1525
|
+
if (!text)
|
|
1526
|
+
throw new Error(`declareParameters: "${key}.${paramId}" has no value — an empty parameter is not a declaration`);
|
|
1527
|
+
const bucket = this.localParams.get(key) ?? new Map();
|
|
1528
|
+
bucket.set(paramId, text);
|
|
1529
|
+
this.localParams.set(key, bucket);
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1480
1533
|
/**
|
|
1481
1534
|
* 라우트(공정 순서) — 수율을 거슬러 올릴 때 필요하다. 기본은 모른다(선언 순서를 쓴다).
|
|
1482
1535
|
* 생산 정의를 가진 커널이 override 해서 자기 라우트를 답한다.
|
|
@@ -1659,10 +1712,10 @@ export class FlowEngine {
|
|
|
1659
1712
|
}
|
|
1660
1713
|
/** 명세 모수(숫자) — 선언 없으면 undefined(0 으로 꾸미지 않는다). 소비처가 기본값을 정한다. */
|
|
1661
1714
|
paramNumber(opKey, id) {
|
|
1662
|
-
const
|
|
1663
|
-
if (
|
|
1715
|
+
const raw = this.paramValue(opKey, id);
|
|
1716
|
+
if (raw === undefined)
|
|
1664
1717
|
return undefined;
|
|
1665
|
-
const n = Number(
|
|
1718
|
+
const n = Number(raw);
|
|
1666
1719
|
if (!Number.isFinite(n))
|
|
1667
1720
|
return undefined;
|
|
1668
1721
|
this.noteParamUse(opKey, id);
|
|
@@ -1670,12 +1723,40 @@ export class FlowEngine {
|
|
|
1670
1723
|
}
|
|
1671
1724
|
/** 명세 모수(기간) — ISO 8601 문자열을 밀리초로. 선언 없으면 undefined. */
|
|
1672
1725
|
paramDuration(opKey, id) {
|
|
1673
|
-
const
|
|
1674
|
-
const ms = parseIsoDuration(p?.value);
|
|
1726
|
+
const ms = parseIsoDuration(this.paramValue(opKey, id));
|
|
1675
1727
|
if (ms !== undefined)
|
|
1676
1728
|
this.noteParamUse(opKey, id);
|
|
1677
1729
|
return ms;
|
|
1678
1730
|
}
|
|
1731
|
+
/**
|
|
1732
|
+
* 이 공정의 양품률 — **실측 > 현장 선언 > 원천 명세 > 도메인 상수** (2026-08-19).
|
|
1733
|
+
*
|
|
1734
|
+
* 소요시간(`durationOf`)과 같은 사다리다. 도메인 커널은 이 창구만 부르고 상수는 스스로 든다
|
|
1735
|
+
* (`?? DEFAULT_YIELD`) — 상수를 쓴 사실은 `specCoverage()` 의 `parameterBasis` 가 밝힌다.
|
|
1736
|
+
*
|
|
1737
|
+
* 범위를 벗어난 실측은 쓰지 않는다: 비율이 아닌 값(0 미만·1 초과)은 이력 계산이 틀린 것이므로,
|
|
1738
|
+
* 그것으로 시뮬을 굴리면 조용히 이상한 공장이 된다.
|
|
1739
|
+
*/
|
|
1740
|
+
yieldOf(kind) {
|
|
1741
|
+
const measured = this.yieldEstimator?.estimate({ kind });
|
|
1742
|
+
if (typeof measured === 'number' && measured >= 0 && measured <= 1) {
|
|
1743
|
+
this.noteParamUse(kind, OP_PARAM.yield, 'measured');
|
|
1744
|
+
return measured;
|
|
1745
|
+
}
|
|
1746
|
+
return this.paramNumber(kind, OP_PARAM.yield);
|
|
1747
|
+
}
|
|
1748
|
+
/**
|
|
1749
|
+
* 모수 값 한 개 — **현장이 정한 것이 원천 명세를 이긴다**(소요시간과 같은 순서: ADR-0034).
|
|
1750
|
+
*
|
|
1751
|
+
* 두 읽기(수·기간)가 같은 자리에서 값을 얻어야 우선순위가 한 벌로 남는다 — 예전에는 각자 명세 행만
|
|
1752
|
+
* 뒤졌고, 그래서 현장 선언을 얹을 자리가 아예 없었다.
|
|
1753
|
+
*/
|
|
1754
|
+
paramValue(opKey, id) {
|
|
1755
|
+
const local = this.localParams.get(opKey)?.get(id);
|
|
1756
|
+
if (local !== undefined)
|
|
1757
|
+
return local;
|
|
1758
|
+
return this.operationSpecs.get(opKey)?.parameters?.find(x => x.id === id)?.value;
|
|
1759
|
+
}
|
|
1679
1760
|
noteSpecUse(kind, duration, variability) {
|
|
1680
1761
|
const cur = this.specUse.get(kind);
|
|
1681
1762
|
if (cur) {
|
|
@@ -1684,14 +1765,22 @@ export class FlowEngine {
|
|
|
1684
1765
|
cur.variability = variability;
|
|
1685
1766
|
return;
|
|
1686
1767
|
}
|
|
1687
|
-
this.specUse.set(kind, { duration, ...(variability ? { variability } : {}), params: new Set() });
|
|
1768
|
+
this.specUse.set(kind, { duration, ...(variability ? { variability } : {}), params: new Set(), paramBasis: new Map() });
|
|
1688
1769
|
}
|
|
1689
|
-
|
|
1770
|
+
/**
|
|
1771
|
+
* 모수를 **무엇으로** 읽었나 — id 와 근거를 함께 적는다 (2026-08-19).
|
|
1772
|
+
*
|
|
1773
|
+
* 예전에는 id 만 세었다. 그래서 「수율을 읽었다」는 알 수 있어도 그것이 이력에서 배운 값인지 선언값인지
|
|
1774
|
+
* 코드 상수인지 구별할 수 없었다 — 예측의 자격을 말하려면 그 구별이 필요하다(소요시간은 이미 구별한다).
|
|
1775
|
+
*/
|
|
1776
|
+
noteParamUse(kind, id, basis = 'declared') {
|
|
1690
1777
|
const cur = this.specUse.get(kind);
|
|
1691
|
-
if (cur)
|
|
1778
|
+
if (cur) {
|
|
1692
1779
|
cur.params.add(id);
|
|
1693
|
-
|
|
1694
|
-
|
|
1780
|
+
cur.paramBasis.set(id, basis);
|
|
1781
|
+
return;
|
|
1782
|
+
}
|
|
1783
|
+
this.specUse.set(kind, { duration: 'default', params: new Set([id]), paramBasis: new Map([[id, basis]]) });
|
|
1695
1784
|
}
|
|
1696
1785
|
/**
|
|
1697
1786
|
* 시뮬 명세 자기보고 — **어디까지 데이터로 말했고 어디부터 우리가 코드에 고정한 상수인가.**
|
|
@@ -1708,7 +1797,9 @@ export class FlowEngine {
|
|
|
1708
1797
|
kind,
|
|
1709
1798
|
duration: u.duration,
|
|
1710
1799
|
...(variability ? { variability } : {}),
|
|
1711
|
-
parameters: [...u.params].sort()
|
|
1800
|
+
parameters: [...u.params].sort(),
|
|
1801
|
+
/* 모수를 **무엇으로** 읽었나 — 읽은 것이 있을 때만 낸다(빈 표를 만들지 않는다). */
|
|
1802
|
+
...(u.paramBasis.size ? { parameterBasis: Object.fromEntries([...u.paramBasis.entries()].sort()) } : {})
|
|
1712
1803
|
};
|
|
1713
1804
|
});
|
|
1714
1805
|
return {
|
|
@@ -1947,6 +2038,8 @@ export class FlowEngine {
|
|
|
1947
2038
|
/* 실제 자재 이동 — 인원·설비와 같은 채널(실적을 한 곳에서 읽는다). */
|
|
1948
2039
|
...(t.materialActual?.length ? { materialActual: t.materialActual.map(r => ({ ...r })) } : {}),
|
|
1949
2040
|
...(t.durationMs ? { durationMs: t.durationMs } : {}),
|
|
2041
|
+
/* 품질 판정이 있었으면 **그 자리에서** 실어 보낸다 — 없으면 필드를 만들지 않는다(모름≠양품). */
|
|
2042
|
+
...(t.outcome ? { outcome: t.outcome } : {}),
|
|
1950
2043
|
...(t.priority !== undefined ? { priority: t.priority } : {}),
|
|
1951
2044
|
...(t.startTime ? { startTime: t.startTime } : {}),
|
|
1952
2045
|
...(t.endTime ? { endTime: t.endTime } : {}),
|
package/dist/mes-kernel.js
CHANGED
|
@@ -229,7 +229,9 @@ export class MesKernel extends FlowEngine {
|
|
|
229
229
|
// 마지막 스테이션(조립) → 완성차 (transform 1→1, disposition 으로 수율 loss → OEE 품질)
|
|
230
230
|
const fgStore = this.locationByType('fg-store');
|
|
231
231
|
const wip = order.allocated[0];
|
|
232
|
-
const good = this.rng() < (this.
|
|
232
|
+
const good = this.rng() < (this.yieldOf(t.kind) ?? DEFAULT_YIELD);
|
|
233
|
+
/* 판정을 **작업에 적는다** — 그 자리에서 적어야 저널에 종류와 함께 남고, 이력에서 수율을 배울 수 있다. */
|
|
234
|
+
t.outcome = good ? 'good' : 'scrap';
|
|
233
235
|
this.recordOutput(t.resource, good); // OEE 품질(마지막 자원별 양품/불량)
|
|
234
236
|
const disp = good ? DISP.sellable : DISP.non_sellable;
|
|
235
237
|
const outputEpc = sgtinUri(CP, product.ref, ++this.prodSeq);
|
|
@@ -352,7 +354,8 @@ export class MesKernel extends FlowEngine {
|
|
|
352
354
|
}
|
|
353
355
|
const fgStore = this.locationByType('fg-store');
|
|
354
356
|
const wip = order.allocated[0];
|
|
355
|
-
const good = this.rng() < (this.
|
|
357
|
+
const good = this.rng() < (this.yieldOf(t.kind) ?? DEFAULT_YIELD);
|
|
358
|
+
t.outcome = good ? 'good' : 'scrap'; // 같은 규율 — 판정은 일어난 자리에서 적는다
|
|
356
359
|
this.recordOutput(t.resource, good);
|
|
357
360
|
const disp = good ? DISP.sellable : DISP.non_sellable;
|
|
358
361
|
const outEpc = this.serialOf(rc.outputs[0].material, ++this.prodSeq);
|
package/dist/task-fold.d.ts
CHANGED
|
@@ -23,6 +23,16 @@ export interface TaskFacets {
|
|
|
23
23
|
order?: string;
|
|
24
24
|
/** 가장 최근 도착 지점. 진행에 따라 바뀌므로 마지막 것이 사실이다. */
|
|
25
25
|
location?: string;
|
|
26
|
+
/**
|
|
27
|
+
* 품질 판정 결과 — **판정이 있었을 때만** (2026-08-19).
|
|
28
|
+
*
|
|
29
|
+
* 이 축이 없으면 「이 공정의 양품률이 얼마였나」를 저널에서 되짚을 수 없다: 양품/불량은 설비 누적
|
|
30
|
+
* 카운터(종류 모름)와 EPCIS disposition(종류 모름)에만 남아 있었다. 판정은 작업 완료에서 일어나므로
|
|
31
|
+
* 그 자리에서 적고, 여기서 그대로 접는다.
|
|
32
|
+
*
|
|
33
|
+
* **없음은 「양품」이 아니라 「판정하지 않았다」**다(이동·체류에는 품질 판정이 없다).
|
|
34
|
+
*/
|
|
35
|
+
outcome?: 'good' | 'scrap';
|
|
26
36
|
}
|
|
27
37
|
/** 작업 하나의 이정표 — 소비처가 이것으로 구간·지표를 만든다. */
|
|
28
38
|
export interface TaskRecord {
|
package/dist/task-fold.js
CHANGED
|
@@ -46,6 +46,9 @@ export function foldTaskRecords(rows) {
|
|
|
46
46
|
f.kind = d.kind;
|
|
47
47
|
if (f.order === undefined && d.orderId)
|
|
48
48
|
f.order = d.orderId;
|
|
49
|
+
/* 품질 판정은 완료 전이에 실려 온다 — 한 번 실린 값은 바뀌지 않는다(정정이 오면 마지막 것이 사실). */
|
|
50
|
+
if (d.outcome === 'good' || d.outcome === 'scrap')
|
|
51
|
+
f.outcome = d.outcome;
|
|
49
52
|
const loc = d.toNode ?? d.fromNode;
|
|
50
53
|
if (loc)
|
|
51
54
|
f.location = loc;
|
package/dist-cjs/index.cjs
CHANGED
|
@@ -2630,6 +2630,7 @@ function foldTaskRecords(rows) {
|
|
|
2630
2630
|
if (f.resource === void 0 && d.resourceRef) f.resource = d.resourceRef;
|
|
2631
2631
|
if (f.kind === void 0 && d.kind) f.kind = d.kind;
|
|
2632
2632
|
if (f.order === void 0 && d.orderId) f.order = d.orderId;
|
|
2633
|
+
if (d.outcome === "good" || d.outcome === "scrap") f.outcome = d.outcome;
|
|
2633
2634
|
const loc = d.toNode ?? d.fromNode;
|
|
2634
2635
|
if (loc) f.location = loc;
|
|
2635
2636
|
if (d.status === "created") rec.createdMs = at;
|
|
@@ -2966,7 +2967,16 @@ var ATTENTION_PROPERTY = {
|
|
|
2966
2967
|
var ATTENTION_DEFAULTS = { congestionPct: 90, scrapPct: 15, scrapMinSamples: 10 };
|
|
2967
2968
|
var OPERATION_PROPERTY = {
|
|
2968
2969
|
/** 그 현장에서 이 공정이 걸리는 시간 — 실측이 없을 때 상수를 대신한다. */
|
|
2969
|
-
duration: "operation.duration"
|
|
2970
|
+
duration: "operation.duration",
|
|
2971
|
+
/**
|
|
2972
|
+
* 양품률 — 이 공정을 지난 것 중 몇이 양품인가. **커널이 읽는 값은 0..1** 이다(`OP_PARAM.yield`).
|
|
2973
|
+
*
|
|
2974
|
+
* 기본값이 코드 상수(0.8)였고, 그 상수가 불량 과다 주목 신호까지 만들었다 — 현장이 자기 수를 말할
|
|
2975
|
+
* 자리가 필요하다. 사람이 넣는 단위(퍼센트)와 커널이 읽는 값(비율)이 다르므로 환산은 호스트가 한다.
|
|
2976
|
+
*/
|
|
2977
|
+
yield: "operation.yield",
|
|
2978
|
+
/** 셋업·체인지오버 시간 — 가동 앞에 붙는다(`OP_PARAM.setupDuration`, ISO 8601). */
|
|
2979
|
+
setupDuration: "operation.setupDuration"
|
|
2970
2980
|
};
|
|
2971
2981
|
function deriveAttentions(view, acked, nowIso, thresholds) {
|
|
2972
2982
|
const out = [];
|
|
@@ -3163,6 +3173,13 @@ var FlowEngine = class {
|
|
|
3163
3173
|
policy;
|
|
3164
3174
|
/** duration 시임(선택) — 미주입 시 명세, 명세도 없으면 도메인 상수. 이력 보정 추정기가 여기 들어온다. */
|
|
3165
3175
|
durationEstimator;
|
|
3176
|
+
/**
|
|
3177
|
+
* 양품률 시임(선택) — **이력에서 배운 수율.** 소요시간과 같은 자리·같은 규율이다 (2026-08-19).
|
|
3178
|
+
*
|
|
3179
|
+
* 없으면 선언값(현장→원천), 그것도 없으면 도메인 상수. 무엇을 썼는지는 `specCoverage()` 가 밝힌다.
|
|
3180
|
+
* 주입은 호스트가 한다(저널을 접는 쪽이 이력을 안다) — 커널은 시임만 든다.
|
|
3181
|
+
*/
|
|
3182
|
+
yieldEstimator;
|
|
3166
3183
|
/**
|
|
3167
3184
|
* 오퍼레이션 명세(선택) — 작업 종류(`FlowTask.kind` = `OperationDef.key`) → 소요·변동·모수.
|
|
3168
3185
|
* 도메인 정의에서 실어 온다(`loadOperations`). 없으면 커널 기본값을 쓰고 그 사실을 `specCoverage()` 가 밝힌다.
|
|
@@ -3175,6 +3192,13 @@ var FlowEngine = class {
|
|
|
3175
3192
|
* 없다), 행을 지어 만들면 지어낸 `intent` 가 능력 계산까지 오염시킨다.
|
|
3176
3193
|
*/
|
|
3177
3194
|
localDurations = /* @__PURE__ */ new Map();
|
|
3195
|
+
/**
|
|
3196
|
+
* 현장이 선언한 공정 모수(작업 종류 → 모수 id → 값) — `declareParameters` 로 들어온다.
|
|
3197
|
+
*
|
|
3198
|
+
* 값은 **표준 표기 그대로** 든다(ISA-95 `Parameter.Value.ValueString`): 수율은 0..1, 셋업은 ISO 8601.
|
|
3199
|
+
* 읽는 쪽(`paramNumber`·`paramDuration`)이 명세 행보다 이것을 먼저 본다.
|
|
3200
|
+
*/
|
|
3201
|
+
localParams = /* @__PURE__ */ new Map();
|
|
3178
3202
|
/** 관측 구동(P0) — 이벤트를 접는 투영기와 그 사실. tick 과 섞이지 않게 명시적으로 들고 있다. */
|
|
3179
3203
|
observer;
|
|
3180
3204
|
/** 관측분이 아직 커널 상태로 옮겨지지 않았다 — 스냅샷·fork 직전에 한 번만 옮긴다. */
|
|
@@ -3987,7 +4011,12 @@ var FlowEngine = class {
|
|
|
3987
4011
|
clone.rng.state = this.rng.state;
|
|
3988
4012
|
clone.durationEstimator = this.durationEstimator;
|
|
3989
4013
|
clone.operationSpecs = this.operationSpecs;
|
|
3990
|
-
clone.specUse = new Map(
|
|
4014
|
+
clone.specUse = new Map(
|
|
4015
|
+
[...this.specUse.entries()].map(([k, u]) => [
|
|
4016
|
+
k,
|
|
4017
|
+
{ duration: u.duration, variability: u.variability, params: new Set(u.params), paramBasis: new Map(u.paramBasis) }
|
|
4018
|
+
])
|
|
4019
|
+
);
|
|
3991
4020
|
return clone;
|
|
3992
4021
|
}
|
|
3993
4022
|
/**
|
|
@@ -4213,6 +4242,37 @@ var FlowEngine = class {
|
|
|
4213
4242
|
this.localDurations.set(key, ms2);
|
|
4214
4243
|
}
|
|
4215
4244
|
}
|
|
4245
|
+
/**
|
|
4246
|
+
* **현장이 정한 공정 모수** — 수율·셋업처럼 「얼마나 성공하나·바꾸는 데 얼마나 걸리나」 (2026-08-19).
|
|
4247
|
+
*
|
|
4248
|
+
* ── 왜 필요한가 ─────────────────────────────────────────────────────────────
|
|
4249
|
+
* 양품률의 기본값은 커널 소스의 상수 0.8 이었다. 그 상수는 조용하지 않다: **불량 판정(주목 신호)까지
|
|
4250
|
+
* 만든다** — 초기 라인이든 성숙한 라인이든 같은 0.8 로 굴러가고, 화면은 그 결과를 사실처럼 보인다.
|
|
4251
|
+
* 셋업도 같다(기본 상수). 현장이 자기 수를 말할 문이 없었다.
|
|
4252
|
+
*
|
|
4253
|
+
* ── 소요시간과 같은 규율 ────────────────────────────────────────────────────
|
|
4254
|
+
* · 값은 **표준 표기 그대로** 받는다(ISA-95 `Parameter.Value.ValueString`): 수율 0..1, 셋업 ISO 8601.
|
|
4255
|
+
* 퍼센트·분 같은 사람 단위를 커널이 알 필요는 없다 — 그 환산은 값을 받는 쪽(호스트)이 한다.
|
|
4256
|
+
* · **현장 선언이 원천 명세를 이긴다**(ADR-0034) — 원천 사본이 낡았을 때 고칠 자리가 그것뿐이다.
|
|
4257
|
+
* · 명세 행이 없는 종류에도 얹힌다 — 행을 지어 만들면 지어낸 `intent` 가 능력 계산을 오염시킨다.
|
|
4258
|
+
* · 무엇을 썼는지는 `specCoverage()` 의 `parameters` 가 그대로 밝힌다.
|
|
4259
|
+
* · 빈 이름·빈 값은 **던진다**: 받아 두고 무시하면 화면은 「넣었습니다」라고 말하고 시뮬은 상수로 돈다.
|
|
4260
|
+
*/
|
|
4261
|
+
declareParameters(params) {
|
|
4262
|
+
for (const [kind, entries] of Object.entries(params ?? {})) {
|
|
4263
|
+
const key = String(kind ?? "").trim();
|
|
4264
|
+
if (!key) throw new Error("declareParameters: an operation kind is empty \u2014 say which operation the parameter belongs to");
|
|
4265
|
+
for (const [id, value] of Object.entries(entries ?? {})) {
|
|
4266
|
+
const paramId = String(id ?? "").trim();
|
|
4267
|
+
if (!paramId) throw new Error(`declareParameters: operation "${key}" has a parameter with no id \u2014 the value would never be read`);
|
|
4268
|
+
const text = String(value ?? "").trim();
|
|
4269
|
+
if (!text) throw new Error(`declareParameters: "${key}.${paramId}" has no value \u2014 an empty parameter is not a declaration`);
|
|
4270
|
+
const bucket = this.localParams.get(key) ?? /* @__PURE__ */ new Map();
|
|
4271
|
+
bucket.set(paramId, text);
|
|
4272
|
+
this.localParams.set(key, bucket);
|
|
4273
|
+
}
|
|
4274
|
+
}
|
|
4275
|
+
}
|
|
4216
4276
|
/**
|
|
4217
4277
|
* 라우트(공정 순서) — 수율을 거슬러 올릴 때 필요하다. 기본은 모른다(선언 순서를 쓴다).
|
|
4218
4278
|
* 생산 정의를 가진 커널이 override 해서 자기 라우트를 답한다.
|
|
@@ -4377,20 +4437,47 @@ var FlowEngine = class {
|
|
|
4377
4437
|
}
|
|
4378
4438
|
/** 명세 모수(숫자) — 선언 없으면 undefined(0 으로 꾸미지 않는다). 소비처가 기본값을 정한다. */
|
|
4379
4439
|
paramNumber(opKey, id) {
|
|
4380
|
-
const
|
|
4381
|
-
if (
|
|
4382
|
-
const n = Number(
|
|
4440
|
+
const raw = this.paramValue(opKey, id);
|
|
4441
|
+
if (raw === void 0) return void 0;
|
|
4442
|
+
const n = Number(raw);
|
|
4383
4443
|
if (!Number.isFinite(n)) return void 0;
|
|
4384
4444
|
this.noteParamUse(opKey, id);
|
|
4385
4445
|
return n;
|
|
4386
4446
|
}
|
|
4387
4447
|
/** 명세 모수(기간) — ISO 8601 문자열을 밀리초로. 선언 없으면 undefined. */
|
|
4388
4448
|
paramDuration(opKey, id) {
|
|
4389
|
-
const
|
|
4390
|
-
const ms2 = parseIsoDuration(p?.value);
|
|
4449
|
+
const ms2 = parseIsoDuration(this.paramValue(opKey, id));
|
|
4391
4450
|
if (ms2 !== void 0) this.noteParamUse(opKey, id);
|
|
4392
4451
|
return ms2;
|
|
4393
4452
|
}
|
|
4453
|
+
/**
|
|
4454
|
+
* 이 공정의 양품률 — **실측 > 현장 선언 > 원천 명세 > 도메인 상수** (2026-08-19).
|
|
4455
|
+
*
|
|
4456
|
+
* 소요시간(`durationOf`)과 같은 사다리다. 도메인 커널은 이 창구만 부르고 상수는 스스로 든다
|
|
4457
|
+
* (`?? DEFAULT_YIELD`) — 상수를 쓴 사실은 `specCoverage()` 의 `parameterBasis` 가 밝힌다.
|
|
4458
|
+
*
|
|
4459
|
+
* 범위를 벗어난 실측은 쓰지 않는다: 비율이 아닌 값(0 미만·1 초과)은 이력 계산이 틀린 것이므로,
|
|
4460
|
+
* 그것으로 시뮬을 굴리면 조용히 이상한 공장이 된다.
|
|
4461
|
+
*/
|
|
4462
|
+
yieldOf(kind) {
|
|
4463
|
+
const measured = this.yieldEstimator?.estimate({ kind });
|
|
4464
|
+
if (typeof measured === "number" && measured >= 0 && measured <= 1) {
|
|
4465
|
+
this.noteParamUse(kind, OP_PARAM.yield, "measured");
|
|
4466
|
+
return measured;
|
|
4467
|
+
}
|
|
4468
|
+
return this.paramNumber(kind, OP_PARAM.yield);
|
|
4469
|
+
}
|
|
4470
|
+
/**
|
|
4471
|
+
* 모수 값 한 개 — **현장이 정한 것이 원천 명세를 이긴다**(소요시간과 같은 순서: ADR-0034).
|
|
4472
|
+
*
|
|
4473
|
+
* 두 읽기(수·기간)가 같은 자리에서 값을 얻어야 우선순위가 한 벌로 남는다 — 예전에는 각자 명세 행만
|
|
4474
|
+
* 뒤졌고, 그래서 현장 선언을 얹을 자리가 아예 없었다.
|
|
4475
|
+
*/
|
|
4476
|
+
paramValue(opKey, id) {
|
|
4477
|
+
const local = this.localParams.get(opKey)?.get(id);
|
|
4478
|
+
if (local !== void 0) return local;
|
|
4479
|
+
return this.operationSpecs.get(opKey)?.parameters?.find((x) => x.id === id)?.value;
|
|
4480
|
+
}
|
|
4394
4481
|
noteSpecUse(kind, duration, variability) {
|
|
4395
4482
|
const cur = this.specUse.get(kind);
|
|
4396
4483
|
if (cur) {
|
|
@@ -4398,12 +4485,22 @@ var FlowEngine = class {
|
|
|
4398
4485
|
if (variability) cur.variability = variability;
|
|
4399
4486
|
return;
|
|
4400
4487
|
}
|
|
4401
|
-
this.specUse.set(kind, { duration, ...variability ? { variability } : {}, params: /* @__PURE__ */ new Set() });
|
|
4488
|
+
this.specUse.set(kind, { duration, ...variability ? { variability } : {}, params: /* @__PURE__ */ new Set(), paramBasis: /* @__PURE__ */ new Map() });
|
|
4402
4489
|
}
|
|
4403
|
-
|
|
4490
|
+
/**
|
|
4491
|
+
* 모수를 **무엇으로** 읽었나 — id 와 근거를 함께 적는다 (2026-08-19).
|
|
4492
|
+
*
|
|
4493
|
+
* 예전에는 id 만 세었다. 그래서 「수율을 읽었다」는 알 수 있어도 그것이 이력에서 배운 값인지 선언값인지
|
|
4494
|
+
* 코드 상수인지 구별할 수 없었다 — 예측의 자격을 말하려면 그 구별이 필요하다(소요시간은 이미 구별한다).
|
|
4495
|
+
*/
|
|
4496
|
+
noteParamUse(kind, id, basis = "declared") {
|
|
4404
4497
|
const cur = this.specUse.get(kind);
|
|
4405
|
-
if (cur)
|
|
4406
|
-
|
|
4498
|
+
if (cur) {
|
|
4499
|
+
cur.params.add(id);
|
|
4500
|
+
cur.paramBasis.set(id, basis);
|
|
4501
|
+
return;
|
|
4502
|
+
}
|
|
4503
|
+
this.specUse.set(kind, { duration: "default", params: /* @__PURE__ */ new Set([id]), paramBasis: /* @__PURE__ */ new Map([[id, basis]]) });
|
|
4407
4504
|
}
|
|
4408
4505
|
/**
|
|
4409
4506
|
* 시뮬 명세 자기보고 — **어디까지 데이터로 말했고 어디부터 우리가 코드에 고정한 상수인가.**
|
|
@@ -4419,7 +4516,9 @@ var FlowEngine = class {
|
|
|
4419
4516
|
kind,
|
|
4420
4517
|
duration: u.duration,
|
|
4421
4518
|
...variability ? { variability } : {},
|
|
4422
|
-
parameters: [...u.params].sort()
|
|
4519
|
+
parameters: [...u.params].sort(),
|
|
4520
|
+
/* 모수를 **무엇으로** 읽었나 — 읽은 것이 있을 때만 낸다(빈 표를 만들지 않는다). */
|
|
4521
|
+
...u.paramBasis.size ? { parameterBasis: Object.fromEntries([...u.paramBasis.entries()].sort()) } : {}
|
|
4423
4522
|
};
|
|
4424
4523
|
});
|
|
4425
4524
|
return {
|
|
@@ -4644,6 +4743,8 @@ var FlowEngine = class {
|
|
|
4644
4743
|
/* 실제 자재 이동 — 인원·설비와 같은 채널(실적을 한 곳에서 읽는다). */
|
|
4645
4744
|
...t.materialActual?.length ? { materialActual: t.materialActual.map((r) => ({ ...r })) } : {},
|
|
4646
4745
|
...t.durationMs ? { durationMs: t.durationMs } : {},
|
|
4746
|
+
/* 품질 판정이 있었으면 **그 자리에서** 실어 보낸다 — 없으면 필드를 만들지 않는다(모름≠양품). */
|
|
4747
|
+
...t.outcome ? { outcome: t.outcome } : {},
|
|
4647
4748
|
...t.priority !== void 0 ? { priority: t.priority } : {},
|
|
4648
4749
|
...t.startTime ? { startTime: t.startTime } : {},
|
|
4649
4750
|
...t.endTime ? { endTime: t.endTime } : {},
|
|
@@ -6106,7 +6207,8 @@ var MesKernel = class extends FlowEngine {
|
|
|
6106
6207
|
}
|
|
6107
6208
|
const fgStore = this.locationByType("fg-store");
|
|
6108
6209
|
const wip = order.allocated[0];
|
|
6109
|
-
const good = this.rng() < (this.
|
|
6210
|
+
const good = this.rng() < (this.yieldOf(t.kind) ?? DEFAULT_YIELD);
|
|
6211
|
+
t.outcome = good ? "good" : "scrap";
|
|
6110
6212
|
this.recordOutput(t.resource, good);
|
|
6111
6213
|
const disp = good ? DISP.sellable : DISP.non_sellable;
|
|
6112
6214
|
const outputEpc = sgtinUri(CP2, product.ref, ++this.prodSeq);
|
|
@@ -6218,7 +6320,8 @@ var MesKernel = class extends FlowEngine {
|
|
|
6218
6320
|
}
|
|
6219
6321
|
const fgStore = this.locationByType("fg-store");
|
|
6220
6322
|
const wip = order.allocated[0];
|
|
6221
|
-
const good = this.rng() < (this.
|
|
6323
|
+
const good = this.rng() < (this.yieldOf(t.kind) ?? DEFAULT_YIELD);
|
|
6324
|
+
t.outcome = good ? "good" : "scrap";
|
|
6222
6325
|
this.recordOutput(t.resource, good);
|
|
6223
6326
|
const disp = good ? DISP.sellable : DISP.non_sellable;
|
|
6224
6327
|
const outEpc = this.serialOf(rc.outputs[0].material, ++this.prodSeq);
|
package/package.json
CHANGED