@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.
@@ -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 { invokeLlm, readLlmRuntimeEnv } from './llm.js';
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
- if (!(zh && en) && values.length >= 2) {
135
- zh = zh || true;
136
- en = en || true;
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 [zh, en, values];
131
+ return isRecord(payload.sourceTrace) ? payload.sourceTrace : payload;
139
132
  }
140
- function classifyExchange(exchange) {
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', kinds, uoms, blob };
168
+ return { classification: 'energy_input', ...meta };
155
169
  }
156
170
  if (kindSet.has('waste')) {
157
- return { classification: 'other_input', kinds, uoms, blob };
171
+ return { classification: 'other_input', ...meta };
158
172
  }
159
- if (kindSet.has('raw_material') || kindSet.has('resource')) {
160
- return { classification: 'raw_material_input', kinds, uoms, blob };
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', kinds, uoms, blob };
177
+ return { classification: 'raw_material_input', ...meta };
164
178
  }
165
- return { classification: 'other_input', kinds, uoms, blob };
179
+ return { classification: 'other_input', ...meta };
166
180
  }
167
181
  if (direction === 'output') {
168
- if (kindSet.has('waste') || WASTE_WORDS.some((word) => blob.includes(word))) {
169
- return { classification: 'waste_output', kinds, uoms, blob };
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', kinds, uoms, blob };
192
+ return { classification: 'byproduct_output', ...meta };
173
193
  }
174
194
  if (kindSet.has('product')) {
175
- return { classification: 'product_output', kinds, uoms, blob };
195
+ return { classification: 'product_output', ...meta };
176
196
  }
177
- return { classification: 'other_output', kinds, uoms, blob };
197
+ return { classification: 'other_output', ...meta };
178
198
  }
179
- return { classification: 'other', kinds, uoms, blob };
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 createProcessReviewFinding(options) {
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.name_zh_en_ok) {
257
- findings.push(createProcessReviewFinding({
276
+ if (!base.source_name_ok) {
277
+ findings.push(createProcessQaFinding({
258
278
  processFile,
259
- severity: 'blocker',
260
- code: 'process_missing_bilingual_base_name',
261
- message: 'Process baseName must include usable Chinese and English entries.',
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(createProcessReviewFinding({
286
+ findings.push(createProcessQaFinding({
267
287
  processFile,
268
- severity: 'blocker',
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(createProcessReviewFinding({
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' || isRuntimeRuleBlocker(finding.methodology_rule_id));
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
- ? 'review_findings'
337
+ ? 'review_findings_in_foundry'
318
338
  : 'continue',
319
339
  };
320
340
  }
321
341
  function baseInfoCheck(processPayload) {
322
- const [zhOk, enOk, values] = extractBaseNames(processPayload);
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
- nameOk,
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
- name_zh_en_ok: nameOk,
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 review file to contain a JSON object: ${filePath}`, {
379
- code: 'PROCESS_REVIEW_INPUT_INVALID',
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 review file is missing processDataSet: ${filePath}`, {
390
- code: 'PROCESS_REVIEW_INPUT_INVALID',
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: 'PROCESS_REVIEW_ROWS_INVALID_JSONL_ROW',
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 review rows file contains invalid JSONL at line ${index + 1}.`, {
441
- code: 'PROCESS_REVIEW_ROWS_INVALID_JSONL',
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 review rows file is not valid JSON: ${resolved}`, {
454
- code: 'PROCESS_REVIEW_ROWS_INVALID_JSON',
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: 'PROCESS_REVIEW_ROWS_INVALID_JSON',
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, 'review-input', 'processes');
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, 'review-input', 'materialization-summary.json');
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 review requires exactly one of --rows-file or --run-root.', {
516
- code: 'PROCESS_REVIEW_INPUT_MODE_REQUIRED',
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 review directory not found: ${processDir}`, {
559
- code: 'PROCESS_REVIEW_EXPORTS_NOT_FOUND',
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
- let env = readLlmRuntimeEnv(options.env);
599
- if (options.llmModel) {
600
- env = {
601
- ...env,
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
- '# one_flow_rerun_review_v2_1_zh\n',
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|中英名称|功能单位|系统边界|时间|地理|技术|管理元数据|完整性得分(0-7)|\n',
635
+ '|process file|源语言名称|功能单位|系统边界|时间|地理|技术|管理元数据|完整性得分(0-7)|\n',
653
636
  '|---|---|---|---|---|---|---|---|---:|\n',
654
637
  ];
655
638
  options.baseRows.forEach(([fileName, base]) => {
656
- lines.push(`|${fileName}|${base.name_zh_en_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`);
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
- '# one_flow_rerun_review_v2_1_en\n',
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|zh+en names|functional unit|system boundary|time|geo|tech|admin metadata|completeness(0-7)|\n',
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.name_zh_en_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`);
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: 'PROCESS_REVIEW_INVALID_TIMESTAMP',
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 runProcessReview(options) {
751
- const outDir = path.resolve(requiredNonEmpty(options.outDir, '--out-dir', 'PROCESS_REVIEW_OUT_DIR_REQUIRED'));
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', 'PROCESS_REVIEW_RUN_ID_REQUIRED');
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(createProcessReviewFinding({
787
+ ruleFindings.push(createProcessQaFinding({
799
788
  processFile: fileName,
800
- severity: 'blocker',
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(createProcessReviewFinding({
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(createProcessReviewFinding({
840
+ ruleFindings.push(createProcessQaFinding({
852
841
  processFile: fileName,
853
- severity: relativeDeviation > 0.1 ? 'blocker' : 'warning',
842
+ severity: 'warning',
854
843
  code: 'process_material_balance_deviation',
855
- message: 'Material balance deviation exceeds the review threshold for raw inputs versus product/by-product/waste outputs.',
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
- name_zh_en_ok: base.name_zh_en_ok,
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
- '已基于 exchange comment 标签/描述做口径过滤,仅核算 原材料投入 vs 产品+副产品+废物,能量单列不计入平衡。',
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, 'review-input-summary.json'), resolvedInput.reviewInputSummary);
930
- const ruleFindingsPath = writeJsonLinesArtifact(path.join(outDir, 'process-review-rule-findings.jsonl'), ruleFindings);
931
- const rulesetGatePath = writeJsonArtifact(path.join(outDir, 'process-review-ruleset-gate.json'), rulesetGate);
932
- const reviewZhPath = writeTextArtifact(path.join(outDir, 'one_flow_rerun_review_v2_1_zh.md'), renderZhReview({
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, 'one_flow_rerun_review_v2_1_en.md'), renderEnReview({
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, 'review_summary_v2_1.json'), summary);
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: 'completed_local_process_review',
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
- review_input_summary: reviewInputSummaryPath,
974
+ qa_input_summary: reviewInputSummaryPath,
981
975
  materialization_summary: resolvedInput.materializationSummaryPath,
982
976
  rule_findings: ruleFindingsPath,
983
977
  ruleset_gate: rulesetGatePath,
984
- review_zh: reviewZhPath,
985
- review_en: reviewEnPath,
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-review-report.json'), report);
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
- createProcessReviewFinding,
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=review-process.js.map
1015
+ //# sourceMappingURL=process-qa.js.map