@operato/twin-kernel 0.7.40 → 0.7.41

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.
@@ -1531,11 +1531,11 @@ export interface CommandAck {
1531
1531
  error?: string;
1532
1532
  }
1533
1533
  /**
1534
- * 호스트-facing 커맨드 채널(비동기) — 씬 컴포넌트 등 클라이언트가 트윈 호스트에 커맨드를 던지는 계약.
1534
+ * 호스트-facing 커맨드 채널(비동기) — 씬 컴포넌트 등 클라이언트가 트윈 호스트에 커맨드를 보내는 계약.
1535
1535
  * 커널 in-process `dispatch`(동기)와 달리 원격 호스트(GraphQL/HTTP/…) 전송을 추상화.
1536
1536
  * 씬 컨트롤 컴포넌트는 이 인터페이스에만 의존(커널 레벨) — concrete 전송은 각 호스트(things-factory 등)가 주입.
1537
1537
  * → 컴포넌트가 특정 호스트 구현(things-factory)에 갇히지 않고, 어디서든 개발/재사용 가능.
1538
- * 컴포넌트는 tenantId 등을 모르는 부분 커맨드를 던지고, 호스트가 보강한다(commandId/type/args 만 제공).
1538
+ * 컴포넌트는 tenantId 등을 모르는 부분 커맨드를 보내고, 호스트가 보강한다(commandId/type/args 만 제공).
1539
1539
  */
