@operato/twin-kernel 0.2.3 → 0.3.0
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/README.md +46 -5
- package/dist/capability.js +2 -2
- package/dist/contract.d.ts +336 -34
- package/dist/contract.js +199 -7
- package/dist/divergence.d.ts +1 -1
- package/dist/divergence.js +3 -3
- package/dist/domain-catalog.d.ts +4 -4
- package/dist/domain-catalog.js +4 -4
- package/dist/domain-definition.d.ts +5 -5
- package/dist/domain-definition.js +6 -6
- package/dist/event-journal.d.ts +1 -1
- package/dist/event-journal.js +1 -1
- package/dist/flow-engine.d.ts +55 -31
- package/dist/flow-engine.js +153 -124
- package/dist/forecast.d.ts +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/kernel.js +6 -6
- package/dist/mes-kernel.js +25 -25
- package/dist/mes-profile.d.ts +2 -2
- package/dist/mes-profile.js +11 -11
- package/dist/observed-reducer.d.ts +7 -7
- package/dist/observed-reducer.js +40 -30
- package/dist/task-fold.d.ts +2 -2
- package/dist/task-fold.js +4 -4
- package/dist/vocabulary.d.ts +19 -0
- package/dist/vocabulary.js +66 -0
- package/dist/wms-profile.d.ts +2 -2
- package/dist/wms-profile.js +6 -6
- package/dist/yms-kernel.js +10 -10
- package/dist/yms-profile.d.ts +2 -2
- package/dist/yms-profile.js +6 -6
- package/dist-cjs/index.cjs +413 -214
- package/package.json +1 -1
package/dist/forecast.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ export interface ForecastTwin {
|
|
|
8
8
|
/**
|
|
9
9
|
* 시뮬 시각(ms) — **구동 루프가 시각만 읽을 때 쓰는 값.**
|
|
10
10
|
*
|
|
11
|
-
* 없으면 `getSnapshot().simClockMs` 로 떨어지지만, 그 경로는 전
|
|
11
|
+
* 없으면 `getSnapshot().simClockMs` 로 떨어지지만, 그 경로는 전 자리·물품·자원·작업·오더를 새로
|
|
12
12
|
* 재료화하고 주목 신호까지 계산한 뒤 숫자 하나만 꺼내 버린다 — tick 마다 그러면 상태 크기 × 지평선
|
|
13
13
|
* 길이만큼 낭비가 쌓인다(물품 1만 건·30분 지평선에서 측정: 루프 조건에만 149ms vs 0ms).
|
|
14
14
|
*/
|
package/dist/index.d.ts
CHANGED
|
@@ -24,3 +24,4 @@ export { WmsKernel } from './kernel.ts';
|
|
|
24
24
|
export { YmsKernel } from './yms-kernel.ts';
|
|
25
25
|
export { MesKernel, MES_PART_GTINS, MES_PRODUCT_GTINS, MES_PRODUCTS } from './mes-kernel.ts';
|
|
26
26
|
export type { MesDefinitionSpec } from './mes-kernel.ts';
|
|
27
|
+
export * from './vocabulary.ts';
|
package/dist/index.js
CHANGED
package/dist/kernel.js
CHANGED
|
@@ -21,7 +21,7 @@ export class WmsKernel extends FlowEngine {
|
|
|
21
21
|
}
|
|
22
22
|
/** 입고 도착 — §4 라이프사이클: ASN(PO) → 팔레트 조립 → 수령 → putaway task. */
|
|
23
23
|
onArrival(spec) {
|
|
24
|
-
const dock = this.
|
|
24
|
+
const dock = this.locationByType('dock');
|
|
25
25
|
if (!dock)
|
|
26
26
|
return;
|
|
27
27
|
const epc = ssccUri(COMPANY_PREFIX, ++this.epcSeq); // 팔레트 SSCC
|
|
@@ -104,7 +104,7 @@ export class WmsKernel extends FlowEngine {
|
|
|
104
104
|
* 멀티SKU 는 여러 라인의 팔레트를 한 오더로 모아 단일 출하(finalizeOrder 통합 화물).
|
|
105
105
|
*/
|
|
106
106
|
allocate(o) {
|
|
107
|
-
const staging = this.
|
|
107
|
+
const staging = this.locationByType('staging');
|
|
108
108
|
if (!staging || !o.lines)
|
|
109
109
|
return;
|
|
110
110
|
const chosenAll = [];
|
|
@@ -114,7 +114,7 @@ export class WmsKernel extends FlowEngine {
|
|
|
114
114
|
if (need <= 0)
|
|
115
115
|
continue;
|
|
116
116
|
const available = [...this.items.values()]
|
|
117
|
-
.filter(i => i.gtin === line.gtin && i.disposition === DISP.sellable && this.
|
|
117
|
+
.filter(i => i.gtin === line.gtin && i.disposition === DISP.sellable && this.locations.get(i.location)?.type === 'storage')
|
|
118
118
|
.map(i => ({ epc: i.epc, location: i.location, qty: i.qty ?? 1, expiry: i.expiry }));
|
|
119
119
|
const chosen = this.policy.selectStock({ gtin: line.gtin, qty: need, available });
|
|
120
120
|
for (const epc of chosen) {
|
|
@@ -141,8 +141,8 @@ export class WmsKernel extends FlowEngine {
|
|
|
141
141
|
/** 태스크 완료 — 이동 반영 후 putaway=storing, pick=picking(+전량 시 pack→stage→ship). */
|
|
142
142
|
onTaskComplete(t) {
|
|
143
143
|
const item = this.items.get(t.itemEpc);
|
|
144
|
-
const from = this.
|
|
145
|
-
const to = this.
|
|
144
|
+
const from = this.locations.get(t.fromNode);
|
|
145
|
+
const to = this.locations.get(t.toNode);
|
|
146
146
|
from.occupancy--;
|
|
147
147
|
to.occupancy++;
|
|
148
148
|
item.location = to.id;
|
|
@@ -163,7 +163,7 @@ export class WmsKernel extends FlowEngine {
|
|
|
163
163
|
}
|
|
164
164
|
/** 전량 피킹 → packing(조립)·staging·shipping 마감. 화물 사이트 이탈, 백오더 잔량 재할당. */
|
|
165
165
|
finalizeOrder(order, staging) {
|
|
166
|
-
const shipDock = this.
|
|
166
|
+
const shipDock = this.locationByType('dock-ship') ?? staging;
|
|
167
167
|
const eventTime = this.now();
|
|
168
168
|
const shipment = ssccUri(COMPANY_PREFIX, ++this.epcSeq);
|
|
169
169
|
order.shipmentEpc = shipment;
|
package/dist/mes-kernel.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* ① BOM = 제품별 다품목 레시피(P1=2A+1B / P2=1A+2B — 상이 → 체인지오버 유발).
|
|
7
7
|
* ② 수율 = 일부 불량(non_sellable) → OEE Quality.
|
|
8
8
|
* ③ 셋업/체인지오버 = work-center 가 제품 전환 시 셋업(changeoverKey=제품 gtin) → OEE Availability.
|
|
9
|
-
* ④ OEE = base 가
|
|
9
|
+
* ④ OEE = base 가 설비(설비)별 계측(가동/셋업/기아/품질). cut=cutter·weld=welder 이종 자원.
|
|
10
10
|
*/
|
|
11
11
|
import { firstFitPolicy } from "./allocation-policy.js";
|
|
12
12
|
import { FlowEngine } from "./flow-engine.js";
|
|
@@ -44,10 +44,10 @@ export const MES_PRODUCTS = PRODUCTS.map(p => ({ gtin: p.gtin, label: p.key }));
|
|
|
44
44
|
* 각 스테이션 = 이종 자원(resource) + 제품 전환 시 체인지오버 셋업.
|
|
45
45
|
*/
|
|
46
46
|
const ROUTE = [
|
|
47
|
-
{ kind: 'cut',
|
|
48
|
-
{ kind: 'weld',
|
|
49
|
-
{ kind: 'paint',
|
|
50
|
-
{ kind: 'assembly',
|
|
47
|
+
{ kind: 'cut', locationType: 'cut-station', resource: 'cutter' },
|
|
48
|
+
{ kind: 'weld', locationType: 'weld-station', resource: 'welder' },
|
|
49
|
+
{ kind: 'paint', locationType: 'paint-booth', resource: 'painter' },
|
|
50
|
+
{ kind: 'assembly', locationType: 'assembly-line', resource: 'assembler' }
|
|
51
51
|
];
|
|
52
52
|
export class MesKernel extends FlowEngine {
|
|
53
53
|
wipSeq = 0;
|
|
@@ -73,7 +73,7 @@ export class MesKernel extends FlowEngine {
|
|
|
73
73
|
const a = cmd.args;
|
|
74
74
|
if (!a?.resourceId || !a?.gtin)
|
|
75
75
|
return { commandId: cmd.commandId, accepted: false, errorCode: 'changeover-needs-args', error: 'mes.changeover: resourceId and gtin required' };
|
|
76
|
-
const m = this.
|
|
76
|
+
const m = this.equipment.get(a.resourceId);
|
|
77
77
|
if (!m)
|
|
78
78
|
return { commandId: cmd.commandId, accepted: false, errorCode: 'resource-not-found', errorParams: { resourceId: a.resourceId }, error: `resource-not-found: ${a.resourceId}` };
|
|
79
79
|
if (m.lastChangeoverKey !== a.gtin) {
|
|
@@ -81,7 +81,7 @@ export class MesKernel extends FlowEngine {
|
|
|
81
81
|
* 오퍼레이션별 셋업 명세는 작업 생성 경로(emitStation*)가 소비한다. */
|
|
82
82
|
m.setupMs += DEFAULT_SETUP_MS; // 셋업 = OEE 가용성 손실
|
|
83
83
|
m.lastChangeoverKey = a.gtin;
|
|
84
|
-
this.
|
|
84
|
+
this.emitEquipment(m);
|
|
85
85
|
}
|
|
86
86
|
return { commandId: cmd.commandId, accepted: true };
|
|
87
87
|
}
|
|
@@ -91,7 +91,7 @@ export class MesKernel extends FlowEngine {
|
|
|
91
91
|
onArrival(spec) {
|
|
92
92
|
if (this.mesSpec)
|
|
93
93
|
return this.onArrivalDef(spec);
|
|
94
|
-
const rawStore = this.
|
|
94
|
+
const rawStore = this.locationByType('raw-store');
|
|
95
95
|
if (!rawStore)
|
|
96
96
|
return;
|
|
97
97
|
const gtin = this.pickGtin(spec.content.skuMix);
|
|
@@ -119,7 +119,7 @@ export class MesKernel extends FlowEngine {
|
|
|
119
119
|
if (this.mesSpec)
|
|
120
120
|
return this.allocateDef(o);
|
|
121
121
|
const s0 = ROUTE[0];
|
|
122
|
-
const first = this.
|
|
122
|
+
const first = this.locationByType(s0.locationType);
|
|
123
123
|
const product = this.productOf(o.gtin);
|
|
124
124
|
if (!first || !product)
|
|
125
125
|
return;
|
|
@@ -127,7 +127,7 @@ export class MesKernel extends FlowEngine {
|
|
|
127
127
|
const picks = [];
|
|
128
128
|
for (const line of product.bom) {
|
|
129
129
|
const available = [...this.items.values()]
|
|
130
|
-
.filter(i => i.gtin === line.part.gtin && i.disposition === DISP.sellable && this.
|
|
130
|
+
.filter(i => i.gtin === line.part.gtin && i.disposition === DISP.sellable && this.locations.get(i.location)?.type === 'raw-store')
|
|
131
131
|
.map(i => ({ epc: i.epc, location: i.location, qty: 1 }));
|
|
132
132
|
const chosen = this.policy.selectStock({ gtin: line.part.gtin, qty: line.qty, available });
|
|
133
133
|
if (chosen.length < line.qty)
|
|
@@ -144,8 +144,8 @@ export class MesKernel extends FlowEngine {
|
|
|
144
144
|
}
|
|
145
145
|
/** 라우트 스테이션 태스크 발행(공통) — 제자리 가공(process), 이종 자원, 제품 전환 셋업. */
|
|
146
146
|
emitStation(o, stage, itemEpc, changeoverKey) {
|
|
147
|
-
const
|
|
148
|
-
const task = { id: `task-${++this.taskSeq}`, kind: stage.kind, status: 'created', itemEpc, fromNode:
|
|
147
|
+
const loc = this.locationByType(stage.locationType);
|
|
148
|
+
const task = { id: `task-${++this.taskSeq}`, kind: stage.kind, status: 'created', itemEpc, fromNode: loc.id, toNode: loc.id, resource: null, remainingMs: 0, durationMs: this.durationOf({ kind: stage.kind, fromNode: loc.id, toNode: loc.id, resourceKind: stage.resource }, DEFAULT_CYCLE_MS), orderId: o.id, resourceType: stage.resource, changeoverKey, setupMs: this.paramDuration(stage.kind, OP_PARAM.setupDuration) ?? DEFAULT_SETUP_MS, intent: 'process' };
|
|
149
149
|
this.tasks.set(task.id, task);
|
|
150
150
|
this.emitTask(task);
|
|
151
151
|
}
|
|
@@ -156,13 +156,13 @@ export class MesKernel extends FlowEngine {
|
|
|
156
156
|
const order = this.orders.get(t.orderId);
|
|
157
157
|
const product = this.productOf(order.gtin);
|
|
158
158
|
const i = ROUTE.findIndex(s => s.kind === t.kind);
|
|
159
|
-
const
|
|
159
|
+
const loc = this.locations.get(t.toNode);
|
|
160
160
|
const isLast = i === ROUTE.length - 1;
|
|
161
161
|
if (!isLast) {
|
|
162
162
|
// 중간 스테이션 — 입력(첫=BOM 전부 / 이후=이전 WIP) → 다음 WIP, 다음 스테이션 태스크.
|
|
163
163
|
const inputs = order.allocated.slice();
|
|
164
164
|
const wip = sgtinUri(CP, WIP_ITEMREF, ++this.wipSeq);
|
|
165
|
-
this.transform(inputs, [{ epc: wip, gtin: WIP_GTIN, qty: 1, location:
|
|
165
|
+
this.transform(inputs, [{ epc: wip, gtin: WIP_GTIN, qty: 1, location: loc.id, disposition: DISP.in_progress }], { bizStep: MES_BIZSTEP.producing, disposition: DISP.in_progress, transformationId: order.bizTransaction, readPoint: loc.id, bizTransactionList: [{ type: BTT_PRODORDER, bizTransaction: order.bizTransaction }] });
|
|
166
166
|
order.allocated = [wip];
|
|
167
167
|
const next = ROUTE[i + 1];
|
|
168
168
|
this.emitStation(order, next, wip, product.gtin);
|
|
@@ -171,13 +171,13 @@ export class MesKernel extends FlowEngine {
|
|
|
171
171
|
return;
|
|
172
172
|
}
|
|
173
173
|
// 마지막 스테이션(조립) → 완성차 (transform 1→1, disposition 으로 수율 loss → OEE 품질)
|
|
174
|
-
const fgStore = this.
|
|
174
|
+
const fgStore = this.locationByType('fg-store');
|
|
175
175
|
const wip = order.allocated[0];
|
|
176
176
|
const good = this.rng() < (this.paramNumber(t.kind, OP_PARAM.yield) ?? DEFAULT_YIELD);
|
|
177
177
|
this.recordOutput(t.resource, good); // OEE 품질(마지막 자원별 양품/불량)
|
|
178
178
|
const disp = good ? DISP.sellable : DISP.non_sellable;
|
|
179
179
|
const outputEpc = sgtinUri(CP, product.ref, ++this.prodSeq);
|
|
180
|
-
this.transform([wip], [{ epc: outputEpc, gtin: product.gtin, qty: 1, location: fgStore.id, disposition: disp }], { bizStep: MES_BIZSTEP.producing, disposition: disp, transformationId: order.bizTransaction, readPoint:
|
|
180
|
+
this.transform([wip], [{ epc: outputEpc, gtin: product.gtin, qty: 1, location: fgStore.id, disposition: disp }], { bizStep: MES_BIZSTEP.producing, disposition: disp, transformationId: order.bizTransaction, readPoint: loc.id, bizTransactionList: [{ type: BTT_PRODORDER, bizTransaction: order.bizTransaction }] });
|
|
181
181
|
this.emit(objectEvent({ eventTime: this.now(), action: 'OBSERVE', bizStep: MES_BIZSTEP.storing, disposition: disp, epcList: [outputEpc], quantityList: [{ epcClass: product.gtin, quantity: 1 }], readPoint: fgStore.id, bizLocation: fgStore.id }));
|
|
182
182
|
order.allocated = [];
|
|
183
183
|
order.fulfilled = 1;
|
|
@@ -204,7 +204,7 @@ export class MesKernel extends FlowEngine {
|
|
|
204
204
|
}
|
|
205
205
|
/** 정의 모드 수령 — skuMix gtin 이 레시피 입력 자재면 raw-store 에 생성. */
|
|
206
206
|
onArrivalDef(spec) {
|
|
207
|
-
const rawStore = this.
|
|
207
|
+
const rawStore = this.locationByType('raw-store');
|
|
208
208
|
if (!rawStore)
|
|
209
209
|
return;
|
|
210
210
|
const gtin = this.pickGtin(spec.content.skuMix);
|
|
@@ -228,14 +228,14 @@ export class MesKernel extends FlowEngine {
|
|
|
228
228
|
/** 정의 모드 할당 — 레시피 입력 BOM 전량 확보 후 첫 라우트 스텝 태스크. */
|
|
229
229
|
allocateDef(o) {
|
|
230
230
|
const ops = this.routeOps();
|
|
231
|
-
if (!ops.length || !ops[0].
|
|
231
|
+
if (!ops.length || !ops[0].locationType || !this.locationByType(ops[0].locationType))
|
|
232
232
|
return;
|
|
233
233
|
const rc = this.recipeDef();
|
|
234
234
|
const picks = [];
|
|
235
235
|
for (const line of rc.inputs) {
|
|
236
236
|
const g = this.classOf(line.material);
|
|
237
237
|
const available = [...this.items.values()]
|
|
238
|
-
.filter(i => i.gtin === g && i.disposition === DISP.sellable && this.
|
|
238
|
+
.filter(i => i.gtin === g && i.disposition === DISP.sellable && this.locations.get(i.location)?.type === 'raw-store')
|
|
239
239
|
.map(i => ({ epc: i.epc, location: i.location, qty: 1 }));
|
|
240
240
|
const chosen = this.policy.selectStock({ gtin: g, qty: line.qty, available });
|
|
241
241
|
if (chosen.length < line.qty)
|
|
@@ -251,8 +251,8 @@ export class MesKernel extends FlowEngine {
|
|
|
251
251
|
this.emitOrder(o);
|
|
252
252
|
}
|
|
253
253
|
emitStationDef(o, op, itemEpc) {
|
|
254
|
-
const
|
|
255
|
-
const task = { id: `task-${++this.taskSeq}`, kind: op.key, status: 'created', itemEpc, fromNode:
|
|
254
|
+
const loc = this.locationByType(op.locationType);
|
|
255
|
+
const task = { id: `task-${++this.taskSeq}`, kind: op.key, status: 'created', itemEpc, fromNode: loc.id, toNode: loc.id, resource: null, remainingMs: 0, durationMs: this.durationOf({ kind: op.key, fromNode: loc.id, toNode: loc.id, resourceKind: op.resourceType }, DEFAULT_CYCLE_MS), orderId: o.id, resourceType: op.resourceType, changeoverKey: o.gtin, setupMs: this.paramDuration(op.key, OP_PARAM.setupDuration) ?? DEFAULT_SETUP_MS, intent: op.intent };
|
|
256
256
|
this.tasks.set(task.id, task);
|
|
257
257
|
this.emitTask(task);
|
|
258
258
|
}
|
|
@@ -262,14 +262,14 @@ export class MesKernel extends FlowEngine {
|
|
|
262
262
|
const rc = this.recipeDef();
|
|
263
263
|
const ops = this.routeOps();
|
|
264
264
|
const i = ops.findIndex(s => s.key === t.kind);
|
|
265
|
-
const
|
|
265
|
+
const loc = this.locations.get(t.toNode);
|
|
266
266
|
const bizStep = ops[i]?.bizStep ?? MES_BIZSTEP.producing;
|
|
267
267
|
const isLast = i === ops.length - 1;
|
|
268
268
|
if (!isLast) {
|
|
269
269
|
const inputs = order.allocated.slice();
|
|
270
270
|
const wip = sgtinUri(this.mesSpec.companyPrefix, 'WIP', ++this.wipSeq);
|
|
271
271
|
const wipGtin = sgtinClass(this.mesSpec.companyPrefix, 'WIP');
|
|
272
|
-
this.transform(inputs, [{ epc: wip, gtin: wipGtin, qty: 1, location:
|
|
272
|
+
this.transform(inputs, [{ epc: wip, gtin: wipGtin, qty: 1, location: loc.id, disposition: DISP.in_progress }], { bizStep, disposition: DISP.in_progress, transformationId: order.bizTransaction, readPoint: loc.id, bizTransactionList: [{ type: BTT_PRODORDER, bizTransaction: order.bizTransaction }] });
|
|
273
273
|
order.allocated = [wip];
|
|
274
274
|
const next = ops[i + 1];
|
|
275
275
|
this.emitStationDef(order, next, wip);
|
|
@@ -277,14 +277,14 @@ export class MesKernel extends FlowEngine {
|
|
|
277
277
|
this.emitOrder(order);
|
|
278
278
|
return;
|
|
279
279
|
}
|
|
280
|
-
const fgStore = this.
|
|
280
|
+
const fgStore = this.locationByType('fg-store');
|
|
281
281
|
const wip = order.allocated[0];
|
|
282
282
|
const good = this.rng() < (this.paramNumber(t.kind, OP_PARAM.yield) ?? DEFAULT_YIELD);
|
|
283
283
|
this.recordOutput(t.resource, good);
|
|
284
284
|
const disp = good ? DISP.sellable : DISP.non_sellable;
|
|
285
285
|
const outEpc = this.serialOf(rc.outputs[0].material, ++this.prodSeq);
|
|
286
286
|
const outGtin = this.classOf(rc.outputs[0].material);
|
|
287
|
-
this.transform([wip], [{ epc: outEpc, gtin: outGtin, qty: 1, location: fgStore.id, disposition: disp }], { bizStep, disposition: disp, transformationId: order.bizTransaction, readPoint:
|
|
287
|
+
this.transform([wip], [{ epc: outEpc, gtin: outGtin, qty: 1, location: fgStore.id, disposition: disp }], { bizStep, disposition: disp, transformationId: order.bizTransaction, readPoint: loc.id, bizTransactionList: [{ type: BTT_PRODORDER, bizTransaction: order.bizTransaction }] });
|
|
288
288
|
this.emit(objectEvent({ eventTime: this.now(), action: 'OBSERVE', bizStep: MES_BIZSTEP.storing, disposition: disp, epcList: [outEpc], quantityList: [{ epcClass: outGtin, quantity: 1 }], readPoint: fgStore.id, bizLocation: fgStore.id }));
|
|
289
289
|
order.allocated = [];
|
|
290
290
|
order.fulfilled = 1;
|
package/dist/mes-profile.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ export declare const MES_BIZSTEP: {
|
|
|
8
8
|
export declare const BTT_PRODORDER = "urn:epcglobal:cbv:btt:prodorder";
|
|
9
9
|
/** 직렬 SGTIN URI (원자재 단위·완제품). */
|
|
10
10
|
export declare function sgtinUri(companyPrefix: string, itemRef: string, serial: number): string;
|
|
11
|
-
/** MES
|
|
11
|
+
/** MES 자리 타입 카탈로그 — 커널이 키로 쓰는 제조 로케이션 타입(locationByType/.type). 도메인 SSOT.
|
|
12
12
|
* 트레일러 제조 라인: 자재→프레임 절단→용접→도장→조립→완성차(kernel ROUTE 와 일치). */
|
|
13
|
-
export declare const
|
|
13
|
+
export declare const MES_LOCATION_TYPES: readonly ["raw-store", "cut-station", "weld-station", "paint-booth", "assembly-line", "fg-store"];
|
|
14
14
|
export declare const MES_TYPES: TwinTypeInfo[];
|
package/dist/mes-profile.js
CHANGED
|
@@ -10,15 +10,15 @@ export const BTT_PRODORDER = 'urn:epcglobal:cbv:btt:prodorder';
|
|
|
10
10
|
export function sgtinUri(companyPrefix, itemRef, serial) {
|
|
11
11
|
return `urn:epc:id:sgtin:${companyPrefix}.${itemRef}.${serial}`;
|
|
12
12
|
}
|
|
13
|
-
/** MES
|
|
13
|
+
/** MES 자리 타입 카탈로그 — 커널이 키로 쓰는 제조 로케이션 타입(locationByType/.type). 도메인 SSOT.
|
|
14
14
|
* 트레일러 제조 라인: 자재→프레임 절단→용접→도장→조립→완성차(kernel ROUTE 와 일치). */
|
|
15
|
-
export const
|
|
15
|
+
export const MES_LOCATION_TYPES = ['raw-store', 'cut-station', 'weld-station', 'paint-booth', 'assembly-line', 'fg-store'];
|
|
16
16
|
/*
|
|
17
|
-
* 트윈 타입 서술(ADR-0018 확장) —
|
|
18
|
-
* MES=ISA-95 앵커: 스테이션=WorkCenter, 저장소=bizLocation.
|
|
17
|
+
* 트윈 타입 서술(ADR-0018 확장) — 자리 키는 MES_LOCATION_TYPES 단일 출처에서 파생 + 설비(자원) 타입 추가.
|
|
18
|
+
* MES=ISA-95 앵커: 스테이션=WorkCenter, 저장소=bizLocation. 설비(가공설비)=Equipment/자산(GIAI). key 는 flow resourceType 과 일치.
|
|
19
19
|
*/
|
|
20
|
-
// label 은 언어 중립 i18n 키(twin.type.<key>) — cls(표준 클래스)만
|
|
21
|
-
const
|
|
20
|
+
// label 은 언어 중립 i18n 키(twin.type.<key>) — cls(표준 클래스)만 자리별 메타로 유지. 사람 언어는 표현계층(L2).
|
|
21
|
+
const MES_LOCATION_CLS = {
|
|
22
22
|
'raw-store': { epcis: 'bizLocation' },
|
|
23
23
|
'cut-station': { isa95: 'WorkCenter', epcis: 'bizLocation' },
|
|
24
24
|
'weld-station': { isa95: 'WorkCenter', epcis: 'bizLocation' },
|
|
@@ -27,9 +27,9 @@ const MES_NODE_CLS = {
|
|
|
27
27
|
'fg-store': { epcis: 'bizLocation' }
|
|
28
28
|
};
|
|
29
29
|
export const MES_TYPES = [
|
|
30
|
-
...
|
|
31
|
-
{ key: 'cutter', role: '
|
|
32
|
-
{ key: 'welder', role: '
|
|
33
|
-
{ key: 'painter', role: '
|
|
34
|
-
{ key: 'assembler', role: '
|
|
30
|
+
...MES_LOCATION_TYPES.map((k) => ({ key: k, role: 'location', label: `twin.type.${k}`, standardClass: MES_LOCATION_CLS[k] ?? {}, identity: { scheme: 'gs1:SGLN' }, capabilities: ['storable'] })),
|
|
31
|
+
{ key: 'cutter', role: 'equipment', label: 'twin.type.cutter', standardClass: { isa95: 'Equipment', iso55000: 'Asset' }, identity: { scheme: 'gs1:GIAI' }, capabilities: ['processable', 'operable'] },
|
|
32
|
+
{ key: 'welder', role: 'equipment', label: 'twin.type.welder', standardClass: { isa95: 'Equipment', iso55000: 'Asset' }, identity: { scheme: 'gs1:GIAI' }, capabilities: ['processable', 'operable'] },
|
|
33
|
+
{ key: 'painter', role: 'equipment', label: 'twin.type.painter', standardClass: { isa95: 'Equipment', iso55000: 'Asset' }, identity: { scheme: 'gs1:GIAI' }, capabilities: ['processable', 'operable'] },
|
|
34
|
+
{ key: 'assembler', role: 'equipment', label: 'twin.type.assembler', standardClass: { isa95: 'Equipment', iso55000: 'Asset' }, identity: { scheme: 'gs1:GIAI' }, capabilities: ['processable', 'operable'] }
|
|
35
35
|
];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AssetState, BoardDef, CanonicalEnvelope,
|
|
1
|
+
import type { AssetState, BoardDef, CanonicalEnvelope, LocationState, ItemState, EquipmentState, PersonState, TaskState, OrderState } from './contract.ts';
|
|
2
2
|
/**
|
|
3
3
|
* 마스터 동기 — 선언적 로케이션 upsert/remove.
|
|
4
4
|
*
|
|
@@ -8,7 +8,7 @@ import type { AssetState, BoardDef, CanonicalEnvelope, NodeState, ItemState, Mov
|
|
|
8
8
|
*/
|
|
9
9
|
export interface MasterUpdate {
|
|
10
10
|
op: 'upsert' | 'remove';
|
|
11
|
-
|
|
11
|
+
location: {
|
|
12
12
|
id: string;
|
|
13
13
|
type?: string;
|
|
14
14
|
capacity?: number;
|
|
@@ -28,14 +28,14 @@ export interface ProjectedState {
|
|
|
28
28
|
correctiveEventIDs: string[];
|
|
29
29
|
eventID?: string;
|
|
30
30
|
}[];
|
|
31
|
-
|
|
31
|
+
locations: LocationState[];
|
|
32
32
|
items: ItemState[];
|
|
33
33
|
/** 사람 — 등급·교대·투입. 인원을 선언하지 않은 트윈에서는 빈 배열. */
|
|
34
34
|
persons: PersonState[];
|
|
35
35
|
/** 물리 자산(반복사용) — 자산을 선언하지 않은 트윈에서는 빈 배열. */
|
|
36
36
|
assets: AssetState[];
|
|
37
37
|
tasks: TaskState[];
|
|
38
|
-
|
|
38
|
+
equipment: EquipmentState[];
|
|
39
39
|
orders: OrderState[];
|
|
40
40
|
}
|
|
41
41
|
export declare class ObservedReducer {
|
|
@@ -46,7 +46,7 @@ export declare class ObservedReducer {
|
|
|
46
46
|
/** 아직 관측되지 않은 자식의 담김 — 물품을 지어내지 않고 보류했다가 등장할 때 붙인다. */
|
|
47
47
|
private pendingParent;
|
|
48
48
|
private tasks;
|
|
49
|
-
private
|
|
49
|
+
private equipment;
|
|
50
50
|
private persons;
|
|
51
51
|
private assets;
|
|
52
52
|
private orders;
|
|
@@ -59,7 +59,7 @@ export declare class ObservedReducer {
|
|
|
59
59
|
/**
|
|
60
60
|
* 관측된 로케이션을 구조로 승격 — **이벤트가 가르쳐 준 것을 구조에서 지우지 않는다.**
|
|
61
61
|
*
|
|
62
|
-
* 마스터에 없는 로케이션에서 물품이 관측되면, 예전에는 물품의 `location` 에만 남고 `
|
|
62
|
+
* 마스터에 없는 로케이션에서 물품이 관측되면, 예전에는 물품의 `location` 에만 남고 `locations` 에는
|
|
63
63
|
* 나타나지 않았다. 그 결과 그 자리는 스키매틱에 없고, 점유가 집계되지 않고, 병목 주목이 뜰 수
|
|
64
64
|
* 없었다 — **사실은 들어왔는데 구조가 모르는 상태.** 이제 최소 형태로 승격한다:
|
|
65
65
|
* 종류는 모르므로 `unknown`, **용량은 비워 둔다**(발명하지 않는다), 출처는 `observed`.
|
|
@@ -98,6 +98,6 @@ export declare class ObservedReducer {
|
|
|
98
98
|
private mergeItem;
|
|
99
99
|
/** 이탈(DELETE) — 아이템 + 조립 자식(재귀) 제거. 화물 SSCC DELETE 시 팔레트도 함께 이탈. */
|
|
100
100
|
private remove;
|
|
101
|
-
/** 현재 투영 State —
|
|
101
|
+
/** 현재 투영 State — 자리 점유는 아이템 위치 집계로 유도(pure projection). */
|
|
102
102
|
snapshot(): ProjectedState;
|
|
103
103
|
}
|
package/dist/observed-reducer.js
CHANGED
|
@@ -16,10 +16,10 @@
|
|
|
16
16
|
*
|
|
17
17
|
* 두 갈래 이벤트를 함께 접는다:
|
|
18
18
|
* - EPCIS(epcis.*) → 재고/위치/조립 (What/Where)
|
|
19
|
-
* - 운영 델타(task/equipment/order.status) → tasks·
|
|
19
|
+
* - 운영 델타(task/equipment/order.status) → tasks·equipment·orders (EPCIS 로 재구성 불가한 절반)
|
|
20
20
|
* 마스터(로케이션)는 board 초기화 + applyMaster 로 갱신(마스터 동기).
|
|
21
21
|
*/
|
|
22
|
-
import { OP_EVENT,
|
|
22
|
+
import { OP_EVENT, locationStatusOf, readBoardEquipment, readBoardLocations, readBoardAssets } from "./contract.js";
|
|
23
23
|
import { ILMD_ATTR, parseEpc } from "./epcis.js";
|
|
24
24
|
/**
|
|
25
25
|
* 투영이 들고 있는 물품 — **계약(ItemState)을 축소하지 않는다.**
|
|
@@ -39,7 +39,7 @@ export class ObservedReducer {
|
|
|
39
39
|
/** 아직 관측되지 않은 자식의 담김 — 물품을 지어내지 않고 보류했다가 등장할 때 붙인다. */
|
|
40
40
|
pendingParent = new Map(); // 자식 EPC → 부모(물류단위)
|
|
41
41
|
tasks = new Map();
|
|
42
|
-
|
|
42
|
+
equipment = new Map();
|
|
43
43
|
persons = new Map();
|
|
44
44
|
assets = new Map();
|
|
45
45
|
orders = new Map();
|
|
@@ -47,36 +47,36 @@ export class ObservedReducer {
|
|
|
47
47
|
/** 받은 정정 선언 — 상태에 반영하지 않되 **버리지도 않는다**(소비처가 볼 수 있게). */
|
|
48
48
|
corrections = [];
|
|
49
49
|
constructor(board) {
|
|
50
|
-
for (const n of board
|
|
50
|
+
for (const n of readBoardLocations(board))
|
|
51
51
|
this.master.set(n.id, { id: n.id, type: n.type, capacity: n.capacity, parallelism: n.parallelism, parentId: n.parentId, origin: 'master' });
|
|
52
|
-
//
|
|
53
|
-
for (const m of board
|
|
54
|
-
this.
|
|
52
|
+
// 설비 기준선(마스터) — equipment.status 델타로 갱신됨.
|
|
53
|
+
for (const m of readBoardEquipment(board))
|
|
54
|
+
this.equipment.set(m.id, { id: m.id, kind: m.kind, status: 'idle', location: m.homeLocation, homeLocation: m.homeLocation, ...(m.properties ? { properties: m.properties } : {}), ...(m.testSpecificationIds ? { testSpecificationIds: m.testSpecificationIds } : {}), origin: 'master' });
|
|
55
55
|
// 사람 기준선(마스터) — person.status 델타로 갱신됨.
|
|
56
56
|
for (const p of board.persons ?? [])
|
|
57
|
-
this.persons.set(p.id, { id: p.id,
|
|
58
|
-
for (const a of board
|
|
59
|
-
this.assets.set(a.id, { id: a.id,
|
|
57
|
+
this.persons.set(p.id, { id: p.id, personnelClassIds: p.personnelClassIds, status: 'idle', ...(p.homeLocation ? { location: p.homeLocation } : {}), ...(p.properties ? { properties: p.properties } : {}), ...(p.testSpecificationIds ? { testSpecificationIds: p.testSpecificationIds } : {}) });
|
|
58
|
+
for (const a of readBoardAssets(board))
|
|
59
|
+
this.assets.set(a.id, { id: a.id, assetClassIds: a.assetClassIds, location: a.homeLocation, status: 'idle', ...(a.properties ? { properties: a.properties } : {}), ...(a.testSpecificationIds ? { testSpecificationIds: a.testSpecificationIds } : {}) });
|
|
60
60
|
}
|
|
61
61
|
/** 마스터 동기 — 로케이션 추가/변경/제거. */
|
|
62
62
|
applyMaster(u) {
|
|
63
63
|
if (u.op === 'remove') {
|
|
64
|
-
this.master.delete(u.
|
|
64
|
+
this.master.delete(u.location.id);
|
|
65
65
|
return;
|
|
66
66
|
}
|
|
67
|
-
const cur = this.master.get(u.
|
|
67
|
+
const cur = this.master.get(u.location.id);
|
|
68
68
|
/* **모르는 용량을 0 으로 뭉개지 않는다.** 0 은 "자리가 없다" 는 사실 주장이고, 미지정은 "모른다" 다.
|
|
69
|
-
* 계약(`
|
|
70
|
-
* 성립하고 배정 정책이 그
|
|
69
|
+
* 계약(`LocationState.capacity`)이 선택 필드로 둔 이유가 이것이며, 0 으로 채우면 포화 판정이 거짓으로
|
|
70
|
+
* 성립하고 배정 정책이 그 자리를 영구히 배제한다. 그리고 upsert 가 **구역 소속(parentId)을 지우지
|
|
71
71
|
* 않는다** — 마스터가 말하지 않은 것은 기존 값을 지키는 것이 upsert 의 뜻이다. */
|
|
72
|
-
const capacity = u.
|
|
73
|
-
const parallelism = u.
|
|
74
|
-
this.master.set(u.
|
|
75
|
-
id: u.
|
|
76
|
-
type: u.
|
|
72
|
+
const capacity = u.location.capacity ?? cur?.capacity;
|
|
73
|
+
const parallelism = u.location.parallelism ?? cur?.parallelism;
|
|
74
|
+
this.master.set(u.location.id, {
|
|
75
|
+
id: u.location.id,
|
|
76
|
+
type: u.location.type ?? cur?.type ?? UNKNOWN_TYPE,
|
|
77
77
|
...(capacity === undefined ? {} : { capacity }),
|
|
78
78
|
...(parallelism === undefined ? {} : { parallelism }),
|
|
79
|
-
...(u.
|
|
79
|
+
...(u.location.parentId ?? cur?.parentId ? { parentId: u.location.parentId ?? cur?.parentId } : {}),
|
|
80
80
|
/* 마스터가 말한 것은 마스터 출처다 — 관측으로 알게 된 것(origin='observed')을 덮어 승격한다. */
|
|
81
81
|
origin: 'master'
|
|
82
82
|
});
|
|
@@ -84,7 +84,7 @@ export class ObservedReducer {
|
|
|
84
84
|
/**
|
|
85
85
|
* 관측된 로케이션을 구조로 승격 — **이벤트가 가르쳐 준 것을 구조에서 지우지 않는다.**
|
|
86
86
|
*
|
|
87
|
-
* 마스터에 없는 로케이션에서 물품이 관측되면, 예전에는 물품의 `location` 에만 남고 `
|
|
87
|
+
* 마스터에 없는 로케이션에서 물품이 관측되면, 예전에는 물품의 `location` 에만 남고 `locations` 에는
|
|
88
88
|
* 나타나지 않았다. 그 결과 그 자리는 스키매틱에 없고, 점유가 집계되지 않고, 병목 주목이 뜰 수
|
|
89
89
|
* 없었다 — **사실은 들어왔는데 구조가 모르는 상태.** 이제 최소 형태로 승격한다:
|
|
90
90
|
* 종류는 모르므로 `unknown`, **용량은 비워 둔다**(발명하지 않는다), 출처는 `observed`.
|
|
@@ -155,13 +155,17 @@ export class ObservedReducer {
|
|
|
155
155
|
}
|
|
156
156
|
case OP_EVENT.equipment: {
|
|
157
157
|
const d = e.data;
|
|
158
|
-
if (this.stale(`
|
|
158
|
+
if (this.stale(`eq:${d.moverId}`, e))
|
|
159
159
|
return;
|
|
160
160
|
/* 마스터에 없던 자원도 관측으로 자란다(예전부터 그랬다) — 이제 그 사실을 출처로 표시하고,
|
|
161
161
|
* 자원이 있다고 말하는 자리도 구조로 승격한다(로케이션만 자라지 않던 비대칭 해소). */
|
|
162
162
|
this.touchLocation(d.location);
|
|
163
|
-
const known = this.
|
|
164
|
-
|
|
163
|
+
const known = this.equipment.get(d.moverId);
|
|
164
|
+
/* 소속(homeLocation)은 델타가 말해 주면 그 값, 안 말해 주면 **이미 아는 값을 지킨다** —
|
|
165
|
+
마스터로 알던 소속을 관측 델타 하나가 지워 버리면 그 설비가 롤업에서 통째로 빠진다.
|
|
166
|
+
소속은 자주 바뀌는 사실이 아니므로 침묵을 "소속 없음" 으로 읽지 않는다. */
|
|
167
|
+
const homeLocation = d.homeLocation ?? known?.homeLocation;
|
|
168
|
+
this.equipment.set(d.moverId, { id: d.moverId, kind: d.kind, status: d.status, location: d.location, ...(homeLocation ? { homeLocation } : {}), taskId: d.taskId, motion: d.motion, origin: known?.origin ?? 'observed' });
|
|
165
169
|
break;
|
|
166
170
|
}
|
|
167
171
|
case OP_EVENT.person: {
|
|
@@ -169,11 +173,17 @@ export class ObservedReducer {
|
|
|
169
173
|
if (this.stale(`person:${d.personId}`, e))
|
|
170
174
|
return;
|
|
171
175
|
/* 사람도 관측으로 자란다(자원과 같은 정책) — 마스터에 없던 사람이 이벤트에 나오면 승격한다. */
|
|
176
|
+
const known = this.persons.get(d.personId);
|
|
177
|
+
/* 마스터로 알던 사실(등급·위치·속성)은 델타가 말하지 않으면 **지키지 않고 지우면** 안 된다 —
|
|
178
|
+
설비 소속과 같은 규율이다(침묵을 "없어졌다" 로 읽지 않는다). */
|
|
172
179
|
this.persons.set(d.personId, {
|
|
173
180
|
id: d.personId,
|
|
174
|
-
|
|
181
|
+
personnelClassIds: d.personnelClassIds ?? known?.personnelClassIds,
|
|
175
182
|
status: d.status,
|
|
176
183
|
taskId: d.taskId,
|
|
184
|
+
...((d.location ?? known?.location) ? { location: d.location ?? known?.location } : {}),
|
|
185
|
+
...(known?.properties ? { properties: known.properties } : {}),
|
|
186
|
+
...(known?.testSpecificationIds ? { testSpecificationIds: known.testSpecificationIds } : {}),
|
|
177
187
|
...(d.offShift ? { offShift: true } : {})
|
|
178
188
|
});
|
|
179
189
|
break;
|
|
@@ -185,7 +195,7 @@ export class ObservedReducer {
|
|
|
185
195
|
const cur = this.assets.get(d.assetId);
|
|
186
196
|
this.assets.set(d.assetId, {
|
|
187
197
|
id: d.assetId,
|
|
188
|
-
|
|
198
|
+
assetClassIds: d.assetClassIds ?? cur?.assetClassIds,
|
|
189
199
|
location: d.location ?? cur?.location,
|
|
190
200
|
status: d.status,
|
|
191
201
|
taskId: d.taskId,
|
|
@@ -351,7 +361,7 @@ export class ObservedReducer {
|
|
|
351
361
|
this.remove(c);
|
|
352
362
|
}
|
|
353
363
|
}
|
|
354
|
-
/** 현재 투영 State —
|
|
364
|
+
/** 현재 투영 State — 자리 점유는 아이템 위치 집계로 유도(pure projection). */
|
|
355
365
|
snapshot() {
|
|
356
366
|
const occ = new Map();
|
|
357
367
|
for (const it of this.items.values())
|
|
@@ -359,9 +369,9 @@ export class ObservedReducer {
|
|
|
359
369
|
return {
|
|
360
370
|
revision: this.revision,
|
|
361
371
|
...(this.corrections.length ? { corrections: this.corrections.map(c => ({ ...c })) } : {}),
|
|
362
|
-
|
|
372
|
+
locations: [...this.master.values()].map(n => {
|
|
363
373
|
const occupancy = occ.get(n.id) ?? 0;
|
|
364
|
-
const status =
|
|
374
|
+
const status = locationStatusOf({ occupancy, capacity: n.capacity });
|
|
365
375
|
return {
|
|
366
376
|
id: n.id, type: n.type, occupancy,
|
|
367
377
|
...(status ? { status } : {}),
|
|
@@ -377,7 +387,7 @@ export class ObservedReducer {
|
|
|
377
387
|
persons: [...this.persons.values()].map(p => ({ ...p })),
|
|
378
388
|
assets: [...this.assets.values()].map(a => ({ ...a })),
|
|
379
389
|
tasks: [...this.tasks.values()].map(t => ({ ...t })),
|
|
380
|
-
|
|
390
|
+
equipment: [...this.equipment.values()].map(m => ({ ...m })),
|
|
381
391
|
orders: [...this.orders.values()].map(o => ({ ...o }))
|
|
382
392
|
};
|
|
383
393
|
}
|
package/dist/task-fold.d.ts
CHANGED
|
@@ -15,14 +15,14 @@ export interface TaskDeltaRow {
|
|
|
15
15
|
}
|
|
16
16
|
/** 작업에 붙어 있는 축의 값 — 어느 전이에서 왔든 모은다. */
|
|
17
17
|
export interface TaskFacets {
|
|
18
|
-
/** 수행 자원(
|
|
18
|
+
/** 수행 자원(설비). 설계상 자원을 쓰지 않는 공정(체류)에서는 없는 것이 정상이다. */
|
|
19
19
|
resource?: string;
|
|
20
20
|
/** 작업 종류(커널 어휘 — 사람이 읽는 라벨은 표현 계층의 몫). */
|
|
21
21
|
kind?: string;
|
|
22
22
|
/** 소속 오더. **없으면 없는 것이다** — 다른 식별자로 대체하지 않는다. */
|
|
23
23
|
order?: string;
|
|
24
24
|
/** 가장 최근 도착 지점. 진행에 따라 바뀌므로 마지막 것이 사실이다. */
|
|
25
|
-
|
|
25
|
+
location?: string;
|
|
26
26
|
}
|
|
27
27
|
/** 작업 하나의 이정표 — 소비처가 이것으로 구간·지표를 만든다. */
|
|
28
28
|
export interface TaskRecord {
|
package/dist/task-fold.js
CHANGED
|
@@ -38,7 +38,7 @@ export function foldTaskRecords(rows) {
|
|
|
38
38
|
rec = { taskId: id, facets: {} };
|
|
39
39
|
byTask.set(id, rec);
|
|
40
40
|
}
|
|
41
|
-
/* 축의 값 — 처음 본 것을 남긴다(작업의 종류·오더·자원은 바뀌지 않는다).
|
|
41
|
+
/* 축의 값 — 처음 본 것을 남긴다(작업의 종류·오더·자원은 바뀌지 않는다). 자리는 예외로 마지막 것. */
|
|
42
42
|
const f = rec.facets;
|
|
43
43
|
if (f.resource === undefined && d.resourceRef)
|
|
44
44
|
f.resource = d.resourceRef;
|
|
@@ -46,9 +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
|
-
const
|
|
50
|
-
if (
|
|
51
|
-
f.
|
|
49
|
+
const loc = d.toNode ?? d.fromNode;
|
|
50
|
+
if (loc)
|
|
51
|
+
f.location = loc;
|
|
52
52
|
if (d.status === 'created')
|
|
53
53
|
rec.createdMs = at;
|
|
54
54
|
else if (d.status === 'in-progress')
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** 금지 어휘 — 표준 어휘로 대체된 옛 낱말. 파생 식별자까지 잡도록 부분 일치로 본다. */
|
|
2
|
+
export declare const RETIRED_VOCABULARY: readonly ["mover", "Mover", "MOVER", "node", "Node", "NODE"];
|
|
3
|
+
/**
|
|
4
|
+
* 예외 — 남아 있는 것이 **정당한** 토큰. 각 항목에 이유가 붙는다.
|
|
5
|
+
*
|
|
6
|
+
* 판정은 "이 토큰을 정확히 포함하는가" 다(부분 문자열). 그래서 `moverId` 를 허용하면 `moverIds` 도
|
|
7
|
+
* 통과한다 — 와이어 필드에서 파생한 변수까지 허용하려는 의도다.
|
|
8
|
+
*/
|
|
9
|
+
export declare const VOCABULARY_EXCEPTIONS: {
|
|
10
|
+
token: string;
|
|
11
|
+
why: string;
|
|
12
|
+
}[];
|
|
13
|
+
/**
|
|
14
|
+
* 한 줄에서 금지 어휘 위반을 찾는다 — 예외에 걸리는 부분은 먼저 지워 놓고 본다.
|
|
15
|
+
*
|
|
16
|
+
* 반환은 위반 낱말 목록(중복 제거 전). 비어 있으면 그 줄은 통과.
|
|
17
|
+
*/
|
|
18
|
+
export declare const GUARD_PRAGMA = "vocabulary-guard: allow";
|
|
19
|
+
export declare function retiredVocabularyIn(line: string): string[];
|