@operato/twin-kernel 0.6.14 → 0.7.1
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/capability.d.ts +29 -1
- package/dist/capability.js +23 -1
- package/dist/capacity.d.ts +5 -5
- package/dist/capacity.js +7 -7
- package/dist/contract.d.ts +176 -32
- package/dist/contract.js +66 -7
- package/dist/counterfactual.d.ts +1 -1
- package/dist/counterfactual.js +2 -2
- package/dist/domain-catalog.d.ts +83 -17
- package/dist/domain-catalog.js +99 -14
- package/dist/domain-definition.d.ts +17 -1
- package/dist/ems-kernel.d.ts +83 -0
- package/dist/ems-kernel.js +307 -0
- package/dist/ems-profile.d.ts +16 -0
- package/dist/ems-profile.js +133 -0
- package/dist/energy-attribution.d.ts +208 -0
- package/dist/energy-attribution.js +229 -0
- package/dist/energy-ingest.d.ts +39 -0
- package/dist/energy-ingest.js +91 -0
- package/dist/epcis.d.ts +3 -3
- package/dist/epcis.js +2 -2
- package/dist/event-journal.d.ts +2 -2
- package/dist/event-journal.js +1 -1
- package/dist/flow-engine.d.ts +20 -20
- package/dist/flow-engine.js +40 -40
- package/dist/forecast.js +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.js +5 -0
- package/dist/kernel.d.ts +3 -3
- package/dist/kernel.js +5 -5
- package/dist/mes-kernel.d.ts +2 -2
- package/dist/mes-kernel.js +2 -2
- package/dist/mes-profile.js +25 -1
- package/dist/observed-reducer.d.ts +25 -4
- package/dist/observed-reducer.js +30 -9
- package/dist/operations-capability.d.ts +8 -8
- package/dist/operations-capability.js +2 -2
- package/dist/task-fold.d.ts +1 -1
- package/dist/task-fold.js +1 -1
- package/dist/twin-observer.js +1 -1
- package/dist/wms-profile.d.ts +0 -11
- package/dist/wms-profile.js +17 -2
- package/dist/yms-profile.js +16 -1
- package/dist-cjs/index.cjs +814 -56
- package/package.json +1 -1
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 에너지 귀속 — **이 kWh 는 누구의 것인가.** 설계: `design/profiles/ems.md` §8·§10(7단계)
|
|
3
|
+
*
|
|
4
|
+
* ── 왜 규칙이 따로 필요한가 ─────────────────────────────────────────────────
|
|
5
|
+
* 에너지의 값은 대개 「이 라인이 얼마 썼나」·「이 오더 한 대에 몇 kWh 들었나」로 쓰인다. 그런데 현장의
|
|
6
|
+
* **계량 알갱이는 대개 소비처보다 굵다** — 구역 계량기 하나가 공조·조명·컨베이어를 함께 잰다
|
|
7
|
+
* (ISO 50001 의 SEU 가 그 알갱이다). 그래서 개체별 kWh 는 **측정이 아니라 배분**인 경우가 많다.
|
|
8
|
+
*
|
|
9
|
+
* 그 사실을 값에 실어 보내지 않으면, 배분한 숫자가 측정한 숫자와 같은 무게로 읽힌다 — 그것이 CBAM·
|
|
10
|
+
* ESG 보고에서 첫 질문에 깨지는 지점이다. 그래서 이 모듈이 내는 모든 몫은 **근거(basis)를 함께** 갖는다.
|
|
11
|
+
*
|
|
12
|
+
* ── 세 가지 근거 ────────────────────────────────────────────────────────────
|
|
13
|
+
* · `measured` — 그 소비처에 **전용 계량기**가 있다(1:1). 가장 강하다.
|
|
14
|
+
* · `apportioned` — 알갱이가 굵어 **나눈 것**이다. 무엇으로 나눴는지(`weightKind`)를 함께 낸다.
|
|
15
|
+
* · `unattributed` — 나눌 근거가 없다. **그대로 남긴다.**
|
|
16
|
+
*
|
|
17
|
+
* ── 균등 분배를 기본값으로 두지 않는다 ──────────────────────────────────────
|
|
18
|
+
* 몫이 없을 때 인원수로 나누듯 균등하게 나누면 숫자는 나오지만 그것은 **짐작**이다. 기본값이 되면
|
|
19
|
+
* 아무도 그것이 짐작인 줄 모른다. 그래서 균등(`equal`)은 **호출자가 명시적으로 고를 때만** 쓴다.
|
|
20
|
+
*
|
|
21
|
+
* ── 합은 보존된다 ───────────────────────────────────────────────────────────
|
|
22
|
+
* 배분한 합 + 미귀속 합 = 측정한 합. 이 불변식이 깨지면 어딘가에서 전기가 사라지거나 생겨난 것이고,
|
|
23
|
+
* 그 위의 모든 원단위가 거짓이 된다. 시험이 그것을 지킨다.
|
|
24
|
+
*/
|
|
25
|
+
/** 부동소수 비교 — kWh 는 소수점이 있고, 합 보존을 정수로 요구하면 거짓 실패가 난다. */
|
|
26
|
+
const near = (a, b, eps = 1e-9) => Math.abs(a - b) <= eps;
|
|
27
|
+
/**
|
|
28
|
+
* 구간 에너지를 소비처에 귀속시킨다 — **나눌 수 없으면 나누지 않는다.**
|
|
29
|
+
*
|
|
30
|
+
* @param weightKind 배분에 쓸 몫의 뜻. **`equal` 은 호출자가 명시적으로 고를 때만** 쓰인다(위 주석).
|
|
31
|
+
*/
|
|
32
|
+
export function attributeEnergy(opts) {
|
|
33
|
+
const byId = new Map(opts.consumers.map(c => [c.id, c]));
|
|
34
|
+
/* 전용 계량기 색인 — 소비처가 「이 계량기는 내 것」이라고 선언한 경우. */
|
|
35
|
+
const dedicated = new Map();
|
|
36
|
+
for (const c of opts.consumers)
|
|
37
|
+
if (c.meterId)
|
|
38
|
+
dedicated.set(c.meterId, c);
|
|
39
|
+
const shares = [];
|
|
40
|
+
const unattributed = [];
|
|
41
|
+
const excluded = [];
|
|
42
|
+
const overhead = [];
|
|
43
|
+
for (const pool of opts.pools) {
|
|
44
|
+
const kWh = Number(pool.kWh);
|
|
45
|
+
if (!Number.isFinite(kWh))
|
|
46
|
+
continue; // 값이 아닌 것은 귀속의 대상이 아니다(0 으로 만들지 않는다)
|
|
47
|
+
/* ① 전용 계량기 — 통째로 그 소비처의 것이다. 가장 강한 근거이므로 먼저 본다. */
|
|
48
|
+
const own = dedicated.get(pool.meterId);
|
|
49
|
+
if (own) {
|
|
50
|
+
shares.push({ consumerId: own.id, kWh, basis: 'measured', poolMeterId: pool.meterId });
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
/*
|
|
54
|
+
* ①-b **공통(간접) 풀** — 직접 귀속되지 않는 것이 정상이다.
|
|
55
|
+
*
|
|
56
|
+
* 배부를 요청하지 않았으면 공통 바구니에 그대로 둔다(결손 칸에 담지 않는다 — 정상을 결함으로
|
|
57
|
+
* 보이게 만들지 않는다). 요청했으면 지목된 공정 소비처들에 나누고 `overhead: true` 를 붙인다.
|
|
58
|
+
*/
|
|
59
|
+
if (pool.overhead) {
|
|
60
|
+
const alloc = opts.overheadAllocation;
|
|
61
|
+
const targets = (alloc?.processConsumerIds ?? []).map(id => byId.get(id)).filter((c) => !!c);
|
|
62
|
+
const wOf = (c) => alloc?.weightKind === 'equal' ? 1 : Number.isFinite(Number(c.weight)) && Number(c.weight) > 0 ? Number(c.weight) : 0;
|
|
63
|
+
const sharing = alloc?.weightKind === 'equal' ? targets : targets.filter(c => wOf(c) > 0);
|
|
64
|
+
const ws = sharing.map(wOf);
|
|
65
|
+
const total = ws.reduce((a, b) => a + b, 0);
|
|
66
|
+
if (!alloc || total <= 0) {
|
|
67
|
+
overhead.push({ poolMeterId: pool.meterId, kWh });
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
let done = 0;
|
|
71
|
+
sharing.forEach((c, i) => {
|
|
72
|
+
const share = ws[i] / total;
|
|
73
|
+
const amount = i === sharing.length - 1 ? kWh - done : kWh * share;
|
|
74
|
+
done += amount;
|
|
75
|
+
shares.push({
|
|
76
|
+
consumerId: c.id,
|
|
77
|
+
kWh: amount,
|
|
78
|
+
basis: 'apportioned',
|
|
79
|
+
poolMeterId: pool.meterId,
|
|
80
|
+
weightKind: alloc.weightKind,
|
|
81
|
+
weightShare: share,
|
|
82
|
+
overhead: true
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
/* ② 덮는 소비처가 선언되지 않았다 — 나눌 대상이 없다. */
|
|
88
|
+
const covered = (pool.consumerIds ?? []).map(id => byId.get(id)).filter((c) => !!c);
|
|
89
|
+
if (!covered.length) {
|
|
90
|
+
unattributed.push({ poolMeterId: pool.meterId, kWh, reason: 'no-consumers' });
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
/* ③ 하나뿐이면 그것도 측정이다 — 그 계량기가 그 소비처만 덮는다는 선언이므로. */
|
|
94
|
+
if (covered.length === 1) {
|
|
95
|
+
shares.push({ consumerId: covered[0].id, kWh, basis: 'measured', poolMeterId: pool.meterId });
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
/* ④ 여럿이면 몫이 필요하다. `equal` 은 호출자가 고른 경우에만 몫을 만든다. */
|
|
99
|
+
const kind = opts.weightKind;
|
|
100
|
+
const weightOf = (c) => kind === 'equal' ? 1 : Number.isFinite(Number(c.weight)) && Number(c.weight) > 0 ? Number(c.weight) : 0;
|
|
101
|
+
/*
|
|
102
|
+
* 몫이 0 인 소비처는 **배분에서 뺀다**(그리고 뺐다는 사실을 낸다) — 0 을 주면 「재어 보니 0」이라는
|
|
103
|
+
* 주장이 되고, 대기전력이 있는 설비에서 그것은 거짓이다.
|
|
104
|
+
*/
|
|
105
|
+
const sharing = kind === 'equal' ? covered : covered.filter(c => weightOf(c) > 0);
|
|
106
|
+
if (kind && sharing.length < covered.length) {
|
|
107
|
+
for (const c of covered)
|
|
108
|
+
if (weightOf(c) <= 0)
|
|
109
|
+
excluded.push({ consumerId: c.id, poolMeterId: pool.meterId, reason: 'zero-weight' });
|
|
110
|
+
}
|
|
111
|
+
const weights = sharing.map(weightOf);
|
|
112
|
+
const sum = weights.reduce((a, b) => a + b, 0);
|
|
113
|
+
if (!kind || sum <= 0) {
|
|
114
|
+
unattributed.push({
|
|
115
|
+
poolMeterId: pool.meterId,
|
|
116
|
+
kWh,
|
|
117
|
+
reason: 'no-weights',
|
|
118
|
+
consumerIds: covered.map(c => c.id)
|
|
119
|
+
});
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
/*
|
|
123
|
+
* 비례 배분 — 마지막 소비처가 **나머지를 받는다**(합 보존).
|
|
124
|
+
*
|
|
125
|
+
* 각자 반올림하면 합이 원값과 어긋나고, 그 차이는 원단위·보고로 전파된다. 반올림은 표현의 일이고
|
|
126
|
+
* 여기서는 하지 않는다.
|
|
127
|
+
*/
|
|
128
|
+
let given = 0;
|
|
129
|
+
sharing.forEach((c, i) => {
|
|
130
|
+
const share = weights[i] / sum;
|
|
131
|
+
const amount = i === sharing.length - 1 ? kWh - given : kWh * share;
|
|
132
|
+
given += amount;
|
|
133
|
+
shares.push({
|
|
134
|
+
consumerId: c.id,
|
|
135
|
+
kWh: amount,
|
|
136
|
+
basis: 'apportioned',
|
|
137
|
+
poolMeterId: pool.meterId,
|
|
138
|
+
weightKind: kind,
|
|
139
|
+
weightShare: share
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
const measuredKWh = opts.pools.reduce((a, p) => a + (Number.isFinite(Number(p.kWh)) ? Number(p.kWh) : 0), 0);
|
|
144
|
+
const attributedKWh = shares.reduce((a, s) => a + s.kWh, 0);
|
|
145
|
+
const unattributedKWh = unattributed.reduce((a, u) => a + u.kWh, 0);
|
|
146
|
+
const overheadKWh = overhead.reduce((a, o) => a + o.kWh, 0);
|
|
147
|
+
/* 합 보존은 이 모듈의 존재 이유다 — 깨지면 그 위의 모든 원단위가 거짓이 된다. */
|
|
148
|
+
if (!near(attributedKWh + unattributedKWh + overheadKWh, measuredKWh, 1e-6)) {
|
|
149
|
+
throw new Error(`energy attribution lost or created energy: measured=${measuredKWh} attributed=${attributedKWh} unattributed=${unattributedKWh} overhead=${overheadKWh}`);
|
|
150
|
+
}
|
|
151
|
+
return { shares, unattributed, excluded, overhead, totals: { measuredKWh, attributedKWh, unattributedKWh, overheadKWh } };
|
|
152
|
+
}
|
|
153
|
+
const UNIT = {
|
|
154
|
+
output: 'kWh/unit',
|
|
155
|
+
runtimeHours: 'kW',
|
|
156
|
+
area: 'kWh/m2'
|
|
157
|
+
};
|
|
158
|
+
/**
|
|
159
|
+
* 원단위 — **답할 수 없으면 답하지 않는다.**
|
|
160
|
+
*
|
|
161
|
+
* ── 왜 구간을 맞대어 보나 ───────────────────────────────────────────────────
|
|
162
|
+
* 분자는 에너지 트윈이, 분모는 생산 트윈이 낸다. 두 트윈은 각자의 시계로 돌고, 라이브와 히스토리가
|
|
163
|
+
* 섞이기도 한다. 다른 구간의 두 사실을 나누면 숫자는 나오지만 **아무것도 뜻하지 않는다** — 야간의
|
|
164
|
+
* 전력을 주간의 산출로 나눈 값이 그렇다. 그래서 구간이 어긋나면 거절한다(호출자가 맞춰서 다시 묻는다).
|
|
165
|
+
*
|
|
166
|
+
* ── 왜 0 을 무한으로 만들지 않나 ────────────────────────────────────────────
|
|
167
|
+
* 그 구간에 아무것도 만들지 않았다면 「대당 에너지」는 **정의되지 않는다.** `Infinity` 를 내면 화면이
|
|
168
|
+
* 그것을 큰 수로 그리고, 사용자는 최악의 원단위를 본 것으로 읽는다.
|
|
169
|
+
*/
|
|
170
|
+
export function energyIntensity(input) {
|
|
171
|
+
const kWh = Number(input.kWh);
|
|
172
|
+
if (!Number.isFinite(kWh))
|
|
173
|
+
return { value: null, reason: 'no-energy' };
|
|
174
|
+
const den = Number(input.denominator?.value);
|
|
175
|
+
if (!Number.isFinite(den))
|
|
176
|
+
return { value: null, reason: 'no-denominator' };
|
|
177
|
+
const ew = input.energyWindow;
|
|
178
|
+
const dw = input.denominator?.window;
|
|
179
|
+
/* 두 구간을 다 알 때만 맞대어 본다 — 하나를 모르면 어긋남을 주장할 수 없다(모름은 거절의 근거가 아니다). */
|
|
180
|
+
if (ew && dw && (ew.startMs !== dw.startMs || ew.endMs !== dw.endMs))
|
|
181
|
+
return { value: null, reason: 'window-mismatch' };
|
|
182
|
+
if (den === 0)
|
|
183
|
+
return { value: null, reason: 'zero-denominator' };
|
|
184
|
+
const window = ew ?? dw;
|
|
185
|
+
return {
|
|
186
|
+
value: kWh / den,
|
|
187
|
+
unit: UNIT[input.denominator.kind] ?? 'kWh',
|
|
188
|
+
kWh,
|
|
189
|
+
denominator: den,
|
|
190
|
+
...(window ? { window } : { window: { startMs: 0, endMs: 0 } })
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* 마감된 수요 구간들에서 그 범위의 전력량을 만든다 — **파생의 근거를 함께.**
|
|
195
|
+
*
|
|
196
|
+
* 범위에 **걸친** 구간은 세지 않는다(부분을 비례로 자르면 그 비례가 또 하나의 추정이 된다).
|
|
197
|
+
* 온전히 들어오는 구간만 센다 — 그래서 실제로 센 범위를 함께 낸다.
|
|
198
|
+
*/
|
|
199
|
+
export function energyOfWindows(windows, range) {
|
|
200
|
+
const inRange = (w) => !range || (w.startMs >= range.startMs && w.endMs <= range.endMs);
|
|
201
|
+
let kWh = 0;
|
|
202
|
+
let counted = 0;
|
|
203
|
+
let skipped = 0;
|
|
204
|
+
let first;
|
|
205
|
+
let last;
|
|
206
|
+
for (const w of windows ?? []) {
|
|
207
|
+
if (!inRange(w))
|
|
208
|
+
continue;
|
|
209
|
+
const mean = Number(w.meanKW);
|
|
210
|
+
const hours = (Number(w.endMs) - Number(w.startMs)) / 3_600_000;
|
|
211
|
+
if (!Number.isFinite(mean) || !Number.isFinite(hours) || hours <= 0) {
|
|
212
|
+
skipped++;
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
kWh += mean * hours;
|
|
216
|
+
counted++;
|
|
217
|
+
if (first === undefined || w.startMs < first)
|
|
218
|
+
first = w.startMs;
|
|
219
|
+
if (last === undefined || w.endMs > last)
|
|
220
|
+
last = w.endMs;
|
|
221
|
+
}
|
|
222
|
+
return {
|
|
223
|
+
...(counted > 0 ? { kWh } : {}),
|
|
224
|
+
basis: 'mean-kw',
|
|
225
|
+
counted,
|
|
226
|
+
skipped,
|
|
227
|
+
...(first !== undefined && last !== undefined ? { window: { startMs: first, endMs: last } } : {})
|
|
228
|
+
};
|
|
229
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type CanonicalEnvelope } from './contract.ts';
|
|
2
|
+
/**
|
|
3
|
+
* 정규 에너지 표본 — **커넥터가 이 모양으로 맞춰 준다.**
|
|
4
|
+
*
|
|
5
|
+
* 필드 이름은 계약이다(`EnergyMeasuredData` 와 같은 이름). 원 시스템의 낱말(`ActivePower`·`P_kW`·
|
|
6
|
+
* `MMXU.TotW`)을 여기서 받지 않는다 — 그 번역이 커넥터의 일이고, 커널까지 방언이 들어오면
|
|
7
|
+
* 소비처마다 다른 이름을 알아야 한다.
|
|
8
|
+
*/
|
|
9
|
+
export interface EnergyRecord {
|
|
10
|
+
meterId: string;
|
|
11
|
+
/** 유효전력(kW) — 그 계량 주기의 평균. 없으면 「못 읽었다」이고 0 이 아니다. */
|
|
12
|
+
kW?: number;
|
|
13
|
+
/** 계기 적산값(kWh) — 차분은 소비처가 한다(계기 교체·리셋을 지어내지 않는다). */
|
|
14
|
+
kWh?: number;
|
|
15
|
+
powerFactor?: number;
|
|
16
|
+
/** 계측 시각(ISO) — **지어낼 수 없는 값**이다. */
|
|
17
|
+
at?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface EnergyIngestOptions {
|
|
20
|
+
tenantId: string;
|
|
21
|
+
/** 레코드에 시각이 없을 때 쓸 값 — **주지 않으면 그 레코드를 거부한다.** */
|
|
22
|
+
defaultEventTime?: string;
|
|
23
|
+
}
|
|
24
|
+
export interface EnergyIngestResult {
|
|
25
|
+
accepted: CanonicalEnvelope[];
|
|
26
|
+
rejected: {
|
|
27
|
+
record: unknown;
|
|
28
|
+
errors: string[];
|
|
29
|
+
}[];
|
|
30
|
+
}
|
|
31
|
+
/** 이 레코드가 에너지 표본인가 — 라우팅 판정을 한 곳에 둔다(소비처가 각자 짐작하지 않게). */
|
|
32
|
+
export declare function isEnergyRecord(record: unknown): boolean;
|
|
33
|
+
/**
|
|
34
|
+
* 표본 레코드들을 봉투로 — 유효한 것만 통과하고 나머지는 이유와 함께 남는다.
|
|
35
|
+
*
|
|
36
|
+
* `eventTime` 은 **계측이 말한 시각**(`at`)이 먼저다. 봉투와 페이로드에 같은 값을 싣는다: 커널은
|
|
37
|
+
* 페이로드를 먼저 보고, 저널·시간여행은 봉투를 본다 — 둘이 다르면 같은 사실이 두 시각을 갖는다.
|
|
38
|
+
*/
|
|
39
|
+
export declare function ingestEnergyRecords(records: EnergyRecord | EnergyRecord[] | undefined | null, opts: EnergyIngestOptions): EnergyIngestResult;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 에너지 계측 인제스트 — **표본 레코드 → 봉투.** 설계: `design/profiles/ems.md` §4.1
|
|
3
|
+
*
|
|
4
|
+
* ── 왜 따로 있나 (2026-08-14) ───────────────────────────────────────────────
|
|
5
|
+
* 라이브 인제스트 경로는 **EPCIS 하나만 알았다.** 호스트의 정규 레코드 룰이 모든 레코드를
|
|
6
|
+
* `type: 'ObjectEvent'` 로 만들었기 때문이다(`canonical-ingest.ts`). 그 길로 계측을 넣으면 둘 중
|
|
7
|
+
* 하나가 된다: `epc` 가 없어 검증에서 거부되거나, 엉뚱하게 **물품 관측**으로 읽힌다.
|
|
8
|
+
*
|
|
9
|
+
* 에너지는 물(物)의 계보가 아니므로 EPCIS 어휘를 쓰지 않는다(§4). 그래서 정규 레코드의 **종류를
|
|
10
|
+
* 하나 더 인정**한다 — 봉투는 같은 것을 쓰고(저널·리플레이·시간여행을 그대로 얻는다) 어휘만 자기 것이다.
|
|
11
|
+
*
|
|
12
|
+
* ── 검증은 커널의 일이다 ────────────────────────────────────────────────────
|
|
13
|
+
* 매핑(원 시스템 스키마 → 정규 레코드)은 커넥터의 몫이고, **무엇이 유효한 사실인가**는 커널이 정한다
|
|
14
|
+
* (`face2-adapters.md` §7 의 규율: 매핑=밖, 검증=커널). 그래서 이 판정이 여기 있다.
|
|
15
|
+
*
|
|
16
|
+
* ── 무엇을 거부하나 ─────────────────────────────────────────────────────────
|
|
17
|
+
* 지어낼 수 없는 것이 빠지면 거부한다 — 계량 지점(`meterId`)과 시각(`at`)이다. 지금 시각으로 메우면
|
|
18
|
+
* 남의 구간에 실리고, 지점을 지어내면 어디의 소비인지 모르는 값이 누적된다. **거부한 것은 이유와 함께
|
|
19
|
+
* 돌려준다**(조용히 버리지 않는다 — 소비처가 그 수를 세어 사람에게 말할 수 있어야 한다).
|
|
20
|
+
*
|
|
21
|
+
* 값(`kW`)이 없는 표본은 **거부하지 않는다**: 계량기가 살아 있다는 사실 자체가 관측이고, 커널이
|
|
22
|
+
* 「받았지만 부하를 못 읽었다」를 구별해 낸다(`observedAbsence: 'no-load-samples'`).
|
|
23
|
+
*/
|
|
24
|
+
import { ENERGY_EVENT } from "./contract.js";
|
|
25
|
+
/** 이 레코드가 에너지 표본인가 — 라우팅 판정을 한 곳에 둔다(소비처가 각자 짐작하지 않게). */
|
|
26
|
+
export function isEnergyRecord(record) {
|
|
27
|
+
if (!record || typeof record !== 'object')
|
|
28
|
+
return false;
|
|
29
|
+
const r = record;
|
|
30
|
+
/* 계량 지점이 있고 EPCIS 어휘가 없으면 에너지다. `epc` 가 함께 있으면 둘 중 무엇인지 알 수 없으므로
|
|
31
|
+
에너지로 받지 않는다 — 그 판단은 커넥터가 명확히 해야 한다. */
|
|
32
|
+
return typeof r.meterId === 'string' && r.meterId.trim().length > 0 && r.epc === undefined;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* 표본 레코드들을 봉투로 — 유효한 것만 통과하고 나머지는 이유와 함께 남는다.
|
|
36
|
+
*
|
|
37
|
+
* `eventTime` 은 **계측이 말한 시각**(`at`)이 먼저다. 봉투와 페이로드에 같은 값을 싣는다: 커널은
|
|
38
|
+
* 페이로드를 먼저 보고, 저널·시간여행은 봉투를 본다 — 둘이 다르면 같은 사실이 두 시각을 갖는다.
|
|
39
|
+
*/
|
|
40
|
+
export function ingestEnergyRecords(records, opts) {
|
|
41
|
+
const arr = Array.isArray(records) ? records : records ? [records] : [];
|
|
42
|
+
const accepted = [];
|
|
43
|
+
const rejected = [];
|
|
44
|
+
let seq = 0;
|
|
45
|
+
for (const record of arr) {
|
|
46
|
+
const errors = [];
|
|
47
|
+
const meterId = String(record?.meterId ?? '').trim();
|
|
48
|
+
if (!meterId)
|
|
49
|
+
errors.push('meterId 없음 — 어디의 소비인지 모르는 값은 누적할 수 없다');
|
|
50
|
+
const at = String(record?.at ?? '').trim() || opts.defaultEventTime;
|
|
51
|
+
const atMs = at ? Date.parse(at) : Number.NaN;
|
|
52
|
+
if (!Number.isFinite(atMs))
|
|
53
|
+
errors.push('at 없음/형식 오류 — 지금 시각으로 메우면 남의 수요 구간에 실린다');
|
|
54
|
+
/* 값이 있으면 수여야 한다 — 문자열·NaN 을 그대로 흘리면 커널이 합에서 조용히 빠뜨린다. */
|
|
55
|
+
const num = (v, name) => {
|
|
56
|
+
if (v === undefined || v === null || v === '')
|
|
57
|
+
return undefined;
|
|
58
|
+
const n = Number(v);
|
|
59
|
+
if (!Number.isFinite(n)) {
|
|
60
|
+
errors.push(`${name} 가 수가 아니다: ${JSON.stringify(v)}`);
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
return n;
|
|
64
|
+
};
|
|
65
|
+
const kW = num(record?.kW, 'kW');
|
|
66
|
+
const kWh = num(record?.kWh, 'kWh');
|
|
67
|
+
const powerFactor = num(record?.powerFactor, 'powerFactor');
|
|
68
|
+
if (errors.length) {
|
|
69
|
+
rejected.push({ record, errors });
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
const eventTime = new Date(atMs).toISOString();
|
|
73
|
+
const data = {
|
|
74
|
+
meterId,
|
|
75
|
+
/* 계약은 `kW` 를 필수로 두지만 **못 읽은 표본도 사실**이다 — 그 경우 값을 비우고 보낸다.
|
|
76
|
+
커널이 「받았지만 부하를 못 읽었다」로 세고, 구간 마감에 그 이유를 싣는다. */
|
|
77
|
+
...(kW !== undefined ? { kW } : {}),
|
|
78
|
+
...(kWh !== undefined ? { kWh } : {}),
|
|
79
|
+
...(powerFactor !== undefined ? { powerFactor } : {}),
|
|
80
|
+
at: eventTime
|
|
81
|
+
};
|
|
82
|
+
accepted.push({
|
|
83
|
+
eventId: `${opts.tenantId}-energy-${++seq}`,
|
|
84
|
+
eventType: ENERGY_EVENT.measured,
|
|
85
|
+
eventTime,
|
|
86
|
+
tenantId: opts.tenantId,
|
|
87
|
+
data
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
return { accepted, rejected };
|
|
91
|
+
}
|
package/dist/epcis.d.ts
CHANGED
|
@@ -127,7 +127,7 @@ interface EpcisHeader {
|
|
|
127
127
|
/**
|
|
128
128
|
* **기록** 시각(ISO) — 발생 시각과 별개다.
|
|
129
129
|
*
|
|
130
|
-
* 이 둘이
|
|
130
|
+
* 이 둘이 어긋나는 것이 실 연동의 정상이다(현장에서 일어난 뒤 늦게 도착). 소비처가 순서를 판정할 때
|
|
131
131
|
* `eventTime` 만 보면 늦게 도착한 옛 이벤트가 최신 상태를 덮는다 — 그 판정의 재료가 이 값이다.
|
|
132
132
|
*/
|
|
133
133
|
recordTime?: string;
|
|
@@ -228,7 +228,7 @@ export declare const ILMD_ATTR: {
|
|
|
228
228
|
* 두 숫자 마디의 자릿수 합은 13(점 제외), Lot 은 GS3A3Component(URI 이스케이프 허용).
|
|
229
229
|
*/
|
|
230
230
|
export declare function lgtinClass(companyPrefix: string, itemRefAndIndicator: string, lot: string): string;
|
|
231
|
-
/** 식별자를 뜯어 본 결과 — 소비처가 문자열을 자르지 않게 한다(자르면 규칙이
|
|
231
|
+
/** 식별자를 뜯어 본 결과 — 소비처가 문자열을 자르지 않게 한다(자르면 규칙이 어긋난다). */
|
|
232
232
|
export interface ParsedEpc {
|
|
233
233
|
/** 표준 스킴. 모르면 'unknown'(원문을 그대로 남긴다 — 추측하지 않는다). */
|
|
234
234
|
scheme: 'sgtin' | 'lgtin' | 'idpat' | 'sscc' | 'gdti' | 'grai' | 'giai' | 'sgln' | 'unknown';
|
|
@@ -246,7 +246,7 @@ export interface ParsedEpc {
|
|
|
246
246
|
/**
|
|
247
247
|
* EPC/클래스 식별자 파서 — **표준 지식이라 커널이 소유한다.**
|
|
248
248
|
*
|
|
249
|
-
* 소비처가 `uri.split(':').pop()` 으로 자르면 규칙이
|
|
249
|
+
* 소비처가 `uri.split(':').pop()` 으로 자르면 규칙이 어긋난다(실제로 화면이 LGTIN 의 마지막 마디인
|
|
250
250
|
* 로트를 SKU 이름으로 표시할 위험이 있었다). 뜯는 일은 여기 한 곳에서 한다.
|
|
251
251
|
*/
|
|
252
252
|
export declare function parseEpc(uri: string): ParsedEpc;
|
package/dist/epcis.js
CHANGED
|
@@ -72,7 +72,7 @@ export function lgtinClass(companyPrefix, itemRefAndIndicator, lot) {
|
|
|
72
72
|
/**
|
|
73
73
|
* EPC/클래스 식별자 파서 — **표준 지식이라 커널이 소유한다.**
|
|
74
74
|
*
|
|
75
|
-
* 소비처가 `uri.split(':').pop()` 으로 자르면 규칙이
|
|
75
|
+
* 소비처가 `uri.split(':').pop()` 으로 자르면 규칙이 어긋난다(실제로 화면이 LGTIN 의 마지막 마디인
|
|
76
76
|
* 로트를 SKU 이름으로 표시할 위험이 있었다). 뜯는 일은 여기 한 곳에서 한다.
|
|
77
77
|
*/
|
|
78
78
|
export function parseEpc(uri) {
|
|
@@ -240,7 +240,7 @@ export function validateEpcisEvent(e) {
|
|
|
240
240
|
v.push('recordTime ISO8601 아님');
|
|
241
241
|
}
|
|
242
242
|
/* 개체·로트 마스터데이터는 **생겨나는 순간에만** 실린다(§7.3.8) — ObjectEvent(ADD) · Transformation.
|
|
243
|
-
* 아무 이벤트에나 허용하면 "생애 동안 정적" 이라는 성질이 깨지고 이벤트마다 값이
|
|
243
|
+
* 아무 이벤트에나 허용하면 "생애 동안 정적" 이라는 성질이 깨지고 이벤트마다 값이 어긋난다. */
|
|
244
244
|
if (e.ilmd !== undefined) {
|
|
245
245
|
const allowed = (e.type === 'ObjectEvent' && e.action === 'ADD') || e.type === 'TransformationEvent';
|
|
246
246
|
if (!allowed)
|
package/dist/event-journal.d.ts
CHANGED
|
@@ -32,7 +32,7 @@ export interface StructureSegment {
|
|
|
32
32
|
/**
|
|
33
33
|
* 구조가 바뀐 지점마다 무엇이 사라졌는지. 조용히 넘어가지 않는다.
|
|
34
34
|
*
|
|
35
|
-
*
|
|
35
|
+
* 구조 전환의 결과 자체는 `StructureShift`(계약) 한 벌이고, 재생은 거기에 **몇 번째 마디였나**만
|
|
36
36
|
* 더한다 — 두 벌로 두면 한쪽에 필드가 늘어날 때 다른 쪽이 조용히 뒤처진다.
|
|
37
37
|
*/
|
|
38
38
|
export interface SegmentShift extends StructureShift {
|
|
@@ -45,7 +45,7 @@ export interface SegmentShift extends StructureShift {
|
|
|
45
45
|
* 새 공장에 대고 접게 되어 이력이 거짓말을 하기 때문이다(도장 부스가 둘이던 시절의 사실을 여섯 개짜리
|
|
46
46
|
* 공장에 접는다). 역사를 잃거나 거짓말을 하거나, 둘뿐이었다.
|
|
47
47
|
*
|
|
48
|
-
* 셋째 길이 이것이다: 마디마다 **그때의 구조**로 접고, 경계에서 구조만
|
|
48
|
+
* 셋째 길이 이것이다: 마디마다 **그때의 구조**로 접고, 경계에서 구조만 전환한다(관측된 사실은 이어
|
|
49
49
|
* 간다). 그러면 이력이 "그때 그 공장의 사실" 로 계속 읽힌다.
|
|
50
50
|
*
|
|
51
51
|
* 경계에서 사라진 자원은 결과에 실어 보낸다 — 수가 줄어든 것을 사용자가 눈치채지 못하면 안 된다.
|
package/dist/event-journal.js
CHANGED
|
@@ -46,7 +46,7 @@ export function replay(model, events) {
|
|
|
46
46
|
* 새 공장에 대고 접게 되어 이력이 거짓말을 하기 때문이다(도장 부스가 둘이던 시절의 사실을 여섯 개짜리
|
|
47
47
|
* 공장에 접는다). 역사를 잃거나 거짓말을 하거나, 둘뿐이었다.
|
|
48
48
|
*
|
|
49
|
-
* 셋째 길이 이것이다: 마디마다 **그때의 구조**로 접고, 경계에서 구조만
|
|
49
|
+
* 셋째 길이 이것이다: 마디마다 **그때의 구조**로 접고, 경계에서 구조만 전환한다(관측된 사실은 이어
|
|
50
50
|
* 간다). 그러면 이력이 "그때 그 공장의 사실" 로 계속 읽힌다.
|
|
51
51
|
*
|
|
52
52
|
* 경계에서 사라진 자원은 결과에 실어 보낸다 — 수가 줄어든 것을 사용자가 눈치채지 못하면 안 된다.
|
package/dist/flow-engine.d.ts
CHANGED
|
@@ -135,7 +135,7 @@ export interface FlowTask {
|
|
|
135
135
|
* **실제로 들어가고 나온 자재** — ISA-95 `JobResponse.MaterialActual`.
|
|
136
136
|
*
|
|
137
137
|
* 명세(계획)가 아니라 **일어난 일**이다. 투입 인원·설비가 이미 작업 델타에 타고 있으니 자재도 같은
|
|
138
|
-
* 채널에 실어야 실적을 **한 곳에서** 읽는다(EPCIS 이벤트에서
|
|
138
|
+
* 채널에 실어야 실적을 **한 곳에서** 읽는다(EPCIS 이벤트에서 다시 계산하려면 작업과 잇는 끈이 없다).
|
|
139
139
|
*/
|
|
140
140
|
materialActual?: {
|
|
141
141
|
definitionId: string;
|
|
@@ -340,7 +340,7 @@ export declare abstract class FlowEngine implements TwinKernel {
|
|
|
340
340
|
*/
|
|
341
341
|
observe(): void;
|
|
342
342
|
/**
|
|
343
|
-
* 돌면서 공장을
|
|
343
|
+
* 돌면서 공장을 전환한다 — **현실이 안 멈추므로 미러도 멈출 수 없다.**
|
|
344
344
|
*
|
|
345
345
|
* ── 왜 관측 구동에만 여는가 ────────────────────────────────────────────────
|
|
346
346
|
* 미러는 이미 있는 현실을 따라갈 뿐이라, 설비가 한 대 늘었다고 멈췄다 서는 것은 그 사이의
|
|
@@ -353,7 +353,7 @@ export declare abstract class FlowEngine implements TwinKernel {
|
|
|
353
353
|
* 자원은 버리되 **몇 개를 버렸는지 돌려준다.** 여기서는 그 판단을 커널 자신의 선언 표에도
|
|
354
354
|
* 그대로 옮긴다. 관측층만 갈면 사라진 설비가 커널 표에 남아 **화면이 없는 설비를 그린다.**
|
|
355
355
|
*
|
|
356
|
-
* 관측으로 알게 된 자리는 선언에 없어도 지운 뒤 다시
|
|
356
|
+
* 관측으로 알게 된 자리는 선언에 없어도 지운 뒤 다시 주입한다 — 다음 스냅샷에서 관측층이
|
|
357
357
|
* 되살리므로(`settleObserved` → `hydrateObserved`), 여기서 지켜야 할 것은 **선언분**뿐이다.
|
|
358
358
|
*/
|
|
359
359
|
adoptStructure(def: TwinModelDef): StructureShift;
|
|
@@ -432,7 +432,7 @@ export declare abstract class FlowEngine implements TwinKernel {
|
|
|
432
432
|
protected computeAttentions(): Attention[];
|
|
433
433
|
/**
|
|
434
434
|
* fork — 현재 상태를 정확히 복제한 새 엔진 (디지털트윈 본연: "현재로부터 예측").
|
|
435
|
-
* 원본(live/sim)은 계속 진행, fork 는 what-if 를 앞으로
|
|
435
|
+
* 원본(live/sim)은 계속 진행, fork 는 what-if 를 앞으로 시뮬레이션해 forecast·발산(predicted vs actual) 검사에 쓴다.
|
|
436
436
|
* fork 는 자기 구독자·시나리오를 갖고 원본과 격리(handlers·gens 비움, generating=false).
|
|
437
437
|
* rng 는 fork 의 시나리오 load 시 재시드(드레인 예측은 생성 없어 rng 무관·결정적).
|
|
438
438
|
*/
|
|
@@ -441,7 +441,7 @@ export declare abstract class FlowEngine implements TwinKernel {
|
|
|
441
441
|
* 이 커널의 **지금**(ms) — 시각으로 바뀌는 모든 판정의 단일 기준.
|
|
442
442
|
*
|
|
443
443
|
* ── 관측 모드에서 시계가 멈춰 있었다 ────────────────────────────────────
|
|
444
|
-
* `apply()` 는 `clockMs` 를 밀지 않는다(관측은 시간을
|
|
444
|
+
* `apply()` 는 `clockMs` 를 밀지 않는다(관측은 시간을 진행시키지 않는다). 그래서 라이브 트윈의 "지금" 이
|
|
445
445
|
* **BASE_EPOCH(2026-01-01)에 얼어 있었다.** 실 이벤트는 실제 시각을 달고 오므로 결과가 이렇게 된다:
|
|
446
446
|
* · **납기 초과를 영원히 보고하지 않는다** — 실 납기가 항상 미래로 보인다
|
|
447
447
|
* · 폐기·도입예정 판정이 틀린다(§EffectivePeriod)
|
|
@@ -472,14 +472,14 @@ export declare abstract class FlowEngine implements TwinKernel {
|
|
|
472
472
|
};
|
|
473
473
|
protected randInt(min: number, max: number): number;
|
|
474
474
|
/**
|
|
475
|
-
* 관측 구동(P0 스파이크) — **이벤트로 커널을
|
|
475
|
+
* 관측 구동(P0 스파이크) — **이벤트로 커널을 실행한다.**
|
|
476
476
|
*
|
|
477
477
|
* 상태를 만드는 구동이 둘인데(시뮬 `tick` / 미러 `apply`) 지금은 **모델도 둘**이라 한쪽만 고치면
|
|
478
|
-
*
|
|
478
|
+
* 어긋난다(2026-08-01 하루에 아홉 곳). 근본 해법은 **한 상태 모델 두 구동**이고, 이것은 그 실현
|
|
479
479
|
* 가능성을 재는 스파이크다(design/plans/kernel-unification-live-observe.md P0).
|
|
480
480
|
*
|
|
481
481
|
* 여기서는 **이미 검증된 조각을 조립**한다: 투영기가 이벤트를 접고, 그 결과를 씨앗 경로
|
|
482
|
-
* (`hydrateObserved`)로 커널 상태에
|
|
482
|
+
* (`hydrateObserved`)로 커널 상태에 주입한다. 그래서 관측으로 실행한 커널을 그대로 `fork`·`tick` 할 수
|
|
483
483
|
* 있다 — "미러에서 예측한다" 가 별도 배관 없이 성립하는지가 이 스파이크의 질문이다.
|
|
484
484
|
*
|
|
485
485
|
* **비용은 정직하게**: 이벤트마다 전체를 다시 심으므로 O(상태 크기)다. P1 에서 반영 로직을 순수
|
|
@@ -512,14 +512,14 @@ export declare abstract class FlowEngine implements TwinKernel {
|
|
|
512
512
|
*/
|
|
513
513
|
protected routeKeys(): string[] | undefined;
|
|
514
514
|
/**
|
|
515
|
-
* **이 공장이 하루 몇 대를 낼 수 있는가** —
|
|
515
|
+
* **이 공장이 하루 몇 대를 낼 수 있는가** — 실행해 보지 않고 답한다.
|
|
516
516
|
*
|
|
517
517
|
* 커널이 직접 답하는 이유: 필요한 사실이 전부 여기 있다(공정 명세·설비와 신뢰도·인원·물리자산·
|
|
518
518
|
* 자리·근무 달력·시각 기준). 밖에서 모으면 그 값을 옮겨 적게 되고, 한쪽만 바뀌는 순간 "충분하다"
|
|
519
519
|
* 가 조용히 거짓이 된다. 계산 자체는 순수 함수(`analyzeCapacity`)에 맡긴다.
|
|
520
520
|
*
|
|
521
521
|
* 달력은 **자원이 선언한 것**을 쓴다. 자원마다 다른 달력을 쓰는 현장이면 대표를 고를 수 없으므로
|
|
522
|
-
* 그 사실을 결과에 실어 보낸다(`mixedCalendars`) — 조용히 하나를 골라 계산하면
|
|
522
|
+
* 그 사실을 결과에 실어 보낸다(`mixedCalendars`) — 조용히 하나를 골라 계산하면 상한이 틀린 채로
|
|
523
523
|
* 그럴듯해 보인다.
|
|
524
524
|
*/
|
|
525
525
|
capacity(opts: {
|
|
@@ -562,7 +562,7 @@ export declare abstract class FlowEngine implements TwinKernel {
|
|
|
562
562
|
/**
|
|
563
563
|
* task 소요 산출 — 우선순위: **① 추정기(이력 보정) → ② 명세(ISA-95 Duration + 변동) → ③ 도메인 상수.**
|
|
564
564
|
*
|
|
565
|
-
* 이 순서인 이유: 실측에서 배운 값이 선언값을 이기고, 선언값이 우리가 코드에
|
|
565
|
+
* 이 순서인 이유: 실측에서 배운 값이 선언값을 이기고, 선언값이 우리가 코드에 고정한 상수를 이긴다.
|
|
566
566
|
* 셋 중 무엇을 썼는지는 `specCoverage()` 로 드러낸다 — 상수를 쓴 것이 조용히 넘어가지 않게.
|
|
567
567
|
* "얼마"만 소비하고 "경로"는 씬이 소유한다(좌표-free 유지).
|
|
568
568
|
*/
|
|
@@ -585,7 +585,7 @@ export declare abstract class FlowEngine implements TwinKernel {
|
|
|
585
585
|
private noteSpecUse;
|
|
586
586
|
private noteParamUse;
|
|
587
587
|
/**
|
|
588
|
-
* 시뮬 명세 자기보고 — **어디까지 데이터로 말했고 어디부터 우리가
|
|
588
|
+
* 시뮬 명세 자기보고 — **어디까지 데이터로 말했고 어디부터 우리가 코드에 고정한 상수인가.**
|
|
589
589
|
*
|
|
590
590
|
* 시뮬레이션 결과를 받는 쪽이 이걸 봐야 한다: 소요시간이 전부 기본값이면 그 예측으로 말할 수 있는 것은
|
|
591
591
|
* "같은 조건에서의 상대 비교" 뿐이고 "몇 시에 끝난다" 는 근거가 없다. 그 구분을 숫자로 드러낸다.
|
|
@@ -653,7 +653,7 @@ export declare abstract class FlowEngine implements TwinKernel {
|
|
|
653
653
|
*/
|
|
654
654
|
protected reserve(epcs: string[], bizStep: string): void;
|
|
655
655
|
/**
|
|
656
|
-
* 처분 변화 관측 — **상태와 이벤트를 한 번에.** 둘을 따로 쓰면 반드시
|
|
656
|
+
* 처분 변화 관측 — **상태와 이벤트를 한 번에.** 둘을 따로 쓰면 반드시 어긋난다.
|
|
657
657
|
*
|
|
658
658
|
* 실제로 양쪽으로 갈라져 있었다: 할당은 상태만 바꾸고 이벤트를 안 냈고(미러가 모름), 야드 도크
|
|
659
659
|
* 도착은 이벤트만 내고 상태를 안 바꿨다(이벤트와 상태가 다른 말). 적합성 하네스가 둘 다 잡았다.
|
|
@@ -758,7 +758,7 @@ export declare abstract class FlowEngine implements TwinKernel {
|
|
|
758
758
|
* 지금은 자리를 따지지 않는다 — 따지려면 자산 이송 작업이 먼저 있어야 한다).
|
|
759
759
|
*/
|
|
760
760
|
private claimAssets;
|
|
761
|
-
/** 확보한 자산을 작업에
|
|
761
|
+
/** 확보한 자산을 작업에 배정한다 — 싣는 물류단위(SSCC)가 있으면 연결한다(GRAI ↔ SSCC). */
|
|
762
762
|
private assignAssets;
|
|
763
763
|
/**
|
|
764
764
|
* 작업이 끝나면 자산을 놓아 준다 — **사람과 다른 점: 자산은 도착 자리에 남는다**(물건이므로).
|
|
@@ -805,7 +805,7 @@ export declare abstract class FlowEngine implements TwinKernel {
|
|
|
805
805
|
* 이 작업이 **딛고 선 것**이 아직 있나 — 없으면 무엇이 없는지 답한다.
|
|
806
806
|
*
|
|
807
807
|
* 작업은 혼자 서지 못한다: 옮길 **물품**과, (있다면) 그것을 시킨 **오더** 위에 선다. 진행 중에
|
|
808
|
-
* 둘 중 하나가 사라질 수 있다 — 물품은 포장·출하·소비로, 오더는 이미 이행돼 씨앗이
|
|
808
|
+
* 둘 중 하나가 사라질 수 있다 — 물품은 포장·출하·소비로, 오더는 이미 이행돼 씨앗이 주입하지 않아서.
|
|
809
809
|
*
|
|
810
810
|
* 그때 도메인 훅은 없는 것을 딛으려다 던진다(`order.gtin` · `item.location`). 그 예외 하나가
|
|
811
811
|
* **예측 전체를 죽였다** — 사용자에게는 기능이 통째로 사라진 것으로 보였다. 그래서 완료 **전에**
|
|
@@ -859,7 +859,7 @@ export declare abstract class FlowEngine implements TwinKernel {
|
|
|
859
859
|
* 자원 하나의 **가용 능력** — 계약의 판정에 이 커널의 시각·시간대·필수 시험을 채워 넘긴다.
|
|
860
860
|
*
|
|
861
861
|
* 배정도 스냅샷도 이 자리를 지난다. 예전에는 배정이 조건을 늘어놓고 화면이 플래그를 보고 짐작해서,
|
|
862
|
-
* **자격이 만료된 사람이 화면에서는 `대기` 로 보였다**(
|
|
862
|
+
* **자격이 만료된 사람이 화면에서는 `대기` 로 보였다**(대기 중인 것은 맞지만 쓸 수 있는 것은 아니다).
|
|
863
863
|
* 판정에 필요한 둘(시각·등급 상속)을 아는 것은 커널뿐이므로, 답도 커널이 낸다.
|
|
864
864
|
*/
|
|
865
865
|
private capabilityOfResource;
|
|
@@ -870,7 +870,7 @@ export declare abstract class FlowEngine implements TwinKernel {
|
|
|
870
870
|
* 여기서는 고르기만 한다 — 확정은 호출부가 다른 자원까지 확보한 뒤에 한다.
|
|
871
871
|
*/
|
|
872
872
|
private claimEquipment;
|
|
873
|
-
/** 확보한 사람을 작업에
|
|
873
|
+
/** 확보한 사람을 작업에 배정한다(설비까지 확정된 뒤). */
|
|
874
874
|
private assignCrew;
|
|
875
875
|
/** 작업이 끝나면 사람을 놓아 준다 — 설비 해제와 별개 경로. */
|
|
876
876
|
private releaseCrew;
|
|
@@ -897,7 +897,7 @@ export declare abstract class FlowEngine implements TwinKernel {
|
|
|
897
897
|
* 유효 기간 밖인가 — **네 번째 이유**(§Effectivity). 설비·사람·자산이 같은 규칙을 쓴다.
|
|
898
898
|
*
|
|
899
899
|
* 시뮬 시각을 ISO 로 풀어 계약의 `effectivityAt` 에 넘긴다. **판정은 커널에 두지 않는다** —
|
|
900
|
-
* 호스트(라이브 관측)도 같은 판정을 해야 하고, 규칙이 두 벌이면
|
|
900
|
+
* 호스트(라이브 관측)도 같은 판정을 해야 하고, 규칙이 두 벌이면 어긋난다.
|
|
901
901
|
*/
|
|
902
902
|
protected effectivityOf(r: EffectivePeriod): Effectivity | undefined;
|
|
903
903
|
/**
|
|
@@ -912,7 +912,7 @@ export declare abstract class FlowEngine implements TwinKernel {
|
|
|
912
912
|
/**
|
|
913
913
|
* 스냅샷·델타에 실을 조각 — **선언한 기간과 판정을 함께** 낸다.
|
|
914
914
|
*
|
|
915
|
-
* 판정만 내면 화면이 "왜" 를 말할 수 없고(언제 폐기됐나), 기간만 내면 소비처마다 다시 판정해
|
|
915
|
+
* 판정만 내면 화면이 "왜" 를 말할 수 없고(언제 폐기됐나), 기간만 내면 소비처마다 다시 판정해 어긋난다.
|
|
916
916
|
* 선언이 없으면 아무것도 붙이지 않는다(대부분의 자원이 그렇다 — 필드를 늘리지 않는다).
|
|
917
917
|
*/
|
|
918
918
|
protected effectivePart(r: EffectivePeriod): Partial<EffectivePeriod> & {
|
|
@@ -922,7 +922,7 @@ export declare abstract class FlowEngine implements TwinKernel {
|
|
|
922
922
|
* 지금 근무 시간 밖인가 — **캘린더가 있으면 그것으로, 없으면 옛 `window` 로** 판정한다.
|
|
923
923
|
*
|
|
924
924
|
* `window`(시 단위 하나)는 캘린더의 특수한 경우다. 둘을 한 함수로 모아 두면 인원·설비가 같은 규칙을
|
|
925
|
-
* 쓴다(예전에는 같은 식이 두 곳에 복사돼 있었다 — 한쪽만 고치면 조용히
|
|
925
|
+
* 쓴다(예전에는 같은 식이 두 곳에 복사돼 있었다 — 한쪽만 고치면 조용히 어긋난다).
|
|
926
926
|
*/
|
|
927
927
|
protected offCalendar(r: {
|
|
928
928
|
window?: {
|