@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.
Files changed (59) hide show
  1. package/README.md +97 -16
  2. package/dist/src/cli.js +1429 -131
  3. package/dist/src/cli.js.map +1 -1
  4. package/dist/src/lib/dataset-author.js +100 -0
  5. package/dist/src/lib/dataset-author.js.map +1 -0
  6. package/dist/src/lib/dataset-bilingual.js +545 -0
  7. package/dist/src/lib/dataset-bilingual.js.map +1 -0
  8. package/dist/src/lib/dataset-contract.js +350 -0
  9. package/dist/src/lib/dataset-contract.js.map +1 -0
  10. package/dist/src/lib/dataset-evidence-search.js +636 -0
  11. package/dist/src/lib/dataset-evidence-search.js.map +1 -0
  12. package/dist/src/lib/dataset-import-lca.js +171 -0
  13. package/dist/src/lib/dataset-import-lca.js.map +1 -0
  14. package/dist/src/lib/dataset-remote-refresh.js +166 -0
  15. package/dist/src/lib/dataset-remote-refresh.js.map +1 -0
  16. package/dist/src/lib/dataset-remote-verify.js +543 -0
  17. package/dist/src/lib/dataset-remote-verify.js.map +1 -0
  18. package/dist/src/lib/dataset-validate.js +63 -7
  19. package/dist/src/lib/dataset-validate.js.map +1 -1
  20. package/dist/src/lib/flow-fetch-rows.js +12 -12
  21. package/dist/src/lib/flow-fetch-rows.js.map +1 -1
  22. package/dist/src/lib/flow-payload-validation.js +51 -0
  23. package/dist/src/lib/flow-payload-validation.js.map +1 -0
  24. package/dist/src/lib/flow-publish-reviewed-data.js +16 -0
  25. package/dist/src/lib/flow-publish-reviewed-data.js.map +1 -1
  26. package/dist/src/lib/flow-publish-version.js +182 -12
  27. package/dist/src/lib/flow-publish-version.js.map +1 -1
  28. package/dist/src/lib/{review-flow.js → flow-qa.js} +86 -28
  29. package/dist/src/lib/flow-qa.js.map +1 -0
  30. package/dist/src/lib/identity-preflight.js +1021 -0
  31. package/dist/src/lib/identity-preflight.js.map +1 -0
  32. package/dist/src/lib/{review-lifecyclemodel.js → lifecyclemodel-qa.js} +54 -54
  33. package/dist/src/lib/lifecyclemodel-qa.js.map +1 -0
  34. package/dist/src/lib/process-auto-build.js +147 -0
  35. package/dist/src/lib/process-auto-build.js.map +1 -1
  36. package/dist/src/lib/process-dedup-review.js +51 -0
  37. package/dist/src/lib/process-dedup-review.js.map +1 -1
  38. package/dist/src/lib/process-flow-build-plan.js +1071 -0
  39. package/dist/src/lib/process-flow-build-plan.js.map +1 -0
  40. package/dist/src/lib/process-payload-validation.js +14 -7
  41. package/dist/src/lib/process-payload-validation.js.map +1 -1
  42. package/dist/src/lib/process-publish-build.js +122 -4
  43. package/dist/src/lib/process-publish-build.js.map +1 -1
  44. package/dist/src/lib/{review-process.js → process-qa.js} +259 -117
  45. package/dist/src/lib/process-qa.js.map +1 -0
  46. package/dist/src/lib/process-refresh-references.js +19 -10
  47. package/dist/src/lib/process-refresh-references.js.map +1 -1
  48. package/dist/src/lib/process-required-fields.js +810 -0
  49. package/dist/src/lib/process-required-fields.js.map +1 -0
  50. package/dist/src/lib/process-save-draft-run.js +4 -1
  51. package/dist/src/lib/process-save-draft-run.js.map +1 -1
  52. package/dist/src/lib/publish.js +100 -0
  53. package/dist/src/lib/publish.js.map +1 -1
  54. package/dist/src/lib/runtime-rulesets.js +283 -0
  55. package/dist/src/lib/runtime-rulesets.js.map +1 -0
  56. package/package.json +2 -2
  57. package/dist/src/lib/review-flow.js.map +0 -1
  58. package/dist/src/lib/review-lifecyclemodel.js.map +0 -1
  59. 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 { invokeLlm, readLlmRuntimeEnv } from './llm.js';
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
- if (!(zh && en) && values.length >= 2) {
134
- zh = zh || true;
135
- 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;
136
130
  }
137
- return [zh, en, values];
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', kinds, uoms, blob };
168
+ return { classification: 'energy_input', ...meta };
154
169
  }
