@tiangong-lca/cli 0.0.7 → 0.0.9
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 +97 -16
- package/dist/src/cli.js +1429 -131
- package/dist/src/cli.js.map +1 -1
- package/dist/src/lib/dataset-author.js +100 -0
- package/dist/src/lib/dataset-author.js.map +1 -0
- package/dist/src/lib/dataset-bilingual.js +545 -0
- package/dist/src/lib/dataset-bilingual.js.map +1 -0
- package/dist/src/lib/dataset-contract.js +350 -0
- package/dist/src/lib/dataset-contract.js.map +1 -0
- package/dist/src/lib/dataset-evidence-search.js +636 -0
- package/dist/src/lib/dataset-evidence-search.js.map +1 -0
- package/dist/src/lib/dataset-import-lca.js +171 -0
- package/dist/src/lib/dataset-import-lca.js.map +1 -0
- package/dist/src/lib/dataset-remote-refresh.js +166 -0
- package/dist/src/lib/dataset-remote-refresh.js.map +1 -0
- package/dist/src/lib/dataset-remote-verify.js +543 -0
- package/dist/src/lib/dataset-remote-verify.js.map +1 -0
- package/dist/src/lib/dataset-validate.js +63 -7
- package/dist/src/lib/dataset-validate.js.map +1 -1
- package/dist/src/lib/flow-fetch-rows.js +12 -12
- package/dist/src/lib/flow-fetch-rows.js.map +1 -1
- package/dist/src/lib/flow-payload-validation.js +51 -0
- package/dist/src/lib/flow-payload-validation.js.map +1 -0
- package/dist/src/lib/flow-publish-reviewed-data.js +16 -0
- package/dist/src/lib/flow-publish-reviewed-data.js.map +1 -1
- package/dist/src/lib/flow-publish-version.js +182 -12
- package/dist/src/lib/flow-publish-version.js.map +1 -1
- package/dist/src/lib/{review-flow.js → flow-qa.js} +86 -28
- package/dist/src/lib/flow-qa.js.map +1 -0
- package/dist/src/lib/identity-preflight.js +1021 -0
- package/dist/src/lib/identity-preflight.js.map +1 -0
- package/dist/src/lib/{review-lifecyclemodel.js → lifecyclemodel-qa.js} +54 -54
- package/dist/src/lib/lifecyclemodel-qa.js.map +1 -0
- package/dist/src/lib/process-auto-build.js +147 -0
- package/dist/src/lib/process-auto-build.js.map +1 -1
- package/dist/src/lib/process-dedup-review.js +51 -0
- package/dist/src/lib/process-dedup-review.js.map +1 -1
- package/dist/src/lib/process-flow-build-plan.js +1071 -0
- package/dist/src/lib/process-flow-build-plan.js.map +1 -0
- package/dist/src/lib/process-payload-validation.js +14 -7
- package/dist/src/lib/process-payload-validation.js.map +1 -1
- package/dist/src/lib/process-publish-build.js +122 -4
- package/dist/src/lib/process-publish-build.js.map +1 -1
- package/dist/src/lib/{review-process.js → process-qa.js} +259 -117
- package/dist/src/lib/process-qa.js.map +1 -0
- package/dist/src/lib/process-refresh-references.js +19 -10
- package/dist/src/lib/process-refresh-references.js.map +1 -1
- package/dist/src/lib/process-required-fields.js +810 -0
- package/dist/src/lib/process-required-fields.js.map +1 -0
- package/dist/src/lib/process-save-draft-run.js +4 -1
- package/dist/src/lib/process-save-draft-run.js.map +1 -1
- package/dist/src/lib/publish.js +100 -0
- package/dist/src/lib/publish.js.map +1 -1
- package/dist/src/lib/runtime-rulesets.js +283 -0
- package/dist/src/lib/runtime-rulesets.js.map +1 -0
- package/package.json +2 -2
- package/dist/src/lib/review-flow.js.map +0 -1
- package/dist/src/lib/review-lifecyclemodel.js.map +0 -1
- package/dist/src/lib/review-process.js.map +0 -1
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { writeJsonArtifact, writeTextArtifact } from './artifacts.js';
|
|
3
|
+
import { writeJsonArtifact, writeJsonLinesArtifact, writeTextArtifact } from './artifacts.js';
|
|
4
4
|
import { CliError } from './errors.js';
|
|
5
5
|
import { readJsonInput } from './io.js';
|
|
6
|
-
import {
|
|
6
|
+
import { getRuntimeRuleset, resolveRuntimeRuleId } from './runtime-rulesets.js';
|
|
7
7
|
const KIND_RE = /\[tg_io_kind_tag=([^\]]+)\]/gu;
|
|
8
8
|
const UOM_RE = /\[tg_io_uom_tag=([^\]]+)\]/gu;
|
|
9
9
|
const ENERGY_WORDS = [
|
|
@@ -110,33 +110,36 @@ function extractBaseNames(processPayload) {
|
|
|
110
110
|
'baseName',
|
|
111
111
|
]);
|
|
112
112
|
const items = Array.isArray(base) ? base : base ? [base] : [];
|
|
113
|
-
let zh = false;
|
|
114
|
-
let en = false;
|
|
115
113
|
const values = [];
|
|
116
114
|
items.forEach((item) => {
|
|
117
115
|
if (!isRecord(item)) {
|
|
118
116
|
return;
|
|
119
117
|
}
|
|
120
|
-
const lang = String(item['@xml:lang'] ?? '').toLowerCase();
|
|
121
118
|
const text = String(item['#text'] ?? '').trim();
|
|
122
119
|
if (!text) {
|
|
123
120
|
return;
|
|
124
121
|
}
|
|
125
122
|
values.push(text);
|
|
126
|
-
if (lang.startsWith('zh')) {
|
|
127
|
-
zh = true;
|
|
128
|
-
}
|
|
129
|
-
if (lang.startsWith('en')) {
|
|
130
|
-
en = true;
|
|
131
|
-
}
|
|
132
123
|
});
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
124
|
+
return [values.length > 0, values];
|
|
125
|
+
}
|
|
126
|
+
function sourceTracePayload(exchange) {
|
|
127
|
+
const payload = deepGet(exchange, ['common:other', 'tidasimport:sourceTrace', 'payload']);
|
|
128
|
+
if (!isRecord(payload)) {
|
|
129
|
+
return null;
|
|
136
130
|
}
|
|
137
|
-
return
|
|
131
|
+
return isRecord(payload.sourceTrace) ? payload.sourceTrace : payload;
|
|
132
|
+
}
|
|
133
|
+
function sourceClassification(exchange) {
|
|
134
|
+
const payload = sourceTracePayload(exchange);
|
|
135
|
+
return payload && isRecord(payload.sourceClassification) ? payload.sourceClassification : {};
|
|
136
|
+
}
|
|
137
|
+
function sourceGroup(exchange, groupName) {
|
|
138
|
+
const value = sourceClassification(exchange)[groupName];
|
|
139
|
+
const text = String(value ?? '').trim();
|
|
140
|
+
return text || null;
|
|
138
141
|
}
|
|
139
|
-
function classifyExchange(exchange) {
|
|
142
|
+
function classifyExchange(exchange, referenceFlowId = null) {
|
|
140
143
|
const comments = `${textFromValue(exchange.commonComment)} ${textFromValue(exchange.generalComment)}`.toLowerCase();
|
|
141
144
|
const flowDescription = textFromValue(isRecord(exchange.referenceToFlowDataSet)
|
|
142
145
|
? exchange.referenceToFlowDataSet['common:shortDescription']
|
|
@@ -146,36 +149,54 @@ function classifyExchange(exchange) {
|
|
|
146
149
|
const kindSet = new Set(kinds);
|
|
147
150
|
const uoms = Array.from(blob.matchAll(UOM_RE), (match) => match[1].toLowerCase());
|
|
148
151
|
const direction = String(exchange.exchangeDirection ?? '').toLowerCase();
|
|
152
|
+
const inputGroup = sourceGroup(exchange, 'inputGroup');
|
|
153
|
+
const outputGroup = sourceGroup(exchange, 'outputGroup');
|
|
154
|
+
const internalId = String(exchange['@dataSetInternalID'] ?? '').trim();
|
|
155
|
+
const isReferenceFlow = Boolean(referenceFlowId && internalId === referenceFlowId);
|
|
156
|
+
const meta = {
|
|
157
|
+
kinds,
|
|
158
|
+
uoms,
|
|
159
|
+
blob,
|
|
160
|
+
source_input_group: inputGroup,
|
|
161
|
+
source_output_group: outputGroup,
|
|
162
|
+
reference_flow: isReferenceFlow,
|
|
163
|
+
};
|
|
149
164
|
const isEnergy = ENERGY_WORDS.some((word) => blob.includes(word)) ||
|
|
150
165
|
uoms.some((uom) => uom === 'kwh' || uom === 'mj' || uom === 'gj');
|
|
151
166
|
if (direction === 'input') {
|
|
152
167
|
if (kindSet.has('energy') || isEnergy) {
|
|
153
|
-
return { classification: 'energy_input',
|
|
168
|
+
return { classification: 'energy_input', ...meta };
|
|
154
169
|
}
|
|
155
170
|
if (kindSet.has('waste')) {
|
|
156
|
-
return { classification: 'other_input',
|
|
171
|
+
return { classification: 'other_input', ...meta };
|
|
157
172
|
}
|
|
158
|
-
if (kindSet.has('raw_material') || kindSet.has('resource')) {
|
|
159
|
-
return { classification: 'raw_material_input',
|
|
173
|
+
if (inputGroup === '4' || kindSet.has('raw_material') || kindSet.has('resource')) {
|
|
174
|
+
return { classification: 'raw_material_input', ...meta };
|
|
160
175
|
}
|
|
161
176
|
if (kindSet.has('product') || RAW_WORDS.some((word) => blob.includes(word))) {
|
|
162
|
-
return { classification: 'raw_material_input',
|
|
177
|
+
return { classification: 'raw_material_input', ...meta };
|
|
163
178
|
}
|
|
164
|
-
return { classification: 'other_input',
|
|
179
|
+
return { classification: 'other_input', ...meta };
|
|
165
180
|
}
|
|
166
181
|
if (direction === 'output') {
|
|
167
|
-
if (
|
|
168
|
-
return { classification: '
|
|
182
|
+
if (isReferenceFlow || outputGroup === '0') {
|
|
183
|
+
return { classification: 'product_output', ...meta };
|
|
184
|
+
}
|
|
185
|
+
if (kindSet.has('waste') ||
|
|
186
|
+
outputGroup === '4' ||
|
|
187
|
+
outputGroup === '5' ||
|
|
188
|
+
WASTE_WORDS.some((word) => blob.includes(word))) {
|
|
189
|
+
return { classification: 'waste_output', ...meta };
|
|
169
190
|
}
|
|
170
191
|
if (BYP_WORDS.some((word) => blob.includes(word))) {
|
|
171
|
-
return { classification: 'byproduct_output',
|
|
192
|
+
return { classification: 'byproduct_output', ...meta };
|
|
172
193
|
}
|
|
173
194
|
if (kindSet.has('product')) {
|
|
174
|
-
return { classification: 'product_output',
|
|
195
|
+
return { classification: 'product_output', ...meta };
|
|
175
196
|
}
|
|
176
|
-
return { classification: 'other_output',
|
|
197
|
+
return { classification: 'other_output', ...meta };
|
|
177
198
|
}
|
|
178
|
-
return { classification: 'other',
|
|
199
|
+
return { classification: 'other', ...meta };
|
|
179
200
|
}
|
|
180
201
|
function unitIssueCheck(exchange, uoms, blob) {
|
|
181
202
|
const flowUuid = isRecord(exchange.referenceToFlowDataSet)
|
|
@@ -226,8 +247,99 @@ function unitIssueCheck(exchange, uoms, blob) {
|
|
|
226
247
|
}
|
|
227
248
|
return [];
|
|
228
249
|
}
|
|
250
|
+
function hasNumericAmount(exchange) {
|
|
251
|
+
for (const key of ['meanAmount', 'resultingAmount']) {
|
|
252
|
+
const value = exchange[key];
|
|
253
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
254
|
+
return true;
|
|
255
|
+
}
|
|
256
|
+
if (typeof value === 'string' && value.trim() && Number.isFinite(Number(value))) {
|
|
257
|
+
return true;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
function createProcessQaFinding(options) {
|
|
263
|
+
const methodologyRuleId = resolveRuntimeRuleId('process-authoring/strict', options.code);
|
|
264
|
+
return {
|
|
265
|
+
process_file: options.processFile,
|
|
266
|
+
severity: options.severity,
|
|
267
|
+
code: options.code,
|
|
268
|
+
methodology_rule_id: methodologyRuleId,
|
|
269
|
+
message: options.message,
|
|
270
|
+
source: 'rule',
|
|
271
|
+
...(options.evidence ? { evidence: options.evidence } : {}),
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
function reviewFindingsForBase(processFile, base) {
|
|
275
|
+
const findings = [];
|
|
276
|
+
if (!base.source_name_ok) {
|
|
277
|
+
findings.push(createProcessQaFinding({
|
|
278
|
+
processFile,
|
|
279
|
+
severity: 'warning',
|
|
280
|
+
code: 'process_missing_source_base_name',
|
|
281
|
+
message: 'Process baseName should include a usable source-language entry before Foundry authoring is complete.',
|
|
282
|
+
evidence: { base_names: base.base_names },
|
|
283
|
+
}));
|
|
284
|
+
}
|
|
285
|
+
if (!base.functional_unit_ok) {
|
|
286
|
+
findings.push(createProcessQaFinding({
|
|
287
|
+
processFile,
|
|
288
|
+
severity: 'warning',
|
|
289
|
+
code: 'process_missing_functional_unit',
|
|
290
|
+
message: 'Process quantitative reference functionalUnitOrOther is missing and should be curated by Foundry.',
|
|
291
|
+
}));
|
|
292
|
+
}
|
|
293
|
+
for (const [ok, code, message] of [
|
|
294
|
+
[
|
|
295
|
+
base.system_boundary_ok,
|
|
296
|
+
'process_missing_system_boundary',
|
|
297
|
+
'Process system boundary or typeOfDataSet context is missing.',
|
|
298
|
+
],
|
|
299
|
+
[base.time_ok, 'process_missing_time', 'Process time coverage is missing.'],
|
|
300
|
+
[base.geo_ok, 'process_missing_geography', 'Process geography is missing.'],
|
|
301
|
+
[base.tech_ok, 'process_missing_technology', 'Process technology description is missing.'],
|
|
302
|
+
[
|
|
303
|
+
base.admin_ok,
|
|
304
|
+
'process_missing_admin_metadata',
|
|
305
|
+
'Process administrative metadata is missing.',
|
|
306
|
+
],
|
|
307
|
+
]) {
|
|
308
|
+
if (!ok) {
|
|
309
|
+
findings.push(createProcessQaFinding({
|
|
310
|
+
processFile,
|
|
311
|
+
severity: 'warning',
|
|
312
|
+
code,
|
|
313
|
+
message,
|
|
314
|
+
}));
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return findings;
|
|
318
|
+
}
|
|
319
|
+
function processRulesetGate(findings) {
|
|
320
|
+
const ruleset = getRuntimeRuleset('process-authoring/strict');
|
|
321
|
+
const blockers = findings.filter((finding) => finding.severity === 'blocker');
|
|
322
|
+
const status = blockers.length > 0 ? 'blocked' : findings.length > 0 ? 'needs_review' : 'passed';
|
|
323
|
+
return {
|
|
324
|
+
status,
|
|
325
|
+
ruleset_id: ruleset.id,
|
|
326
|
+
ruleset_version: ruleset.version,
|
|
327
|
+
ruleset_source_version: ruleset.source_version,
|
|
328
|
+
ruleset_rule_ids: ruleset.rule_ids,
|
|
329
|
+
counts: {
|
|
330
|
+
findings: findings.length,
|
|
331
|
+
blockers: blockers.length,
|
|
332
|
+
},
|
|
333
|
+
blockers,
|
|
334
|
+
next_action: status === 'blocked'
|
|
335
|
+
? 'fix_blockers'
|
|
336
|
+
: status === 'needs_review'
|
|
337
|
+
? 'review_findings_in_foundry'
|
|
338
|
+
: 'continue',
|
|
339
|
+
};
|
|
340
|
+
}
|
|
229
341
|
function baseInfoCheck(processPayload) {
|
|
230
|
-
const [
|
|
342
|
+
const [sourceNameOk, values] = extractBaseNames(processPayload);
|
|
231
343
|
const functionalUnit = deepGet(processPayload, [
|
|
232
344
|
'processDataSet',
|
|
233
345
|
'processInformation',
|
|
@@ -253,7 +365,6 @@ function baseInfoCheck(processPayload) {
|
|
|
253
365
|
'processDataSet',
|
|
254
366
|
'administrativeInformation',
|
|
255
367
|
]);
|
|
256
|
-
const nameOk = zhOk && enOk;
|
|
257
368
|
const functionalUnitOk = hasNonEmpty(functionalUnit);
|
|
258
369
|
const systemBoundaryOk = hasNonEmpty(mixAndLocation) || hasNonEmpty(route);
|
|
259
370
|
const timeOk = hasNonEmpty(time);
|
|
@@ -261,7 +372,7 @@ function baseInfoCheck(processPayload) {
|
|
|
261
372
|
const technologyOk = hasNonEmpty(technology);
|
|
262
373
|
const administrativeOk = hasNonEmpty(administrativeInformation);
|
|
263
374
|
const completenessScore = [
|
|
264
|
-
|
|
375
|
+
sourceNameOk,
|
|
265
376
|
functionalUnitOk,
|
|
266
377
|
systemBoundaryOk,
|
|
267
378
|
timeOk,
|
|
@@ -270,7 +381,7 @@ function baseInfoCheck(processPayload) {
|
|
|
270
381
|
administrativeOk,
|
|
271
382
|
].filter(Boolean).length;
|
|
272
383
|
return {
|
|
273
|
-
|
|
384
|
+
source_name_ok: sourceNameOk,
|
|
274
385
|
functional_unit_ok: functionalUnitOk,
|
|
275
386
|
system_boundary_ok: systemBoundaryOk,
|
|
276
387
|
time_ok: timeOk,
|
|
@@ -283,8 +394,8 @@ function baseInfoCheck(processPayload) {
|
|
|
283
394
|
}
|
|
284
395
|
function unwrapProcessPayload(value, filePath) {
|
|
285
396
|
if (!isRecord(value)) {
|
|
286
|
-
throw new CliError(`Expected process
|
|
287
|
-
code: '
|
|
397
|
+
throw new CliError(`Expected process QA file to contain a JSON object: ${filePath}`, {
|
|
398
|
+
code: 'PROCESS_QA_INPUT_INVALID',
|
|
288
399
|
exitCode: 2,
|
|
289
400
|
});
|
|
290
401
|
}
|
|
@@ -294,8 +405,8 @@ function unwrapProcessPayload(value, filePath) {
|
|
|
294
405
|
(isRecord(value.json) && value.json) ||
|
|
295
406
|
value;
|
|
296
407
|
if (!isRecord(candidate.processDataSet)) {
|
|
297
|
-
throw new CliError(`Process
|
|
298
|
-
code: '
|
|
408
|
+
throw new CliError(`Process QA file is missing processDataSet: ${filePath}`, {
|
|
409
|
+
code: 'PROCESS_QA_INPUT_INVALID',
|
|
299
410
|
exitCode: 2,
|
|
300
411
|
});
|
|
301
412
|
}
|
|
@@ -335,7 +446,7 @@ function loadReviewRows(rowsFile) {
|
|
|
335
446
|
const parsed = JSON.parse(line);
|
|
336
447
|
if (!isRecord(parsed)) {
|
|
337
448
|
throw new CliError(`Expected JSON object rows in JSONL file: ${resolved}`, {
|
|
338
|
-
code: '
|
|
449
|
+
code: 'PROCESS_QA_ROWS_INVALID_JSONL_ROW',
|
|
339
450
|
exitCode: 2,
|
|
340
451
|
});
|
|
341
452
|
}
|
|
@@ -345,8 +456,8 @@ function loadReviewRows(rowsFile) {
|
|
|
345
456
|
if (error instanceof CliError) {
|
|
346
457
|
throw error;
|
|
347
458
|
}
|
|
348
|
-
throw new CliError(`Process
|
|
349
|
-
code: '
|
|
459
|
+
throw new CliError(`Process QA rows file contains invalid JSONL at line ${index + 1}.`, {
|
|
460
|
+
code: 'PROCESS_QA_ROWS_INVALID_JSONL',
|
|
350
461
|
exitCode: 2,
|
|
351
462
|
details: String(error),
|
|
352
463
|
});
|
|
@@ -358,8 +469,8 @@ function loadReviewRows(rowsFile) {
|
|
|
358
469
|
parsed = JSON.parse(text);
|
|
359
470
|
}
|
|
360
471
|
catch (error) {
|
|
361
|
-
throw new CliError(`Process
|
|
362
|
-
code: '
|
|
472
|
+
throw new CliError(`Process QA rows file is not valid JSON: ${resolved}`, {
|
|
473
|
+
code: 'PROCESS_QA_ROWS_INVALID_JSON',
|
|
363
474
|
exitCode: 2,
|
|
364
475
|
details: String(error),
|
|
365
476
|
});
|
|
@@ -371,13 +482,13 @@ function loadReviewRows(rowsFile) {
|
|
|
371
482
|
return parsed.rows;
|
|
372
483
|
}
|
|
373
484
|
throw new CliError(`Expected JSON array of objects or a report object with rows[]: ${resolved}`, {
|
|
374
|
-
code: '
|
|
485
|
+
code: 'PROCESS_QA_ROWS_INVALID_JSON',
|
|
375
486
|
exitCode: 2,
|
|
376
487
|
});
|
|
377
488
|
}
|
|
378
489
|
function materializeRowsFile(rowsFile, outDir) {
|
|
379
490
|
const rows = loadReviewRows(rowsFile);
|
|
380
|
-
const targetDir = path.join(outDir, '
|
|
491
|
+
const targetDir = path.join(outDir, 'qa-input', 'processes');
|
|
381
492
|
mkdirSync(targetDir, { recursive: true });
|
|
382
493
|
const byKey = {};
|
|
383
494
|
let duplicateCount = 0;
|
|
@@ -403,7 +514,7 @@ function materializeRowsFile(rowsFile, outDir) {
|
|
|
403
514
|
file: filePath,
|
|
404
515
|
});
|
|
405
516
|
});
|
|
406
|
-
const summaryPath = path.join(outDir, '
|
|
517
|
+
const summaryPath = path.join(outDir, 'qa-input', 'materialization-summary.json');
|
|
407
518
|
writeJsonArtifact(summaryPath, {
|
|
408
519
|
source_rows_file: path.resolve(rowsFile),
|
|
409
520
|
input_row_count: rows.length,
|
|
@@ -420,8 +531,8 @@ function materializeRowsFile(rowsFile, outDir) {
|
|
|
420
531
|
function resolveReviewInput(options) {
|
|
421
532
|
const declaredModes = [Boolean(options.rowsFile), Boolean(options.runRoot)].filter(Boolean);
|
|
422
533
|
if (declaredModes.length !== 1) {
|
|
423
|
-
throw new CliError('Process
|
|
424
|
-
code: '
|
|
534
|
+
throw new CliError('Process QA requires exactly one of --rows-file or --run-root.', {
|
|
535
|
+
code: 'PROCESS_QA_INPUT_MODE_REQUIRED',
|
|
425
536
|
exitCode: 2,
|
|
426
537
|
});
|
|
427
538
|
}
|
|
@@ -463,8 +574,8 @@ function resolveReviewInput(options) {
|
|
|
463
574
|
}
|
|
464
575
|
function readProcessFiles(processDir) {
|
|
465
576
|
if (!existsSync(processDir) || !statSync(processDir).isDirectory()) {
|
|
466
|
-
throw new CliError(`Process
|
|
467
|
-
code: '
|
|
577
|
+
throw new CliError(`Process QA directory not found: ${processDir}`, {
|
|
578
|
+
code: 'PROCESS_QA_EXPORTS_NOT_FOUND',
|
|
468
579
|
exitCode: 2,
|
|
469
580
|
});
|
|
470
581
|
}
|
|
@@ -475,7 +586,7 @@ function readProcessFiles(processDir) {
|
|
|
475
586
|
}
|
|
476
587
|
function buildPrompt(processSummaries) {
|
|
477
588
|
return [
|
|
478
|
-
'请基于以下 process
|
|
589
|
+
'请基于以下 process 摘要做语义一致性审核(源语言名称、边界表达、修订建议)。',
|
|
479
590
|
'要求:',
|
|
480
591
|
'1) 只根据给定摘要;',
|
|
481
592
|
'2) 证据不足必须明确标注;',
|
|
@@ -497,71 +608,35 @@ function parseLlmJsonOutput(output) {
|
|
|
497
608
|
}
|
|
498
609
|
}
|
|
499
610
|
async function runOptionalLlmReview(processSummaries, options) {
|
|
611
|
+
void processSummaries;
|
|
612
|
+
void options.env;
|
|
613
|
+
void options.fetchImpl;
|
|
614
|
+
void options.outDir;
|
|
500
615
|
if (!options.enableLlm) {
|
|
501
616
|
return {
|
|
502
617
|
enabled: false,
|
|
503
618
|
reason: 'disabled',
|
|
504
619
|
};
|
|
505
620
|
}
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
model: options.llmModel,
|
|
511
|
-
};
|
|
512
|
-
}
|
|
513
|
-
try {
|
|
514
|
-
const response = await invokeLlm({
|
|
515
|
-
env,
|
|
516
|
-
input: {
|
|
517
|
-
prompt: '你是严谨的LCA审核助手。只给基于输入证据的判断,不得臆造。输出必须是JSON对象。',
|
|
518
|
-
context: buildPrompt(processSummaries),
|
|
519
|
-
},
|
|
520
|
-
fetchImpl: options.fetchImpl,
|
|
521
|
-
timeoutMs: 45_000,
|
|
522
|
-
cacheDir: path.join(options.outDir, '.llm-cache'),
|
|
523
|
-
tracePath: path.join(options.outDir, 'llm-trace.jsonl'),
|
|
524
|
-
module: 'review-process',
|
|
525
|
-
stage: 'semantic-review',
|
|
526
|
-
runId: 'review-process',
|
|
527
|
-
});
|
|
528
|
-
const parsed = parseLlmJsonOutput(response.output);
|
|
529
|
-
if (!parsed) {
|
|
530
|
-
return {
|
|
531
|
-
enabled: true,
|
|
532
|
-
ok: false,
|
|
533
|
-
reason: 'llm_non_json_output',
|
|
534
|
-
raw: response.output.slice(0, 8_000),
|
|
535
|
-
};
|
|
536
|
-
}
|
|
537
|
-
return {
|
|
538
|
-
enabled: true,
|
|
539
|
-
ok: true,
|
|
540
|
-
result: parsed,
|
|
541
|
-
};
|
|
542
|
-
}
|
|
543
|
-
catch (error) {
|
|
544
|
-
return {
|
|
545
|
-
enabled: true,
|
|
546
|
-
ok: false,
|
|
547
|
-
reason: error instanceof Error ? error.message : String(error),
|
|
548
|
-
};
|
|
549
|
-
}
|
|
621
|
+
return {
|
|
622
|
+
enabled: false,
|
|
623
|
+
reason: 'moved_to_foundry_process_curation',
|
|
624
|
+
};
|
|
550
625
|
}
|
|
551
626
|
function formatPercent(value) {
|
|
552
627
|
return value === null ? '' : `${(value * 100).toFixed(2)}%`;
|
|
553
628
|
}
|
|
554
629
|
function renderZhReview(options) {
|
|
555
630
|
const lines = [
|
|
556
|
-
'#
|
|
631
|
+
'# one_flow_rerun_qa_v2_1_zh\n',
|
|
557
632
|
`- run_id: \`${options.runId}\`\n`,
|
|
558
633
|
`- logic_version: \`${options.logicVersion}\`\n`,
|
|
559
634
|
'\n## 2.1 基础信息核查\n',
|
|
560
|
-
'|process file
|
|
635
|
+
'|process file|源语言名称|功能单位|系统边界|时间|地理|技术|管理元数据|完整性得分(0-7)|\n',
|
|
561
636
|
'|---|---|---|---|---|---|---|---|---:|\n',
|
|
562
637
|
];
|
|
563
638
|
options.baseRows.forEach(([fileName, base]) => {
|
|
564
|
-
lines.push(`|${fileName}|${base.
|
|
639
|
+
lines.push(`|${fileName}|${base.source_name_ok ? '✅' : '❌'}|${base.functional_unit_ok ? '✅' : '❌'}|${base.system_boundary_ok ? '✅' : '❌'}|${base.time_ok ? '✅' : '❌'}|${base.geo_ok ? '✅' : '❌'}|${base.tech_ok ? '✅' : '❌'}|${base.admin_ok ? '✅' : '❌'}|${base.completeness_score}|\n`);
|
|
565
640
|
});
|
|
566
641
|
lines.push('\n## 物料平衡口径\n- 物料平衡:仅核查 `原材料投入 = 产品+副产品+废物`\n- 能量投入:单列记录,不计入平衡\n');
|
|
567
642
|
lines.push('\n## 分过程结果\n|process file|原材料投入|产品|副产品|废物|能量投入(不计平衡)|差值(输出-投入)|相对偏差|\n|---|---:|---:|---:|---:|---:|---:|---:|\n');
|
|
@@ -595,15 +670,15 @@ function renderZhReview(options) {
|
|
|
595
670
|
}
|
|
596
671
|
function renderEnReview(options) {
|
|
597
672
|
const lines = [
|
|
598
|
-
'#
|
|
673
|
+
'# one_flow_rerun_qa_v2_1_en\n',
|
|
599
674
|
`- run_id: \`${options.runId}\`\n`,
|
|
600
675
|
`- logic_version: \`${options.logicVersion}\`\n`,
|
|
601
676
|
'\n## 2.1 Basic info checks\n',
|
|
602
|
-
'|process file|
|
|
677
|
+
'|process file|source-language name|functional unit|system boundary|time|geo|tech|admin metadata|completeness(0-7)|\n',
|
|
603
678
|
'|---|---|---|---|---|---|---|---|---:|\n',
|
|
604
679
|
];
|
|
605
680
|
options.baseRows.forEach(([fileName, base]) => {
|
|
606
|
-
lines.push(`|${fileName}|${base.
|
|
681
|
+
lines.push(`|${fileName}|${base.source_name_ok ? '✅' : '❌'}|${base.functional_unit_ok ? '✅' : '❌'}|${base.system_boundary_ok ? '✅' : '❌'}|${base.time_ok ? '✅' : '❌'}|${base.geo_ok ? '✅' : '❌'}|${base.tech_ok ? '✅' : '❌'}|${base.admin_ok ? '✅' : '❌'}|${base.completeness_score}|\n`);
|
|
607
682
|
});
|
|
608
683
|
lines.push('\n## Material balance scope\n- Check only `raw material input = product + by-product + waste`\n- Energy inputs are listed but excluded from balance\n');
|
|
609
684
|
lines.push('\n## Per-process results\n|process file|raw material in|product|by-product|waste|energy in (excluded)|delta(out-in)|relative deviation|\n|---|---:|---:|---:|---:|---:|---:|---:|\n');
|
|
@@ -622,7 +697,7 @@ function renderTiming(options) {
|
|
|
622
697
|
const end = new Date(options.endTs);
|
|
623
698
|
if (Number.isNaN(start.getTime()) || Number.isNaN(end.getTime())) {
|
|
624
699
|
throw new CliError('Expected --start-ts and --end-ts to be valid ISO timestamps.', {
|
|
625
|
-
code: '
|
|
700
|
+
code: 'PROCESS_QA_INVALID_TIMESTAMP',
|
|
626
701
|
exitCode: 2,
|
|
627
702
|
});
|
|
628
703
|
}
|
|
@@ -655,15 +730,15 @@ function renderUnitIssues(runId, unitIssues) {
|
|
|
655
730
|
});
|
|
656
731
|
return lines.join('');
|
|
657
732
|
}
|
|
658
|
-
export async function
|
|
659
|
-
const outDir = path.resolve(requiredNonEmpty(options.outDir, '--out-dir', '
|
|
733
|
+
export async function runProcessQa(options) {
|
|
734
|
+
const outDir = path.resolve(requiredNonEmpty(options.outDir, '--out-dir', 'PROCESS_QA_OUT_DIR_REQUIRED'));
|
|
660
735
|
const resolvedInput = resolveReviewInput({
|
|
661
736
|
rowsFile: options.rowsFile,
|
|
662
737
|
runRoot: options.runRoot,
|
|
663
738
|
runId: options.runId,
|
|
664
739
|
outDir,
|
|
665
740
|
});
|
|
666
|
-
const runId = requiredNonEmpty(resolvedInput.runId, '--run-id', '
|
|
741
|
+
const runId = requiredNonEmpty(resolvedInput.runId, '--run-id', 'PROCESS_QA_RUN_ID_REQUIRED');
|
|
667
742
|
const logicVersion = options.logicVersion?.trim() || 'v2.1';
|
|
668
743
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
669
744
|
const env = options.env ?? process.env;
|
|
@@ -676,6 +751,7 @@ export async function runProcessReview(options) {
|
|
|
676
751
|
const baseRows = [];
|
|
677
752
|
const rows = [];
|
|
678
753
|
const unitIssues = [];
|
|
754
|
+
const ruleFindings = [];
|
|
679
755
|
const processSummariesForLlm = [];
|
|
680
756
|
let totalRaw = 0;
|
|
681
757
|
let totalProduct = 0;
|
|
@@ -690,16 +766,35 @@ export async function runProcessReview(options) {
|
|
|
690
766
|
: isRecord(exchangesValue)
|
|
691
767
|
? [exchangesValue]
|
|
692
768
|
: [];
|
|
769
|
+
const referenceFlowId = String(deepGet(processPayload, [
|
|
770
|
+
'processDataSet',
|
|
771
|
+
'processInformation',
|
|
772
|
+
'quantitativeReference',
|
|
773
|
+
'referenceToReferenceFlow',
|
|
774
|
+
]) ?? '').trim() || null;
|
|
693
775
|
const base = baseInfoCheck(processPayload);
|
|
694
776
|
const fileName = path.basename(filePath);
|
|
695
777
|
baseRows.push([fileName, base]);
|
|
778
|
+
ruleFindings.push(...reviewFindingsForBase(fileName, base));
|
|
696
779
|
let rawInput = 0;
|
|
697
780
|
let product = 0;
|
|
698
781
|
let byproduct = 0;
|
|
699
782
|
let waste = 0;
|
|
700
783
|
let energyExcluded = 0;
|
|
701
784
|
exchanges.forEach((exchange) => {
|
|
702
|
-
const classified = classifyExchange(exchange);
|
|
785
|
+
const classified = classifyExchange(exchange, referenceFlowId);
|
|
786
|
+
if (!hasNumericAmount(exchange)) {
|
|
787
|
+
ruleFindings.push(createProcessQaFinding({
|
|
788
|
+
processFile: fileName,
|
|
789
|
+
severity: 'warning',
|
|
790
|
+
code: 'process_missing_exchange_amount',
|
|
791
|
+
message: 'Process exchange is missing both numeric meanAmount and resultingAmount and should be curated by Foundry.',
|
|
792
|
+
evidence: {
|
|
793
|
+
exchange_internal_id: String(exchange['@dataSetInternalID'] ?? ''),
|
|
794
|
+
direction: String(exchange.exchangeDirection ?? ''),
|
|
795
|
+
},
|
|
796
|
+
}));
|
|
797
|
+
}
|
|
703
798
|
const amount = toNumber(exchange.meanAmount ?? exchange.resultingAmount);
|
|
704
799
|
if (classified.classification === 'raw_material_input') {
|
|
705
800
|
rawInput += amount;
|
|
@@ -716,7 +811,17 @@ export async function runProcessReview(options) {
|
|
|
716
811
|
else if (classified.classification === 'energy_input') {
|
|
717
812
|
energyExcluded += amount;
|
|
718
813
|
}
|
|
719
|
-
|
|
814
|
+
const currentUnitIssues = unitIssueCheck(exchange, classified.uoms, classified.blob);
|
|
815
|
+
unitIssues.push(...currentUnitIssues);
|
|
816
|
+
currentUnitIssues.forEach((issue) => {
|
|
817
|
+
ruleFindings.push(createProcessQaFinding({
|
|
818
|
+
processFile: fileName,
|
|
819
|
+
severity: 'warning',
|
|
820
|
+
code: 'process_exchange_unit_semantic_mismatch',
|
|
821
|
+
message: 'Exchange unit tag conflicts with flow description semantics.',
|
|
822
|
+
evidence: { ...issue },
|
|
823
|
+
}));
|
|
824
|
+
});
|
|
720
825
|
});
|
|
721
826
|
const balanceOut = product + byproduct + waste;
|
|
722
827
|
const delta = balanceOut - rawInput;
|
|
@@ -731,6 +836,22 @@ export async function runProcessReview(options) {
|
|
|
731
836
|
delta,
|
|
732
837
|
relative_deviation: relativeDeviation,
|
|
733
838
|
});
|
|
839
|
+
if (relativeDeviation !== null && relativeDeviation > 0.05) {
|
|
840
|
+
ruleFindings.push(createProcessQaFinding({
|
|
841
|
+
processFile: fileName,
|
|
842
|
+
severity: 'warning',
|
|
843
|
+
code: 'process_material_balance_deviation',
|
|
844
|
+
message: 'Material balance deviation exceeds the QA threshold for raw inputs versus product/by-product/waste outputs.',
|
|
845
|
+
evidence: {
|
|
846
|
+
raw_input: rawInput,
|
|
847
|
+
product,
|
|
848
|
+
byproduct,
|
|
849
|
+
waste,
|
|
850
|
+
relative_deviation: relativeDeviation,
|
|
851
|
+
policy_decision_owner: 'foundry',
|
|
852
|
+
},
|
|
853
|
+
}));
|
|
854
|
+
}
|
|
734
855
|
totalRaw += rawInput;
|
|
735
856
|
totalProduct += product;
|
|
736
857
|
totalByproduct += byproduct;
|
|
@@ -741,7 +862,7 @@ export async function runProcessReview(options) {
|
|
|
741
862
|
process_file: fileName,
|
|
742
863
|
base_names: base.base_names.slice(0, 4),
|
|
743
864
|
base_checks: {
|
|
744
|
-
|
|
865
|
+
source_name_ok: base.source_name_ok,
|
|
745
866
|
functional_unit_ok: base.functional_unit_ok,
|
|
746
867
|
system_boundary_ok: base.system_boundary_ok,
|
|
747
868
|
time_ok: base.time_ok,
|
|
@@ -770,7 +891,7 @@ export async function runProcessReview(options) {
|
|
|
770
891
|
energy_excluded: totalEnergy,
|
|
771
892
|
};
|
|
772
893
|
const evidenceStrong = [
|
|
773
|
-
'
|
|
894
|
+
'已优先使用 quantitativeReference.referenceToReferenceFlow、EcoSpold inputGroup/outputGroup 和 exchange 标签/描述做口径过滤,仅核算 原材料投入 vs 产品+副产品+废物,能量单列不计入平衡。',
|
|
774
895
|
...(unitIssues.length > 0
|
|
775
896
|
? ['发现单位疑似错误时均附带 flow 描述与单位标签的直接矛盾证据。']
|
|
776
897
|
: []),
|
|
@@ -786,15 +907,21 @@ export async function runProcessReview(options) {
|
|
|
786
907
|
fetchImpl,
|
|
787
908
|
outDir,
|
|
788
909
|
});
|
|
910
|
+
const rulesetGate = processRulesetGate(ruleFindings);
|
|
789
911
|
const summary = {
|
|
790
912
|
run_id: runId,
|
|
791
913
|
logic_version: logicVersion,
|
|
792
914
|
process_count: processFiles.length,
|
|
793
915
|
totals,
|
|
916
|
+
ruleset_gate: rulesetGate,
|
|
794
917
|
llm: llmResult,
|
|
918
|
+
policy_decision_owner: 'foundry',
|
|
919
|
+
qa_mode: 'deterministic_qa_report',
|
|
795
920
|
};
|
|
796
|
-
const reviewInputSummaryPath = writeJsonArtifact(path.join(outDir, '
|
|
797
|
-
const
|
|
921
|
+
const reviewInputSummaryPath = writeJsonArtifact(path.join(outDir, 'qa-input-summary.json'), resolvedInput.reviewInputSummary);
|
|
922
|
+
const ruleFindingsPath = writeJsonLinesArtifact(path.join(outDir, 'process-qa-rule-findings.jsonl'), ruleFindings);
|
|
923
|
+
const rulesetGatePath = writeJsonArtifact(path.join(outDir, 'process-qa-ruleset-gate.json'), rulesetGate);
|
|
924
|
+
const reviewZhPath = writeTextArtifact(path.join(outDir, 'one_flow_rerun_qa_v2_1_zh.md'), renderZhReview({
|
|
798
925
|
runId,
|
|
799
926
|
logicVersion,
|
|
800
927
|
baseRows,
|
|
@@ -804,7 +931,7 @@ export async function runProcessReview(options) {
|
|
|
804
931
|
evidenceStrong,
|
|
805
932
|
evidenceWeak,
|
|
806
933
|
}));
|
|
807
|
-
const reviewEnPath = writeTextArtifact(path.join(outDir, '
|
|
934
|
+
const reviewEnPath = writeTextArtifact(path.join(outDir, 'one_flow_rerun_qa_v2_1_en.md'), renderEnReview({
|
|
808
935
|
runId,
|
|
809
936
|
logicVersion,
|
|
810
937
|
baseRows,
|
|
@@ -820,11 +947,11 @@ export async function runProcessReview(options) {
|
|
|
820
947
|
processCount: processFiles.length,
|
|
821
948
|
}));
|
|
822
949
|
const unitIssuePath = writeTextArtifact(path.join(outDir, 'flow_unit_issue_log.md'), renderUnitIssues(runId, unitIssues));
|
|
823
|
-
const summaryPath = writeJsonArtifact(path.join(outDir, '
|
|
950
|
+
const summaryPath = writeJsonArtifact(path.join(outDir, 'qa_summary_v2_1.json'), summary);
|
|
824
951
|
const report = {
|
|
825
952
|
schema_version: 1,
|
|
826
953
|
generated_at_utc: (options.now ?? (() => new Date()))().toISOString(),
|
|
827
|
-
status: '
|
|
954
|
+
status: 'completed_local_process_qa',
|
|
828
955
|
run_id: runId,
|
|
829
956
|
run_root: resolvedInput.runRoot,
|
|
830
957
|
rows_file: resolvedInput.rowsFile,
|
|
@@ -832,13 +959,24 @@ export async function runProcessReview(options) {
|
|
|
832
959
|
input_mode: resolvedInput.inputMode,
|
|
833
960
|
effective_processes_dir: resolvedInput.effectiveProcessesDir,
|
|
834
961
|
logic_version: logicVersion,
|
|
962
|
+
ruleset_id: rulesetGate.ruleset_id,
|
|
963
|
+
ruleset_version: rulesetGate.ruleset_version,
|
|
964
|
+
ruleset_source_version: rulesetGate.ruleset_source_version,
|
|
965
|
+
ruleset_rule_ids: rulesetGate.ruleset_rule_ids,
|
|
835
966
|
process_count: processFiles.length,
|
|
836
967
|
totals,
|
|
968
|
+
rule_finding_count: ruleFindings.length,
|
|
969
|
+
blocker_count: rulesetGate.counts.blockers,
|
|
970
|
+
ruleset_gate: rulesetGate,
|
|
971
|
+
policy_decision_owner: 'foundry',
|
|
972
|
+
qa_mode: 'deterministic_qa_report',
|
|
837
973
|
files: {
|
|
838
|
-
|
|
974
|
+
qa_input_summary: reviewInputSummaryPath,
|
|
839
975
|
materialization_summary: resolvedInput.materializationSummaryPath,
|
|
840
|
-
|
|
841
|
-
|
|
976
|
+
rule_findings: ruleFindingsPath,
|
|
977
|
+
ruleset_gate: rulesetGatePath,
|
|
978
|
+
qa_zh: reviewZhPath,
|
|
979
|
+
qa_en: reviewEnPath,
|
|
842
980
|
timing: timingPath,
|
|
843
981
|
unit_issue_log: unitIssuePath,
|
|
844
982
|
summary: summaryPath,
|
|
@@ -846,7 +984,7 @@ export async function runProcessReview(options) {
|
|
|
846
984
|
},
|
|
847
985
|
llm: llmResult,
|
|
848
986
|
};
|
|
849
|
-
const reportPath = writeJsonArtifact(path.join(outDir, 'process-
|
|
987
|
+
const reportPath = writeJsonArtifact(path.join(outDir, 'process-qa-report.json'), report);
|
|
850
988
|
report.files.report = reportPath;
|
|
851
989
|
writeJsonArtifact(reportPath, report);
|
|
852
990
|
return report;
|
|
@@ -860,6 +998,10 @@ export const __testInternals = {
|
|
|
860
998
|
extractBaseNames,
|
|
861
999
|
classifyExchange,
|
|
862
1000
|
unitIssueCheck,
|
|
1001
|
+
hasNumericAmount,
|
|
1002
|
+
createProcessQaFinding,
|
|
1003
|
+
reviewFindingsForBase,
|
|
1004
|
+
processRulesetGate,
|
|
863
1005
|
baseInfoCheck,
|
|
864
1006
|
unwrapProcessPayload,
|
|
865
1007
|
buildPrompt,
|
|
@@ -870,4 +1012,4 @@ export const __testInternals = {
|
|
|
870
1012
|
renderTiming,
|
|
871
1013
|
renderUnitIssues,
|
|
872
1014
|
};
|
|
873
|
-
//# sourceMappingURL=
|
|
1015
|
+
//# sourceMappingURL=process-qa.js.map
|