@operato/twin-kernel 0.6.6 → 0.6.7
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 +61 -0
- package/dist/contract.js +37 -0
- package/dist/flow-engine.d.ts +6 -1
- package/dist/flow-engine.js +23 -19
- package/dist-cjs/index.cjs +42 -14
- package/package.json +1 -1
package/dist/contract.d.ts
CHANGED
|
@@ -216,6 +216,45 @@ export declare function testPassedAt(r: TestResult, at?: ISOTime): boolean;
|
|
|
216
216
|
* 없는 것으로 막지 않는다). 결과가 있으면 그것이 유효한 합격이어야 한다.
|
|
217
217
|
*/
|
|
218
218
|
export declare function meetsTests(required: readonly string[], results: readonly TestResult[] | undefined, at?: ISOTime): boolean;
|
|
219
|
+
/**
|
|
220
|
+
* 자원이 **지금 쓰일 수 있나, 아니면 왜 못 쓰이나** — ISA-95 `PersonnelCapability`/`EquipmentCapability`.
|
|
221
|
+
*
|
|
222
|
+
* ── 왜 계약이 이것을 소유해야 하나 ────────────────────────────────────────────
|
|
223
|
+
* 커널은 배정할 때 이미 이 판정을 한다(교대·고장·보류·유효기간·시험 만료). 그런데 그 규칙이 **엔진 안의
|
|
224
|
+
* 필터 조건으로만** 있어서, 화면은 같은 판정을 자기 코드로 다시 만들었다(`reasonOf`). 규칙이 두 벌이면
|
|
225
|
+
* 반드시 갈라진다 — 배정은 막는데 화면은 "가용" 이라 말하는 순간이 온다. 그 어긋남은 조용하다.
|
|
226
|
+
*
|
|
227
|
+
* 그래서 **이유까지 계약이 낸다.** 화면·예측·AI 가 같은 낱말로 말하고, 새 이유가 생기면(시험 만료가
|
|
228
|
+
* 그랬다) 한 곳만 늘어난다.
|
|
229
|
+
*
|
|
230
|
+
* ── 이유의 순서가 뜻이다 ──────────────────────────────────────────────────────
|
|
231
|
+
* 여러 이유가 겹칠 수 있다(폐기한 설비가 고장 상태로 남아 있는 것). **먼저 오는 것을 답한다** —
|
|
232
|
+
* "이미 모델 밖" 이 "고장" 보다 앞선다(폐기한 설비의 고장은 고칠 일이 아니다).
|
|
233
|
+
*/
|
|
234
|
+
export type CapabilityReason =
|
|
235
|
+
/** 유효기간 전 — 아직 없는 자원(도입 예정). 기다릴 일이다. */
|
|
236
|
+
'not-yet'
|
|
237
|
+
/** 유효기간 후 — 이미 없는 자원(폐기·퇴사). 지울 일이다. */
|
|
238
|
+
| 'retired'
|
|
239
|
+
/** 사람이 막았다(`held`) — 지시로 보류. */
|
|
240
|
+
| 'held'
|
|
241
|
+
/** 고장 — 설비만. 고칠 일이다. */
|
|
242
|
+
| 'down'
|
|
243
|
+
/** 근무·가동 시간 밖(교대 사이) — 기다리면 돌아온다. */
|
|
244
|
+
| 'off-shift'
|
|
245
|
+
/** 근무일이 아니다(휴일) — 하루 통째로 쉰다. `off-shift` 와 기다릴 시간이 다르다. */
|
|
246
|
+
| 'resting'
|
|
247
|
+
/** 요구된 시험의 결과가 만료·불합격 — 자격이 성립하지 않는다(§TestResult). */
|
|
248
|
+
| 'test-expired'
|
|
249
|
+
/** 지금 다른 일을 하고 있다 — 능력은 있고 여유가 없다. */
|
|
250
|
+
| 'working'
|
|
251
|
+
/** 쓸 수 있다. */
|
|
252
|
+
| 'available';
|
|
253
|
+
/** 가용 여부와 그 이유 — `available` 이면 `reason: 'available'`. */
|
|
254
|
+
export interface Capability {
|
|
255
|
+
available: boolean;
|
|
256
|
+
reason: CapabilityReason;
|
|
257
|
+
}
|
|
219
258
|
export interface TestSpecification {
|
|
220
259
|
/** 표준 `ID` — 자원의 `testSpecificationIds` 가 이 값을 가리킨다. */
|
|
221
260
|
id: string;
|
|
@@ -578,6 +617,28 @@ export declare function offCalendarAt(r: {
|
|
|
578
617
|
};
|
|
579
618
|
workCalendar?: WorkCalendarEntry[];
|
|
580
619
|
}, ms: number, utcOffsetMinutes?: number): boolean;
|
|
620
|
+
/**
|
|
621
|
+
* 자원의 **가용 능력을 판정한다** — 하나의 규칙, 하나의 자리.
|
|
622
|
+
*
|
|
623
|
+
* 부르는 쪽이 시각과 시간대를 준다(커널은 `now()`·`utcOffsetMinutes`, 호스트는 관측 시각). 주지 않으면
|
|
624
|
+
* 시각에 달린 판정(유효기간·교대·시험 만료)은 **하지 않는다** — 모르면 판단하지 않는다는 규율이다.
|
|
625
|
+
*
|
|
626
|
+
* `requiredTests` 는 부르는 쪽이 등급에서 모아 넘긴다(등급 정의를 아는 것은 부르는 쪽이다).
|
|
627
|
+
*/
|
|
628
|
+
export declare function capabilityOf(r: {
|
|
629
|
+
status?: string;
|
|
630
|
+
held?: boolean;
|
|
631
|
+
window?: {
|
|
632
|
+
startHour: number;
|
|
633
|
+
endHour: number;
|
|
634
|
+
};
|
|
635
|
+
workCalendar?: WorkCalendarEntry[];
|
|
636
|
+
testResults?: TestResult[];
|
|
637
|
+
} & EffectivePeriod, ctx?: {
|
|
638
|
+
at?: ISOTime;
|
|
639
|
+
utcOffsetMinutes?: number;
|
|
640
|
+
requiredTests?: readonly string[];
|
|
641
|
+
}): Capability;
|
|
581
642
|
export interface LocationState {
|
|
582
643
|
id: string;
|
|
583
644
|
type: string;
|
package/dist/contract.js
CHANGED
|
@@ -517,6 +517,43 @@ export function offCalendarAt(r, ms, utcOffsetMinutes) {
|
|
|
517
517
|
const h = Math.floor(minute / 60);
|
|
518
518
|
return !(w.startHour <= w.endHour ? h >= w.startHour && h < w.endHour : h >= w.startHour || h < w.endHour);
|
|
519
519
|
}
|
|
520
|
+
/**
|
|
521
|
+
* 자원의 **가용 능력을 판정한다** — 하나의 규칙, 하나의 자리.
|
|
522
|
+
*
|
|
523
|
+
* 부르는 쪽이 시각과 시간대를 준다(커널은 `now()`·`utcOffsetMinutes`, 호스트는 관측 시각). 주지 않으면
|
|
524
|
+
* 시각에 달린 판정(유효기간·교대·시험 만료)은 **하지 않는다** — 모르면 판단하지 않는다는 규율이다.
|
|
525
|
+
*
|
|
526
|
+
* `requiredTests` 는 부르는 쪽이 등급에서 모아 넘긴다(등급 정의를 아는 것은 부르는 쪽이다).
|
|
527
|
+
*/
|
|
528
|
+
export function capabilityOf(r, ctx) {
|
|
529
|
+
const at = ctx?.at;
|
|
530
|
+
/* 순서가 뜻이다 — "이미 모델 밖" 이 "고장" 보다 앞선다(폐기한 설비의 고장은 고칠 일이 아니다). */
|
|
531
|
+
const eff = effectivityAt(r, at);
|
|
532
|
+
if (eff === 'not-yet')
|
|
533
|
+
return { available: false, reason: 'not-yet' };
|
|
534
|
+
if (eff === 'expired')
|
|
535
|
+
return { available: false, reason: 'retired' };
|
|
536
|
+
if (r.held)
|
|
537
|
+
return { available: false, reason: 'held' };
|
|
538
|
+
if (r.status === 'down')
|
|
539
|
+
return { available: false, reason: 'down' };
|
|
540
|
+
if (at) {
|
|
541
|
+
const ms = Date.parse(at);
|
|
542
|
+
if (Number.isFinite(ms)) {
|
|
543
|
+
const why = offCalendarReasonAt(r, ms, ctx?.utcOffsetMinutes);
|
|
544
|
+
if (why === 'non-working')
|
|
545
|
+
return { available: false, reason: 'resting' };
|
|
546
|
+
if (why === 'off-hours')
|
|
547
|
+
return { available: false, reason: 'off-shift' };
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
/* 자격은 **결과가 선언됐을 때만** 제약이다(§meetsTests) — 없는 것으로 막으면 라인이 굶는다. */
|
|
551
|
+
if (ctx?.requiredTests?.length && !meetsTests(ctx.requiredTests, r.testResults, at))
|
|
552
|
+
return { available: false, reason: 'test-expired' };
|
|
553
|
+
if (r.status && r.status !== 'idle' && r.status !== 'available')
|
|
554
|
+
return { available: false, reason: 'working' };
|
|
555
|
+
return { available: true, reason: 'available' };
|
|
556
|
+
}
|
|
520
557
|
// ── 운영 델타(비-EPCIS) — State 채널의 나머지 절반 ──────────────────────────
|
|
521
558
|
// EPCIS 이벤트는 재고/위치만 재구성 가능. tasks·equipment·orders 의 운영 상태는
|
|
522
559
|
// 이 델타로 미러한다. envelope.eventType = 'task.status' | 'equipment.status' | 'order.status'.
|
package/dist/flow-engine.d.ts
CHANGED
|
@@ -809,7 +809,12 @@ export declare abstract class FlowEngine implements TwinKernel {
|
|
|
809
809
|
* 판정 규칙은 계약이 소유한다(`meetsTests`) — 여기서 다시 적으면 규칙이 두 곳이 되고, 한쪽이 만료를
|
|
810
810
|
* 조용히 통과시킨다.
|
|
811
811
|
*/
|
|
812
|
-
|
|
812
|
+
/**
|
|
813
|
+
* 이 사람이 속한 등급들이 **요구하는 시험 목록** — 판정은 계약이 한다(`capabilityOf`).
|
|
814
|
+
*
|
|
815
|
+
* 상속을 타고 닫은 등급 전부의 요구를 모은다: 상위 등급이 요구하는 시험도 자격의 조건이다.
|
|
816
|
+
*/
|
|
817
|
+
private requiredTestsOf;
|
|
813
818
|
private claimPersonnel;
|
|
814
819
|
/**
|
|
815
820
|
* 필요 설비를 고른다 — **인원·자산과 같은 규칙**(등급으로 요구, 부분 확보 없이 전량 아니면 대기).
|
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,
|
|
12
|
+
import { OP_EVENT, CMD, locationStatusOf, readBoardEquipment, readBoardLocations, readBoardAssets, classClosure, capabilityOf, 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
15
|
import { parseIsoDuration } from "./iso-duration.js";
|
|
@@ -1854,16 +1854,21 @@ export class FlowEngine {
|
|
|
1854
1854
|
* 판정 규칙은 계약이 소유한다(`meetsTests`) — 여기서 다시 적으면 규칙이 두 곳이 되고, 한쪽이 만료를
|
|
1855
1855
|
* 조용히 통과시킨다.
|
|
1856
1856
|
*/
|
|
1857
|
-
|
|
1857
|
+
/**
|
|
1858
|
+
* 이 사람이 속한 등급들이 **요구하는 시험 목록** — 판정은 계약이 한다(`capabilityOf`).
|
|
1859
|
+
*
|
|
1860
|
+
* 상속을 타고 닫은 등급 전부의 요구를 모은다: 상위 등급이 요구하는 시험도 자격의 조건이다.
|
|
1861
|
+
*/
|
|
1862
|
+
requiredTestsOf(p) {
|
|
1858
1863
|
const defs = this.classDefs.personnel;
|
|
1859
1864
|
if (!defs?.length)
|
|
1860
|
-
return
|
|
1865
|
+
return [];
|
|
1861
1866
|
const closure = classClosure(p.personnelClassIds, defs, this.now());
|
|
1862
1867
|
const required = [];
|
|
1863
1868
|
for (const d of defs)
|
|
1864
1869
|
if (closure.has(d.id))
|
|
1865
1870
|
required.push(...(d.testSpecificationIds ?? []));
|
|
1866
|
-
return
|
|
1871
|
+
return required;
|
|
1867
1872
|
}
|
|
1868
1873
|
claimPersonnel(t) {
|
|
1869
1874
|
const need = this.operationSpecs.get(t.kind)?.personnelSpecification;
|
|
@@ -1874,24 +1879,23 @@ export class FlowEngine {
|
|
|
1874
1879
|
const want = Math.max(0, Math.floor(req.quantity ?? 0));
|
|
1875
1880
|
if (!want)
|
|
1876
1881
|
continue;
|
|
1877
|
-
const avail = [...this.persons.values()].filter(p => p.
|
|
1878
|
-
!picked.includes(p.id) &&
|
|
1879
|
-
!this.personOffShift(p) &&
|
|
1880
|
-
!this.outOfEffect(p) && // 입사 전·퇴사 후는 이 시각의 모델에 없다
|
|
1881
|
-
/* 자격은 **상속을 타고 닫아** 판정한다 — 사람은 여러 등급에 속할 수 있고(표준), 등급은
|
|
1882
|
-
다른 등급을 상속한다. "생산직 2명" 요구를 "용접 자격자" 가 만족해야 한다.
|
|
1883
|
-
유효기간 밖의 등급은 닫힘에서 빠진다(만료된 자격으로 배정되지 않는다). */
|
|
1884
|
-
(req.personnelClass === undefined ||
|
|
1885
|
-
classClosure(p.personnelClassIds, this.classDefs.personnel, this.now()).has(req.personnelClass)) &&
|
|
1882
|
+
const avail = [...this.persons.values()].filter(p => !picked.includes(p.id) &&
|
|
1886
1883
|
/*
|
|
1887
|
-
*
|
|
1888
|
-
*
|
|
1884
|
+
* **판정은 한 벌이다**(`capabilityOf`). 예전에는 이 자리에 조건을 늘어놓았고 화면은 같은
|
|
1885
|
+
* 판정을 자기 코드로 다시 만들었다 — 규칙이 두 벌이면 갈라지고, 배정은 막는데 화면은
|
|
1886
|
+
* "가용" 이라 말하는 순간이 온다(그 어긋남은 조용하다).
|
|
1889
1887
|
*
|
|
1890
|
-
*
|
|
1891
|
-
* 굶고, 원본이 결과를 주지 않는 현장에서는 그 제약이 선언되지 않은 것이다("선언한 것만
|
|
1892
|
-
* 제약이 된다" — 이 커널의 규율).
|
|
1888
|
+
* 요구된 시험은 등급에서 모아 넘긴다 — 등급 정의를 아는 것은 이쪽이다.
|
|
1893
1889
|
*/
|
|
1894
|
-
|
|
1890
|
+
capabilityOf(p, {
|
|
1891
|
+
at: this.now(),
|
|
1892
|
+
utcOffsetMinutes: this.boardDef?.utcOffsetMinutes,
|
|
1893
|
+
requiredTests: this.requiredTestsOf(p)
|
|
1894
|
+
}).available &&
|
|
1895
|
+
/* 자격은 **상속을 타고 닫아** 판정한다 — 사람은 여러 등급에 속할 수 있고(표준), 등급은
|
|
1896
|
+
다른 등급을 상속한다. "생산직 2명" 요구를 "용접 자격자" 가 만족해야 한다. */
|
|
1897
|
+
(req.personnelClass === undefined ||
|
|
1898
|
+
classClosure(p.personnelClassIds, this.classDefs.personnel, this.now()).has(req.personnelClass)));
|
|
1895
1899
|
if (avail.length < want)
|
|
1896
1900
|
return null; // 한 등급이라도 모자라면 시작하지 않는다
|
|
1897
1901
|
for (let i = 0; i < want; i++)
|
package/dist-cjs/index.cjs
CHANGED
|
@@ -73,6 +73,7 @@ __export(index_exports, {
|
|
|
73
73
|
axisInfo: () => axisInfo,
|
|
74
74
|
axisSource: () => axisSource,
|
|
75
75
|
capabilitiesForType: () => capabilitiesForType,
|
|
76
|
+
capabilityOf: () => capabilityOf,
|
|
76
77
|
classClosure: () => classClosure,
|
|
77
78
|
compareStates: () => compareStates,
|
|
78
79
|
computeOee: () => computeOee,
|
|
@@ -409,6 +410,26 @@ function offCalendarAt(r, ms2, utcOffsetMinutes) {
|
|
|
409
410
|
const h = Math.floor(minute / 60);
|
|
410
411
|
return !(w.startHour <= w.endHour ? h >= w.startHour && h < w.endHour : h >= w.startHour || h < w.endHour);
|
|
411
412
|
}
|
|
413
|
+
function capabilityOf(r, ctx) {
|
|
414
|
+
const at = ctx?.at;
|
|
415
|
+
const eff = effectivityAt(r, at);
|
|
416
|
+
if (eff === "not-yet") return { available: false, reason: "not-yet" };
|
|
417
|
+
if (eff === "expired") return { available: false, reason: "retired" };
|
|
418
|
+
if (r.held) return { available: false, reason: "held" };
|
|
419
|
+
if (r.status === "down") return { available: false, reason: "down" };
|
|
420
|
+
if (at) {
|
|
421
|
+
const ms2 = Date.parse(at);
|
|
422
|
+
if (Number.isFinite(ms2)) {
|
|
423
|
+
const why = offCalendarReasonAt(r, ms2, ctx?.utcOffsetMinutes);
|
|
424
|
+
if (why === "non-working") return { available: false, reason: "resting" };
|
|
425
|
+
if (why === "off-hours") return { available: false, reason: "off-shift" };
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
if (ctx?.requiredTests?.length && !meetsTests(ctx.requiredTests, r.testResults, at))
|
|
429
|
+
return { available: false, reason: "test-expired" };
|
|
430
|
+
if (r.status && r.status !== "idle" && r.status !== "available") return { available: false, reason: "working" };
|
|
431
|
+
return { available: true, reason: "available" };
|
|
432
|
+
}
|
|
412
433
|
var OP_EVENT = {
|
|
413
434
|
task: "task.status",
|
|
414
435
|
equipment: "equipment.status",
|
|
@@ -3849,13 +3870,18 @@ var FlowEngine = class {
|
|
|
3849
3870
|
* 판정 규칙은 계약이 소유한다(`meetsTests`) — 여기서 다시 적으면 규칙이 두 곳이 되고, 한쪽이 만료를
|
|
3850
3871
|
* 조용히 통과시킨다.
|
|
3851
3872
|
*/
|
|
3852
|
-
|
|
3873
|
+
/**
|
|
3874
|
+
* 이 사람이 속한 등급들이 **요구하는 시험 목록** — 판정은 계약이 한다(`capabilityOf`).
|
|
3875
|
+
*
|
|
3876
|
+
* 상속을 타고 닫은 등급 전부의 요구를 모은다: 상위 등급이 요구하는 시험도 자격의 조건이다.
|
|
3877
|
+
*/
|
|
3878
|
+
requiredTestsOf(p) {
|
|
3853
3879
|
const defs = this.classDefs.personnel;
|
|
3854
|
-
if (!defs?.length) return
|
|
3880
|
+
if (!defs?.length) return [];
|
|
3855
3881
|
const closure = classClosure(p.personnelClassIds, defs, this.now());
|
|
3856
3882
|
const required = [];
|
|
3857
3883
|
for (const d of defs) if (closure.has(d.id)) required.push(...d.testSpecificationIds ?? []);
|
|
3858
|
-
return
|
|
3884
|
+
return required;
|
|
3859
3885
|
}
|
|
3860
3886
|
claimPersonnel(t) {
|
|
3861
3887
|
const need = this.operationSpecs.get(t.kind)?.personnelSpecification;
|
|
@@ -3865,19 +3891,20 @@ var FlowEngine = class {
|
|
|
3865
3891
|
const want = Math.max(0, Math.floor(req.quantity ?? 0));
|
|
3866
3892
|
if (!want) continue;
|
|
3867
3893
|
const avail = [...this.persons.values()].filter(
|
|
3868
|
-
(p) =>
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
(req.personnelClass === void 0 || classClosure(p.personnelClassIds, this.classDefs.personnel, this.now()).has(req.personnelClass)) && /*
|
|
3873
|
-
* **요구된 시험을 만족하나** — 등급이 요구하고(`testSpecificationIds`) 사람이 결과를 든다
|
|
3874
|
-
* (`testResults`). 만료·불합격이면 그 자격은 성립하지 않는다.
|
|
3894
|
+
(p) => !picked.includes(p.id) && /*
|
|
3895
|
+
* **판정은 한 벌이다**(`capabilityOf`). 예전에는 이 자리에 조건을 늘어놓았고 화면은 같은
|
|
3896
|
+
* 판정을 자기 코드로 다시 만들었다 — 규칙이 두 벌이면 갈라지고, 배정은 막는데 화면은
|
|
3897
|
+
* "가용" 이라 말하는 순간이 온다(그 어긋남은 조용하다).
|
|
3875
3898
|
*
|
|
3876
|
-
*
|
|
3877
|
-
* 굶고, 원본이 결과를 주지 않는 현장에서는 그 제약이 선언되지 않은 것이다("선언한 것만
|
|
3878
|
-
* 제약이 된다" — 이 커널의 규율).
|
|
3899
|
+
* 요구된 시험은 등급에서 모아 넘긴다 — 등급 정의를 아는 것은 이쪽이다.
|
|
3879
3900
|
*/
|
|
3880
|
-
|
|
3901
|
+
capabilityOf(p, {
|
|
3902
|
+
at: this.now(),
|
|
3903
|
+
utcOffsetMinutes: this.boardDef?.utcOffsetMinutes,
|
|
3904
|
+
requiredTests: this.requiredTestsOf(p)
|
|
3905
|
+
}).available && /* 자격은 **상속을 타고 닫아** 판정한다 — 사람은 여러 등급에 속할 수 있고(표준), 등급은
|
|
3906
|
+
다른 등급을 상속한다. "생산직 2명" 요구를 "용접 자격자" 가 만족해야 한다. */
|
|
3907
|
+
(req.personnelClass === void 0 || classClosure(p.personnelClassIds, this.classDefs.personnel, this.now()).has(req.personnelClass))
|
|
3881
3908
|
);
|
|
3882
3909
|
if (avail.length < want) return null;
|
|
3883
3910
|
for (let i = 0; i < want; i++) picked.push(avail[i].id);
|
|
@@ -5159,6 +5186,7 @@ function retiredVocabularyIn(line) {
|
|
|
5159
5186
|
axisInfo,
|
|
5160
5187
|
axisSource,
|
|
5161
5188
|
capabilitiesForType,
|
|
5189
|
+
capabilityOf,
|
|
5162
5190
|
classClosure,
|
|
5163
5191
|
compareStates,
|
|
5164
5192
|
computeOee,
|
package/package.json
CHANGED