155
170
  if (kindSet.has('waste')) {
156
- return { classification: 'other_input', kinds, uoms, blob };
171
+ return { classification: 'other_input', ...meta };
157
172
  }
158
- if (kindSet.has('raw_material') || kindSet.has('resource')) {
159
- 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 };
160
175
  }
161
176
  if (kindSet.has('product') || RAW_WORDS.some((word) => blob.includes(word))) {
162
- return { classification: 'raw_material_input', kinds, uoms, blob };
177
+ return { classification: 'raw_material_input', ...meta };
163
178
  }
164
- return { classification: 'other_input', kinds, uoms, blob };
179
+ return { classification: 'other_input', ...meta };
165
180
  }
166
181
  if (direction === 'output') {
167
- if (kindSet.has('waste') || WASTE_WORDS.some((word) => blob.includes(word))) {
168
- 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 };
169
190
  }
170
191
  if (BYP_WORDS.some((word) => blob.includes(word))) {
171
- return { classification: 'byproduct_output', kinds, uoms, blob };
192
+ return { classification: 'byproduct_output', ...meta };
172
193
  }
173
194
  if (kindSet.has('product')) {
174
- return { classification: 'product_output', kinds, uoms, blob };
195
+ return { classification: 'product_output', ...meta };
175
196
  }
176
- return { classification: 'other_output', kinds, uoms, blob };
197
+ return { classification: 'other_output', ...meta };
177
198
  }
178
- return { classification: 'other', kinds, uoms, blob };
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 [zhOk, enOk, values] = extractBaseNames(processPayload);
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
- nameOk,
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
- name_zh_en_ok: nameOk,
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 review file to contain a JSON object: ${filePath}`, {
287
- 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',
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 review file is missing processDataSet: ${filePath}`, {
298
- code: 'PROCESS_REVIEW_INPUT_INVALID',
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: 'PROCESS_REVIEW_ROWS_INVALID_JSONL_ROW',
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 review rows file contains invalid JSONL at line ${index + 1}.`, {
349
- 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',
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 review rows file is not valid JSON: ${resolved}`, {
362
- 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',
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: 'PROCESS_REVIEW_ROWS_INVALID_JSON',
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, 'review-input', 'processes');
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, 'review-input', 'materialization-summary.json');
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 review requires exactly one of --rows-file or --run-root.', {
424
- 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',
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 review directory not found: ${processDir}`, {
467
- code: 'PROCESS_REVIEW_EXPORTS_NOT_FOUND',
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
- let env = readLlmRuntimeEnv(options.env);
507
- if (options.llmModel) {
508
- env = {
509
- ...env,
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
- '# one_flow_rerun_review_v2_1_zh\n',
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|中英名称|功能单位|系统边界|时间|地理|技术|管理元数据|完整性得分(0-7)|\n',
635
+ '|process file|源语言名称|功能单位|系统边界|时间|地理|技术|管理元数据|完整性得分(0-7)|\n',
561
636
  '|---|---|---|---|---|---|---|---|---:|\n',
562
637
  ];
563
638
  options.baseRows.forEach(([fileName, base]) => {
564
- 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`);
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
- '# one_flow_rerun_review_v2_1_en\n',
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|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',
603
678
  '|---|---|---|---|---|---|---|---|---:|\n',
604
679
  ];
605
680
  options.baseRows.forEach(([fileName, base]) => {
606
- 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`);
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: 'PROCESS_REVIEW_INVALID_TIMESTAMP',
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 runProcessReview(options) {
659
- 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'));
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', 'PROCESS_REVIEW_RUN_ID_REQUIRED');
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
- unitIssues.push(...unitIssueCheck(exchange, classified.uoms, classified.blob));
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
- name_zh_en_ok: base.name_zh_en_ok,
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
- '已基于 exchange comment 标签/描述做口径过滤,仅核算 原材料投入 vs 产品+副产品+废物,能量单列不计入平衡。',
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, 'review-input-summary.json'), resolvedInput.reviewInputSummary);
797
- 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({
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, '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({
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, 'review_summary_v2_1.json'), summary);
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: 'completed_local_process_review',
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
- review_input_summary: reviewInputSummaryPath,
974
+ qa_input_summary: reviewInputSummaryPath,
839
975
  materialization_summary: resolvedInput.materializationSummaryPath,
840
- review_zh: reviewZhPath,
841
- review_en: reviewEnPath,
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-review-report.json'), report);
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=review-process.js.map
1015
+ //# sourceMappingURL=process-qa.js.map