1540
1540
  export interface TwinCommandChannel {
1541
1541
  dispatch(command: Pick<Command, 'commandId' | 'type' | 'args'>): Promise<CommandAck>;
package/dist/contract.js CHANGED
@@ -150,7 +150,7 @@ export function testPassedAt(r, at) {
150
150
  return false;
151
151
  if (!r.expiresAt || !at)
152
152
  return r.result === 'pass';
153
- return Date.parse(at) <= Date.parse(r.expiresAt);
153
+ return parsedMs(at) <= parsedMs(r.expiresAt);
154
154
  }
155
155
  /**
156
156
  * 이 개체가 **요구된 시험들을 만족하나** — 등급이 요구하고 개체가 기록을 든다.
@@ -293,8 +293,8 @@ export function priorityRank(p) {
293
293
  export function dueStatusOf(x, nowIso) {
294
294
  if (!x.endTime || !nowIso)
295
295
  return undefined;
296
- const due = Date.parse(x.endTime);
297
- const now = Date.parse(nowIso);
296
+ const due = parsedMs(x.endTime);
297
+ const now = parsedMs(nowIso);
298
298
  if (!Number.isFinite(due) || !Number.isFinite(now))
299
299
  return undefined;
300
300
  return now > due ? 'late' : 'on-time';
@@ -663,7 +663,7 @@ export function capabilityOf(r, ctx) {
663
663
  if (r.status === 'down')
664
664
  return { available: false, reason: 'down' };
665
665
  if (at) {
666
- const ms = Date.parse(at);
666
+ const ms = parsedMs(at);
667
667
  if (Number.isFinite(ms)) {
668
668
  const why = offCalendarReasonAt(r, ms, ctx?.utcOffsetMinutes);
669
669
  if (why === 'non-working')
@@ -225,7 +225,7 @@ export declare const propertiesOf: (axis: string) => TwinPropertyInfo[];
225
225
  */
226
226
  export declare const axisSource: (axis: string) => "document" | "state" | "journal";
227
227
  /**
228
- * 문서에서 이 축을 찾을 자리 — **문서 축이 아니면 던진다.**
228
+ * 문서에서 이 축을 찾을 자리 — **문서 축이 아니면 오류를 낸다.**
229
229
  *
230
230
  * 상태·저널 축에는 board 안의 자리가 없다. 그런데 옛 소비처는 `info.path` 를 무조건 읽었으므로,
231
231
  * 그 자리가 `undefined` 면 board 루트를 읽고 **오류 없이 0 을 답한다**(이 프로젝트가 겪은 실패 모양).
@@ -202,7 +202,7 @@ export const propertiesOf = (axis) => TWIN_PROPERTIES.filter(p => p.axis === axi
202
202
  */
203
203
  export const axisSource = (axis) => TWIN_AXES.find(a => a.axis === axis)?.source ?? 'document';
204
204
  /**
205
- * 문서에서 이 축을 찾을 자리 — **문서 축이 아니면 던진다.**
205
+ * 문서에서 이 축을 찾을 자리 — **문서 축이 아니면 오류를 낸다.**
206
206
  *
207
207
  * 상태·저널 축에는 board 안의 자리가 없다. 그런데 옛 소비처는 `info.path` 를 무조건 읽었으므로,
208
208
  * 그 자리가 `undefined` 면 board 루트를 읽고 **오류 없이 0 을 답한다**(이 프로젝트가 겪은 실패 모양).
@@ -621,7 +621,7 @@ export class EmsKernel extends FlowEngine {
621
621
  * **아무 일도 하지 않는다** — 그런데 조용히 넘기지 않는다: 이 커널이 그런 요청을 받았다는 것은
622
622
  * **배선이 잘못됐다는 사실**이고(EMS 트윈에 물류 명령을 보낸 것), 조용히 넘기면 그 사실이 사라진다.
623
623
  *
624
- * 던지지도 않는다: 저널 재생 중이라면 트윈 전체가 멈춘다. 그래서 커널이 담을 줄 모르는 사건을
624
+ * 오류를 내지도 않는다: 저널 재생 중이라면 트윈 전체가 멈춘다. 그래서 커널이 담을 줄 모르는 사건을
625
625
  * 세는 자리(`ObservedReducer.unhandled`)와 같은 규율으로, **개수를 세어 상태로 낸다.**
626
626
  */
627
627
  flowRequests = new Map();
package/dist/kernel.d.ts CHANGED
@@ -16,7 +16,7 @@ export declare class WmsKernel extends FlowEngine {
16
16
  *
17
17
  * 여기서 이름을 여는 것이 답이 아니다 — 이 경로는 부품·라우트가 내장 상수인 레거시 시나리오라,
18
18
  * 이름만 열면 **지원하는 척**이 된다(MES 가 같은 이유로 같은 결론을 냈다). 답은 요구를 분명히 말하는
19
- * 것이고, 던지면 호스트가 그 트윈만 멈추고 이유를 등록부에 적는다(`tick-failed`) — 다른 트윈은 계속 돈다.
19
+ * 것이고, 오류를 내면 호스트가 그 트윈만 멈추고 이유를 등록부에 적는다(`tick-failed`) — 다른 트윈은 계속 돈다.
20
20
  * 조용한 정지는 그 사실조차 남기지 않는다.
21
21
  */
22
22
  private builtInLocation;
package/dist/kernel.js CHANGED
@@ -43,7 +43,7 @@ export class WmsKernel extends FlowEngine {
43
43
  *
44
44
  * 여기서 이름을 여는 것이 답이 아니다 — 이 경로는 부품·라우트가 내장 상수인 레거시 시나리오라,
45
45
  * 이름만 열면 **지원하는 척**이 된다(MES 가 같은 이유로 같은 결론을 냈다). 답은 요구를 분명히 말하는
46
- * 것이고, 던지면 호스트가 그 트윈만 멈추고 이유를 등록부에 적는다(`tick-failed`) — 다른 트윈은 계속 돈다.
46
+ * 것이고, 오류를 내면 호스트가 그 트윈만 멈추고 이유를 등록부에 적는다(`tick-failed`) — 다른 트윈은 계속 돈다.
47
47
  * 조용한 정지는 그 사실조차 남기지 않는다.
48
48
  */
49
49
  builtInLocation(type, what) {
@@ -417,13 +417,34 @@ export class MesKernel extends FlowEngine {
417
417
  }
418
418
  const rc = this.recipeDef(o);
419
419
  const picks = [];
420
+ /*
421
+ * ── 자리별 색인을 사용한다 (2026-08-21) ────────────────────────────────────
422
+ * 예전에는 투입 줄마다 `[...this.items.values()].filter(...)` 로 **물품 전체를 순회**했다. 오더 하나에
423
+ * 투입 줄이 셋이면 세 번이다. 규모 기준선이 그것을 보여 주었다 — 물품이 4배 늘 때 주문당 비용이
424
+ * 3.3~3.5배가 됐다(거의 선형). 목표 규모는 물품 10만이고, 그때 이 순회는 틱을 밀어 낸다.
425
+ *
426
+ * 필터 조건이 `gtin` + `disposition` + **자리 타입**이므로, 자리 타입에 해당하는 자리들만 찾아
427
+ * `items.at(자리)` 색인을 사용하면 그 자리에 있는 것만 본다. 자리 수는 물품 수와 무관하게 작다.
428
+ */
429
+ const locsOfType = new Map();
430
+ for (const n of this.locations.values()) {
431
+ const bin = locsOfType.get(n.type);
432
+ if (bin)
433
+ bin.push(n);
434
+ else
435
+ locsOfType.set(n.type, [n]);
436
+ }
420
437
  for (const line of rc.inputs) {
421
438
  const g = this.classOf(line.material);
422
- /* 자리 필터는 남는다 — 완제품도 `sellable` 이라, 반제품→완제품 체인에서 이것이 없으면 산출물을 자재로 집어먹는다. */
439
+ /* 자리 필터는 남는다 — 완제품도 `sellable` 이라, 반제품→완제품 체인에서 이것이 없으면 산출물을 자재로 소비한다. */
423
440
  const fromType = this.locationTypeOfMaterial(line.material);
424
- const available = [...this.items.values()]
425
- .filter(i => i.gtin === g && i.disposition === DISP.sellable && this.locations.get(i.location)?.type === fromType)
426
- .map(i => ({ epc: i.epc, location: i.location, qty: 1 }));
441
+ const available = [];
442
+ for (const n of locsOfType.get(fromType) ?? []) {
443
+ for (const i of this.items.at(n.id)) {
444
+ if (i.gtin === g && i.disposition === DISP.sellable)
445
+ available.push({ epc: i.epc, location: i.location, qty: 1 });
446
+ }
447
+ }
427
448
  const chosen = this.policy.selectStock({ gtin: g, qty: line.qty, available });
428
449
  if (chosen.length < line.qty)
429
450
  return; // 자재 부족 → 대기
@@ -55,7 +55,7 @@ export interface ProjectedState {
55
55
  * 아무것도 담기지 않는다. 그것을 조용히 두면 트윈은 「정상인데 빈」 모습이 된다.
56
56
  *
57
57
  * 비어 있지 않으면 뜻은 하나다: **이 트윈에 맞지 않는 어휘가 들어오고 있다**(커넥터 배선 오류이거나
58
- * 커널이 낡았다). 세는 것에 그치고 판단은 소비처가 한다 — 여기서 던지면 저널 재생이 멈춘다.
58
+ * 커널이 낡았다). 세는 것에 그치고 판단은 소비처가 한다 — 여기서 오류를 내면 저널 재생이 멈춘다.
59
59
  */
60
60
  unhandled?: {
61
61
  eventType: string;
@@ -263,7 +263,7 @@ function hierarchyOf(s, levelOfType) {
263
263
  function testPassedAt(r, at) {
264
264
  if (r.result !== "pass") return false;
265
265
  if (!r.expiresAt || !at) return r.result === "pass";
266
- return Date.parse(at) <= Date.parse(r.expiresAt);
266
+ return parsedMs(at) <= parsedMs(r.expiresAt);
267
267
  }
268
268
  function meetsTests(required, results, at) {
269
269
  if (!required.length) return true;
@@ -326,8 +326,8 @@ function priorityRank(p) {
326
326
  }
327
327
  function dueStatusOf(x, nowIso) {
328
328
  if (!x.endTime || !nowIso) return void 0;
329
- const due = Date.parse(x.endTime);
330
- const now = Date.parse(nowIso);
329
+ const due = parsedMs(x.endTime);
330
+ const now = parsedMs(nowIso);
331
331
  if (!Number.isFinite(due) || !Number.isFinite(now)) return void 0;
332
332
  return now > due ? "late" : "on-time";
333
333
  }
@@ -495,7 +495,7 @@ function capabilityOf(r, ctx) {
495
495
  if (r.held) return { available: false, reason: "held" };
496
496
  if (r.status === "down") return { available: false, reason: "down" };
497
497
  if (at) {
498
- const ms2 = Date.parse(at);
498
+ const ms2 = parsedMs(at);
499
499
  if (Number.isFinite(ms2)) {
500
500
  const why = offCalendarReasonAt(r, ms2, ctx?.utcOffsetMinutes);
501
501
  if (why === "non-working") return { available: false, reason: "resting" };
@@ -5995,7 +5995,7 @@ var WmsKernel = class extends FlowEngine {
5995
5995
  *
5996
5996
  * 여기서 이름을 여는 것이 답이 아니다 — 이 경로는 부품·라우트가 내장 상수인 레거시 시나리오라,
5997
5997
  * 이름만 열면 **지원하는 척**이 된다(MES 가 같은 이유로 같은 결론을 냈다). 답은 요구를 분명히 말하는
5998
- * 것이고, 던지면 호스트가 그 트윈만 멈추고 이유를 등록부에 적는다(`tick-failed`) — 다른 트윈은 계속 돈다.
5998
+ * 것이고, 오류를 내면 호스트가 그 트윈만 멈추고 이유를 등록부에 적는다(`tick-failed`) — 다른 트윈은 계속 돈다.
5999
5999
  * 조용한 정지는 그 사실조차 남기지 않는다.
6000
6000
  */
6001
6001
  builtInLocation(type, what) {
@@ -6826,10 +6826,21 @@ var MesKernel = class extends FlowEngine {
6826
6826
  }
6827
6827
  const rc = this.recipeDef(o);
6828
6828
  const picks = [];
6829
+ const locsOfType = /* @__PURE__ */ new Map();
6830
+ for (const n of this.locations.values()) {
6831
+ const bin = locsOfType.get(n.type);
6832
+ if (bin) bin.push(n);
6833
+ else locsOfType.set(n.type, [n]);
6834
+ }
6829
6835
  for (const line of rc.inputs) {
6830
6836
  const g = this.classOf(line.material);
6831
6837
  const fromType = this.locationTypeOfMaterial(line.material);
6832
- const available = [...this.items.values()].filter((i) => i.gtin === g && i.disposition === DISP.sellable && this.locations.get(i.location)?.type === fromType).map((i) => ({ epc: i.epc, location: i.location, qty: 1 }));
6838
+ const available = [];
6839
+ for (const n of locsOfType.get(fromType) ?? []) {
6840
+ for (const i of this.items.at(n.id)) {
6841
+ if (i.gtin === g && i.disposition === DISP.sellable) available.push({ epc: i.epc, location: i.location, qty: 1 });
6842
+ }
6843
+ }
6833
6844
  const chosen = this.policy.selectStock({ gtin: g, qty: line.qty, available });
6834
6845
  if (chosen.length < line.qty) return;
6835
6846
  picks.push(...chosen);
@@ -7361,7 +7372,7 @@ var EmsKernel = class extends FlowEngine {
7361
7372
  * **아무 일도 하지 않는다** — 그런데 조용히 넘기지 않는다: 이 커널이 그런 요청을 받았다는 것은
7362
7373
  * **배선이 잘못됐다는 사실**이고(EMS 트윈에 물류 명령을 보낸 것), 조용히 넘기면 그 사실이 사라진다.
7363
7374
  *
7364
- * 던지지도 않는다: 저널 재생 중이라면 트윈 전체가 멈춘다. 그래서 커널이 담을 줄 모르는 사건을
7375
+ * 오류를 내지도 않는다: 저널 재생 중이라면 트윈 전체가 멈춘다. 그래서 커널이 담을 줄 모르는 사건을
7365
7376
  * 세는 자리(`ObservedReducer.unhandled`)와 같은 규율으로, **개수를 세어 상태로 낸다.**
7366
7377
  */
7367
7378
  flowRequests = /* @__PURE__ */ new Map();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@operato/twin-kernel",
3
- "version": "0.7.40",
3
+ "version": "0.7.41",
4
4
  "type": "module",
5
5
  "description": "Twin Domain Kernel — framework-agnostic, zero-dep (domain + sim + 3-channel contract). WMS/YMS/MES, EPCIS 2.0 · ISA-95.",
6
6
  "publishConfig": {