@tiangong-lca/cli 0.0.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/README.md +368 -0
- package/bin/tiangong.d.ts +1 -0
- package/bin/tiangong.js +32 -0
- package/dist/scripts/assert-full-coverage.js +77 -0
- package/dist/scripts/assert-full-coverage.js.map +1 -0
- package/dist/src/cli.js +2740 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/lib/artifacts.js +86 -0
- package/dist/src/lib/artifacts.js.map +1 -0
- package/dist/src/lib/dotenv.js +36 -0
- package/dist/src/lib/dotenv.js.map +1 -0
- package/dist/src/lib/env.js +139 -0
- package/dist/src/lib/env.js.map +1 -0
- package/dist/src/lib/errors.js +38 -0
- package/dist/src/lib/errors.js.map +1 -0
- package/dist/src/lib/flow-build-alias-map.js +299 -0
- package/dist/src/lib/flow-build-alias-map.js.map +1 -0
- package/dist/src/lib/flow-get.js +81 -0
- package/dist/src/lib/flow-get.js.map +1 -0
- package/dist/src/lib/flow-governance.js +164 -0
- package/dist/src/lib/flow-governance.js.map +1 -0
- package/dist/src/lib/flow-list.js +187 -0
- package/dist/src/lib/flow-list.js.map +1 -0
- package/dist/src/lib/flow-publish-reviewed-data.js +808 -0
- package/dist/src/lib/flow-publish-reviewed-data.js.map +1 -0
- package/dist/src/lib/flow-publish-version.js +541 -0
- package/dist/src/lib/flow-publish-version.js.map +1 -0
- package/dist/src/lib/flow-read.js +310 -0
- package/dist/src/lib/flow-read.js.map +1 -0
- package/dist/src/lib/flow-regen-product.js +1396 -0
- package/dist/src/lib/flow-regen-product.js.map +1 -0
- package/dist/src/lib/flow-remediate.js +821 -0
- package/dist/src/lib/flow-remediate.js.map +1 -0
- package/dist/src/lib/http.js +45 -0
- package/dist/src/lib/http.js.map +1 -0
- package/dist/src/lib/io.js +30 -0
- package/dist/src/lib/io.js.map +1 -0
- package/dist/src/lib/kb-search.js +83 -0
- package/dist/src/lib/kb-search.js.map +1 -0
- package/dist/src/lib/lifecyclemodel-auto-build.js +1328 -0
- package/dist/src/lib/lifecyclemodel-auto-build.js.map +1 -0
- package/dist/src/lib/lifecyclemodel-orchestrate.js +1556 -0
- package/dist/src/lib/lifecyclemodel-orchestrate.js.map +1 -0
- package/dist/src/lib/lifecyclemodel-publish-build.js +296 -0
- package/dist/src/lib/lifecyclemodel-publish-build.js.map +1 -0
- package/dist/src/lib/lifecyclemodel-publish-resulting-process.js +101 -0
- package/dist/src/lib/lifecyclemodel-publish-resulting-process.js.map +1 -0
- package/dist/src/lib/lifecyclemodel-resulting-process.js +1152 -0
- package/dist/src/lib/lifecyclemodel-resulting-process.js.map +1 -0
- package/dist/src/lib/lifecyclemodel-validate-build.js +238 -0
- package/dist/src/lib/lifecyclemodel-validate-build.js.map +1 -0
- package/dist/src/lib/llm.js +383 -0
- package/dist/src/lib/llm.js.map +1 -0
- package/dist/src/lib/process-auto-build.js +717 -0
- package/dist/src/lib/process-auto-build.js.map +1 -0
- package/dist/src/lib/process-batch-build.js +409 -0
- package/dist/src/lib/process-batch-build.js.map +1 -0
- package/dist/src/lib/process-get.js +66 -0
- package/dist/src/lib/process-get.js.map +1 -0
- package/dist/src/lib/process-publish-build.js +451 -0
- package/dist/src/lib/process-publish-build.js.map +1 -0
- package/dist/src/lib/process-resume-build.js +364 -0
- package/dist/src/lib/process-resume-build.js.map +1 -0
- package/dist/src/lib/publish.js +562 -0
- package/dist/src/lib/publish.js.map +1 -0
- package/dist/src/lib/remote.js +88 -0
- package/dist/src/lib/remote.js.map +1 -0
- package/dist/src/lib/review-flow.js +1110 -0
- package/dist/src/lib/review-flow.js.map +1 -0
- package/dist/src/lib/review-lifecyclemodel.js +727 -0
- package/dist/src/lib/review-lifecyclemodel.js.map +1 -0
- package/dist/src/lib/review-process.js +702 -0
- package/dist/src/lib/review-process.js.map +1 -0
- package/dist/src/lib/run.js +106 -0
- package/dist/src/lib/run.js.map +1 -0
- package/dist/src/lib/state-lock.js +150 -0
- package/dist/src/lib/state-lock.js.map +1 -0
- package/dist/src/lib/supabase-client.js +269 -0
- package/dist/src/lib/supabase-client.js.map +1 -0
- package/dist/src/lib/supabase-json-ordered-write.js +186 -0
- package/dist/src/lib/supabase-json-ordered-write.js.map +1 -0
- package/dist/src/lib/supabase-rest.js +138 -0
- package/dist/src/lib/supabase-rest.js.map +1 -0
- package/dist/src/lib/supabase-session.js +424 -0
- package/dist/src/lib/supabase-session.js.map +1 -0
- package/dist/src/lib/tidas-sdk-package-validator.js +493 -0
- package/dist/src/lib/tidas-sdk-package-validator.js.map +1 -0
- package/dist/src/lib/unstructured.js +203 -0
- package/dist/src/lib/unstructured.js.map +1 -0
- package/dist/src/lib/user-api-key.js +75 -0
- package/dist/src/lib/user-api-key.js.map +1 -0
- package/dist/src/lib/validation.js +191 -0
- package/dist/src/lib/validation.js.map +1 -0
- package/dist/src/main.js +38 -0
- package/dist/src/main.js.map +1 -0
- package/package.json +81 -0
|
@@ -0,0 +1,1328 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { existsSync, mkdirSync, readdirSync } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { writeJsonArtifact, writeTextArtifact } from './artifacts.js';
|
|
5
|
+
import { CliError } from './errors.js';
|
|
6
|
+
import { readJsonInput } from './io.js';
|
|
7
|
+
import { buildRunId, buildRunManifest, ensureRunLayout, writeLatestRunId, } from './run.js';
|
|
8
|
+
const UUID_NAMESPACE_URL = '6ba7b811-9dad-11d1-80b4-00c04fd430c8';
|
|
9
|
+
const EPSILON = 1e-10;
|
|
10
|
+
const DEFAULT_MANIFEST = {
|
|
11
|
+
run_label: 'lifecyclemodel-automated-builder',
|
|
12
|
+
allow_remote_write: false,
|
|
13
|
+
discovery: {
|
|
14
|
+
sources: [
|
|
15
|
+
{ kind: 'account_processes', selector: 'all-accessible' },
|
|
16
|
+
{
|
|
17
|
+
kind: 'public_open_data',
|
|
18
|
+
table: 'processes',
|
|
19
|
+
filters: { state_code: 100 },
|
|
20
|
+
},
|
|
21
|
+
],
|
|
22
|
+
supporting_open_tables: ['flows', 'sources', 'lifecyclemodels'],
|
|
23
|
+
batch_limit: 200,
|
|
24
|
+
reference_model_queries: [],
|
|
25
|
+
reference_model_select_limit: 3,
|
|
26
|
+
},
|
|
27
|
+
selection: {
|
|
28
|
+
mode: 'graph_first_local_build',
|
|
29
|
+
max_models: 25,
|
|
30
|
+
max_processes_per_model: 12,
|
|
31
|
+
decision_factors: [
|
|
32
|
+
'shared product system or classification lineage',
|
|
33
|
+
'explicit exchange connectivity',
|
|
34
|
+
'quantitative reference completeness',
|
|
35
|
+
'geography and time coherence',
|
|
36
|
+
],
|
|
37
|
+
},
|
|
38
|
+
reuse: {
|
|
39
|
+
reusable_process_dirs: [],
|
|
40
|
+
include_reference_model_resulting_processes: true,
|
|
41
|
+
},
|
|
42
|
+
output: {
|
|
43
|
+
write_local_models: true,
|
|
44
|
+
emit_validation_report: false,
|
|
45
|
+
},
|
|
46
|
+
local_runs: [],
|
|
47
|
+
publish: {
|
|
48
|
+
enabled: false,
|
|
49
|
+
mode: 'deferred_to_publish_build',
|
|
50
|
+
target_runs: [],
|
|
51
|
+
select_after_insert: true,
|
|
52
|
+
max_attempts: 5,
|
|
53
|
+
retry_delay_seconds: 2,
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
function isRecord(value) {
|
|
57
|
+
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
|
58
|
+
}
|
|
59
|
+
function ensureList(value) {
|
|
60
|
+
if (value === undefined || value === null) {
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
return Array.isArray(value) ? value : [value];
|
|
64
|
+
}
|
|
65
|
+
function copyJson(value) {
|
|
66
|
+
if (value === undefined) {
|
|
67
|
+
return value;
|
|
68
|
+
}
|
|
69
|
+
return JSON.parse(JSON.stringify(value));
|
|
70
|
+
}
|
|
71
|
+
function nonEmptyString(value) {
|
|
72
|
+
if (typeof value !== 'string') {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
const normalized = value.trim();
|
|
76
|
+
return normalized || null;
|
|
77
|
+
}
|
|
78
|
+
function numberOrZero(value) {
|
|
79
|
+
if (typeof value === 'number') {
|
|
80
|
+
return Number.isFinite(value) ? value : 0;
|
|
81
|
+
}
|
|
82
|
+
if (typeof value === 'string' && value.trim()) {
|
|
83
|
+
const parsed = Number(value);
|
|
84
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
85
|
+
}
|
|
86
|
+
return 0;
|
|
87
|
+
}
|
|
88
|
+
function firstText(value) {
|
|
89
|
+
if (Array.isArray(value)) {
|
|
90
|
+
for (const item of value) {
|
|
91
|
+
if (isRecord(item)) {
|
|
92
|
+
const text = nonEmptyString(item['#text']);
|
|
93
|
+
if (text) {
|
|
94
|
+
return text;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return '';
|
|
99
|
+
}
|
|
100
|
+
if (isRecord(value)) {
|
|
101
|
+
return nonEmptyString(value['#text']) ?? '';
|
|
102
|
+
}
|
|
103
|
+
return nonEmptyString(value) ?? '';
|
|
104
|
+
}
|
|
105
|
+
function langTextMap(value) {
|
|
106
|
+
const mapping = {};
|
|
107
|
+
for (const item of ensureList(value)) {
|
|
108
|
+
if (!isRecord(item)) {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
const lang = nonEmptyString(item['@xml:lang'])?.toLowerCase() ?? 'en';
|
|
112
|
+
const text = nonEmptyString(item['#text']);
|
|
113
|
+
if (text && !mapping[lang]) {
|
|
114
|
+
mapping[lang] = text;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (Object.keys(mapping).length > 0) {
|
|
118
|
+
return mapping;
|
|
119
|
+
}
|
|
120
|
+
const text = firstText(value).trim();
|
|
121
|
+
return text ? { en: text } : {};
|
|
122
|
+
}
|
|
123
|
+
function localizedText(value, preferred = 'zh') {
|
|
124
|
+
const mapping = langTextMap(value);
|
|
125
|
+
if (mapping[preferred]) {
|
|
126
|
+
return mapping[preferred];
|
|
127
|
+
}
|
|
128
|
+
if (preferred.startsWith('zh')) {
|
|
129
|
+
for (const candidate of ['zh-cn', 'zh-hans', 'zh']) {
|
|
130
|
+
if (mapping[candidate]) {
|
|
131
|
+
return mapping[candidate];
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (mapping.en) {
|
|
136
|
+
return mapping.en;
|
|
137
|
+
}
|
|
138
|
+
return Object.values(mapping)[0] ?? '';
|
|
139
|
+
}
|
|
140
|
+
function multilangText(value) {
|
|
141
|
+
const mapping = langTextMap(value);
|
|
142
|
+
return [mapping.en ?? Object.values(mapping)[0] ?? '', mapping.zh ?? mapping['zh-cn'] ?? ''];
|
|
143
|
+
}
|
|
144
|
+
function multilangFromText(enText, zhText) {
|
|
145
|
+
const items = [];
|
|
146
|
+
if (enText) {
|
|
147
|
+
items.push({ '@xml:lang': 'en', '#text': enText });
|
|
148
|
+
}
|
|
149
|
+
if (zhText) {
|
|
150
|
+
items.push({ '@xml:lang': 'zh', '#text': zhText });
|
|
151
|
+
}
|
|
152
|
+
return items;
|
|
153
|
+
}
|
|
154
|
+
function tokenizeText(value) {
|
|
155
|
+
const cleaned = value
|
|
156
|
+
.toLowerCase()
|
|
157
|
+
.replace(/[^a-z0-9]+/gu, ' ')
|
|
158
|
+
.trim();
|
|
159
|
+
return new Set(cleaned.split(/\s+/u).filter((token) => token.length >= 3));
|
|
160
|
+
}
|
|
161
|
+
function buildNameSummary(nameInfo) {
|
|
162
|
+
const base = langTextMap(nameInfo.baseName);
|
|
163
|
+
const route = langTextMap(nameInfo.treatmentStandardsRoutes);
|
|
164
|
+
const mix = langTextMap(nameInfo.mixAndLocationTypes);
|
|
165
|
+
const functional = langTextMap(nameInfo.functionalUnitFlowProperties);
|
|
166
|
+
const langOrder = [];
|
|
167
|
+
[base, route, mix, functional].forEach((mapping) => {
|
|
168
|
+
Object.keys(mapping).forEach((lang) => {
|
|
169
|
+
if (!langOrder.includes(lang)) {
|
|
170
|
+
langOrder.push(lang);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
if (langOrder.length === 0) {
|
|
175
|
+
return [];
|
|
176
|
+
}
|
|
177
|
+
const fallback = (mapping, lang) => mapping[lang] ??
|
|
178
|
+
mapping.zh ??
|
|
179
|
+
mapping['zh-cn'] ??
|
|
180
|
+
mapping.en ??
|
|
181
|
+
Object.values(mapping)[0] ??
|
|
182
|
+
'';
|
|
183
|
+
const summary = [];
|
|
184
|
+
langOrder.forEach((lang) => {
|
|
185
|
+
const text = [
|
|
186
|
+
fallback(base, lang),
|
|
187
|
+
fallback(route, lang),
|
|
188
|
+
fallback(mix, lang),
|
|
189
|
+
fallback(functional, lang),
|
|
190
|
+
]
|
|
191
|
+
.filter(Boolean)
|
|
192
|
+
.join('; ')
|
|
193
|
+
.trim();
|
|
194
|
+
if (text) {
|
|
195
|
+
summary.push({ '@xml:lang': lang, '#text': text });
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
return summary;
|
|
199
|
+
}
|
|
200
|
+
function extractClassificationPath(classificationInfo) {
|
|
201
|
+
const carrier = isRecord(classificationInfo)
|
|
202
|
+
? isRecord(classificationInfo['common:classification'])
|
|
203
|
+
? classificationInfo['common:classification']
|
|
204
|
+
: {}
|
|
205
|
+
: {};
|
|
206
|
+
return ensureList(carrier['common:class'])
|
|
207
|
+
.map((item) => (isRecord(item) ? nonEmptyString(item['#text']) : null))
|
|
208
|
+
.filter((item) => Boolean(item));
|
|
209
|
+
}
|
|
210
|
+
function classificationOverlap(left, right) {
|
|
211
|
+
let overlap = 0;
|
|
212
|
+
const maxLength = Math.min(left.length, right.length);
|
|
213
|
+
for (let index = 0; index < maxLength; index += 1) {
|
|
214
|
+
if (left[index] !== right[index]) {
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
overlap += 1;
|
|
218
|
+
}
|
|
219
|
+
return overlap;
|
|
220
|
+
}
|
|
221
|
+
function parseUuidBytes(uuid) {
|
|
222
|
+
const hex = uuid.replace(/-/gu, '').toLowerCase();
|
|
223
|
+
if (!/^[0-9a-f]{32}$/u.test(hex)) {
|
|
224
|
+
throw new CliError(`Invalid UUID value: ${uuid}`, {
|
|
225
|
+
code: 'LIFECYCLEMODEL_AUTO_BUILD_UUID_INVALID',
|
|
226
|
+
exitCode: 2,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
const bytes = new Uint8Array(16);
|
|
230
|
+
for (let index = 0; index < 16; index += 1) {
|
|
231
|
+
bytes[index] = Number.parseInt(hex.slice(index * 2, index * 2 + 2), 16);
|
|
232
|
+
}
|
|
233
|
+
return bytes;
|
|
234
|
+
}
|
|
235
|
+
function bytesToUuid(bytes) {
|
|
236
|
+
const hex = Array.from(bytes, (value) => value.toString(16).padStart(2, '0')).join('');
|
|
237
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
238
|
+
}
|
|
239
|
+
function uuid5FromText(namespaceUuid, value) {
|
|
240
|
+
const namespaceBytes = Buffer.from(parseUuidBytes(namespaceUuid));
|
|
241
|
+
const hash = createHash('sha1').update(namespaceBytes).update(value, 'utf8').digest();
|
|
242
|
+
const bytes = Uint8Array.from(hash.subarray(0, 16));
|
|
243
|
+
bytes[6] = (bytes[6] & 0x0f) | 0x50;
|
|
244
|
+
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
|
245
|
+
return bytesToUuid(bytes);
|
|
246
|
+
}
|
|
247
|
+
function formatNumber(value) {
|
|
248
|
+
if (!Number.isFinite(value) || Math.abs(value) < EPSILON) {
|
|
249
|
+
return '0';
|
|
250
|
+
}
|
|
251
|
+
const normalized = Number.parseFloat(value.toPrecision(15));
|
|
252
|
+
if (Number.isInteger(normalized)) {
|
|
253
|
+
return String(normalized);
|
|
254
|
+
}
|
|
255
|
+
if (Math.abs(normalized) >= 1e6 || Math.abs(normalized) < 1e-6) {
|
|
256
|
+
return normalized.toFixed(12).replace(/0+$/u, '').replace(/\.$/u, '');
|
|
257
|
+
}
|
|
258
|
+
return String(normalized);
|
|
259
|
+
}
|
|
260
|
+
function toJsonNumber(value) {
|
|
261
|
+
return Number.parseFloat(formatNumber(value));
|
|
262
|
+
}
|
|
263
|
+
function requiredRequestObject(input) {
|
|
264
|
+
if (!isRecord(input)) {
|
|
265
|
+
throw new CliError('lifecyclemodel auto-build request must be a JSON object.', {
|
|
266
|
+
code: 'LIFECYCLEMODEL_AUTO_BUILD_REQUEST_INVALID',
|
|
267
|
+
exitCode: 2,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
return input;
|
|
271
|
+
}
|
|
272
|
+
function deepMerge(base, override) {
|
|
273
|
+
if (isRecord(base) && isRecord(override)) {
|
|
274
|
+
const merged = copyJson(base);
|
|
275
|
+
Object.entries(override).forEach(([key, value]) => {
|
|
276
|
+
merged[key] = deepMerge(merged[key], value);
|
|
277
|
+
});
|
|
278
|
+
return merged;
|
|
279
|
+
}
|
|
280
|
+
return copyJson(override);
|
|
281
|
+
}
|
|
282
|
+
function normalizeLocalRuns(value, requestDir) {
|
|
283
|
+
const seen = new Set();
|
|
284
|
+
const normalized = ensureList(value)
|
|
285
|
+
.map((item) => nonEmptyString(item))
|
|
286
|
+
.filter((item) => Boolean(item))
|
|
287
|
+
.map((item) => path.resolve(requestDir, item));
|
|
288
|
+
if (normalized.length === 0) {
|
|
289
|
+
throw new CliError('lifecyclemodel auto-build local_runs must contain at least one run directory.', {
|
|
290
|
+
code: 'LIFECYCLEMODEL_AUTO_BUILD_LOCAL_RUNS_REQUIRED',
|
|
291
|
+
exitCode: 2,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
normalized.forEach((item) => {
|
|
295
|
+
if (seen.has(item)) {
|
|
296
|
+
throw new CliError(`Duplicate lifecyclemodel auto-build local_run: ${item}`, {
|
|
297
|
+
code: 'LIFECYCLEMODEL_AUTO_BUILD_LOCAL_RUN_DUPLICATE',
|
|
298
|
+
exitCode: 2,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
seen.add(item);
|
|
302
|
+
});
|
|
303
|
+
return normalized;
|
|
304
|
+
}
|
|
305
|
+
function resolveRunRoot(requestDir, runId, outDirOverride, requestOutDir) {
|
|
306
|
+
const override = nonEmptyString(outDirOverride);
|
|
307
|
+
if (override) {
|
|
308
|
+
return path.resolve(requestDir, override);
|
|
309
|
+
}
|
|
310
|
+
const requestValue = nonEmptyString(requestOutDir);
|
|
311
|
+
if (requestValue) {
|
|
312
|
+
return path.resolve(requestDir, requestValue);
|
|
313
|
+
}
|
|
314
|
+
return path.join(requestDir, 'artifacts', 'lifecyclemodel_auto_build', runId);
|
|
315
|
+
}
|
|
316
|
+
function buildLayout(runRoot, runId) {
|
|
317
|
+
const collectionDir = path.dirname(runRoot);
|
|
318
|
+
const layout = {
|
|
319
|
+
namespace: 'lifecyclemodel_auto_build',
|
|
320
|
+
runId,
|
|
321
|
+
artifactsRoot: path.dirname(collectionDir),
|
|
322
|
+
collectionDir,
|
|
323
|
+
runRoot,
|
|
324
|
+
cacheDir: path.join(runRoot, 'cache'),
|
|
325
|
+
inputsDir: path.join(runRoot, 'request'),
|
|
326
|
+
outputsDir: path.join(runRoot, 'models'),
|
|
327
|
+
reportsDir: path.join(runRoot, 'reports'),
|
|
328
|
+
logsDir: path.join(runRoot, 'logs'),
|
|
329
|
+
manifestsDir: path.join(runRoot, 'manifests'),
|
|
330
|
+
latestRunIdPath: path.join(collectionDir, '.latest_run_id'),
|
|
331
|
+
};
|
|
332
|
+
return {
|
|
333
|
+
...layout,
|
|
334
|
+
requestDir: path.join(runRoot, 'request'),
|
|
335
|
+
selectionDir: path.join(runRoot, 'selection'),
|
|
336
|
+
discoveryDir: path.join(runRoot, 'discovery'),
|
|
337
|
+
modelsDir: path.join(runRoot, 'models'),
|
|
338
|
+
requestSnapshotPath: path.join(runRoot, 'request', 'lifecyclemodel-auto-build.request.json'),
|
|
339
|
+
normalizedRequestPath: path.join(runRoot, 'request', 'request.normalized.json'),
|
|
340
|
+
runPlanPath: path.join(runRoot, 'run-plan.json'),
|
|
341
|
+
resolvedManifestPath: path.join(runRoot, 'resolved-manifest.json'),
|
|
342
|
+
selectionBriefPath: path.join(runRoot, 'selection', 'selection-brief.md'),
|
|
343
|
+
referenceModelSummaryPath: path.join(runRoot, 'discovery', 'reference-model-summary.json'),
|
|
344
|
+
invocationIndexPath: path.join(runRoot, 'manifests', 'invocation-index.json'),
|
|
345
|
+
runManifestPath: path.join(runRoot, 'manifests', 'run-manifest.json'),
|
|
346
|
+
reportPath: path.join(runRoot, 'reports', 'lifecyclemodel-auto-build-report.json'),
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
function ensureEmptyRunRoot(runRoot) {
|
|
350
|
+
if (!existsSync(runRoot)) {
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
if (readdirSync(runRoot).length > 0) {
|
|
354
|
+
throw new CliError(`lifecyclemodel auto-build run root already exists and is not empty: ${runRoot}`, {
|
|
355
|
+
code: 'LIFECYCLEMODEL_AUTO_BUILD_RUN_EXISTS',
|
|
356
|
+
exitCode: 2,
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
function ensureLayout(layout) {
|
|
361
|
+
ensureRunLayout(layout);
|
|
362
|
+
[layout.requestDir, layout.selectionDir, layout.discoveryDir, layout.modelsDir].forEach((dirPath) => {
|
|
363
|
+
mkdirSync(dirPath, { recursive: true });
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
export function normalizeLifecyclemodelAutoBuildRequest(input, options) {
|
|
367
|
+
const request = requiredRequestObject(input);
|
|
368
|
+
const requestPath = path.resolve(options.inputPath);
|
|
369
|
+
const requestDir = path.dirname(requestPath);
|
|
370
|
+
const mergedManifest = deepMerge(DEFAULT_MANIFEST, request);
|
|
371
|
+
if (mergedManifest.allow_remote_write === true) {
|
|
372
|
+
throw new CliError('allow_remote_write=true is not supported by tiangong lifecyclemodel auto-build. Keep the run read-only.', {
|
|
373
|
+
code: 'LIFECYCLEMODEL_AUTO_BUILD_REMOTE_WRITE_UNSUPPORTED',
|
|
374
|
+
exitCode: 2,
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
const now = options.now ?? new Date();
|
|
378
|
+
const runId = nonEmptyString(options.runIdOverride) ??
|
|
379
|
+
buildRunId({
|
|
380
|
+
namespace: 'lifecyclemodel_auto_build',
|
|
381
|
+
subject: nonEmptyString(mergedManifest.run_label) ??
|
|
382
|
+
path.basename(requestPath, path.extname(requestPath)),
|
|
383
|
+
operation: 'build',
|
|
384
|
+
now,
|
|
385
|
+
});
|
|
386
|
+
return {
|
|
387
|
+
schema_version: 1,
|
|
388
|
+
request_path: requestPath,
|
|
389
|
+
run_id: runId,
|
|
390
|
+
run_root: resolveRunRoot(requestDir, runId, options.outDir, request.out_dir),
|
|
391
|
+
manifest: mergedManifest,
|
|
392
|
+
local_runs: normalizeLocalRuns(mergedManifest.local_runs, requestDir),
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
function buildSelectionBrief(manifest) {
|
|
396
|
+
const selection = isRecord(manifest.selection) ? manifest.selection : {};
|
|
397
|
+
const decisionFactors = ensureList(selection.decision_factors)
|
|
398
|
+
.map((item) => nonEmptyString(item))
|
|
399
|
+
.filter((item) => Boolean(item));
|
|
400
|
+
const referenceQueries = isRecord(manifest.discovery)
|
|
401
|
+
? ensureList(manifest.discovery.reference_model_queries)
|
|
402
|
+
.map((item) => nonEmptyString(item))
|
|
403
|
+
.filter((item) => Boolean(item))
|
|
404
|
+
: [];
|
|
405
|
+
const lines = [
|
|
406
|
+
'# Selection Brief',
|
|
407
|
+
'',
|
|
408
|
+
'This CLI slice is local-first and read-only.',
|
|
409
|
+
'',
|
|
410
|
+
'Candidate processes are grouped by the provided process run directories.',
|
|
411
|
+
'The CLI then infers graph structure from shared flow UUIDs, chooses one reference process, and assembles native `json_ordered` lifecyclemodel artifacts.',
|
|
412
|
+
'',
|
|
413
|
+
'Decision factors:',
|
|
414
|
+
...decisionFactors.map((item) => `- ${item}`),
|
|
415
|
+
];
|
|
416
|
+
if (referenceQueries.length > 0) {
|
|
417
|
+
lines.push('', 'Reference lifecyclemodel discovery was requested in the manifest, but this first CLI slice defers remote read-only discovery to future commands.', ...referenceQueries.map((item) => `- deferred reference query: ${item}`));
|
|
418
|
+
}
|
|
419
|
+
lines.push('', 'Explicitly deferred in this slice:', '- remote discovery', '- MCP writes', '- LLM-driven selection', '- validate-build', '- publish-build', '');
|
|
420
|
+
return lines.join('\n');
|
|
421
|
+
}
|
|
422
|
+
function buildReferenceModelSummary(manifest) {
|
|
423
|
+
const discovery = isRecord(manifest.discovery) ? manifest.discovery : {};
|
|
424
|
+
const queries = ensureList(discovery.reference_model_queries)
|
|
425
|
+
.map((item) => nonEmptyString(item))
|
|
426
|
+
.filter((item) => Boolean(item));
|
|
427
|
+
return {
|
|
428
|
+
executed: false,
|
|
429
|
+
queries,
|
|
430
|
+
reason: queries.length > 0
|
|
431
|
+
? 'reference model discovery is deferred in the first native CLI auto-build slice'
|
|
432
|
+
: 'reference model discovery not requested',
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
function buildInvocationIndex(normalized, options, layout, now) {
|
|
436
|
+
const command = ['lifecyclemodel', 'auto-build', '--input', options.inputPath];
|
|
437
|
+
if (options.outDir) {
|
|
438
|
+
command.push('--out-dir', options.outDir);
|
|
439
|
+
}
|
|
440
|
+
return {
|
|
441
|
+
schema_version: 1,
|
|
442
|
+
invocations: [
|
|
443
|
+
{
|
|
444
|
+
command,
|
|
445
|
+
cwd: options.cwd ?? process.cwd(),
|
|
446
|
+
created_at: now.toISOString(),
|
|
447
|
+
request_path: normalized.request_path,
|
|
448
|
+
report_path: layout.reportPath,
|
|
449
|
+
},
|
|
450
|
+
],
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
function buildPlan(normalized, layout, now) {
|
|
454
|
+
return {
|
|
455
|
+
skill: 'lifecyclemodel-automated-builder',
|
|
456
|
+
mode: 'read_only_local_build',
|
|
457
|
+
created_at: now.toISOString(),
|
|
458
|
+
manifest: normalized.manifest,
|
|
459
|
+
runtime: {
|
|
460
|
+
cli: {
|
|
461
|
+
command: 'lifecyclemodel auto-build',
|
|
462
|
+
entry: 'native_ts',
|
|
463
|
+
node_version: process.version,
|
|
464
|
+
remote_write: false,
|
|
465
|
+
mcp_used: false,
|
|
466
|
+
},
|
|
467
|
+
},
|
|
468
|
+
guardrails: [
|
|
469
|
+
'do not write remote lifecyclemodel rows in auto-build',
|
|
470
|
+
'do not require MCP or LLM to assemble the initial local lifecyclemodel graph',
|
|
471
|
+
'emit native json_ordered only',
|
|
472
|
+
'defer validate-build and publish-build to dedicated follow-up commands',
|
|
473
|
+
],
|
|
474
|
+
artifacts: {
|
|
475
|
+
root: layout.runRoot,
|
|
476
|
+
run_plan: layout.runPlanPath,
|
|
477
|
+
resolved_manifest: layout.resolvedManifestPath,
|
|
478
|
+
selection_brief: layout.selectionBriefPath,
|
|
479
|
+
discovery_dir: layout.discoveryDir,
|
|
480
|
+
reference_model_summary: layout.referenceModelSummaryPath,
|
|
481
|
+
models_dir: layout.modelsDir,
|
|
482
|
+
reports_dir: layout.reportsDir,
|
|
483
|
+
},
|
|
484
|
+
stages: [
|
|
485
|
+
{
|
|
486
|
+
name: 'load-local-runs',
|
|
487
|
+
mode: 'local',
|
|
488
|
+
description: 'Load one or more process-automated-builder run directories.',
|
|
489
|
+
},
|
|
490
|
+
{
|
|
491
|
+
name: 'infer-graph',
|
|
492
|
+
mode: 'local',
|
|
493
|
+
description: 'Infer process-to-process links from shared flow UUIDs.',
|
|
494
|
+
},
|
|
495
|
+
{
|
|
496
|
+
name: 'select-reference-process',
|
|
497
|
+
mode: 'local',
|
|
498
|
+
description: 'Pick the lifecyclemodel reference process from the target flow and graph shape.',
|
|
499
|
+
},
|
|
500
|
+
{
|
|
501
|
+
name: 'assemble-lifecyclemodel',
|
|
502
|
+
mode: 'local',
|
|
503
|
+
description: 'Write native json_ordered lifecyclemodel artifacts.',
|
|
504
|
+
},
|
|
505
|
+
{
|
|
506
|
+
name: 'validate',
|
|
507
|
+
mode: 'deferred',
|
|
508
|
+
description: 'Deferred to tiangong lifecyclemodel validate-build.',
|
|
509
|
+
},
|
|
510
|
+
{
|
|
511
|
+
name: 'publish',
|
|
512
|
+
mode: 'deferred',
|
|
513
|
+
description: 'Deferred to tiangong lifecyclemodel publish-build.',
|
|
514
|
+
},
|
|
515
|
+
],
|
|
516
|
+
local_runs: normalized.local_runs,
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
function extractFlowDatasetFromState(state) {
|
|
520
|
+
const flowDataset = isRecord(state.flow_dataset) ? state.flow_dataset : {};
|
|
521
|
+
if (isRecord(flowDataset.flowDataSet)) {
|
|
522
|
+
return flowDataset.flowDataSet;
|
|
523
|
+
}
|
|
524
|
+
return flowDataset;
|
|
525
|
+
}
|
|
526
|
+
function buildGlobalReference(refObjectId, version, shortDescription, datasetType, uri) {
|
|
527
|
+
return {
|
|
528
|
+
'@refObjectId': refObjectId,
|
|
529
|
+
'@type': datasetType,
|
|
530
|
+
'@uri': uri,
|
|
531
|
+
'@version': version,
|
|
532
|
+
'common:shortDescription': copyJson(shortDescription),
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
function resolvePublicationBlock(dataset) {
|
|
536
|
+
const administrativeInformation = isRecord(dataset.administrativeInformation)
|
|
537
|
+
? dataset.administrativeInformation
|
|
538
|
+
: {};
|
|
539
|
+
if (isRecord(administrativeInformation.publicationAndOwnership)) {
|
|
540
|
+
return administrativeInformation.publicationAndOwnership;
|
|
541
|
+
}
|
|
542
|
+
if (isRecord(administrativeInformation['common:publicationAndOwnership'])) {
|
|
543
|
+
return administrativeInformation['common:publicationAndOwnership'];
|
|
544
|
+
}
|
|
545
|
+
return {};
|
|
546
|
+
}
|
|
547
|
+
function loadProcessRecord(filePath, sourceKind, sourceLabel) {
|
|
548
|
+
const raw = readJsonInput(filePath);
|
|
549
|
+
if (!isRecord(raw) || !isRecord(raw.processDataSet)) {
|
|
550
|
+
throw new CliError(`lifecyclemodel auto-build process file must contain processDataSet: ${filePath}`, {
|
|
551
|
+
code: 'LIFECYCLEMODEL_AUTO_BUILD_PROCESS_INVALID',
|
|
552
|
+
exitCode: 2,
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
const dataset = raw.processDataSet;
|
|
556
|
+
const processInformation = isRecord(dataset.processInformation) ? dataset.processInformation : {};
|
|
557
|
+
const dataSetInformation = isRecord(processInformation.dataSetInformation)
|
|
558
|
+
? processInformation.dataSetInformation
|
|
559
|
+
: {};
|
|
560
|
+
const nameInfo = isRecord(dataSetInformation.name) ? dataSetInformation.name : {};
|
|
561
|
+
const geography = isRecord(processInformation.geography) ? processInformation.geography : {};
|
|
562
|
+
const location = isRecord(geography.locationOfOperationSupplyOrProduction)
|
|
563
|
+
? geography.locationOfOperationSupplyOrProduction
|
|
564
|
+
: {};
|
|
565
|
+
const technology = isRecord(processInformation.technology) ? processInformation.technology : {};
|
|
566
|
+
const quantitativeReference = isRecord(processInformation.quantitativeReference)
|
|
567
|
+
? processInformation.quantitativeReference
|
|
568
|
+
: {};
|
|
569
|
+
const referenceExchangeInternalId = nonEmptyString(quantitativeReference.referenceToReferenceFlow);
|
|
570
|
+
if (!referenceExchangeInternalId) {
|
|
571
|
+
throw new CliError(`lifecyclemodel auto-build process is missing referenceToReferenceFlow: ${filePath}`, {
|
|
572
|
+
code: 'LIFECYCLEMODEL_AUTO_BUILD_REFERENCE_FLOW_MISSING',
|
|
573
|
+
exitCode: 2,
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
const exchangesCarrier = isRecord(dataset.exchanges) ? dataset.exchanges : {};
|
|
577
|
+
const exchanges = ensureList(exchangesCarrier.exchange).filter(isRecord);
|
|
578
|
+
const referenceExchange = exchanges.find((item) => nonEmptyString(item['@dataSetInternalID']) === referenceExchangeInternalId);
|
|
579
|
+
if (!referenceExchange) {
|
|
580
|
+
throw new CliError(`lifecyclemodel auto-build reference exchange ${referenceExchangeInternalId} not found: ${filePath}`, {
|
|
581
|
+
code: 'LIFECYCLEMODEL_AUTO_BUILD_REFERENCE_EXCHANGE_NOT_FOUND',
|
|
582
|
+
exitCode: 2,
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
const inputAmounts = {};
|
|
586
|
+
const outputAmounts = {};
|
|
587
|
+
exchanges.forEach((exchange) => {
|
|
588
|
+
const flowRef = isRecord(exchange.referenceToFlowDataSet)
|
|
589
|
+
? exchange.referenceToFlowDataSet
|
|
590
|
+
: {};
|
|
591
|
+
const flowUuid = nonEmptyString(flowRef['@refObjectId']);
|
|
592
|
+
if (!flowUuid) {
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
const amount = numberOrZero(exchange.meanAmount ?? exchange.resultingAmount);
|
|
596
|
+
const direction = nonEmptyString(exchange.exchangeDirection);
|
|
597
|
+
if (direction === 'Input') {
|
|
598
|
+
inputAmounts[flowUuid] = amount;
|
|
599
|
+
}
|
|
600
|
+
else if (direction === 'Output') {
|
|
601
|
+
outputAmounts[flowUuid] = amount;
|
|
602
|
+
}
|
|
603
|
+
});
|
|
604
|
+
const processUuid = nonEmptyString(dataSetInformation['common:UUID']);
|
|
605
|
+
if (!processUuid) {
|
|
606
|
+
throw new CliError(`lifecyclemodel auto-build process is missing common:UUID: ${filePath}`, {
|
|
607
|
+
code: 'LIFECYCLEMODEL_AUTO_BUILD_PROCESS_UUID_MISSING',
|
|
608
|
+
exitCode: 2,
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
const publication = resolvePublicationBlock(dataset);
|
|
612
|
+
const version = nonEmptyString(publication['common:dataSetVersion']) ?? '00.00.001';
|
|
613
|
+
const referenceFlowRef = isRecord(referenceExchange.referenceToFlowDataSet)
|
|
614
|
+
? referenceExchange.referenceToFlowDataSet
|
|
615
|
+
: {};
|
|
616
|
+
const referenceFlowUuid = nonEmptyString(referenceFlowRef['@refObjectId']);
|
|
617
|
+
const referenceDirection = nonEmptyString(referenceExchange.exchangeDirection);
|
|
618
|
+
if (!referenceFlowUuid || (referenceDirection !== 'Input' && referenceDirection !== 'Output')) {
|
|
619
|
+
throw new CliError(`lifecyclemodel auto-build reference exchange is incomplete: ${filePath}`, {
|
|
620
|
+
code: 'LIFECYCLEMODEL_AUTO_BUILD_REFERENCE_EXCHANGE_INVALID',
|
|
621
|
+
exitCode: 2,
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
const includedProcessRefs = ensureList(technology.referenceToIncludedProcesses).filter(isRecord);
|
|
625
|
+
const [nameEn, nameZh] = multilangText(nameInfo.baseName);
|
|
626
|
+
const [routeEn] = multilangText(nameInfo.treatmentStandardsRoutes);
|
|
627
|
+
const [mixEn] = multilangText(nameInfo.mixAndLocationTypes);
|
|
628
|
+
const classificationPath = extractClassificationPath(dataSetInformation.classificationInformation);
|
|
629
|
+
return {
|
|
630
|
+
processUuid,
|
|
631
|
+
version,
|
|
632
|
+
raw,
|
|
633
|
+
referenceExchangeInternalId,
|
|
634
|
+
referenceFlowUuid,
|
|
635
|
+
referenceDirection,
|
|
636
|
+
referenceAmount: numberOrZero(referenceExchange.meanAmount ?? referenceExchange.resultingAmount),
|
|
637
|
+
inputAmounts,
|
|
638
|
+
outputAmounts,
|
|
639
|
+
nameEn,
|
|
640
|
+
nameZh,
|
|
641
|
+
routeEn,
|
|
642
|
+
mixEn,
|
|
643
|
+
geographyCode: nonEmptyString(location['@location']) ?? '',
|
|
644
|
+
classificationPath,
|
|
645
|
+
tokenSet: tokenizeText([nameEn, routeEn, mixEn, classificationPath.join(' ')].filter(Boolean).join(' ')),
|
|
646
|
+
sourceKind,
|
|
647
|
+
sourceLabel,
|
|
648
|
+
includedProcessRefCount: includedProcessRefs.length,
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
function scoreEdgeCandidate(src, dst, flowUuid, downstreamAmount) {
|
|
652
|
+
let score = 10;
|
|
653
|
+
const reasons = ['shared flow UUID'];
|
|
654
|
+
if (src.referenceFlowUuid === flowUuid && src.referenceDirection === 'Output') {
|
|
655
|
+
score += 3;
|
|
656
|
+
reasons.push('upstream reference flow matches shared flow');
|
|
657
|
+
}
|
|
658
|
+
if (dst.referenceFlowUuid === flowUuid && dst.referenceDirection === 'Input') {
|
|
659
|
+
score += 3;
|
|
660
|
+
reasons.push('downstream reference flow matches shared flow');
|
|
661
|
+
}
|
|
662
|
+
const classOverlap = classificationOverlap(src.classificationPath, dst.classificationPath);
|
|
663
|
+
if (classOverlap > 0) {
|
|
664
|
+
score += Math.min(classOverlap, 3);
|
|
665
|
+
reasons.push(`classification prefix overlap=${classOverlap}`);
|
|
666
|
+
}
|
|
667
|
+
if (src.geographyCode && dst.geographyCode && src.geographyCode === dst.geographyCode) {
|
|
668
|
+
score += 1;
|
|
669
|
+
reasons.push(`same geography=${src.geographyCode}`);
|
|
670
|
+
}
|
|
671
|
+
const tokenOverlap = [...src.tokenSet].filter((token) => dst.tokenSet.has(token)).length;
|
|
672
|
+
if (tokenOverlap > 0) {
|
|
673
|
+
score += Math.min(tokenOverlap, 4) / 4;
|
|
674
|
+
reasons.push(`token overlap=${tokenOverlap}`);
|
|
675
|
+
}
|
|
676
|
+
const upstreamAmount = src.outputAmounts[flowUuid] ?? 0;
|
|
677
|
+
if (upstreamAmount > 0 && downstreamAmount > 0) {
|
|
678
|
+
const ratio = downstreamAmount / upstreamAmount;
|
|
679
|
+
if (ratio >= 0.5 && ratio <= 2) {
|
|
680
|
+
score += 1;
|
|
681
|
+
reasons.push(`amount ratio plausible=${formatNumber(ratio)}`);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
return { confidence: score, reasons };
|
|
685
|
+
}
|
|
686
|
+
function inferEdges(processMap) {
|
|
687
|
+
const byFlow = new Map();
|
|
688
|
+
Object.entries(processMap).forEach(([processId, record]) => {
|
|
689
|
+
Object.keys(record.outputAmounts).forEach((flowUuid) => {
|
|
690
|
+
const group = byFlow.get(flowUuid) ?? {
|
|
691
|
+
producers: new Set(),
|
|
692
|
+
consumers: new Set(),
|
|
693
|
+
};
|
|
694
|
+
group.producers.add(processId);
|
|
695
|
+
byFlow.set(flowUuid, group);
|
|
696
|
+
});
|
|
697
|
+
Object.keys(record.inputAmounts).forEach((flowUuid) => {
|
|
698
|
+
const group = byFlow.get(flowUuid) ?? {
|
|
699
|
+
producers: new Set(),
|
|
700
|
+
consumers: new Set(),
|
|
701
|
+
};
|
|
702
|
+
group.consumers.add(processId);
|
|
703
|
+
byFlow.set(flowUuid, group);
|
|
704
|
+
});
|
|
705
|
+
});
|
|
706
|
+
const edgeMap = new Map();
|
|
707
|
+
byFlow.forEach((participants, flowUuid) => {
|
|
708
|
+
if (participants.producers.size === 0 || participants.consumers.size === 0) {
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
const passThrough = [...participants.producers].filter((item) => participants.consumers.has(item));
|
|
712
|
+
const candidatePairs = [];
|
|
713
|
+
if (passThrough.length > 0) {
|
|
714
|
+
[...participants.producers]
|
|
715
|
+
.filter((item) => !passThrough.includes(item))
|
|
716
|
+
.forEach((producer) => {
|
|
717
|
+
passThrough.forEach((bridge) => {
|
|
718
|
+
candidatePairs.push([producer, bridge]);
|
|
719
|
+
});
|
|
720
|
+
});
|
|
721
|
+
passThrough.forEach((bridge) => {
|
|
722
|
+
[...participants.consumers]
|
|
723
|
+
.filter((item) => !passThrough.includes(item))
|
|
724
|
+
.forEach((consumer) => {
|
|
725
|
+
candidatePairs.push([bridge, consumer]);
|
|
726
|
+
});
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
else {
|
|
730
|
+
participants.producers.forEach((producer) => {
|
|
731
|
+
participants.consumers.forEach((consumer) => {
|
|
732
|
+
candidatePairs.push([producer, consumer]);
|
|
733
|
+
});
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
candidatePairs.forEach(([src, dst]) => {
|
|
737
|
+
const downstreamAmount = processMap[dst]?.inputAmounts[flowUuid];
|
|
738
|
+
const scored = scoreEdgeCandidate(processMap[src], processMap[dst], flowUuid, downstreamAmount);
|
|
739
|
+
edgeMap.set(`${src}::${dst}::${flowUuid}`, {
|
|
740
|
+
src,
|
|
741
|
+
dst,
|
|
742
|
+
flowUuid,
|
|
743
|
+
downstreamInputAmount: downstreamAmount,
|
|
744
|
+
confidence: scored.confidence,
|
|
745
|
+
reasons: scored.reasons,
|
|
746
|
+
});
|
|
747
|
+
});
|
|
748
|
+
});
|
|
749
|
+
const grouped = new Map();
|
|
750
|
+
[...edgeMap.values()].forEach((edge) => {
|
|
751
|
+
const key = `${edge.dst}::${edge.flowUuid}`;
|
|
752
|
+
const bucket = grouped.get(key) ?? [];
|
|
753
|
+
bucket.push(edge);
|
|
754
|
+
grouped.set(key, bucket);
|
|
755
|
+
});
|
|
756
|
+
const filtered = [];
|
|
757
|
+
grouped.forEach((group) => {
|
|
758
|
+
const ordered = [...group].sort((left, right) => right.confidence - left.confidence || left.src.localeCompare(right.src));
|
|
759
|
+
if (ordered.length >= 2 &&
|
|
760
|
+
ordered[0] &&
|
|
761
|
+
ordered[1] &&
|
|
762
|
+
ordered[0].confidence - ordered[1].confidence >= 2) {
|
|
763
|
+
filtered.push(ordered[0]);
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
filtered.push(...ordered);
|
|
767
|
+
});
|
|
768
|
+
return filtered.sort((left, right) => right.confidence - left.confidence ||
|
|
769
|
+
left.flowUuid.localeCompare(right.flowUuid) ||
|
|
770
|
+
left.src.localeCompare(right.src) ||
|
|
771
|
+
left.dst.localeCompare(right.dst));
|
|
772
|
+
}
|
|
773
|
+
function chooseReferenceProcess(processMap, edges, state, preferredProcessIds) {
|
|
774
|
+
const flowSummary = isRecord(state.flow_summary) ? state.flow_summary : {};
|
|
775
|
+
const targetFlowUuid = nonEmptyString(flowSummary.uuid) ?? '';
|
|
776
|
+
const indegree = new Map();
|
|
777
|
+
const outdegree = new Map();
|
|
778
|
+
edges.forEach((edge) => {
|
|
779
|
+
indegree.set(edge.dst, (indegree.get(edge.dst) ?? 0) + 1);
|
|
780
|
+
outdegree.set(edge.src, (outdegree.get(edge.src) ?? 0) + 1);
|
|
781
|
+
});
|
|
782
|
+
let candidates = Object.entries(processMap)
|
|
783
|
+
.filter(([processId, record]) => preferredProcessIds.has(processId) &&
|
|
784
|
+
record.referenceFlowUuid === targetFlowUuid &&
|
|
785
|
+
record.referenceDirection === 'Output')
|
|
786
|
+
.map(([processId]) => processId);
|
|
787
|
+
if (candidates.length === 0 && targetFlowUuid) {
|
|
788
|
+
candidates = Object.entries(processMap)
|
|
789
|
+
.filter(([processId, record]) => preferredProcessIds.has(processId) && targetFlowUuid in record.outputAmounts)
|
|
790
|
+
.map(([processId]) => processId);
|
|
791
|
+
}
|
|
792
|
+
if (candidates.length === 0) {
|
|
793
|
+
candidates = [...preferredProcessIds].sort();
|
|
794
|
+
}
|
|
795
|
+
const buildRankKey = (processId) => {
|
|
796
|
+
const record = processMap[processId];
|
|
797
|
+
const targetRank = Number(Object.prototype.hasOwnProperty.call(record.outputAmounts, targetFlowUuid));
|
|
798
|
+
const terminalRank = Number((outdegree.get(processId) ?? 0) === 0);
|
|
799
|
+
const indegreeRank = String(indegree.get(processId) ?? 0).padStart(8, '0');
|
|
800
|
+
return `${targetRank}|${terminalRank}|${indegreeRank}|${processId}`;
|
|
801
|
+
};
|
|
802
|
+
return [...candidates]
|
|
803
|
+
.sort((left, right) => buildRankKey(left).localeCompare(buildRankKey(right)))
|
|
804
|
+
.pop();
|
|
805
|
+
}
|
|
806
|
+
function collectReachable(finalProcessId, edges) {
|
|
807
|
+
const reverseAdj = new Map();
|
|
808
|
+
edges.forEach((edge) => {
|
|
809
|
+
const bucket = reverseAdj.get(edge.dst) ?? [];
|
|
810
|
+
bucket.push(edge.src);
|
|
811
|
+
reverseAdj.set(edge.dst, bucket);
|
|
812
|
+
});
|
|
813
|
+
const reachable = new Set([finalProcessId]);
|
|
814
|
+
const queue = [finalProcessId];
|
|
815
|
+
while (queue.length > 0) {
|
|
816
|
+
const current = queue.shift();
|
|
817
|
+
(reverseAdj.get(current) ?? []).forEach((upstream) => {
|
|
818
|
+
if (reachable.has(upstream)) {
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
reachable.add(upstream);
|
|
822
|
+
queue.push(upstream);
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
return reachable;
|
|
826
|
+
}
|
|
827
|
+
function topologicalOrder(processIds, edges) {
|
|
828
|
+
const indegree = new Map();
|
|
829
|
+
const adj = new Map();
|
|
830
|
+
[...processIds].forEach((processId) => {
|
|
831
|
+
indegree.set(processId, 0);
|
|
832
|
+
});
|
|
833
|
+
edges.forEach((edge) => {
|
|
834
|
+
if (!processIds.has(edge.src) || !processIds.has(edge.dst)) {
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
indegree.set(edge.dst, indegree.get(edge.dst) + 1);
|
|
838
|
+
const bucket = adj.get(edge.src) ?? [];
|
|
839
|
+
bucket.push(edge.dst);
|
|
840
|
+
adj.set(edge.src, bucket);
|
|
841
|
+
});
|
|
842
|
+
const queue = [...processIds]
|
|
843
|
+
.filter((processId) => indegree.get(processId) === 0)
|
|
844
|
+
.sort();
|
|
845
|
+
const ordered = [];
|
|
846
|
+
while (queue.length > 0) {
|
|
847
|
+
const current = queue.shift();
|
|
848
|
+
ordered.push(current);
|
|
849
|
+
(adj.get(current) ?? []).sort().forEach((downstream) => {
|
|
850
|
+
indegree.set(downstream, indegree.get(downstream) - 1);
|
|
851
|
+
if (indegree.get(downstream) === 0) {
|
|
852
|
+
queue.push(downstream);
|
|
853
|
+
queue.sort();
|
|
854
|
+
}
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
if (ordered.length !== processIds.size) {
|
|
858
|
+
[...processIds]
|
|
859
|
+
.filter((processId) => !ordered.includes(processId))
|
|
860
|
+
.sort()
|
|
861
|
+
.forEach((processId) => ordered.push(processId));
|
|
862
|
+
}
|
|
863
|
+
return ordered;
|
|
864
|
+
}
|
|
865
|
+
function computeMultiplicationFactors(processMap, reachable, edges, order, finalProcessId) {
|
|
866
|
+
const factors = {};
|
|
867
|
+
[...reachable].forEach((processId) => {
|
|
868
|
+
factors[processId] = 0;
|
|
869
|
+
});
|
|
870
|
+
factors[finalProcessId] = 1;
|
|
871
|
+
const incoming = new Map();
|
|
872
|
+
edges.forEach((edge) => {
|
|
873
|
+
if (!reachable.has(edge.src) || !reachable.has(edge.dst)) {
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
const bucket = incoming.get(edge.dst) ?? [];
|
|
877
|
+
bucket.push(edge);
|
|
878
|
+
incoming.set(edge.dst, bucket);
|
|
879
|
+
});
|
|
880
|
+
[...order].reverse().forEach((current) => {
|
|
881
|
+
const currentFactor = factors[current];
|
|
882
|
+
if (Math.abs(currentFactor) < EPSILON) {
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
(incoming.get(current) ?? []).forEach((edge) => {
|
|
886
|
+
const upstreamOutput = processMap[edge.src]?.outputAmounts[edge.flowUuid] ?? 1;
|
|
887
|
+
const safeUpstreamOutput = Math.abs(upstreamOutput) < EPSILON ? 1 : upstreamOutput;
|
|
888
|
+
const delta = (currentFactor * edge.downstreamInputAmount) / safeUpstreamOutput;
|
|
889
|
+
factors[edge.src] += delta;
|
|
890
|
+
});
|
|
891
|
+
});
|
|
892
|
+
return factors;
|
|
893
|
+
}
|
|
894
|
+
function exchangeAmount(exchange) {
|
|
895
|
+
return numberOrZero(exchange.meanAmount ?? exchange.resultingAmount);
|
|
896
|
+
}
|
|
897
|
+
function cloneExchangeWithAmount(exchange, amount, internalId, quantitativeReference = false) {
|
|
898
|
+
const cloned = copyJson(exchange);
|
|
899
|
+
cloned['@dataSetInternalID'] = internalId;
|
|
900
|
+
const amountValue = toJsonNumber(amount);
|
|
901
|
+
cloned.meanAmount = amountValue;
|
|
902
|
+
if ('resultingAmount' in cloned) {
|
|
903
|
+
cloned.resultingAmount = amountValue;
|
|
904
|
+
}
|
|
905
|
+
cloned.quantitativeReference = quantitativeReference;
|
|
906
|
+
return cloned;
|
|
907
|
+
}
|
|
908
|
+
function buildProcessInstances(processMap, order, edges, factors) {
|
|
909
|
+
const internalIds = Object.fromEntries(order.map((processId, index) => [processId, String(index + 1)]));
|
|
910
|
+
const outgoing = new Map();
|
|
911
|
+
edges.forEach((edge) => {
|
|
912
|
+
if (!(edge.src in internalIds) || !(edge.dst in internalIds)) {
|
|
913
|
+
return;
|
|
914
|
+
}
|
|
915
|
+
const bucket = outgoing.get(edge.src) ?? [];
|
|
916
|
+
bucket.push(edge);
|
|
917
|
+
outgoing.set(edge.src, bucket);
|
|
918
|
+
});
|
|
919
|
+
const processInstances = order.map((processId) => {
|
|
920
|
+
const record = processMap[processId];
|
|
921
|
+
const dataset = record.raw.processDataSet;
|
|
922
|
+
const processInformation = isRecord(dataset.processInformation)
|
|
923
|
+
? dataset.processInformation
|
|
924
|
+
: {};
|
|
925
|
+
const dataSetInformation = isRecord(processInformation.dataSetInformation)
|
|
926
|
+
? processInformation.dataSetInformation
|
|
927
|
+
: {};
|
|
928
|
+
const nameInfo = isRecord(dataSetInformation.name) ? dataSetInformation.name : {};
|
|
929
|
+
const shortDescription = copyJson(nameInfo.baseName ?? []);
|
|
930
|
+
const outputGroups = [];
|
|
931
|
+
const byFlow = new Map();
|
|
932
|
+
(outgoing.get(processId) ?? []).forEach((edge) => {
|
|
933
|
+
const bucket = byFlow.get(edge.flowUuid) ?? [];
|
|
934
|
+
bucket.push(edge);
|
|
935
|
+
byFlow.set(edge.flowUuid, bucket);
|
|
936
|
+
});
|
|
937
|
+
byFlow.forEach((groupedEdges, flowUuid) => {
|
|
938
|
+
const downstreamPayload = groupedEdges.map((edge) => ({
|
|
939
|
+
'@id': internalIds[edge.dst],
|
|
940
|
+
'@flowUUID': flowUuid,
|
|
941
|
+
'@dominant': 'true',
|
|
942
|
+
}));
|
|
943
|
+
outputGroups.push({
|
|
944
|
+
'@dominant': 'true',
|
|
945
|
+
'@flowUUID': flowUuid,
|
|
946
|
+
downstreamProcess: downstreamPayload.length === 1 ? downstreamPayload[0] : downstreamPayload,
|
|
947
|
+
});
|
|
948
|
+
});
|
|
949
|
+
return {
|
|
950
|
+
'@dataSetInternalID': internalIds[processId],
|
|
951
|
+
'@multiplicationFactor': formatNumber(factors[processId] ?? 0),
|
|
952
|
+
referenceToProcess: buildGlobalReference(record.processUuid, record.version, shortDescription, 'process data set', `../processes/${record.processUuid}_${record.version}.xml`),
|
|
953
|
+
...(outputGroups.length > 0
|
|
954
|
+
? {
|
|
955
|
+
connections: {
|
|
956
|
+
outputExchange: outputGroups.length === 1 ? outputGroups[0] : outputGroups,
|
|
957
|
+
},
|
|
958
|
+
}
|
|
959
|
+
: {}),
|
|
960
|
+
};
|
|
961
|
+
});
|
|
962
|
+
return { processInstances, internalIds };
|
|
963
|
+
}
|
|
964
|
+
function buildLifecycleModelDataset(runName, state, processMap, order, edges, factors, finalProcessId) {
|
|
965
|
+
const finalRecord = processMap[finalProcessId];
|
|
966
|
+
const finalDataset = isRecord(finalRecord.raw.processDataSet)
|
|
967
|
+
? finalRecord.raw.processDataSet
|
|
968
|
+
: {};
|
|
969
|
+
const finalProcessInformation = isRecord(finalDataset.processInformation)
|
|
970
|
+
? finalDataset.processInformation
|
|
971
|
+
: {};
|
|
972
|
+
const finalInfo = isRecord(finalProcessInformation.dataSetInformation)
|
|
973
|
+
? finalProcessInformation.dataSetInformation
|
|
974
|
+
: {};
|
|
975
|
+
const finalName = isRecord(finalInfo.name) ? finalInfo.name : {};
|
|
976
|
+
const flowDataset = extractFlowDatasetFromState(state);
|
|
977
|
+
const flowInformation = isRecord(flowDataset.flowInformation) ? flowDataset.flowInformation : {};
|
|
978
|
+
const flowInfo = isRecord(flowInformation.dataSetInformation)
|
|
979
|
+
? flowInformation.dataSetInformation
|
|
980
|
+
: {};
|
|
981
|
+
const flowName = isRecord(flowInfo.name) ? flowInfo.name : {};
|
|
982
|
+
const flowSummary = isRecord(state.flow_summary) ? state.flow_summary : {};
|
|
983
|
+
const modelUuid = uuid5FromText(UUID_NAMESPACE_URL, runName);
|
|
984
|
+
const modelVersion = '01.01.000';
|
|
985
|
+
const { processInstances, internalIds } = buildProcessInstances(processMap, order, edges, factors);
|
|
986
|
+
const administrativeInformation = isRecord(finalDataset.administrativeInformation)
|
|
987
|
+
? finalDataset.administrativeInformation
|
|
988
|
+
: {};
|
|
989
|
+
const commissionerAndGoal = copyJson(administrativeInformation['common:commissionerAndGoal'] ??
|
|
990
|
+
administrativeInformation.commissionerAndGoal ??
|
|
991
|
+
{});
|
|
992
|
+
const dataEntryBy = copyJson(administrativeInformation.dataEntryBy ?? administrativeInformation['common:dataEntryBy'] ?? {});
|
|
993
|
+
if (isRecord(dataEntryBy)) {
|
|
994
|
+
const enteringRef = dataEntryBy['common:referenceToPersonOrEntityEnteringTheData'];
|
|
995
|
+
if (enteringRef !== undefined) {
|
|
996
|
+
dataEntryBy['common:referenceToPersonOrEntityEnteringTheDataSet'] = copyJson(enteringRef);
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
const publication = copyJson(resolvePublicationBlock(finalDataset));
|
|
1000
|
+
publication['common:dataSetVersion'] = modelVersion;
|
|
1001
|
+
publication['common:permanentDataSetURI'] =
|
|
1002
|
+
`https://local.tiangong.invalid/lifecyclemodels/${modelUuid}?version=${modelVersion}`;
|
|
1003
|
+
if (!Array.isArray(publication['common:accessRestrictions'])) {
|
|
1004
|
+
publication['common:accessRestrictions'] = [];
|
|
1005
|
+
}
|
|
1006
|
+
const modellingAndValidation = isRecord(finalDataset.modellingAndValidation)
|
|
1007
|
+
? finalDataset.modellingAndValidation
|
|
1008
|
+
: {};
|
|
1009
|
+
const complianceDeclarations = isRecord(modellingAndValidation.complianceDeclarations)
|
|
1010
|
+
? modellingAndValidation.complianceDeclarations
|
|
1011
|
+
: {};
|
|
1012
|
+
const compliance = copyJson(isRecord(complianceDeclarations.compliance) ? complianceDeclarations.compliance : {});
|
|
1013
|
+
compliance['common:approvalOfOverallCompliance'] ??= 'Fully compliant';
|
|
1014
|
+
compliance['common:nomenclatureCompliance'] ??= 'Not defined';
|
|
1015
|
+
compliance['common:methodologicalCompliance'] ??= 'Not defined';
|
|
1016
|
+
compliance['common:reviewCompliance'] ??= 'Not defined';
|
|
1017
|
+
compliance['common:documentationCompliance'] ??= 'Not defined';
|
|
1018
|
+
compliance['common:qualityCompliance'] ??= 'Not defined';
|
|
1019
|
+
const reviewRef = (isRecord(commissionerAndGoal)
|
|
1020
|
+
? commissionerAndGoal['common:referenceToCommissioner']
|
|
1021
|
+
: undefined) ??
|
|
1022
|
+
(isRecord(dataEntryBy)
|
|
1023
|
+
? dataEntryBy['common:referenceToPersonOrEntityEnteringTheDataSet']
|
|
1024
|
+
: undefined) ??
|
|
1025
|
+
publication['common:referenceToOwnershipOfDataSet'] ??
|
|
1026
|
+
{};
|
|
1027
|
+
const review = {
|
|
1028
|
+
'common:referenceToNameOfReviewerAndInstitution': copyJson(reviewRef),
|
|
1029
|
+
'common:otherReviewDetails': multilangFromText('Local native CLI lifecyclemodel auto-build artifact; not independently reviewed.', '本地原生 CLI lifecyclemodel auto-build 产物,未经过独立评审。'),
|
|
1030
|
+
};
|
|
1031
|
+
const generalComment = ensureList(finalInfo['common:generalComment']).filter((item) => item !== undefined);
|
|
1032
|
+
generalComment.push(...multilangFromText(`Built locally from process-automated-builder run ${runName}. ${nonEmptyString(state.technical_description) ?? ''}`.trim(), `本地基于 process-automated-builder 运行 ${runName} 生成。`));
|
|
1033
|
+
generalComment.push(...multilangFromText('This CLI slice emits native json_ordered only. Platform-specific derivations stay in later publish flows.', '该 CLI 切片只产出原生 json_ordered。平台特有衍生字段保留在后续发布流程。'));
|
|
1034
|
+
const resultingProcess = buildGlobalReference(modelUuid, modelVersion, copyJson(flowName.baseName ?? finalName.baseName ?? []), 'process data set', `../processes/${modelUuid}_${modelVersion}.xml`);
|
|
1035
|
+
const model = {
|
|
1036
|
+
lifeCycleModelDataSet: {
|
|
1037
|
+
'@xmlns': 'http://eplca.jrc.ec.europa.eu/ILCD/LifeCycleModel/2017',
|
|
1038
|
+
'@xmlns:common': 'http://lca.jrc.it/ILCD/Common',
|
|
1039
|
+
'@xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
|
|
1040
|
+
'@version': '1.1',
|
|
1041
|
+
'@xsi:schemaLocation': 'http://eplca.jrc.ec.europa.eu/ILCD/LifeCycleModel/2017 ../../schemas/ILCD_LifeCycleModelDataSet.xsd',
|
|
1042
|
+
lifeCycleModelInformation: {
|
|
1043
|
+
dataSetInformation: {
|
|
1044
|
+
'common:UUID': modelUuid,
|
|
1045
|
+
name: {
|
|
1046
|
+
baseName: copyJson(flowName.baseName ?? finalName.baseName ?? []),
|
|
1047
|
+
treatmentStandardsRoutes: copyJson(flowName.treatmentStandardsRoutes ?? finalName.treatmentStandardsRoutes ?? []),
|
|
1048
|
+
mixAndLocationTypes: copyJson(flowName.mixAndLocationTypes ?? finalName.mixAndLocationTypes ?? []),
|
|
1049
|
+
functionalUnitFlowProperties: multilangFromText(`Reference process output scaled to 1 unit of ${nonEmptyString(flowSummary.base_name_en) ?? nonEmptyString(flowSummary.base_name) ?? firstText(flowName.baseName)}`, `参考过程输出缩放到 1 单位 ${nonEmptyString(flowSummary.base_name_zh) ?? nonEmptyString(flowSummary.base_name) ?? ''}`),
|
|
1050
|
+
},
|
|
1051
|
+
classificationInformation: copyJson(finalInfo.classificationInformation ?? {}),
|
|
1052
|
+
referenceToResultingProcess: resultingProcess,
|
|
1053
|
+
'common:generalComment': generalComment,
|
|
1054
|
+
},
|
|
1055
|
+
quantitativeReference: {
|
|
1056
|
+
referenceToReferenceProcess: internalIds[finalProcessId],
|
|
1057
|
+
},
|
|
1058
|
+
technology: {
|
|
1059
|
+
processes: {
|
|
1060
|
+
processInstance: processInstances.length === 1 ? processInstances[0] : processInstances,
|
|
1061
|
+
},
|
|
1062
|
+
},
|
|
1063
|
+
},
|
|
1064
|
+
modellingAndValidation: {
|
|
1065
|
+
dataSourcesTreatmentEtc: {
|
|
1066
|
+
useAdviceForDataSet: multilangFromText(nonEmptyString(state.scope) ??
|
|
1067
|
+
'Built from local process exports and shared-flow graph inference.', '基于本地 process 导出结果和共享 flow 图推断生成。'),
|
|
1068
|
+
},
|
|
1069
|
+
validation: {
|
|
1070
|
+
review,
|
|
1071
|
+
},
|
|
1072
|
+
complianceDeclarations: {
|
|
1073
|
+
compliance,
|
|
1074
|
+
},
|
|
1075
|
+
},
|
|
1076
|
+
administrativeInformation: {
|
|
1077
|
+
'common:commissionerAndGoal': commissionerAndGoal,
|
|
1078
|
+
dataEntryBy,
|
|
1079
|
+
publicationAndOwnership: publication,
|
|
1080
|
+
},
|
|
1081
|
+
},
|
|
1082
|
+
};
|
|
1083
|
+
return {
|
|
1084
|
+
model,
|
|
1085
|
+
summary: {
|
|
1086
|
+
run_name: runName,
|
|
1087
|
+
model_uuid: modelUuid,
|
|
1088
|
+
model_version: modelVersion,
|
|
1089
|
+
reference_process_uuid: finalProcessId,
|
|
1090
|
+
reference_process_internal_id: internalIds[finalProcessId],
|
|
1091
|
+
reference_flow_uuid: nonEmptyString(flowSummary.uuid),
|
|
1092
|
+
reference_to_resulting_process_uuid: modelUuid,
|
|
1093
|
+
process_count: order.length,
|
|
1094
|
+
edge_count: edges.length,
|
|
1095
|
+
multiplication_factors: Object.fromEntries(order.map((processId) => [processId, formatNumber(factors[processId] ?? 0)])),
|
|
1096
|
+
ordered_processes: order.map((processId) => {
|
|
1097
|
+
const record = processMap[processId];
|
|
1098
|
+
return {
|
|
1099
|
+
process_uuid: processId,
|
|
1100
|
+
name_en: record.nameEn,
|
|
1101
|
+
name_zh: record.nameZh,
|
|
1102
|
+
route_en: record.routeEn,
|
|
1103
|
+
geography_code: record.geographyCode,
|
|
1104
|
+
classification_path: record.classificationPath,
|
|
1105
|
+
};
|
|
1106
|
+
}),
|
|
1107
|
+
source_counts: {
|
|
1108
|
+
local_run_export: Object.keys(processMap).length,
|
|
1109
|
+
used_local_run_processes: order.length,
|
|
1110
|
+
},
|
|
1111
|
+
},
|
|
1112
|
+
};
|
|
1113
|
+
}
|
|
1114
|
+
function buildLocalModels(normalized, layout) {
|
|
1115
|
+
return normalized.local_runs.map((runDir) => {
|
|
1116
|
+
const statePath = path.join(runDir, 'cache', 'process_from_flow_state.json');
|
|
1117
|
+
const processDir = path.join(runDir, 'exports', 'processes');
|
|
1118
|
+
if (!existsSync(statePath)) {
|
|
1119
|
+
throw new CliError(`lifecyclemodel auto-build run is missing state file: ${runDir}`, {
|
|
1120
|
+
code: 'LIFECYCLEMODEL_AUTO_BUILD_STATE_NOT_FOUND',
|
|
1121
|
+
exitCode: 2,
|
|
1122
|
+
});
|
|
1123
|
+
}
|
|
1124
|
+
if (!existsSync(processDir)) {
|
|
1125
|
+
throw new CliError(`lifecyclemodel auto-build run is missing exported processes: ${runDir}`, {
|
|
1126
|
+
code: 'LIFECYCLEMODEL_AUTO_BUILD_PROCESS_EXPORTS_NOT_FOUND',
|
|
1127
|
+
exitCode: 2,
|
|
1128
|
+
});
|
|
1129
|
+
}
|
|
1130
|
+
const state = readJsonInput(statePath);
|
|
1131
|
+
if (!isRecord(state)) {
|
|
1132
|
+
throw new CliError(`lifecyclemodel auto-build state must be a JSON object: ${statePath}`, {
|
|
1133
|
+
code: 'LIFECYCLEMODEL_AUTO_BUILD_STATE_INVALID',
|
|
1134
|
+
exitCode: 2,
|
|
1135
|
+
});
|
|
1136
|
+
}
|
|
1137
|
+
const processFiles = readdirSync(processDir)
|
|
1138
|
+
.filter((item) => item.endsWith('.json'))
|
|
1139
|
+
.sort()
|
|
1140
|
+
.map((item) => path.join(processDir, item));
|
|
1141
|
+
if (processFiles.length === 0) {
|
|
1142
|
+
throw new CliError(`lifecyclemodel auto-build run has no exported process JSON files: ${runDir}`, {
|
|
1143
|
+
code: 'LIFECYCLEMODEL_AUTO_BUILD_PROCESS_EXPORTS_EMPTY',
|
|
1144
|
+
exitCode: 2,
|
|
1145
|
+
});
|
|
1146
|
+
}
|
|
1147
|
+
const processMap = Object.fromEntries(processFiles.map((filePath) => {
|
|
1148
|
+
const record = loadProcessRecord(filePath, 'local_run_export', runDir);
|
|
1149
|
+
return [record.processUuid, record];
|
|
1150
|
+
}));
|
|
1151
|
+
const edges = inferEdges(processMap);
|
|
1152
|
+
const preferredProcessIds = new Set(Object.keys(processMap));
|
|
1153
|
+
const finalProcessId = chooseReferenceProcess(processMap, edges, state, preferredProcessIds);
|
|
1154
|
+
const reachable = collectReachable(finalProcessId, edges);
|
|
1155
|
+
const order = topologicalOrder(reachable, edges);
|
|
1156
|
+
const filteredEdges = edges.filter((edge) => reachable.has(edge.src) && reachable.has(edge.dst));
|
|
1157
|
+
const factors = computeMultiplicationFactors(processMap, reachable, filteredEdges, order, finalProcessId);
|
|
1158
|
+
const runName = path.basename(runDir);
|
|
1159
|
+
const { model, summary } = buildLifecycleModelDataset(runName, state, processMap, order, filteredEdges, factors, finalProcessId);
|
|
1160
|
+
const runOut = path.join(layout.modelsDir, runName);
|
|
1161
|
+
const bundleOut = path.join(runOut, 'tidas_bundle', 'lifecyclemodels');
|
|
1162
|
+
mkdirSync(bundleOut, { recursive: true });
|
|
1163
|
+
const modelUuid = summary.model_uuid;
|
|
1164
|
+
const modelVersion = summary.model_version;
|
|
1165
|
+
const modelFile = path.join(bundleOut, `${modelUuid}_${modelVersion}.json`);
|
|
1166
|
+
const summaryFile = path.join(runOut, 'summary.json');
|
|
1167
|
+
const connectionsFile = path.join(runOut, 'connections.json');
|
|
1168
|
+
const processCatalogFile = path.join(runOut, 'process-catalog.json');
|
|
1169
|
+
writeJsonArtifact(modelFile, model);
|
|
1170
|
+
writeJsonArtifact(summaryFile, summary);
|
|
1171
|
+
writeJsonArtifact(connectionsFile, filteredEdges.map((edge) => ({
|
|
1172
|
+
src: edge.src,
|
|
1173
|
+
dst: edge.dst,
|
|
1174
|
+
flow_uuid: edge.flowUuid,
|
|
1175
|
+
downstream_input_amount: formatNumber(edge.downstreamInputAmount),
|
|
1176
|
+
confidence: formatNumber(edge.confidence),
|
|
1177
|
+
reasons: edge.reasons,
|
|
1178
|
+
src_name_en: processMap[edge.src].nameEn,
|
|
1179
|
+
dst_name_en: processMap[edge.dst].nameEn,
|
|
1180
|
+
src_source_kind: processMap[edge.src].sourceKind,
|
|
1181
|
+
dst_source_kind: processMap[edge.dst].sourceKind,
|
|
1182
|
+
})));
|
|
1183
|
+
writeJsonArtifact(processCatalogFile, Object.entries(processMap)
|
|
1184
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
1185
|
+
.map(([processId, record]) => ({
|
|
1186
|
+
process_uuid: processId,
|
|
1187
|
+
name_en: record.nameEn,
|
|
1188
|
+
name_zh: record.nameZh,
|
|
1189
|
+
route_en: record.routeEn,
|
|
1190
|
+
mix_en: record.mixEn,
|
|
1191
|
+
geography_code: record.geographyCode,
|
|
1192
|
+
classification_path: record.classificationPath,
|
|
1193
|
+
reference_flow_uuid: record.referenceFlowUuid,
|
|
1194
|
+
reference_direction: record.referenceDirection,
|
|
1195
|
+
reference_amount: formatNumber(record.referenceAmount),
|
|
1196
|
+
input_flow_count: Object.keys(record.inputAmounts).length,
|
|
1197
|
+
output_flow_count: Object.keys(record.outputAmounts).length,
|
|
1198
|
+
source_kind: record.sourceKind,
|
|
1199
|
+
source_label: record.sourceLabel,
|
|
1200
|
+
included_process_ref_count: record.includedProcessRefCount,
|
|
1201
|
+
})));
|
|
1202
|
+
return {
|
|
1203
|
+
run_dir: runDir,
|
|
1204
|
+
run_name: runName,
|
|
1205
|
+
model_file: modelFile,
|
|
1206
|
+
summary_file: summaryFile,
|
|
1207
|
+
connections_file: connectionsFile,
|
|
1208
|
+
process_catalog_file: processCatalogFile,
|
|
1209
|
+
summary,
|
|
1210
|
+
};
|
|
1211
|
+
});
|
|
1212
|
+
}
|
|
1213
|
+
function buildNextActions(layout) {
|
|
1214
|
+
return [
|
|
1215
|
+
`inspect: ${layout.runPlanPath}`,
|
|
1216
|
+
`inspect: ${layout.modelsDir}`,
|
|
1217
|
+
`run: tiangong lifecyclemodel validate-build --run-dir ${layout.runRoot}`,
|
|
1218
|
+
`run: tiangong lifecyclemodel publish-build --run-dir ${layout.runRoot}`,
|
|
1219
|
+
];
|
|
1220
|
+
}
|
|
1221
|
+
function buildReport(normalized, layout, localBuildReports, now) {
|
|
1222
|
+
return {
|
|
1223
|
+
schema_version: 1,
|
|
1224
|
+
generated_at_utc: now.toISOString(),
|
|
1225
|
+
status: 'completed_local_lifecyclemodel_auto_build_run',
|
|
1226
|
+
request_path: normalized.request_path,
|
|
1227
|
+
run_id: normalized.run_id,
|
|
1228
|
+
run_root: normalized.run_root,
|
|
1229
|
+
local_run_count: normalized.local_runs.length,
|
|
1230
|
+
built_model_count: localBuildReports.length,
|
|
1231
|
+
files: {
|
|
1232
|
+
request_snapshot: layout.requestSnapshotPath,
|
|
1233
|
+
normalized_request: layout.normalizedRequestPath,
|
|
1234
|
+
run_plan: layout.runPlanPath,
|
|
1235
|
+
resolved_manifest: layout.resolvedManifestPath,
|
|
1236
|
+
selection_brief: layout.selectionBriefPath,
|
|
1237
|
+
reference_model_summary: layout.referenceModelSummaryPath,
|
|
1238
|
+
invocation_index: layout.invocationIndexPath,
|
|
1239
|
+
run_manifest: layout.runManifestPath,
|
|
1240
|
+
report: layout.reportPath,
|
|
1241
|
+
},
|
|
1242
|
+
local_build_reports: localBuildReports,
|
|
1243
|
+
next_actions: buildNextActions(layout),
|
|
1244
|
+
};
|
|
1245
|
+
}
|
|
1246
|
+
export async function runLifecyclemodelAutoBuild(options) {
|
|
1247
|
+
const input = options.inputValue ?? readJsonInput(options.inputPath);
|
|
1248
|
+
const now = options.now ?? new Date();
|
|
1249
|
+
const normalized = normalizeLifecyclemodelAutoBuildRequest(input, {
|
|
1250
|
+
inputPath: options.inputPath,
|
|
1251
|
+
outDir: options.outDir,
|
|
1252
|
+
now,
|
|
1253
|
+
runIdOverride: options.runIdOverride,
|
|
1254
|
+
});
|
|
1255
|
+
const layout = buildLayout(normalized.run_root, normalized.run_id);
|
|
1256
|
+
ensureEmptyRunRoot(layout.runRoot);
|
|
1257
|
+
ensureLayout(layout);
|
|
1258
|
+
writeJsonArtifact(layout.requestSnapshotPath, input);
|
|
1259
|
+
writeJsonArtifact(layout.normalizedRequestPath, normalized);
|
|
1260
|
+
writeTextArtifact(layout.selectionBriefPath, buildSelectionBrief(normalized.manifest));
|
|
1261
|
+
writeJsonArtifact(layout.referenceModelSummaryPath, buildReferenceModelSummary(normalized.manifest));
|
|
1262
|
+
writeJsonArtifact(layout.resolvedManifestPath, normalized.manifest);
|
|
1263
|
+
writeJsonArtifact(layout.invocationIndexPath, buildInvocationIndex(normalized, options, layout, now));
|
|
1264
|
+
writeJsonArtifact(layout.runManifestPath, buildRunManifest({
|
|
1265
|
+
layout,
|
|
1266
|
+
command: options.outDir
|
|
1267
|
+
? [
|
|
1268
|
+
'lifecyclemodel',
|
|
1269
|
+
'auto-build',
|
|
1270
|
+
'--input',
|
|
1271
|
+
options.inputPath,
|
|
1272
|
+
'--out-dir',
|
|
1273
|
+
options.outDir,
|
|
1274
|
+
]
|
|
1275
|
+
: ['lifecyclemodel', 'auto-build', '--input', options.inputPath],
|
|
1276
|
+
cwd: options.cwd,
|
|
1277
|
+
createdAt: now,
|
|
1278
|
+
}));
|
|
1279
|
+
const plan = buildPlan(normalized, layout, now);
|
|
1280
|
+
writeJsonArtifact(layout.runPlanPath, plan);
|
|
1281
|
+
const localBuildReports = buildLocalModels(normalized, layout);
|
|
1282
|
+
const completedPlan = {
|
|
1283
|
+
...plan,
|
|
1284
|
+
local_build_reports: localBuildReports,
|
|
1285
|
+
};
|
|
1286
|
+
writeJsonArtifact(layout.runPlanPath, completedPlan);
|
|
1287
|
+
const report = buildReport(normalized, layout, localBuildReports, now);
|
|
1288
|
+
writeJsonArtifact(layout.reportPath, report);
|
|
1289
|
+
writeLatestRunId(layout, normalized.run_id);
|
|
1290
|
+
return report;
|
|
1291
|
+
}
|
|
1292
|
+
export const __testInternals = {
|
|
1293
|
+
DEFAULT_MANIFEST,
|
|
1294
|
+
copyJson,
|
|
1295
|
+
ensureList,
|
|
1296
|
+
numberOrZero,
|
|
1297
|
+
firstText,
|
|
1298
|
+
langTextMap,
|
|
1299
|
+
localizedText,
|
|
1300
|
+
multilangText,
|
|
1301
|
+
multilangFromText,
|
|
1302
|
+
deepMerge,
|
|
1303
|
+
buildLayout,
|
|
1304
|
+
buildSelectionBrief,
|
|
1305
|
+
buildReferenceModelSummary,
|
|
1306
|
+
normalizeLocalRuns,
|
|
1307
|
+
uuid5FromText,
|
|
1308
|
+
extractClassificationPath,
|
|
1309
|
+
classificationOverlap,
|
|
1310
|
+
extractFlowDatasetFromState,
|
|
1311
|
+
resolvePublicationBlock,
|
|
1312
|
+
loadProcessRecord,
|
|
1313
|
+
scoreEdgeCandidate,
|
|
1314
|
+
inferEdges,
|
|
1315
|
+
chooseReferenceProcess,
|
|
1316
|
+
collectReachable,
|
|
1317
|
+
topologicalOrder,
|
|
1318
|
+
computeMultiplicationFactors,
|
|
1319
|
+
exchangeAmount,
|
|
1320
|
+
cloneExchangeWithAmount,
|
|
1321
|
+
buildProcessInstances,
|
|
1322
|
+
buildLifecycleModelDataset,
|
|
1323
|
+
buildPlan,
|
|
1324
|
+
formatNumber,
|
|
1325
|
+
buildNameSummary,
|
|
1326
|
+
toJsonNumber,
|
|
1327
|
+
};
|
|
1328
|
+
//# sourceMappingURL=lifecyclemodel-auto-build.js.map
|