@tiangong-lca/cli 0.0.8 → 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 +14 -14
- package/dist/src/cli.js +109 -98
- package/dist/src/cli.js.map +1 -1
- package/dist/src/lib/dataset-bilingual.js +9 -9
- package/dist/src/lib/dataset-bilingual.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/{review-flow.js → flow-qa.js} +29 -29
- package/dist/src/lib/flow-qa.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/{review-process.js → process-qa.js} +132 -138
- package/dist/src/lib/process-qa.js.map +1 -0
- package/dist/src/lib/runtime-rulesets.js +2 -2
- package/dist/src/lib/runtime-rulesets.js.map +1 -1
- package/package.json +1 -1
- 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
|
@@ -3,8 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import { writeJsonArtifact, writeJsonLinesArtifact, writeTextArtifact } from './artifacts.js';
|
|
4
4
|
import { CliError } from './errors.js';
|
|
5
5
|
import { readJsonInput } from './io.js';
|
|
6
|
-
import {
|
|
7
|
-
import { getRuntimeRuleset, isRuntimeRuleBlocker, resolveRuntimeRuleId, } from './runtime-rulesets.js';
|
|
6
|
+
import { getRuntimeRuleset, resolveRuntimeRuleId } from './runtime-rulesets.js';
|
|
8
7
|
const KIND_RE = /\[tg_io_kind_tag=([^\]]+)\]/gu;
|
|
9
8
|
const UOM_RE = /\[tg_io_uom_tag=([^\]]+)\]/gu;
|
|
10
9
|
const ENERGY_WORDS = [
|
|
@@ -111,33 +110,36 @@ function extractBaseNames(processPayload) {
|
|
|
111
110
|
'baseName',
|
|
112
111
|
]);
|
|
113
112
|
const items = Array.isArray(base) ? base : base ? [base] : [];
|
|
114
|
-
let zh = false;
|
|
115
|
-
let en = false;
|
|
116
113
|
const values = [];
|
|
117
114
|
items.forEach((item) => {
|
|
118
115
|
if (!isRecord(item)) {
|
|
119
116
|
return;
|
|
120
117
|
}
|
|
121
|
-
const lang = String(item['@xml:lang'] ?? '').toLowerCase();
|
|
122
118
|
const text = String(item['#text'] ?? '').trim();
|
|
123
119
|
if (!text) {
|
|
124
120
|
return;
|
|
125
121
|
}
|
|
126
122
|
values.push(text);
|
|
127
|
-
if (lang.startsWith('zh')) {
|
|
128
|
-
zh = true;
|
|
129
|
-
}
|
|
130
|
-
if (lang.startsWith('en')) {
|
|
131
|
-
en = true;
|
|
132
|
-
}
|
|
133
123
|
});
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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;
|
|
137
130
|
}
|
|
138
|
-
return
|
|
131
|
+
return isRecord(payload.sourceTrace) ? payload.sourceTrace : payload;
|
|
139
132
|
}
|
|
140
|
-
function
|
|
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;
|
|
141
|
+
}
|
|
142
|
+
function classifyExchange(exchange, referenceFlowId = null) {
|
|
141
143
|
const comments = `${textFromValue(exchange.commonComment)} ${textFromValue(exchange.generalComment)}`.toLowerCase();
|
|
142
144
|
const flowDescription = textFromValue(isRecord(exchange.referenceToFlowDataSet)
|
|
143
145
|
? exchange.referenceToFlowDataSet['common:shortDescription']
|
|
@@ -147,36 +149,54 @@ function classifyExchange(exchange) {
|
|
|
147
149
|
const kindSet = new Set(kinds);
|
|
148
150
|
const uoms = Array.from(blob.matchAll(UOM_RE), (match) => match[1].toLowerCase());
|
|
149
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
|
+
};
|
|
150
164
|
const isEnergy = ENERGY_WORDS.some((word) => blob.includes(word)) ||
|
|
151
165
|
uoms.some((uom) => uom === 'kwh' || uom === 'mj' || uom === 'gj');
|
|
152
166
|
if (direction === 'input') {
|
|
153
167
|
if (kindSet.has('energy') || isEnergy) {
|
|
154
|
-
return { classification: 'energy_input',
|
|
168
|
+
return { classification: 'energy_input', ...meta };
|
|
155
169
|
}
|
|
156
170
|
if (kindSet.has('waste')) {
|
|
157
|
-
return { classification: 'other_input',
|
|
171
|
+
return { classification: 'other_input', ...meta };
|
|
158
172
|
}
|
|
159
|
-
if (kindSet.has('raw_material') || kindSet.has('resource')) {
|
|
160
|
-
return { classification: 'raw_material_input',
|
|
173
|
+
if (inputGroup === '4' || kindSet.has('raw_material') || kindSet.has('resource')) {
|
|
174
|
+
return { classification: 'raw_material_input', ...meta };
|
|
161
175
|
}
|
|
162
176
|
if (kindSet.has('product') || RAW_WORDS.some((word) => blob.includes(word))) {
|
|
163
|
-
return { classification: 'raw_material_input',
|
|
177
|
+
return { classification: 'raw_material_input', ...meta };
|
|
164
178
|
}
|
|
165
|
-
return { classification: 'other_input',
|
|
179
|
+
return { classification: 'other_input', ...meta };
|
|
166
180
|
}
|
|
167
181
|
if (direction === 'output') {
|
|
168
|
-
if (
|
|
169
|
-
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 };
|
|
170
190
|
}
|
|
171
191
|
if (BYP_WORDS.some((word) => blob.includes(word))) {
|
|
172
|
-
return { classification: 'byproduct_output',
|
|
192
|
+
return { classification: 'byproduct_output', ...meta };
|
|
173
193
|
}
|
|
174
194
|
if (kindSet.has('product')) {
|
|
175
|
-
return { classification: 'product_output',
|
|
195
|
+
return { classification: 'product_output', ...meta };
|
|
176
196
|
}
|
|
177
|
-
return { classification: 'other_output',
|
|
197
|
+
return { classification: 'other_output', ...meta };
|
|
178
198
|
}
|
|
179
|
-
return { classification: 'other',
|
|
199
|
+
return { classification: 'other', ...meta };
|
|
180
200
|
}
|
|
181
201
|
function unitIssueCheck(exchange, uoms, blob) {
|
|
182
202
|
const flowUuid = isRecord(exchange.referenceToFlowDataSet)
|
|
@@ -239,7 +259,7 @@ function hasNumericAmount(exchange) {
|
|
|
239
259
|
}
|
|
240
260
|
return false;
|
|
241
261
|
}
|
|
242
|
-
function
|
|
262
|
+
function createProcessQaFinding(options) {
|
|
243
263
|
const methodologyRuleId = resolveRuntimeRuleId('process-authoring/strict', options.code);
|
|
244
264
|
return {
|
|
245
265
|
process_file: options.processFile,
|
|
@@ -253,21 +273,21 @@ function createProcessReviewFinding(options) {
|
|
|
253
273
|
}
|
|
254
274
|
function reviewFindingsForBase(processFile, base) {
|
|
255
275
|
const findings = [];
|
|
256
|
-
if (!base.
|
|
257
|
-
findings.push(
|
|
276
|
+
if (!base.source_name_ok) {
|
|
277
|
+
findings.push(createProcessQaFinding({
|
|
258
278
|
processFile,
|
|
259
|
-
severity: '
|
|
260
|
-
code: '
|
|
261
|
-
message: 'Process baseName
|
|
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.',
|
|
262
282
|
evidence: { base_names: base.base_names },
|
|
263
283
|
}));
|
|
264
284
|
}
|
|
265
285
|
if (!base.functional_unit_ok) {
|
|
266
|
-
findings.push(
|
|
286
|
+
findings.push(createProcessQaFinding({
|
|
267
287
|
processFile,
|
|
268
|
-
severity: '
|
|
288
|
+
severity: 'warning',
|
|
269
289
|
code: 'process_missing_functional_unit',
|
|
270
|
-
message: 'Process quantitative reference functionalUnitOrOther is missing.',
|
|
290
|
+
message: 'Process quantitative reference functionalUnitOrOther is missing and should be curated by Foundry.',
|
|
271
291
|
}));
|
|
272
292
|
}
|
|
273
293
|
for (const [ok, code, message] of [
|
|
@@ -286,7 +306,7 @@ function reviewFindingsForBase(processFile, base) {
|
|
|
286
306
|
],
|
|
287
307
|
]) {
|
|
288
308
|
if (!ok) {
|
|
289
|
-
findings.push(
|
|
309
|
+
findings.push(createProcessQaFinding({
|
|
290
310
|
processFile,
|
|
291
311
|
severity: 'warning',
|
|
292
312
|
code,
|
|
@@ -298,7 +318,7 @@ function reviewFindingsForBase(processFile, base) {
|
|
|
298
318
|
}
|
|
299
319
|
function processRulesetGate(findings) {
|
|
300
320
|
const ruleset = getRuntimeRuleset('process-authoring/strict');
|
|
301
|
-
const blockers = findings.filter((finding) => finding.severity === 'blocker'
|
|
321
|
+
const blockers = findings.filter((finding) => finding.severity === 'blocker');
|
|
302
322
|
const status = blockers.length > 0 ? 'blocked' : findings.length > 0 ? 'needs_review' : 'passed';
|
|
303
323
|
return {
|
|
304
324
|
status,
|
|
@@ -314,12 +334,12 @@ function processRulesetGate(findings) {
|
|
|
314
334
|
next_action: status === 'blocked'
|
|
315
335
|
? 'fix_blockers'
|
|
316
336
|
: status === 'needs_review'
|
|
317
|
-
? '
|
|
337
|
+
? 'review_findings_in_foundry'
|
|
318
338
|
: 'continue',
|
|
319
339
|
};
|
|
320
340
|
}
|
|
321
341
|
function baseInfoCheck(processPayload) {
|
|
322
|
-
const [
|
|
342
|
+
const [sourceNameOk, values] = extractBaseNames(processPayload);
|
|
323
343
|
const functionalUnit = deepGet(processPayload, [
|
|
324
344
|
'processDataSet',
|
|
325
345
|
'processInformation',
|
|
@@ -345,7 +365,6 @@ function baseInfoCheck(processPayload) {
|
|
|
345
365
|
'processDataSet',
|
|
346
366
|
'administrativeInformation',
|
|
347
367
|
]);
|
|
348
|
-
const nameOk = zhOk && enOk;
|
|
349
368
|
const functionalUnitOk = hasNonEmpty(functionalUnit);
|
|
350
369
|
const systemBoundaryOk = hasNonEmpty(mixAndLocation) || hasNonEmpty(route);
|
|
351
370
|
const timeOk = hasNonEmpty(time);
|
|
@@ -353,7 +372,7 @@ function baseInfoCheck(processPayload) {
|
|
|
353
372
|
const technologyOk = hasNonEmpty(technology);
|
|
354
373
|
const administrativeOk = hasNonEmpty(administrativeInformation);
|
|
355
374
|
const completenessScore = [
|
|
356
|
-
|
|
375
|
+
sourceNameOk,
|
|
357
376
|
functionalUnitOk,
|
|
358
377
|
systemBoundaryOk,
|
|
359
378
|
timeOk,
|
|
@@ -362,7 +381,7 @@ function baseInfoCheck(processPayload) {
|
|
|
362
381
|
administrativeOk,
|
|
363
382
|
].filter(Boolean).length;
|
|
364
383
|
return {
|
|
365
|
-
|
|
384
|
+
source_name_ok: sourceNameOk,
|
|
366
385
|
functional_unit_ok: functionalUnitOk,
|
|
367
386
|
system_boundary_ok: systemBoundaryOk,
|
|
368
387
|
time_ok: timeOk,
|
|
@@ -375,8 +394,8 @@ function baseInfoCheck(processPayload) {
|
|
|
375
394
|
}
|
|
376
395
|
function unwrapProcessPayload(value, filePath) {
|
|
377
396
|
if (!isRecord(value)) {
|
|
378
|
-
throw new CliError(`Expected process
|
|
379
|
-
code: '
|
|
397
|
+
throw new CliError(`Expected process QA file to contain a JSON object: ${filePath}`, {
|
|
398
|
+
code: 'PROCESS_QA_INPUT_INVALID',
|
|
380
399
|
exitCode: 2,
|
|
381
400
|
});
|
|
382
401
|
}
|
|
@@ -386,8 +405,8 @@ function unwrapProcessPayload(value, filePath) {
|
|
|
386
405
|
(isRecord(value.json) && value.json) ||
|
|
387
406
|
value;
|
|
388
407
|
if (!isRecord(candidate.processDataSet)) {
|
|
389
|
-
throw new CliError(`Process
|
|
390
|
-
code: '
|
|
408
|
+
throw new CliError(`Process QA file is missing processDataSet: ${filePath}`, {
|
|
409
|
+
code: 'PROCESS_QA_INPUT_INVALID',
|
|
391
410
|
exitCode: 2,
|
|
392
411
|
});
|
|
393
412
|
}
|
|
@@ -427,7 +446,7 @@ function loadReviewRows(rowsFile) {
|
|
|
427
446
|
const parsed = JSON.parse(line);
|
|
428
447
|
if (!isRecord(parsed)) {
|
|
429
448
|
throw new CliError(`Expected JSON object rows in JSONL file: ${resolved}`, {
|
|
430
|
-
code: '
|
|
449
|
+
code: 'PROCESS_QA_ROWS_INVALID_JSONL_ROW',
|
|
431
450
|
exitCode: 2,
|
|
432
451
|
});
|
|
433
452
|
}
|
|
@@ -437,8 +456,8 @@ function loadReviewRows(rowsFile) {
|
|
|
437
456
|
if (error instanceof CliError) {
|
|
438
457
|
throw error;
|
|
439
458
|
}
|
|
440
|
-
throw new CliError(`Process
|
|
441
|
-
code: '
|
|
459
|
+
throw new CliError(`Process QA rows file contains invalid JSONL at line ${index + 1}.`, {
|
|
460
|
+
code: 'PROCESS_QA_ROWS_INVALID_JSONL',
|
|
442
461
|
exitCode: 2,
|
|
443
462
|
details: String(error),
|
|
444
463
|
});
|
|
@@ -450,8 +469,8 @@ function loadReviewRows(rowsFile) {
|
|
|
450
469
|
parsed = JSON.parse(text);
|
|
451
470
|
}
|
|
452
471
|
catch (error) {
|
|
453
|
-
throw new CliError(`Process
|
|
454
|
-
code: '
|
|
472
|
+
throw new CliError(`Process QA rows file is not valid JSON: ${resolved}`, {
|
|
473
|
+
code: 'PROCESS_QA_ROWS_INVALID_JSON',
|
|
455
474
|
exitCode: 2,
|
|
456
475
|
details: String(error),
|
|
457
476
|
});
|
|
@@ -463,13 +482,13 @@ function loadReviewRows(rowsFile) {
|
|
|
463
482
|
return parsed.rows;
|
|
464
483
|
}
|
|
465
484
|
throw new CliError(`Expected JSON array of objects or a report object with rows[]: ${resolved}`, {
|
|
466
|
-
code: '
|
|
485
|
+
code: 'PROCESS_QA_ROWS_INVALID_JSON',
|
|
467
486
|
exitCode: 2,
|
|
468
487
|
});
|
|
469
488
|
}
|
|
470
489
|
function materializeRowsFile(rowsFile, outDir) {
|
|
471
490
|
const rows = loadReviewRows(rowsFile);
|
|
472
|
-
const targetDir = path.join(outDir, '
|
|
491
|
+
const targetDir = path.join(outDir, 'qa-input', 'processes');
|
|
473
492
|
mkdirSync(targetDir, { recursive: true });
|
|
474
493
|
const byKey = {};
|
|
475
494
|
let duplicateCount = 0;
|
|
@@ -495,7 +514,7 @@ function materializeRowsFile(rowsFile, outDir) {
|
|
|
495
514
|
file: filePath,
|
|
496
515
|
});
|
|
497
516
|
});
|
|
498
|
-
const summaryPath = path.join(outDir, '
|
|
517
|
+
const summaryPath = path.join(outDir, 'qa-input', 'materialization-summary.json');
|
|
499
518
|
writeJsonArtifact(summaryPath, {
|
|
500
519
|
source_rows_file: path.resolve(rowsFile),
|
|
501
520
|
input_row_count: rows.length,
|
|
@@ -512,8 +531,8 @@ function materializeRowsFile(rowsFile, outDir) {
|
|
|
512
531
|
function resolveReviewInput(options) {
|
|
513
532
|
const declaredModes = [Boolean(options.rowsFile), Boolean(options.runRoot)].filter(Boolean);
|
|
514
533
|
if (declaredModes.length !== 1) {
|
|
515
|
-
throw new CliError('Process
|
|
516
|
-
code: '
|
|
534
|
+
throw new CliError('Process QA requires exactly one of --rows-file or --run-root.', {
|
|
535
|
+
code: 'PROCESS_QA_INPUT_MODE_REQUIRED',
|
|
517
536
|
exitCode: 2,
|
|
518
537
|
});
|
|
519
538
|
}
|
|
@@ -555,8 +574,8 @@ function resolveReviewInput(options) {
|
|
|
555
574
|
}
|
|
556
575
|
function readProcessFiles(processDir) {
|
|
557
576
|
if (!existsSync(processDir) || !statSync(processDir).isDirectory()) {
|
|
558
|
-
throw new CliError(`Process
|
|
559
|
-
code: '
|
|
577
|
+
throw new CliError(`Process QA directory not found: ${processDir}`, {
|
|
578
|
+
code: 'PROCESS_QA_EXPORTS_NOT_FOUND',
|
|
560
579
|
exitCode: 2,
|
|
561
580
|
});
|
|
562
581
|
}
|
|
@@ -567,7 +586,7 @@ function readProcessFiles(processDir) {
|
|
|
567
586
|
}
|
|
568
587
|
function buildPrompt(processSummaries) {
|
|
569
588
|
return [
|
|
570
|
-
'请基于以下 process
|
|
589
|
+
'请基于以下 process 摘要做语义一致性审核(源语言名称、边界表达、修订建议)。',
|
|
571
590
|
'要求:',
|
|
572
591
|
'1) 只根据给定摘要;',
|
|
573
592
|
'2) 证据不足必须明确标注;',
|
|
@@ -589,71 +608,35 @@ function parseLlmJsonOutput(output) {
|
|
|
589
608
|
}
|
|
590
609
|
}
|
|
591
610
|
async function runOptionalLlmReview(processSummaries, options) {
|
|
611
|
+
void processSummaries;
|
|
612
|
+
void options.env;
|
|
613
|
+
void options.fetchImpl;
|
|
614
|
+
void options.outDir;
|
|
592
615
|
if (!options.enableLlm) {
|
|
593
616
|
return {
|
|
594
617
|
enabled: false,
|
|
595
618
|
reason: 'disabled',
|
|
596
619
|
};
|
|
597
620
|
}
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
model: options.llmModel,
|
|
603
|
-
};
|
|
604
|
-
}
|
|
605
|
-
try {
|
|
606
|
-
const response = await invokeLlm({
|
|
607
|
-
env,
|
|
608
|
-
input: {
|
|
609
|
-
prompt: '你是严谨的LCA审核助手。只给基于输入证据的判断,不得臆造。输出必须是JSON对象。',
|
|
610
|
-
context: buildPrompt(processSummaries),
|
|
611
|
-
},
|
|
612
|
-
fetchImpl: options.fetchImpl,
|
|
613
|
-
timeoutMs: 45_000,
|
|
614
|
-
cacheDir: path.join(options.outDir, '.llm-cache'),
|
|
615
|
-
tracePath: path.join(options.outDir, 'llm-trace.jsonl'),
|
|
616
|
-
module: 'review-process',
|
|
617
|
-
stage: 'semantic-review',
|
|
618
|
-
runId: 'review-process',
|
|
619
|
-
});
|
|
620
|
-
const parsed = parseLlmJsonOutput(response.output);
|
|
621
|
-
if (!parsed) {
|
|
622
|
-
return {
|
|
623
|
-
enabled: true,
|
|
624
|
-
ok: false,
|
|
625
|
-
reason: 'llm_non_json_output',
|
|
626
|
-
raw: response.output.slice(0, 8_000),
|
|
627
|
-
};
|
|
628
|
-
}
|
|
629
|
-
return {
|
|
630
|
-
enabled: true,
|
|
631
|
-
ok: true,
|
|
632
|
-
result: parsed,
|
|
633
|
-
};
|
|
634
|
-
}
|
|
635
|
-
catch (error) {
|
|
636
|
-
return {
|
|
637
|
-
enabled: true,
|
|
638
|
-
ok: false,
|
|
639
|
-
reason: error instanceof Error ? error.message : String(error),
|
|
640
|
-
};
|
|
641
|
-
}
|
|
621
|
+
return {
|
|
622
|
+
enabled: false,
|
|
623
|
+
reason: 'moved_to_foundry_process_curation',
|
|
624
|
+
};
|
|
642
625
|
}
|
|
643
626
|
function formatPercent(value) {
|
|
644
627
|
return value === null ? '' : `${(value * 100).toFixed(2)}%`;
|
|
645
628
|
}
|
|
646
629
|
function renderZhReview(options) {
|
|
647
630
|
const lines = [
|
|
648
|
-
'#
|
|
631
|
+
'# one_flow_rerun_qa_v2_1_zh\n',
|
|
649
632
|
`- run_id: \`${options.runId}\`\n`,
|
|
650
633
|
`- logic_version: \`${options.logicVersion}\`\n`,
|
|
651
634
|
'\n## 2.1 基础信息核查\n',
|
|
652
|
-
'|process file
|
|
635
|
+
'|process file|源语言名称|功能单位|系统边界|时间|地理|技术|管理元数据|完整性得分(0-7)|\n',
|
|
653
636
|
'|---|---|---|---|---|---|---|---|---:|\n',
|
|
654
637
|
];
|
|
655
638
|
options.baseRows.forEach(([fileName, base]) => {
|
|
656
|
-
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`);
|
|
657
640
|
});
|
|
658
641
|
lines.push('\n## 物料平衡口径\n- 物料平衡:仅核查 `原材料投入 = 产品+副产品+废物`\n- 能量投入:单列记录,不计入平衡\n');
|
|
659
642
|
lines.push('\n## 分过程结果\n|process file|原材料投入|产品|副产品|废物|能量投入(不计平衡)|差值(输出-投入)|相对偏差|\n|---|---:|---:|---:|---:|---:|---:|---:|\n');
|
|
@@ -687,15 +670,15 @@ function renderZhReview(options) {
|
|
|
687
670
|
}
|
|
688
671
|
function renderEnReview(options) {
|
|
689
672
|
const lines = [
|
|
690
|
-
'#
|
|
673
|
+
'# one_flow_rerun_qa_v2_1_en\n',
|
|
691
674
|
`- run_id: \`${options.runId}\`\n`,
|
|
692
675
|
`- logic_version: \`${options.logicVersion}\`\n`,
|
|
693
676
|
'\n## 2.1 Basic info checks\n',
|
|
694
|
-
'|process file|
|
|
677
|
+
'|process file|source-language name|functional unit|system boundary|time|geo|tech|admin metadata|completeness(0-7)|\n',
|
|
695
678
|
'|---|---|---|---|---|---|---|---|---:|\n',
|
|
696
679
|
];
|
|
697
680
|
options.baseRows.forEach(([fileName, base]) => {
|
|
698
|
-
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`);
|
|
699
682
|
});
|
|
700
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');
|
|
701
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');
|
|
@@ -714,7 +697,7 @@ function renderTiming(options) {
|
|
|
714
697
|
const end = new Date(options.endTs);
|
|
715
698
|
if (Number.isNaN(start.getTime()) || Number.isNaN(end.getTime())) {
|
|
716
699
|
throw new CliError('Expected --start-ts and --end-ts to be valid ISO timestamps.', {
|
|
717
|
-
code: '
|
|
700
|
+
code: 'PROCESS_QA_INVALID_TIMESTAMP',
|
|
718
701
|
exitCode: 2,
|
|
719
702
|
});
|
|
720
703
|
}
|
|
@@ -747,15 +730,15 @@ function renderUnitIssues(runId, unitIssues) {
|
|
|
747
730
|
});
|
|
748
731
|
return lines.join('');
|
|
749
732
|
}
|
|
750
|
-
export async function
|
|
751
|
-
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'));
|
|
752
735
|
const resolvedInput = resolveReviewInput({
|
|
753
736
|
rowsFile: options.rowsFile,
|
|
754
737
|
runRoot: options.runRoot,
|
|
755
738
|
runId: options.runId,
|
|
756
739
|
outDir,
|
|
757
740
|
});
|
|
758
|
-
const runId = requiredNonEmpty(resolvedInput.runId, '--run-id', '
|
|
741
|
+
const runId = requiredNonEmpty(resolvedInput.runId, '--run-id', 'PROCESS_QA_RUN_ID_REQUIRED');
|
|
759
742
|
const logicVersion = options.logicVersion?.trim() || 'v2.1';
|
|
760
743
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
761
744
|
const env = options.env ?? process.env;
|
|
@@ -783,6 +766,12 @@ export async function runProcessReview(options) {
|
|
|
783
766
|
: isRecord(exchangesValue)
|
|
784
767
|
? [exchangesValue]
|
|
785
768
|
: [];
|
|
769
|
+
const referenceFlowId = String(deepGet(processPayload, [
|
|
770
|
+
'processDataSet',
|
|
771
|
+
'processInformation',
|
|
772
|
+
'quantitativeReference',
|
|
773
|
+
'referenceToReferenceFlow',
|
|
774
|
+
]) ?? '').trim() || null;
|
|
786
775
|
const base = baseInfoCheck(processPayload);
|
|
787
776
|
const fileName = path.basename(filePath);
|
|
788
777
|
baseRows.push([fileName, base]);
|
|
@@ -793,13 +782,13 @@ export async function runProcessReview(options) {
|
|
|
793
782
|
let waste = 0;
|
|
794
783
|
let energyExcluded = 0;
|
|
795
784
|
exchanges.forEach((exchange) => {
|
|
796
|
-
const classified = classifyExchange(exchange);
|
|
785
|
+
const classified = classifyExchange(exchange, referenceFlowId);
|
|
797
786
|
if (!hasNumericAmount(exchange)) {
|
|
798
|
-
ruleFindings.push(
|
|
787
|
+
ruleFindings.push(createProcessQaFinding({
|
|
799
788
|
processFile: fileName,
|
|
800
|
-
severity: '
|
|
789
|
+
severity: 'warning',
|
|
801
790
|
code: 'process_missing_exchange_amount',
|
|
802
|
-
message: 'Process exchange is missing both numeric meanAmount and resultingAmount.',
|
|
791
|
+
message: 'Process exchange is missing both numeric meanAmount and resultingAmount and should be curated by Foundry.',
|
|
803
792
|
evidence: {
|
|
804
793
|
exchange_internal_id: String(exchange['@dataSetInternalID'] ?? ''),
|
|
805
794
|
direction: String(exchange.exchangeDirection ?? ''),
|
|
@@ -825,7 +814,7 @@ export async function runProcessReview(options) {
|
|
|
825
814
|
const currentUnitIssues = unitIssueCheck(exchange, classified.uoms, classified.blob);
|
|
826
815
|
unitIssues.push(...currentUnitIssues);
|
|
827
816
|
currentUnitIssues.forEach((issue) => {
|
|
828
|
-
ruleFindings.push(
|
|
817
|
+
ruleFindings.push(createProcessQaFinding({
|
|
829
818
|
processFile: fileName,
|
|
830
819
|
severity: 'warning',
|
|
831
820
|
code: 'process_exchange_unit_semantic_mismatch',
|
|
@@ -848,17 +837,18 @@ export async function runProcessReview(options) {
|
|
|
848
837
|
relative_deviation: relativeDeviation,
|
|
849
838
|
});
|
|
850
839
|
if (relativeDeviation !== null && relativeDeviation > 0.05) {
|
|
851
|
-
ruleFindings.push(
|
|
840
|
+
ruleFindings.push(createProcessQaFinding({
|
|
852
841
|
processFile: fileName,
|
|
853
|
-
severity:
|
|
842
|
+
severity: 'warning',
|
|
854
843
|
code: 'process_material_balance_deviation',
|
|
855
|
-
message: 'Material balance deviation exceeds the
|
|
844
|
+
message: 'Material balance deviation exceeds the QA threshold for raw inputs versus product/by-product/waste outputs.',
|
|
856
845
|
evidence: {
|
|
857
846
|
raw_input: rawInput,
|
|
858
847
|
product,
|
|
859
848
|
byproduct,
|
|
860
849
|
waste,
|
|
861
850
|
relative_deviation: relativeDeviation,
|
|
851
|
+
policy_decision_owner: 'foundry',
|
|
862
852
|
},
|
|
863
853
|
}));
|
|
864
854
|
}
|
|
@@ -872,7 +862,7 @@ export async function runProcessReview(options) {
|
|
|
872
862
|
process_file: fileName,
|
|
873
863
|
base_names: base.base_names.slice(0, 4),
|
|
874
864
|
base_checks: {
|
|
875
|
-
|
|
865
|
+
source_name_ok: base.source_name_ok,
|
|
876
866
|
functional_unit_ok: base.functional_unit_ok,
|
|
877
867
|
system_boundary_ok: base.system_boundary_ok,
|
|
878
868
|
time_ok: base.time_ok,
|
|
@@ -901,7 +891,7 @@ export async function runProcessReview(options) {
|
|
|
901
891
|
energy_excluded: totalEnergy,
|
|
902
892
|
};
|
|
903
893
|
const evidenceStrong = [
|
|
904
|
-
'
|
|
894
|
+
'已优先使用 quantitativeReference.referenceToReferenceFlow、EcoSpold inputGroup/outputGroup 和 exchange 标签/描述做口径过滤,仅核算 原材料投入 vs 产品+副产品+废物,能量单列不计入平衡。',
|
|
905
895
|
...(unitIssues.length > 0
|
|
906
896
|
? ['发现单位疑似错误时均附带 flow 描述与单位标签的直接矛盾证据。']
|
|
907
897
|
: []),
|
|
@@ -925,11 +915,13 @@ export async function runProcessReview(options) {
|
|
|
925
915
|
totals,
|
|
926
916
|
ruleset_gate: rulesetGate,
|
|
927
917
|
llm: llmResult,
|
|
918
|
+
policy_decision_owner: 'foundry',
|
|
919
|
+
qa_mode: 'deterministic_qa_report',
|
|
928
920
|
};
|
|
929
|
-
const reviewInputSummaryPath = writeJsonArtifact(path.join(outDir, '
|
|
930
|
-
const ruleFindingsPath = writeJsonLinesArtifact(path.join(outDir, 'process-
|
|
931
|
-
const rulesetGatePath = writeJsonArtifact(path.join(outDir, 'process-
|
|
932
|
-
const reviewZhPath = writeTextArtifact(path.join(outDir, '
|
|
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({
|
|
933
925
|
runId,
|
|
934
926
|
logicVersion,
|
|
935
927
|
baseRows,
|
|
@@ -939,7 +931,7 @@ export async function runProcessReview(options) {
|
|
|
939
931
|
evidenceStrong,
|
|
940
932
|
evidenceWeak,
|
|
941
933
|
}));
|
|
942
|
-
const reviewEnPath = writeTextArtifact(path.join(outDir, '
|
|
934
|
+
const reviewEnPath = writeTextArtifact(path.join(outDir, 'one_flow_rerun_qa_v2_1_en.md'), renderEnReview({
|
|
943
935
|
runId,
|
|
944
936
|
logicVersion,
|
|
945
937
|
baseRows,
|
|
@@ -955,11 +947,11 @@ export async function runProcessReview(options) {
|
|
|
955
947
|
processCount: processFiles.length,
|
|
956
948
|
}));
|
|
957
949
|
const unitIssuePath = writeTextArtifact(path.join(outDir, 'flow_unit_issue_log.md'), renderUnitIssues(runId, unitIssues));
|
|
958
|
-
const summaryPath = writeJsonArtifact(path.join(outDir, '
|
|
950
|
+
const summaryPath = writeJsonArtifact(path.join(outDir, 'qa_summary_v2_1.json'), summary);
|
|
959
951
|
const report = {
|
|
960
952
|
schema_version: 1,
|
|
961
953
|
generated_at_utc: (options.now ?? (() => new Date()))().toISOString(),
|
|
962
|
-
status: '
|
|
954
|
+
status: 'completed_local_process_qa',
|
|
963
955
|
run_id: runId,
|
|
964
956
|
run_root: resolvedInput.runRoot,
|
|
965
957
|
rows_file: resolvedInput.rowsFile,
|
|
@@ -976,13 +968,15 @@ export async function runProcessReview(options) {
|
|
|
976
968
|
rule_finding_count: ruleFindings.length,
|
|
977
969
|
blocker_count: rulesetGate.counts.blockers,
|
|
978
970
|
ruleset_gate: rulesetGate,
|
|
971
|
+
policy_decision_owner: 'foundry',
|
|
972
|
+
qa_mode: 'deterministic_qa_report',
|
|
979
973
|
files: {
|
|
980
|
-
|
|
974
|
+
qa_input_summary: reviewInputSummaryPath,
|
|
981
975
|
materialization_summary: resolvedInput.materializationSummaryPath,
|
|
982
976
|
rule_findings: ruleFindingsPath,
|
|
983
977
|
ruleset_gate: rulesetGatePath,
|
|
984
|
-
|
|
985
|
-
|
|
978
|
+
qa_zh: reviewZhPath,
|
|
979
|
+
qa_en: reviewEnPath,
|
|
986
980
|
timing: timingPath,
|
|
987
981
|
unit_issue_log: unitIssuePath,
|
|
988
982
|
summary: summaryPath,
|
|
@@ -990,7 +984,7 @@ export async function runProcessReview(options) {
|
|
|
990
984
|
},
|
|
991
985
|
llm: llmResult,
|
|
992
986
|
};
|
|
993
|
-
const reportPath = writeJsonArtifact(path.join(outDir, 'process-
|
|
987
|
+
const reportPath = writeJsonArtifact(path.join(outDir, 'process-qa-report.json'), report);
|
|
994
988
|
report.files.report = reportPath;
|
|
995
989
|
writeJsonArtifact(reportPath, report);
|
|
996
990
|
return report;
|
|
@@ -1005,7 +999,7 @@ export const __testInternals = {
|
|
|
1005
999
|
classifyExchange,
|
|
1006
1000
|
unitIssueCheck,
|
|
1007
1001
|
hasNumericAmount,
|
|
1008
|
-
|
|
1002
|
+
createProcessQaFinding,
|
|
1009
1003
|
reviewFindingsForBase,
|
|
1010
1004
|
processRulesetGate,
|
|
1011
1005
|
baseInfoCheck,
|
|
@@ -1018,4 +1012,4 @@ export const __testInternals = {
|
|
|
1018
1012
|
renderTiming,
|
|
1019
1013
|
renderUnitIssues,
|
|
1020
1014
|
};
|
|
1021
|
-
//# sourceMappingURL=
|
|
1015
|
+
//# sourceMappingURL=process-qa.js.map
|