@operato/twin-kernel 0.7.58 → 0.7.60
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/observed-reducer.js +20 -0
- package/dist/operational-ingest.d.ts +1 -1
- package/dist/operational-ingest.js +35 -0
- package/dist-cjs/index.cjs +29 -0
- package/package.json +1 -1
package/dist/observed-reducer.js
CHANGED
|
@@ -630,6 +630,26 @@ export class ObservedReducer {
|
|
|
630
630
|
* 덮었다**(rack-1 의 100개가 사라지고 합계가 60이 됐다). 로트가 나뉘어 놓이는 것은 일상이다.
|
|
631
631
|
*/
|
|
632
632
|
const subLotId = `${qe.epcClass}@${loc}`;
|
|
633
|
+
/*
|
|
634
|
+
* ── ★ **이 갈래가 문을 지나지 않았다** (2026-08-24 실측) ──────────────
|
|
635
|
+
* 개체 관측은 `stale()` 을 지나는데 **수량 관측은 지나지 않았다.** 둘이 걸려 있었다.
|
|
636
|
+
*
|
|
637
|
+
* ① **순서 판정이 없었다** — 늦게 온 옛 관측이 최신 수량을 덮는다. 실 연동에서 순서는 뒤집힌다
|
|
638
|
+
* (재시도·큐·배치). 비직렬 자재가 주된 경로인 원본(식품 제조)에서는 이것이 재고의 대부분이다.
|
|
639
|
+
*
|
|
640
|
+
* ② **트윈의 시계가 밀리지 않았다** — `stale()` 이 관측 시각을 기억하는 유일한 자리이고
|
|
641
|
+
* (`observedAtMs`), `nowMs` 가 그 값을 「지금」으로 쓴다. 그래서 포천 미러는 재고 관측이
|
|
642
|
+
* 오늘 시각으로 계속 들어오는데도 `nowTime` 이 **2026-04-15 에 멈춰 있었다** — 시계를 밀던
|
|
643
|
+
* 유일한 스트림(공정·로트)이 4월에 멈춘 것이다. 그 결과 주의 신호의 지속시간이 언제나 0 이고,
|
|
644
|
+
* 시각으로 재는 모든 판단이 조용히 틀렸다.
|
|
645
|
+
*
|
|
646
|
+
* 에너지 경로가 같은 문을 우회해 같은 사고가 났고(2026-08-18) 그때 `noteObservedAt` 을 냈다.
|
|
647
|
+
* **세 번째다.** 축을 늘릴 때 이 문을 지나는지 보는 것이 규율이다.
|
|
648
|
+
*
|
|
649
|
+
* 키에 서브로트를 넣는다 — 같은 로트의 다른 자리가 서로를 막지 않아야 한다(§`subLotId`).
|
|
650
|
+
*/
|
|
651
|
+
if (envelope && this.stale(`item:${subLotId}`, envelope))
|
|
652
|
+
continue;
|
|
633
653
|
this.items.set(subLotId, this.mergeItem(qe.epcClass, { location: loc, disposition: ev.disposition, ilmd: ev.ilmd }, qe, mine, subLotId));
|
|
634
654
|
}
|
|
635
655
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { IngestResult } from './face2-adapter.ts';
|
|
2
2
|
/** 이 문이 받는 여섯 가지 — 리듀서가 접는 것과 같은 목록(주목 확인은 우리 안의 행위라 제외). */
|
|
3
|
-
export type OperationalKind = 'task' | 'equipment' | 'person' | 'asset' | 'order' | 'quality';
|
|
3
|
+
export type OperationalKind = 'task' | 'equipment' | 'person' | 'asset' | 'order' | 'quality' | 'test';
|
|
4
4
|
/**
|
|
5
5
|
* 정규 운영 레코드 — **델타의 필드 이름 + 시각(`at`)**.
|
|
6
6
|
*
|
|
@@ -119,6 +119,27 @@ const SPECS = {
|
|
|
119
119
|
/* 누적 카운터가 없으면 OEE 가 양품률을 못 센다 — 판정 하나만으로는 비율이 나오지 않는다. */
|
|
120
120
|
required: ['moverId', 'good', 'goodCount', 'scrapCount'], // vocabulary-guard: allow
|
|
121
121
|
fields: { moverId: 'string', good: 'boolean', goodCount: 'number', scrapCount: 'number', recordTime: 'string' } // vocabulary-guard: allow
|
|
122
|
+
},
|
|
123
|
+
/*
|
|
124
|
+
* **시험 결과** — 대상을 가리켜 들어온다(표준 `TestResult.TestableObjectID`).
|
|
125
|
+
*
|
|
126
|
+
* `result` 를 **요구하지 않는다**: 재기만 하고 판정하지 않는 원천이 정상이고, 요구하면 그 원천의
|
|
127
|
+
* 사실을 아예 담을 수 없다(§`TestResult.result`). 비어 있으면 커널이 선언된 기준으로 판정하고
|
|
128
|
+
* `derived` 를 세운다 — 판정하지 못하면 비워 둔다.
|
|
129
|
+
*
|
|
130
|
+
* `propertyMeasurements` 안쪽은 재검사하지 않는다 — 그 모양은 `PropertyMeasurement` 계약이고,
|
|
131
|
+
* 여기서 두 번 지키면 두 벌이 된다(설비 `motion` 과 같은 규율).
|
|
132
|
+
*/
|
|
133
|
+
test: {
|
|
134
|
+
eventType: OP_EVENT.test,
|
|
135
|
+
identity: 'testableObjectId',
|
|
136
|
+
/* 무엇을 어느 기준으로 시험했나 — 둘 중 하나가 없으면 그 결과는 아무 데도 붙지 못한다. */
|
|
137
|
+
required: ['testableObjectId', 'specId'],
|
|
138
|
+
fields: {
|
|
139
|
+
testableObjectId: 'string', specId: 'string', result: 'string', at: 'string', expiresAt: 'string',
|
|
140
|
+
derived: 'boolean', propertyMeasurements: 'object[]', recordTime: 'string'
|
|
141
|
+
},
|
|
142
|
+
enums: { result: ['pass', 'fail'] }
|
|
122
143
|
}
|
|
123
144
|
};
|
|
124
145
|
/**
|
|
@@ -152,6 +173,20 @@ export function operationalKindOf(record) {
|
|
|
152
173
|
return 'task'; // 작업이 든 `orderId` 는 소속(참조)이다
|
|
153
174
|
if (has('orderId'))
|
|
154
175
|
return 'order';
|
|
176
|
+
/*
|
|
177
|
+
* ── ★ **채널을 열고 들어오는 길을 내지 않았다** (2026-08-24) ─────────────────
|
|
178
|
+
* `OP_EVENT.test` 를 계약에 냈는데 이 라우팅이 `testableObjectId` 를 보지 않았다. 그래서 커넥터의
|
|
179
|
+
* 시험 결과가 **어느 통도 아니어서 조용히 버려졌다** — 거부 목록에도 남지 않았다(운영 경로를 아예
|
|
180
|
+
* 지나지 않으므로).
|
|
181
|
+
*
|
|
182
|
+
* 같은 부류가 하루에 세 번 났다: `ilmd`(매핑에 자리 없음) · 사건 시각(이름 어긋남) · 그리고 이것.
|
|
183
|
+
* **계약에 자리를 만드는 것과 그 자리로 가는 길을 내는 것은 다른 일이다.** 앞의 것만 하면 보내는
|
|
184
|
+
* 쪽에는 「실었다」로 보이고 화면에는 「없다」로 보인다.
|
|
185
|
+
*
|
|
186
|
+
* 순서상 뒤에 둔다 — 시험 결과가 작업·오더를 함께 가리킬 수 있고, 그때 그것은 **그 작업의 사실**이다.
|
|
187
|
+
*/
|
|
188
|
+
if (has('testableObjectId'))
|
|
189
|
+
return 'test';
|
|
155
190
|
return undefined;
|
|
156
191
|
}
|
|
157
192
|
/** 이 레코드가 운영 사실인가 — 호스트의 라우팅이 묻는 자리. */
|
package/dist-cjs/index.cjs
CHANGED
|
@@ -1812,6 +1812,7 @@ var ObservedReducer = class {
|
|
|
1812
1812
|
if (!qe?.epcClass) continue;
|
|
1813
1813
|
const mine = all.filter((x) => x.epcClass === qe.epcClass);
|
|
1814
1814
|
const subLotId = `${qe.epcClass}@${loc}`;
|
|
1815
|
+
if (envelope && this.stale(`item:${subLotId}`, envelope)) continue;
|
|
1815
1816
|
this.items.set(subLotId, this.mergeItem(qe.epcClass, { location: loc, disposition: ev.disposition, ilmd: ev.ilmd }, qe, mine, subLotId));
|
|
1816
1817
|
}
|
|
1817
1818
|
}
|
|
@@ -8989,6 +8990,33 @@ var SPECS = {
|
|
|
8989
8990
|
// vocabulary-guard: allow
|
|
8990
8991
|
fields: { moverId: "string", good: "boolean", goodCount: "number", scrapCount: "number", recordTime: "string" }
|
|
8991
8992
|
// vocabulary-guard: allow
|
|
8993
|
+
},
|
|
8994
|
+
/*
|
|
8995
|
+
* **시험 결과** — 대상을 가리켜 들어온다(표준 `TestResult.TestableObjectID`).
|
|
8996
|
+
*
|
|
8997
|
+
* `result` 를 **요구하지 않는다**: 재기만 하고 판정하지 않는 원천이 정상이고, 요구하면 그 원천의
|
|
8998
|
+
* 사실을 아예 담을 수 없다(§`TestResult.result`). 비어 있으면 커널이 선언된 기준으로 판정하고
|
|
8999
|
+
* `derived` 를 세운다 — 판정하지 못하면 비워 둔다.
|
|
9000
|
+
*
|
|
9001
|
+
* `propertyMeasurements` 안쪽은 재검사하지 않는다 — 그 모양은 `PropertyMeasurement` 계약이고,
|
|
9002
|
+
* 여기서 두 번 지키면 두 벌이 된다(설비 `motion` 과 같은 규율).
|
|
9003
|
+
*/
|
|
9004
|
+
test: {
|
|
9005
|
+
eventType: OP_EVENT.test,
|
|
9006
|
+
identity: "testableObjectId",
|
|
9007
|
+
/* 무엇을 어느 기준으로 시험했나 — 둘 중 하나가 없으면 그 결과는 아무 데도 붙지 못한다. */
|
|
9008
|
+
required: ["testableObjectId", "specId"],
|
|
9009
|
+
fields: {
|
|
9010
|
+
testableObjectId: "string",
|
|
9011
|
+
specId: "string",
|
|
9012
|
+
result: "string",
|
|
9013
|
+
at: "string",
|
|
9014
|
+
expiresAt: "string",
|
|
9015
|
+
derived: "boolean",
|
|
9016
|
+
propertyMeasurements: "object[]",
|
|
9017
|
+
recordTime: "string"
|
|
9018
|
+
},
|
|
9019
|
+
enums: { result: ["pass", "fail"] }
|
|
8992
9020
|
}
|
|
8993
9021
|
};
|
|
8994
9022
|
function operationalKindOf(record) {
|
|
@@ -9001,6 +9029,7 @@ function operationalKindOf(record) {
|
|
|
9001
9029
|
if (has("assetId")) return "asset";
|
|
9002
9030
|
if (has("taskId")) return "task";
|
|
9003
9031
|
if (has("orderId")) return "order";
|
|
9032
|
+
if (has("testableObjectId")) return "test";
|
|
9004
9033
|
return void 0;
|
|
9005
9034
|
}
|
|
9006
9035
|
function isOperationalRecord(record) {
|
package/package.json
CHANGED