kld-sdd 2.6.16 → 2.6.21

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 (70) hide show
  1. package/bin/kld-sdd-init.js +10 -0
  2. package/kld-sdd-guide.html +22 -0
  3. package/lib/device-auth-cli.js +130 -0
  4. package/lib/device-auth.js +345 -0
  5. package/lib/init-account-binding.js +108 -0
  6. package/lib/init.js +238 -55
  7. package/lib/skills-bundle.js +20 -1
  8. package/lib/tool-profiles.js +8 -0
  9. package/package.json +7 -3
  10. package/skywalk-sdd/apply-worktree-finish.cjs +2 -23
  11. package/skywalk-sdd/context-client.cjs +38 -87
  12. package/skywalk-sdd/index.cjs +860 -132
  13. package/skywalk-sdd/kb-sync-identity.cjs +780 -0
  14. package/skywalk-sdd/kb-upload.cjs +505 -0
  15. package/skywalk-sdd/lib/shared.cjs +811 -0
  16. package/skywalk-sdd/lib/usage-contract.cjs +276 -0
  17. package/skywalk-sdd/lib/usage-reporter.cjs +354 -0
  18. package/skywalk-sdd/lib/user-config.cjs +157 -0
  19. package/skywalk-sdd/metrics-v3.cjs +138 -8
  20. package/skywalk-sdd/ontology/archive-package.cjs +19 -34
  21. package/skywalk-sdd/ontology/change-lock.cjs +3 -7
  22. package/skywalk-sdd/ontology/external-key.cjs +18 -4
  23. package/skywalk-sdd/ontology/id.cjs +26 -5
  24. package/skywalk-sdd/ontology/identity-index.cjs +3 -7
  25. package/skywalk-sdd/ontology/resolve-spec-root.cjs +20 -6
  26. package/skywalk-sdd/ontology/runtime.cjs +16 -12
  27. package/skywalk-sdd/ontology/traceability-validator.cjs +7 -4
  28. package/skywalk-sdd/reporting/change-report-markdown.cjs +137 -19
  29. package/skywalk-sdd/reporting/change-report-model.cjs +993 -14
  30. package/skywalk-sdd/reporting/change-report-renderer.cjs +106 -41
  31. package/skywalk-sdd/reporting/change-report-view-model.cjs +272 -43
  32. package/skywalk-sdd/reporting/core-metric-definitions.cjs +192 -0
  33. package/skywalk-sdd/spec-root.cjs +31 -0
  34. package/templates/git-hooks/pre-commit-consistency-check.cjs +271 -116
  35. package/templates/git-hooks/pre-push-consistency-check.cjs +252 -123
  36. package/templates/hooks/codebuddy/hooks/hook-gate-core.cjs +327 -0
  37. package/templates/hooks/codebuddy/hooks/sdd-apply-test-gate.cjs +54 -9
  38. package/templates/hooks/codebuddy/hooks/sdd-mid-checkpoint.cjs +63 -6
  39. package/templates/hooks/codebuddy/hooks/sdd-tdd-rhythm-gate.cjs +113 -65
  40. package/templates/openspec/proposal.md +7 -3
  41. package/templates/openspec/spec.md +3 -3
  42. package/templates/skills/kld-sdd/opsx-apply/SKILL.md +8 -6
  43. package/templates/skills/kld-sdd/opsx-apply/checklist.md +2 -0
  44. package/templates/skills/kld-sdd/opsx-apply/reference.md +29 -7
  45. package/templates/skills/kld-sdd/opsx-archive/SKILL.md +6 -5
  46. package/templates/skills/kld-sdd/opsx-check/SKILL.md +49 -17
  47. package/templates/skills/kld-sdd/opsx-check/checklist.md +4 -2
  48. package/templates/skills/kld-sdd/opsx-consistency-check/SKILL.md +187 -257
  49. package/templates/skills/kld-sdd/opsx-consistency-check/reference.md +129 -0
  50. package/templates/skills/kld-sdd/opsx-design/SKILL.md +2 -2
  51. package/templates/skills/kld-sdd/opsx-explore/SKILL.md +2 -2
  52. package/templates/skills/kld-sdd/opsx-kb-config/SKILL.md +185 -0
  53. package/templates/skills/kld-sdd/opsx-kb-config/reference.md +127 -0
  54. package/templates/skills/kld-sdd/opsx-kb-ingest/SKILL.md +218 -53
  55. package/templates/skills/kld-sdd/opsx-kb-ingest/reference.md +51 -9
  56. package/templates/skills/kld-sdd/opsx-ontology-query/SKILL.md +12 -50
  57. package/templates/skills/kld-sdd/opsx-ontology-query/phase-3-postchange.md +2 -2
  58. package/templates/skills/kld-sdd/opsx-ontology-query/reference.md +1 -1
  59. package/templates/skills/kld-sdd/opsx-propose/SKILL.md +35 -23
  60. package/templates/skills/kld-sdd/opsx-propose/checklist.md +2 -0
  61. package/templates/skills/kld-sdd/opsx-propose/reference.md +22 -17
  62. package/templates/skills/kld-sdd/opsx-rules/SKILL.md +2 -2
  63. package/templates/skills/kld-sdd/opsx-spec/SKILL.md +19 -15
  64. package/templates/skills/kld-sdd/opsx-spec/checklist.md +2 -0
  65. package/templates/skills/kld-sdd/opsx-task/SKILL.md +2 -4
  66. package/templates/skills/kld-sdd/opsx-test/SKILL.md +2 -2
  67. package/templates/skills/kld-sdd/tdd-core/reference.md +1 -1
  68. package/templates/skills/kld-sdd/tdd-rules/rules/test-skeleton-telemetry.md +1 -1
  69. package/templates/skills/kld-sdd/opsx-kb-ingest/state.example.json +0 -7
  70. package/templates/skills/kld-sdd/opsx-ontology-query/state.example.json +0 -7
@@ -3,12 +3,25 @@
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
5
  const crypto = require('crypto');
6
+ const {
7
+ CORE_METRIC_DEFINITIONS,
8
+ localizeStageName,
9
+ validateCoreMetricResult,
10
+ } = require('./core-metric-definitions.cjs');
6
11
 
7
12
  let formatArtifactSize;
13
+ let computeTelemetryHealthScore;
14
+ let resolveScoreStatus;
8
15
  try {
9
- ({ formatArtifactSize } = require('../metrics-v3.cjs'));
16
+ ({
17
+ formatArtifactSize,
18
+ computeTelemetryHealthScore,
19
+ resolveScoreStatus,
20
+ } = require('../metrics-v3.cjs'));
10
21
  } catch {
11
22
  formatArtifactSize = null;
23
+ computeTelemetryHealthScore = null;
24
+ resolveScoreStatus = null;
12
25
  }
13
26
 
14
27
  function formatArtifactSizeDisplay(bytes) {
@@ -36,6 +49,26 @@ const EVIDENCE_LABELS = {
36
49
  task_update_reuse: '测试结果被重复引用',
37
50
  };
38
51
 
52
+ const RESOLVED_ISSUE_DISPOSITIONS = new Set([
53
+ 'fixed',
54
+ 'accepted',
55
+ 'waived',
56
+ 'informational',
57
+ 'ignored',
58
+ 'not-applicable',
59
+ ]);
60
+ const CANONICAL_ISSUE_DISPOSITIONS = new Set([
61
+ ...RESOLVED_ISSUE_DISPOSITIONS,
62
+ 'needs_input',
63
+ 'open',
64
+ ]);
65
+
66
+ function statusForDisposition(disposition, fallbackStatus = null) {
67
+ if (RESOLVED_ISSUE_DISPOSITIONS.has(disposition)) return 'resolved';
68
+ if (disposition === 'needs_input' || disposition === 'open') return 'open';
69
+ return fallbackStatus || 'open';
70
+ }
71
+
39
72
  function eventTimestamp(event) {
40
73
  const raw = event?.timestamp || event?.ended_at || event?.started_at;
41
74
  const value = Date.parse(raw || '');
@@ -162,7 +195,7 @@ function issueGuidance(code) {
162
195
  expected: 'task_update 应直接引用覆盖当前任务的 strict 成功测试。',
163
196
  root_cause: 'test_event_id 缺失、无效或没有覆盖当前任务。',
164
197
  impact: '测试存在,但任务到测试的直接审计链不完整。',
165
- remediation: ' task_update 精确关联到覆盖当前任务的 strict 测试事件。',
198
+ remediation: '在任务完成事件 task_update 中填写 test_event_id,精确关联到覆盖当前任务的严格测试事件。',
166
199
  },
167
200
  TASK_TEST_EVIDENCE_MISSING: {
168
201
  expected: '任务应有严格成功测试,或有可审计的不适用理由。',
@@ -195,10 +228,9 @@ function normalizeIssue(alert, index) {
195
228
  ...(Array.isArray(alert.evidence_event_ids) ? alert.evidence_event_ids : []),
196
229
  ].filter(Boolean))];
197
230
  const subject = alert.subject || alert.task_id || alert.pair_id || null;
198
- const status = alert.status
199
- || (['fixed', 'accepted', 'waived'].includes(alert.disposition) ? 'resolved' : 'open');
231
+ const status = statusForDisposition(alert.disposition, alert.status);
200
232
  return {
201
- issue_id: alert.issue_id || alert.key || `${code}:${subject || 'change'}:${index + 1}`,
233
+ issue_id: alert.issue_id || alert.key || `${code}:${subject || 'change'}`,
202
234
  category: alert.category || 'evidence',
203
235
  code,
204
236
  label: EVIDENCE_LABELS[code] || code,
@@ -236,11 +268,57 @@ function normalizeEvidenceSummary(report) {
236
268
  ...item,
237
269
  code: item.warning,
238
270
  occurrences: 1,
239
- status: ['fixed', 'accepted', 'waived'].includes(item.disposition) ? 'resolved' : 'open',
271
+ status: statusForDisposition(item.disposition),
240
272
  last_seen_event_id: item.evidence_event_id,
241
273
  })),
242
274
  ];
243
- const issues = history.map(normalizeIssue);
275
+ const issueByKey = new Map();
276
+ history.map(normalizeIssue).forEach((issue) => {
277
+ const stableKey = issue.issue_id
278
+ || `${issue.code}:${issue.task_id || issue.pair_id || issue.subject || 'change'}`;
279
+ const previous = issueByKey.get(stableKey);
280
+ if (!previous) {
281
+ issueByKey.set(stableKey, issue);
282
+ return;
283
+ }
284
+ const previousAt = Date.parse(
285
+ previous.history?.resolved_at
286
+ || previous.history?.last_seen_at
287
+ || previous.history?.first_seen_at
288
+ || '',
289
+ );
290
+ const currentAt = Date.parse(
291
+ issue.history?.resolved_at
292
+ || issue.history?.last_seen_at
293
+ || issue.history?.first_seen_at
294
+ || '',
295
+ );
296
+ const latest = !Number.isFinite(previousAt)
297
+ || (Number.isFinite(currentAt) && currentAt >= previousAt)
298
+ ? issue
299
+ : previous;
300
+ issueByKey.set(stableKey, {
301
+ ...previous,
302
+ ...latest,
303
+ occurrence_count: Math.max(previous.occurrence_count, issue.occurrence_count),
304
+ evidence_event_ids: [...new Set([
305
+ ...previous.evidence_event_ids,
306
+ ...issue.evidence_event_ids,
307
+ ])],
308
+ related_event_ids: [...new Set([
309
+ ...previous.related_event_ids,
310
+ ...issue.related_event_ids,
311
+ ])],
312
+ history: {
313
+ first_seen_at: previous.history?.first_seen_at || issue.history?.first_seen_at || null,
314
+ last_seen_at: latest.history?.last_seen_at || previous.history?.last_seen_at || null,
315
+ resolved_at: latest.status === 'resolved'
316
+ ? (latest.history?.resolved_at || previous.history?.resolved_at || null)
317
+ : null,
318
+ },
319
+ });
320
+ });
321
+ const issues = [...issueByKey.values()];
244
322
  const groups = new Map();
245
323
  for (const issue of issues) {
246
324
  const code = issue.code;
@@ -284,33 +362,741 @@ function normalizeChangedFiles(changedFiles = {}) {
284
362
  ? [...new Set(changedFiles.final_output_files)]
285
363
  : (Array.isArray(changedFiles.files) ? [...new Set(changedFiles.files)] : null);
286
364
  const taskCount = changedFiles.task_event_files_changed
287
- ?? changedFiles.files_changed
288
365
  ?? taskEventFiles?.length
289
366
  ?? null;
290
367
  const finalCount = changedFiles.final_output_files_changed
291
368
  ?? finalOutputFiles?.length
292
369
  ?? null;
370
+ const taskSet = new Set(taskEventFiles || []);
371
+ const finalSet = new Set(finalOutputFiles || []);
372
+ const onlyInTaskEvents = [...taskSet].filter(file => !finalSet.has(file)).sort();
373
+ const onlyInFinalOutput = [...finalSet].filter(file => !taskSet.has(file)).sort();
293
374
  const countsDiffer = taskCount != null && finalCount != null && taskCount !== finalCount;
375
+ const listsDiffer = taskEventFiles != null
376
+ && finalOutputFiles != null
377
+ && (onlyInTaskEvents.length > 0 || onlyInFinalOutput.length > 0);
378
+ let reconciliationStatus = changedFiles.reconciliation_status || 'consistent';
379
+ let reconciliationNote = changedFiles.reconciliation_note || null;
380
+ if (taskCount == null && finalCount == null) {
381
+ reconciliationStatus = 'unavailable';
382
+ reconciliationNote = reconciliationNote
383
+ || '任务过程和最终输出均没有可用的文件证据,暂时无法对账。';
384
+ } else if (taskCount == null && finalCount != null) {
385
+ reconciliationStatus = 'task-evidence-missing';
386
+ reconciliationNote = '任务事件未记录文件清单或无文件原因;仅展示最终输出快照。';
387
+ } else if (taskCount != null && finalCount == null) {
388
+ reconciliationStatus = 'final-snapshot-missing';
389
+ reconciliationNote = '最终输出文件快照不可用,无法与任务事件对账。';
390
+ } else if (countsDiffer) {
391
+ reconciliationStatus = 'count-mismatch';
392
+ } else if (listsDiffer) {
393
+ reconciliationStatus = 'list-mismatch';
394
+ }
294
395
  const normalized = {
295
396
  ...changedFiles,
296
397
  task_event_files: taskEventFiles,
297
398
  task_event_files_changed: taskCount,
298
399
  final_output_files: finalOutputFiles,
299
400
  final_output_files_changed: finalCount,
300
- diff_source: changedFiles.diff_source || 'dual-source',
401
+ diff_source: changedFiles.diff_source
402
+ || (taskCount == null && finalCount == null ? null : 'dual-source'),
301
403
  line_count_estimate: changedFiles.line_count_estimate ?? null,
302
- reconciliation_status: changedFiles.reconciliation_status
303
- || (countsDiffer ? 'count-mismatch' : 'consistent'),
304
- reconciliation_note: changedFiles.reconciliation_note
404
+ only_in_task_events: onlyInTaskEvents,
405
+ only_in_final_output: onlyInFinalOutput,
406
+ reconciliation_status: reconciliationStatus,
407
+ reconciliation_note: reconciliationNote
305
408
  || (countsDiffer
306
409
  ? `任务事件累计涉及 ${taskCount} 个文件;最终输出快照包含 ${finalCount} 个文件。两者用途不同,不应合并为一个口径。`
307
- : '任务事件与最终输出文件口径一致。'),
410
+ : (listsDiffer
411
+ ? `文件数量均为 ${taskCount},但清单不同:任务过程独有 ${onlyInTaskEvents.length} 个,最终交付独有 ${onlyInFinalOutput.length} 个。`
412
+ : '任务过程中涉及的文件与最终实际交付的文件清单一致。')),
308
413
  };
309
414
  // v4:不再对外输出 legacy_fields;若调用方传入则显式剔除
310
415
  delete normalized.legacy_fields;
311
416
  return normalized;
312
417
  }
313
418
 
419
+ const STANDARD_STAGES = new Set(['propose', 'spec', 'design', 'task', 'check', 'apply', 'archive']);
420
+ const REWORK_REASON_LABELS = {
421
+ incomplete: '上一次没有正常结束',
422
+ 'prev-failed': '上一次执行失败',
423
+ 'code-changed-after-pass': '通过后发生开发活动,再次执行',
424
+ 'recheck-no-change': '期间没有开发活动,属于重复复检',
425
+ 'pre-archive-recheck': '归档前复检',
426
+ unspecified: '现有证据无法判断原因',
427
+ };
428
+ const CHECK_FALLBACK_REASON_LABELS = {
429
+ SUBAGENT_CAPABILITY_UNAVAILABLE: '当前宿主不提供可用的子代理能力',
430
+ SUBAGENT_PERMISSION_DENIED: '当前权限不允许启动子代理',
431
+ SUBAGENT_START_FAILED: '子代理启动失败',
432
+ SUBAGENT_TIMEOUT: '子代理执行超时',
433
+ SUBAGENT_RESULT_INVALID: '子代理返回结果不完整或无法校验',
434
+ };
435
+
436
+ function hasFiniteMetricNumber(value) {
437
+ if (typeof value === 'number') return Number.isFinite(value);
438
+ if (typeof value === 'string' && value.trim()) return Number.isFinite(Number(value));
439
+ return false;
440
+ }
441
+
442
+ function humanDuration(ms) {
443
+ if (!hasFiniteMetricNumber(ms) || Number(ms) < 0) return '暂无数据';
444
+ const seconds = Math.round(Number(ms) / 1000);
445
+ const minutes = Math.floor(seconds / 60);
446
+ const remainder = seconds % 60;
447
+ return `${minutes > 0 ? `${minutes}分` : ''}${remainder}秒`;
448
+ }
449
+
450
+ function evidenceFor(report, key) {
451
+ const metricEvidence = report.metrics?.metric_evidence?.[key]
452
+ || report.metric_provenance?.[key]
453
+ || report.metric_evidence?.[key]
454
+ || {};
455
+ const ids = metricEvidence.event_ids
456
+ || metricEvidence.evidence_event_ids
457
+ || [];
458
+ return {
459
+ status: metricEvidence.status || null,
460
+ reason: metricEvidence.reason || null,
461
+ source_event_count: metricEvidence.source_event_count ?? null,
462
+ event_ids: Array.isArray(ids) ? ids : [],
463
+ };
464
+ }
465
+
466
+ function trustFor(report, key, fallbackStatus, explanation) {
467
+ const evidence = evidenceFor(report, key);
468
+ const status = evidence.status || fallbackStatus || 'provisional';
469
+ const defaultExplanation = (() => {
470
+ if (status === 'trusted' || status === 'verified') {
471
+ return '原始输入和来源证据完整。';
472
+ }
473
+ if (['missing', 'unavailable', 'pending-upstream'].includes(status)) {
474
+ return '当前没有可用的计算结果或来源证据。';
475
+ }
476
+ return '已有计算结果,但证据或独立复核信息仍不完整。';
477
+ })();
478
+ return {
479
+ status,
480
+ explanation: explanation || defaultExplanation,
481
+ };
482
+ }
483
+
484
+ function roundMetric(value, digits = 4) {
485
+ if (!Number.isFinite(Number(value))) return null;
486
+ const factor = 10 ** digits;
487
+ return Math.round(Number(value) * factor) / factor;
488
+ }
489
+
490
+ function applyConsistencyCheck(detail, expectedValue, comparable) {
491
+ const actualValue = detail?.result?.value;
492
+ if (!comparable || !hasFiniteMetricNumber(expectedValue)) {
493
+ return detail;
494
+ }
495
+ if (!hasFiniteMetricNumber(actualValue)) {
496
+ return {
497
+ ...detail,
498
+ trust: {
499
+ status: 'missing',
500
+ explanation: `按本页原始数据可复算为 ${expectedValue},但报告结果缺失,不能标记为可信。`,
501
+ detail_downgrade: true,
502
+ },
503
+ };
504
+ }
505
+ const actual = Number(actualValue);
506
+ const expected = Number(expectedValue);
507
+ if (Math.abs(actual - expected) <= 0.0001) return detail;
508
+ return {
509
+ ...detail,
510
+ trust: {
511
+ status: 'inconsistent',
512
+ explanation: `计算明细与结果不一致:按本页原始数据复算应为 ${expected},报告结果为 ${actual}。请检查上游采集或指标生成过程。`,
513
+ },
514
+ };
515
+ }
516
+
517
+ function markDetailInconsistent(detail, explanation) {
518
+ return {
519
+ ...detail,
520
+ trust: {
521
+ status: 'inconsistent',
522
+ explanation: `计算明细与结果不一致:${explanation}。请检查上游采集或指标生成过程。`,
523
+ },
524
+ };
525
+ }
526
+
527
+ function markDetailProvisional(detail, explanation) {
528
+ if (['inconsistent', 'missing'].includes(detail?.trust?.status)) return detail;
529
+ return {
530
+ ...detail,
531
+ trust: {
532
+ status: 'provisional',
533
+ explanation,
534
+ detail_downgrade: true,
535
+ },
536
+ };
537
+ }
538
+
539
+ function validateMetricDetailResult(metricKey, detail) {
540
+ const validation = validateCoreMetricResult(metricKey, detail?.result?.value);
541
+ if (validation.status === 'missing') {
542
+ return {
543
+ ...detail,
544
+ trust: {
545
+ status: 'missing',
546
+ explanation: '报告结果缺失或不是有效数字,不能标记为可信。',
547
+ detail_downgrade: true,
548
+ },
549
+ };
550
+ }
551
+ if (validation.status === 'invalid') {
552
+ return {
553
+ ...detail,
554
+ result: {
555
+ ...(detail.result || {}),
556
+ display_value: `无效值(原始记录:${detail.result.value})`,
557
+ },
558
+ trust: {
559
+ status: 'inconsistent',
560
+ explanation: `${validation.explanation}请检查上游采集或指标生成过程。`,
561
+ },
562
+ };
563
+ }
564
+ return detail;
565
+ }
566
+
567
+ function metricValuesDiffer(left, right) {
568
+ return left !== null
569
+ && left !== undefined
570
+ && right !== null
571
+ && right !== undefined
572
+ && Number.isFinite(Number(left))
573
+ && Number.isFinite(Number(right))
574
+ && Math.abs(Number(left) - Number(right)) > 0.0001;
575
+ }
576
+
577
+ function buildMetricDetails(report) {
578
+ const metrics = report.metrics || {};
579
+ const efficiency = metrics.efficiency || {};
580
+ const quality = metrics.quality || {};
581
+ const process = metrics.process || {};
582
+ const telemetry = metrics.telemetry_health || {};
583
+ const nodes = Array.isArray(report.stage_timeline?.nodes) ? report.stage_timeline.nodes : [];
584
+ const includedNodes = nodes.filter(node => STANDARD_STAGES.has(node.stage || node.command));
585
+ const excludedNodes = nodes.filter(node => !STANDARD_STAGES.has(node.stage || node.command));
586
+ const includedNodeDurations = includedNodes.map(node => (
587
+ hasFiniteMetricNumber(node.duration_ms) ? Number(node.duration_ms) : null
588
+ ));
589
+ const invalidDetailedStageDurationCount = includedNodeDurations
590
+ .filter(duration => duration == null || duration < 0)
591
+ .length;
592
+ const hasDetailedStageDuration = includedNodes.length > 0
593
+ && invalidDetailedStageDurationCount === 0;
594
+ const detailedStageDuration = hasDetailedStageDuration
595
+ ? includedNodeDurations.reduce((sum, duration) => sum + duration, 0)
596
+ : null;
597
+ const stageDuration = efficiency.total_stage_duration_ms
598
+ ?? efficiency.measurement_stage_duration_ms
599
+ ?? process.rework_summary?.total_stage_duration_ms
600
+ ?? null;
601
+ const scenarioCount = efficiency.scenario_count ?? null;
602
+ const e1Value = efficiency.e1_scenario_delivery_efficiency_ms ?? null;
603
+ const e1Numerator = hasDetailedStageDuration ? detailedStageDuration : stageDuration;
604
+ const expectedE1 = e1Numerator != null
605
+ && Number.isFinite(Number(e1Numerator))
606
+ && Number(scenarioCount) > 0
607
+ ? roundMetric(Number(e1Numerator) / Number(scenarioCount))
608
+ : null;
609
+ const endpointElapsed = efficiency.e1_lead_time_ms ?? null;
610
+ const e1Evidence = evidenceFor(report, 'e1');
611
+
612
+ let e1 = applyConsistencyCheck({
613
+ ...CORE_METRIC_DEFINITIONS.e1,
614
+ raw_values: {
615
+ total_stage_duration_ms: stageDuration,
616
+ detailed_stage_duration_ms: detailedStageDuration,
617
+ endpoint_elapsed_ms: endpointElapsed,
618
+ scenario_count: scenarioCount,
619
+ stage_execution_count: nodes.length,
620
+ included_stage_execution_count: includedNodes.length,
621
+ excluded_stage_execution_count: excludedNodes.length,
622
+ expected_result: expectedE1,
623
+ },
624
+ raw_inputs: [
625
+ {
626
+ label: '标准阶段总耗时(含返工)',
627
+ value: stageDuration,
628
+ display_value: humanDuration(stageDuration),
629
+ source: '标准阶段开始/结束事件配对后的耗时合计',
630
+ included: true,
631
+ },
632
+ {
633
+ label: '验收场景总数',
634
+ value: scenarioCount,
635
+ display_value: scenarioCount == null ? '暂无数据' : `${scenarioCount} 条`,
636
+ source: (efficiency.scenario_sources || []).map(item => item.path).filter(Boolean).join('、') || 'spec.md 场景标题',
637
+ included: true,
638
+ },
639
+ ...nodes.map(node => ({
640
+ label: `${node.stage || node.command || '未知阶段'} · 第 ${node.round || 1} 轮`,
641
+ value: node.duration_ms ?? null,
642
+ display_value: humanDuration(node.duration_ms),
643
+ source: node.event_id || node.start_event_id || '阶段时间线',
644
+ included: STANDARD_STAGES.has(node.stage || node.command),
645
+ exclusion_reason: STANDARD_STAGES.has(node.stage || node.command)
646
+ ? null
647
+ : '辅助阶段不进入 E1 分子',
648
+ })),
649
+ ],
650
+ calculation_steps: [
651
+ hasDetailedStageDuration
652
+ ? `逐条相加 ${includedNodes.length} 条计入的标准阶段执行记录,明细合计为 ${detailedStageDuration} ms;汇总字段记录为 ${stageDuration ?? '暂无数据'} ms。`
653
+ : (includedNodes.length > 0
654
+ ? `${invalidDetailedStageDurationCount} 条计入的阶段明细缺少耗时或耗时无效,不能独立求和;暂用汇总字段 ${stageDuration ?? '暂无数据'} ms。`
655
+ : `当前没有可用于逐条求和的阶段明细,使用汇总字段 ${stageDuration ?? '暂无数据'} ms。`),
656
+ `从规约文档统计 ${scenarioCount ?? '暂无数据'} 条“##### 场景”验收场景。`,
657
+ e1Numerator != null && scenarioCount > 0
658
+ ? `代入公式:${e1Numerator} ÷ ${scenarioCount} = ${expectedE1} ms/场景;再与报告结果 ${e1Value} 对照。`
659
+ : '当前缺少可用分子或分母,不能完成除法。',
660
+ ],
661
+ result: {
662
+ value: e1Value,
663
+ display_value: e1Value == null ? '暂无数据' : `${humanDuration(e1Value)}/场景`,
664
+ unit: 'ms/场景',
665
+ },
666
+ trust: trustFor(report, 'e1', e1Evidence.status),
667
+ evidence: [
668
+ ...(efficiency.scenario_sources || []).map(item => ({
669
+ type: 'scenario-source',
670
+ path: item.path || null,
671
+ count: item.count ?? null,
672
+ scenarios: Array.isArray(item.scenarios) ? item.scenarios : [],
673
+ })),
674
+ ...e1Evidence.event_ids.map(eventId => ({ type: 'event', event_id: eventId })),
675
+ ],
676
+ caveats: [
677
+ ...CORE_METRIC_DEFINITIONS.e1.scheme_notes,
678
+ endpointElapsed == null
679
+ ? '端到端经过时间未采集。'
680
+ : `${endpointElapsed} ms 是从起点到终点的端到端经过时间,包含阶段间空隙,不能当作 E1 分子。`,
681
+ ],
682
+ }, expectedE1, expectedE1 != null);
683
+ if (hasDetailedStageDuration && metricValuesDiffer(stageDuration, detailedStageDuration)) {
684
+ e1 = markDetailInconsistent(
685
+ e1,
686
+ `计入公式的阶段明细合计为 ${detailedStageDuration} ms,但汇总字段为 ${stageDuration} ms`,
687
+ );
688
+ }
689
+ if (includedNodes.length > 0 && !hasDetailedStageDuration) {
690
+ e1 = markDetailProvisional(
691
+ e1,
692
+ `${invalidDetailedStageDurationCount} 条计入的阶段明细缺少耗时或耗时无效,不能独立复算 E1;当前结果仅按汇总字段核对。`,
693
+ );
694
+ }
695
+
696
+ const q3Total = quality.q3_total ?? null;
697
+ const q3Errors = quality.q3_errors ?? null;
698
+ const q3Value = quality.q3_spec_quality_score ?? null;
699
+ const q3OperandsValid = hasFiniteMetricNumber(q3Total)
700
+ && Number(q3Total) > 0
701
+ && hasFiniteMetricNumber(q3Errors)
702
+ && Number(q3Errors) >= 0
703
+ && Number(q3Errors) <= Number(q3Total);
704
+ const expectedQ3 = q3OperandsValid
705
+ ? roundMetric(((Number(q3Total) - Number(q3Errors)) / Number(q3Total)) * 100)
706
+ : null;
707
+ const q3Review = quality.q3_review || {};
708
+ const q3ReviewMode = q3Review.review_execution_mode || null;
709
+ const q3ReviewIndependence = q3Review.reviewer_independence
710
+ || quality.q3_reviewer_independence
711
+ || 'unknown';
712
+ const q3MetadataStatus = typeof resolveScoreStatus === 'function'
713
+ ? resolveScoreStatus(q3ReviewIndependence, q3Review)
714
+ : 'provisional';
715
+ const q3Verified = quality.q3_score_status === 'verified'
716
+ && q3MetadataStatus === 'verified';
717
+ const q3ReviewExplanation = q3Verified
718
+ ? `由独立子代理 ${q3Review.reviewer_agent_id} 完成复核,作者会话与评审会话不同。`
719
+ : (q3ReviewMode === 'subagent'
720
+ ? '本次由独立子代理执行检查,但作者身份、评审身份或会话差异证据不完整,尚不能确认独立性。'
721
+ : (q3ReviewMode === 'main-agent-fallback'
722
+ ? `由主代理自查,未经过独立复核。回退原因:${q3Review.fallback_reason || CHECK_FALLBACK_REASON_LABELS[q3Review.fallback_reason_code] || q3Review.fallback_reason_code || '未记录具体原因'}。`
723
+ : '旧事件没有记录是否使用独立子代理,不能确认独立复核。'));
724
+ const q3Evidence = evidenceFor(report, 'q3');
725
+ const q3Trust = q3Verified
726
+ ? trustFor(report, 'q3', 'verified', q3ReviewExplanation)
727
+ : {
728
+ status: 'provisional',
729
+ explanation: q3ReviewExplanation,
730
+ detail_downgrade: true,
731
+ };
732
+ let q3 = applyConsistencyCheck({
733
+ ...CORE_METRIC_DEFINITIONS.q3,
734
+ raw_values: {
735
+ total: q3Total,
736
+ errors: q3Errors,
737
+ warnings: quality.q3_warnings ?? null,
738
+ suggestions: quality.q3_suggestions ?? null,
739
+ category_scores: quality.q3_category_scores || {},
740
+ expected_result: expectedQ3,
741
+ review: {
742
+ executor: q3ReviewMode === 'subagent'
743
+ ? '独立子代理'
744
+ : (q3ReviewMode === 'main-agent-fallback' ? '主代理' : '无法确认'),
745
+ independently_verified: q3Verified,
746
+ review_execution_mode: q3ReviewMode,
747
+ reviewer_agent_id: q3Review.reviewer_agent_id || null,
748
+ author_session_id: q3Review.author_session_id || null,
749
+ reviewer_session_id: q3Review.review_session_id || q3Review.reviewer_session_id || null,
750
+ fallback_reason_code: q3Review.fallback_reason_code || null,
751
+ fallback_reason: q3Review.fallback_reason || null,
752
+ },
753
+ },
754
+ raw_inputs: [
755
+ { label: '检查项总数', value: q3Total, source: '最新 check_result.check_results.total', included: true },
756
+ { label: '错误数', value: q3Errors, source: '最新 check_result.check_results.errors', included: true },
757
+ { label: '警告数', value: quality.q3_warnings ?? null, source: '最新 Check 结果', included: false, exclusion_reason: '警告不进入当前扣分公式' },
758
+ { label: '建议数', value: quality.q3_suggestions ?? null, source: '最新 Check 结果', included: false, exclusion_reason: '建议不进入当前扣分公式' },
759
+ ],
760
+ calculation_steps: [
761
+ q3OperandsValid
762
+ ? `代入公式:(${q3Total} - ${q3Errors}) ÷ ${q3Total} × 100 = ${expectedQ3};再与报告结果 ${q3Value} 对照。`
763
+ : '旧事件没有保留检查项总数或错误数,分数可读,但当前报告无法完整复算。',
764
+ `复核可信度:${q3ReviewExplanation}`,
765
+ ],
766
+ result: { value: q3Value, display_value: q3Value == null ? '暂无数据' : `${q3Value}/100`, unit: '分' },
767
+ trust: q3Trust,
768
+ evidence: q3Evidence.event_ids.map(eventId => ({ type: 'event', event_id: eventId })),
769
+ caveats: CORE_METRIC_DEFINITIONS.q3.scheme_notes,
770
+ }, expectedQ3, expectedQ3 != null);
771
+ if (!q3OperandsValid) {
772
+ q3 = markDetailProvisional(
773
+ q3,
774
+ '检查项总数或错误数缺失、超出合理范围,不能独立复算 Q3;当前分数仅作参考。',
775
+ );
776
+ }
777
+
778
+ const firstApply = nodes.find(node => (node.stage || node.command) === 'apply') || null;
779
+ const p4Facts = process.p4_evidence || {};
780
+ const p4Value = process.p4_quality_gate_enforcement_rate ?? null;
781
+ const firstApplyStartedAt = p4Facts.first_apply_started_at || firstApply?.start_ts || null;
782
+ const firstApplyStartedMs = Date.parse(firstApplyStartedAt || '');
783
+ const hasQualifyingCheckDetails = Array.isArray(p4Facts.qualifying_checks);
784
+ const candidateChecks = hasQualifyingCheckDetails
785
+ ? p4Facts.qualifying_checks
786
+ : nodes.filter(node => (node.stage || node.command) === 'check');
787
+ const qualifyingChecks = candidateChecks.filter((check) => {
788
+ const startedAt = check.started_at || check.start_ts || null;
789
+ const endedAt = check.ended_at || check.end_ts || null;
790
+ const startedMs = Date.parse(startedAt || '');
791
+ const endedMs = Date.parse(endedAt || '');
792
+ const hasStartEvidence = Boolean(check.start_event_id || startedAt);
793
+ const hasEndEvidence = Boolean(check.end_event_id || endedAt);
794
+ return ['success', 'partial'].includes(check.result)
795
+ && Number.isFinite(firstApplyStartedMs)
796
+ && hasStartEvidence
797
+ && hasEndEvidence
798
+ && Number.isFinite(startedMs)
799
+ && Number.isFinite(endedMs)
800
+ && startedMs <= endedMs
801
+ && endedMs <= firstApplyStartedMs;
802
+ });
803
+ const derivedQualifyingCheckCount = qualifyingChecks.length;
804
+ const qualifyingCheckCount = p4Facts.qualifying_check_count ?? derivedQualifyingCheckCount;
805
+ const expectedP4 = Number.isFinite(firstApplyStartedMs)
806
+ ? (derivedQualifyingCheckCount > 0 ? 1 : 0)
807
+ : null;
808
+ const p4Evidence = evidenceFor(report, 'p4');
809
+ let p4 = applyConsistencyCheck({
810
+ ...CORE_METRIC_DEFINITIONS.p4,
811
+ raw_values: {
812
+ first_apply_started_at: firstApplyStartedAt,
813
+ first_apply_event_id: p4Facts.first_apply_event_id || firstApply?.start_event_id || null,
814
+ qualifying_check_count: qualifyingCheckCount,
815
+ derived_qualifying_check_count: derivedQualifyingCheckCount,
816
+ qualifying_checks: candidateChecks,
817
+ derived_qualifying_checks: qualifyingChecks,
818
+ expected_result: expectedP4,
819
+ },
820
+ raw_inputs: [
821
+ { label: '首次 Apply 开始时间', value: firstApplyStartedAt, source: p4Facts.first_apply_event_id || '阶段时间线', included: true },
822
+ { label: '首次 Apply 前按明细复算的有效 Check 数', value: derivedQualifyingCheckCount, source: '逐条核对 Check 结果、结束时间和首次 Apply 开始时间', included: true },
823
+ { label: '指标汇总字段中的有效 Check 数', value: qualifyingCheckCount, source: 'p4_evidence.qualifying_check_count', included: false, exclusion_reason: '只用于与明细复算结果对账' },
824
+ ],
825
+ calculation_steps: [
826
+ `找到首次 Apply:${firstApplyStartedAt || '暂无可定位记录'}。`,
827
+ `逐条筛选后,在首次 Apply 前找到 ${derivedQualifyingCheckCount} 次已配对且结果为成功或部分通过的 Check;汇总字段记录为 ${qualifyingCheckCount} 次。`,
828
+ `按明细复算 P4 = ${expectedP4 == null ? '暂无数据' : `${expectedP4}(${expectedP4 * 100}%)`};报告结果为 ${p4Value ?? '暂无数据'}。`,
829
+ ],
830
+ result: { value: p4Value, display_value: p4Value == null ? '暂无数据' : `${p4Value * 100}%`, unit: '比例' },
831
+ trust: trustFor(report, 'p4', p4Evidence.status),
832
+ evidence: p4Evidence.event_ids.map(eventId => ({ type: 'event', event_id: eventId })),
833
+ caveats: CORE_METRIC_DEFINITIONS.p4.scheme_notes,
834
+ }, expectedP4, expectedP4 != null);
835
+ if (hasQualifyingCheckDetails
836
+ && metricValuesDiffer(qualifyingCheckCount, derivedQualifyingCheckCount)) {
837
+ p4 = markDetailInconsistent(
838
+ p4,
839
+ `Check 明细复算得到 ${derivedQualifyingCheckCount} 次有效检查,但汇总字段为 ${qualifyingCheckCount} 次`,
840
+ );
841
+ }
842
+
843
+ const rework = process.rework_summary || {};
844
+ const pRValue = rework.total_rework_attempts ?? null;
845
+ const hasReworkByStage = Array.isArray(rework.by_stage);
846
+ const reworkByStage = hasReworkByStage ? rework.by_stage : [];
847
+ const reworkStageDetails = reworkByStage.map((item) => {
848
+ const rawTotalAttempts = item.total_attempts;
849
+ const rawReworkAttempts = item.rework_attempts;
850
+ const totalAttemptsProvided = rawTotalAttempts !== null
851
+ && rawTotalAttempts !== undefined
852
+ && rawTotalAttempts !== '';
853
+ const reworkAttemptsProvided = rawReworkAttempts !== null
854
+ && rawReworkAttempts !== undefined
855
+ && rawReworkAttempts !== '';
856
+ const totalAttemptsValid = totalAttemptsProvided
857
+ && hasFiniteMetricNumber(rawTotalAttempts)
858
+ && Number.isInteger(Number(rawTotalAttempts))
859
+ && Number(rawTotalAttempts) >= 1;
860
+ const reworkAttemptsValid = reworkAttemptsProvided
861
+ && hasFiniteMetricNumber(rawReworkAttempts)
862
+ && Number.isInteger(Number(rawReworkAttempts))
863
+ && Number(rawReworkAttempts) >= 0;
864
+ const totalAttempts = totalAttemptsValid
865
+ ? Number(rawTotalAttempts)
866
+ : null;
867
+ const recordedReworkAttempts = reworkAttemptsValid
868
+ ? Number(rawReworkAttempts)
869
+ : null;
870
+ const derivedReworkAttempts = totalAttemptsProvided
871
+ ? (totalAttemptsValid ? totalAttempts - 1 : null)
872
+ : recordedReworkAttempts;
873
+ const invalidFields = [
874
+ ...(totalAttemptsProvided && !totalAttemptsValid
875
+ ? ['执行次数必须是大于等于 1 的整数']
876
+ : []),
877
+ ...(reworkAttemptsProvided && !reworkAttemptsValid
878
+ ? ['重复次数必须是大于等于 0 的整数']
879
+ : []),
880
+ ];
881
+ return {
882
+ ...item,
883
+ raw_total_attempts: rawTotalAttempts ?? null,
884
+ raw_rework_attempts: rawReworkAttempts ?? null,
885
+ total_attempts: totalAttempts,
886
+ rework_attempts: recordedReworkAttempts,
887
+ derived_rework_attempts: derivedReworkAttempts,
888
+ derivation_source: totalAttemptsValid
889
+ ? 'total_attempts'
890
+ : (!totalAttemptsProvided && reworkAttemptsValid ? 'rework_attempts' : 'unavailable'),
891
+ invalid_fields: invalidFields,
892
+ };
893
+ });
894
+ const hasObservedReworkEvidence = reworkByStage.length > 0
895
+ || rework.total_attempts == null
896
+ || (hasFiniteMetricNumber(rework.total_attempts) && Number(rework.total_attempts) > 0);
897
+ const canRecomputePR = hasReworkByStage
898
+ && hasObservedReworkEvidence
899
+ && reworkStageDetails.every(item => item.derived_rework_attempts != null);
900
+ const expectedPR = canRecomputePR
901
+ ? reworkStageDetails.reduce((sum, item) => sum + item.derived_rework_attempts, 0)
902
+ : null;
903
+ const conflictingReworkDetails = reworkStageDetails.filter(item => (
904
+ item.total_attempts != null
905
+ && item.rework_attempts != null
906
+ && metricValuesDiffer(item.derived_rework_attempts, item.rework_attempts)
907
+ ));
908
+ const invalidReworkDetails = reworkStageDetails
909
+ .filter(item => item.invalid_fields.length > 0);
910
+ const reasonCounts = rework.reasons?.by_category || {};
911
+ const reasonDetails = rework.reasons?.details || [];
912
+ const pREvidence = evidenceFor(report, 'p_r');
913
+ let pR = applyConsistencyCheck({
914
+ ...CORE_METRIC_DEFINITIONS.p_r,
915
+ raw_values: {
916
+ total_attempts: rework.total_attempts ?? null,
917
+ canonical_attempts: rework.canonical_attempts ?? null,
918
+ total_rework_attempts: pRValue,
919
+ by_stage: reworkStageDetails,
920
+ expected_result: expectedPR,
921
+ reasons: Object.fromEntries(Object.entries(reasonCounts).map(([key, value]) => [
922
+ key,
923
+ { count: value, label: REWORK_REASON_LABELS[key] || key },
924
+ ])),
925
+ reason_details: reasonDetails.map(item => ({
926
+ ...item,
927
+ reason_label: REWORK_REASON_LABELS[item.reason] || item.reason,
928
+ })),
929
+ },
930
+ raw_inputs: reworkStageDetails.flatMap(item => ([
931
+ {
932
+ label: `${localizeStageName(item.command)} · 执行次数`,
933
+ value: item.raw_total_attempts,
934
+ display_value: item.raw_total_attempts == null ? '暂无数据' : `${item.raw_total_attempts} 次`,
935
+ source: item.canonical_event_id || '阶段配对结果',
936
+ included: item.total_attempts != null,
937
+ exclusion_reason: item.invalid_fields.some(reason => reason.startsWith('执行次数'))
938
+ ? '执行次数格式无效'
939
+ : (item.total_attempts == null
940
+ ? '旧数据没有记录执行次数,改用已记录的重复次数'
941
+ : null),
942
+ },
943
+ {
944
+ label: `${localizeStageName(item.command)} · 用于计算的重复次数`,
945
+ value: item.derived_rework_attempts,
946
+ display_value: item.derived_rework_attempts == null
947
+ ? '暂无数据'
948
+ : `${item.derived_rework_attempts} 次`,
949
+ source: item.derivation_source === 'total_attempts'
950
+ ? '由执行次数按 max(执行次数 - 1, 0) 推导'
951
+ : (item.derivation_source === 'rework_attempts'
952
+ ? '旧数据中已记录的重复次数字段'
953
+ : '当前没有可用的次数数据'),
954
+ included: item.derived_rework_attempts != null,
955
+ exclusion_reason: item.derived_rework_attempts == null ? '没有可用的执行或重复次数' : null,
956
+ },
957
+ {
958
+ label: `${localizeStageName(item.command)} · 已记录的重复次数`,
959
+ value: item.raw_rework_attempts,
960
+ display_value: item.raw_rework_attempts == null ? '暂无数据' : `${item.raw_rework_attempts} 次`,
961
+ source: '旧数据中已记录的重复次数字段',
962
+ included: item.derivation_source === 'rework_attempts',
963
+ exclusion_reason: item.invalid_fields.some(reason => reason.startsWith('重复次数'))
964
+ ? '重复次数格式无效'
965
+ : (item.derivation_source === 'total_attempts'
966
+ ? '执行次数完整时,此字段只用于交叉核对'
967
+ : null),
968
+ },
969
+ ])),
970
+ calculation_steps: [
971
+ canRecomputePR
972
+ ? `逐阶段优先计算 max(执行次数 - 1, 0);旧数据缺少执行次数时使用已记录的重复次数。按 ${reworkStageDetails.length} 条阶段明细合计 ${expectedPR} 次;报告结果为 ${pRValue ?? '暂无数据'} 次。`
973
+ : (hasReworkByStage
974
+ ? `逐阶段明细中仍有执行次数和重复次数均缺失的记录,无法独立复算;报告结果为 ${pRValue ?? '暂无数据'} 次。`
975
+ : `旧数据没有逐阶段明细,无法独立复算;报告结果为 ${pRValue ?? '暂无数据'} 次。`),
976
+ ...reworkStageDetails.map(item => (
977
+ item.invalid_fields.length > 0
978
+ ? `${localizeStageName(item.command)}:${item.invalid_fields.join(';')}。`
979
+ : item.total_attempts == null
980
+ ? `${localizeStageName(item.command)}:执行次数暂无数据,使用已记录的重复次数 ${item.rework_attempts ?? '暂无数据'} 次。`
981
+ : `${localizeStageName(item.command)}:执行次数 ${item.total_attempts} 次,按公式得到重复次数 ${item.derived_rework_attempts} 次;记录的重复次数为 ${item.rework_attempts ?? '暂无数据'} 次。`
982
+ )),
983
+ ...reasonDetails.map(item => `${localizeStageName(item.command)}:${REWORK_REASON_LABELS[item.reason] || item.reason}。`),
984
+ ],
985
+ result: { value: pRValue, display_value: pRValue == null ? '暂无数据' : `${pRValue} 次`, unit: '次' },
986
+ trust: trustFor(report, 'p_r', pREvidence.status),
987
+ evidence: pREvidence.event_ids.map(eventId => ({ type: 'event', event_id: eventId })),
988
+ caveats: CORE_METRIC_DEFINITIONS.p_r.scheme_notes,
989
+ }, expectedPR, expectedPR != null);
990
+ if (invalidReworkDetails.length > 0) {
991
+ const invalid = invalidReworkDetails[0];
992
+ pR = markDetailInconsistent(
993
+ pR,
994
+ `${localizeStageName(invalid.command)}的${invalid.invalid_fields.join(';')}`,
995
+ );
996
+ } else if (conflictingReworkDetails.length > 0) {
997
+ const conflict = conflictingReworkDetails[0];
998
+ pR = markDetailInconsistent(
999
+ pR,
1000
+ `${localizeStageName(conflict.command)}的执行次数 ${conflict.total_attempts} 次按公式推导为重复次数 ${conflict.derived_rework_attempts} 次,但记录的重复次数为 ${conflict.rework_attempts} 次`,
1001
+ );
1002
+ } else if (hasReworkByStage && !canRecomputePR) {
1003
+ pR = markDetailProvisional(
1004
+ pR,
1005
+ '逐阶段明细存在执行次数和重复次数均缺失的记录,不能独立复算 P-R;当前结果仅作参考。',
1006
+ );
1007
+ }
1008
+
1009
+ const healthInputs = [
1010
+ ['未闭环阶段', telemetry.open_stages, 0.18],
1011
+ ['孤儿事件', telemetry.orphan_events, 0.18],
1012
+ ['未知命令', telemetry.unknown_command_events, 0.12],
1013
+ ['无效阶段名', telemetry.invalid_stage_events, 0.12],
1014
+ ['缺少耗时', telemetry.null_duration_stages, 0.08],
1015
+ ['过期数据格式', telemetry.outdated_schema_events, 0.06],
1016
+ ['缺少变更名', telemetry.missing_change_events, 0.04],
1017
+ ['缺少能力域名', telemetry.missing_capability_events, 0.01],
1018
+ ].map(([label, count, weight]) => {
1019
+ const normalizedCount = hasFiniteMetricNumber(count)
1020
+ && Number.isInteger(Number(count))
1021
+ && Number(count) >= 0
1022
+ ? Number(count)
1023
+ : null;
1024
+ return {
1025
+ label,
1026
+ count: normalizedCount,
1027
+ weight,
1028
+ weighted_value: normalizedCount == null ? null : normalizedCount * weight,
1029
+ };
1030
+ });
1031
+ const invalidHealthInputs = healthInputs.filter(item => item.count == null);
1032
+ const weightedIssueValue = invalidHealthInputs.length === 0
1033
+ ? healthInputs.reduce((sum, item) => sum + item.weighted_value, 0)
1034
+ : null;
1035
+ const stageEvents = telemetry.stage_events ?? null;
1036
+ const pHValue = telemetry.p_h_telemetry_health_score ?? null;
1037
+ const canRecomputePH = hasFiniteMetricNumber(stageEvents)
1038
+ && Number(stageEvents) > 0
1039
+ && invalidHealthInputs.length === 0;
1040
+ const healthCalculation = canRecomputePH && typeof computeTelemetryHealthScore === 'function'
1041
+ ? computeTelemetryHealthScore({
1042
+ weighted_issue_value: weightedIssueValue,
1043
+ stage_event_count: stageEvents,
1044
+ })
1045
+ : (canRecomputePH
1046
+ ? {
1047
+ ratio: Math.max(0, roundMetric(1 - weightedIssueValue / Number(stageEvents), 2)),
1048
+ score: Math.max(0, roundMetric(1 - weightedIssueValue / Number(stageEvents), 2)) * 100,
1049
+ }
1050
+ : { ratio: null, score: null });
1051
+ const expectedPH = canRecomputePH ? healthCalculation.score : null;
1052
+ const pHEvidence = evidenceFor(report, 'p_h');
1053
+ let pH = applyConsistencyCheck({
1054
+ ...CORE_METRIC_DEFINITIONS.p_h,
1055
+ raw_values: {
1056
+ stage_events: stageEvents,
1057
+ weighted_issue_value: weightedIssueValue,
1058
+ normalized_ratio: canRecomputePH ? healthCalculation.ratio : null,
1059
+ deductions: healthInputs,
1060
+ expected_result: expectedPH,
1061
+ },
1062
+ raw_inputs: healthInputs.map(item => ({
1063
+ label: item.label,
1064
+ value: item.count,
1065
+ display_value: item.count == null ? '暂无数据' : `${item.count} 个 × ${item.weight}`,
1066
+ source: 'skywalk-sdd 诊断结果',
1067
+ included: true,
1068
+ weight: item.weight,
1069
+ weighted_value: item.weighted_value,
1070
+ })),
1071
+ calculation_steps: [
1072
+ invalidHealthInputs.length === 0
1073
+ ? `八类问题的加权问题值合计为 ${weightedIssueValue}。`
1074
+ : `八类问题中有 ${invalidHealthInputs.length} 类缺少有效计数,当前不能得到加权问题值。`,
1075
+ canRecomputePH
1076
+ ? `按发布口径先计算并将比例保留两位:1 - ${weightedIssueValue} ÷ ${stageEvents} = ${healthCalculation.ratio};再乘以 100 得 ${expectedPH} 分;报告结果为 ${pHValue} 分。`
1077
+ : '标准阶段事件分母或八类问题计数缺失,当前无法完成归一化。',
1078
+ ],
1079
+ result: { value: pHValue, display_value: pHValue == null ? '暂无数据' : `${pHValue}/100`, unit: '分' },
1080
+ trust: trustFor(report, 'p_h', pHEvidence.status),
1081
+ evidence: pHEvidence.event_ids.map(eventId => ({ type: 'event', event_id: eventId })),
1082
+ caveats: CORE_METRIC_DEFINITIONS.p_h.scheme_notes,
1083
+ }, expectedPH, expectedPH != null);
1084
+ if (!canRecomputePH) {
1085
+ pH = markDetailProvisional(
1086
+ pH,
1087
+ '标准阶段事件总数或八类问题计数缺失、无效,不能独立复算 P-H;当前分数仅作参考。',
1088
+ );
1089
+ }
1090
+
1091
+ e1 = validateMetricDetailResult('e1', e1);
1092
+ q3 = validateMetricDetailResult('q3', q3);
1093
+ p4 = validateMetricDetailResult('p4', p4);
1094
+ pR = validateMetricDetailResult('p_r', pR);
1095
+ pH = validateMetricDetailResult('p_h', pH);
1096
+
1097
+ return { e1, q3, p4, p_r: pR, p_h: pH };
1098
+ }
1099
+
314
1100
  function resolveArtifact(projectRoot, artifactPath, options = {}) {
315
1101
  if (!artifactPath) return { path: null, exists: false, size_bytes: null, size_display: null, sha256: null };
316
1102
  const absolute = path.isAbsolute(artifactPath)
@@ -389,7 +1175,7 @@ function buildWarningDispositions(report) {
389
1175
  ...(report.evidence_alerts?.resolved || []),
390
1176
  ...(report.check_warning_dispositions || []),
391
1177
  ];
392
- const allowed = new Set(['fixed', 'accepted', 'waived', 'needs_input', 'open']);
1178
+ const allowed = CANONICAL_ISSUE_DISPOSITIONS;
393
1179
  const checkWarnings = new Set((report.check_warning_dispositions || []).map(item => item.warning));
394
1180
  const byWarning = new Map();
395
1181
  for (const alert of alerts) {
@@ -409,6 +1195,188 @@ function buildWarningDispositions(report) {
409
1195
  return [...byWarning.values()];
410
1196
  }
411
1197
 
1198
+ function propagateMetricConsistency(report, metricDetails) {
1199
+ const inconsistentMetrics = Object.entries(metricDetails || {})
1200
+ .filter(([, detail]) => detail?.trust?.status === 'inconsistent')
1201
+ .map(([key]) => key);
1202
+ const detailDowngrades = Object.entries(metricDetails || {})
1203
+ .filter(([, detail]) => ['missing', 'provisional'].includes(detail?.trust?.status))
1204
+ .map(([key, detail]) => ({ key, status: detail.trust.status }));
1205
+ const affectedMetrics = new Set([
1206
+ ...inconsistentMetrics,
1207
+ ...detailDowngrades.map(item => item.key),
1208
+ ]);
1209
+
1210
+ const minimum = report.minimum_metric_set || {};
1211
+ const minimumMetrics = Array.isArray(minimum.minimum_metrics)
1212
+ ? minimum.minimum_metrics.slice()
1213
+ : ['e1', 'q3', 'p4', 'p_r', 'p_h'];
1214
+ const inconsistentSet = new Set(inconsistentMetrics);
1215
+ const evidenceStatus = { ...(minimum.evidence_status || {}) };
1216
+ const evidence = { ...(minimum.evidence || {}) };
1217
+ for (const key of inconsistentMetrics) {
1218
+ evidenceStatus[key] = 'inconsistent';
1219
+ evidence[key] = {
1220
+ ...(evidence[key] || {}),
1221
+ status: 'inconsistent',
1222
+ reason: 'metric-detail-calculation-mismatch',
1223
+ };
1224
+ }
1225
+ for (const item of detailDowngrades) {
1226
+ evidenceStatus[item.key] = item.status;
1227
+ evidence[item.key] = {
1228
+ ...(evidence[item.key] || {}),
1229
+ status: item.status,
1230
+ reason: item.status === 'missing'
1231
+ ? 'metric-result-missing'
1232
+ : 'metric-detail-evidence-incomplete',
1233
+ };
1234
+ }
1235
+
1236
+ const provisionalMetrics = (minimum.provisional_metrics || [])
1237
+ .filter(key => !inconsistentSet.has(key)
1238
+ && !detailDowngrades.some(item => item.key === key && item.status === 'missing'));
1239
+ for (const item of detailDowngrades) {
1240
+ if (item.status === 'provisional' && !provisionalMetrics.includes(item.key)) {
1241
+ provisionalMetrics.push(item.key);
1242
+ }
1243
+ }
1244
+ const numericTotal = hasFiniteMetricNumber(minimum.numeric_total)
1245
+ && Number(minimum.numeric_total) > 0
1246
+ ? Number(minimum.numeric_total)
1247
+ : minimumMetrics.length;
1248
+ const availableMetrics = minimumMetrics.filter(key => (
1249
+ validateCoreMetricResult(key, metricDetails[key]?.result?.value).valid
1250
+ ));
1251
+ const declaredTrusted = Array.isArray(minimum.trusted_metrics)
1252
+ ? minimum.trusted_metrics
1253
+ : (Array.isArray(minimum.trusted) ? minimum.trusted : []);
1254
+ const trustedMetrics = declaredTrusted.filter(key => (
1255
+ availableMetrics.includes(key)
1256
+ && !affectedMetrics.has(key)
1257
+ && ['trusted', 'verified'].includes(metricDetails[key]?.trust?.status)
1258
+ ));
1259
+ const missingMetrics = minimumMetrics.filter(key => !availableMetrics.includes(key));
1260
+ const minimumMetricSet = {
1261
+ ...minimum,
1262
+ minimum_metrics: minimumMetrics,
1263
+ available: availableMetrics,
1264
+ missing: missingMetrics,
1265
+ coverage_rate: numericTotal > 0
1266
+ ? roundMetric(availableMetrics.length / numericTotal)
1267
+ : 0,
1268
+ numeric_available: availableMetrics.length,
1269
+ numeric_total: numericTotal,
1270
+ numeric_coverage_rate: numericTotal > 0
1271
+ ? roundMetric(availableMetrics.length / numericTotal)
1272
+ : 0,
1273
+ trusted_available: trustedMetrics.length,
1274
+ trusted_metrics: trustedMetrics,
1275
+ provisional: provisionalMetrics.length,
1276
+ provisional_metrics: provisionalMetrics,
1277
+ missing_count: missingMetrics.length,
1278
+ inconsistent: inconsistentMetrics.length,
1279
+ inconsistent_metrics: inconsistentMetrics,
1280
+ trusted_coverage_rate: numericTotal > 0
1281
+ ? roundMetric(trustedMetrics.length / numericTotal)
1282
+ : 0,
1283
+ evidence_status: evidenceStatus,
1284
+ evidence,
1285
+ };
1286
+ if (Object.hasOwn(minimum, 'trusted')) {
1287
+ minimumMetricSet.trusted = trustedMetrics;
1288
+ }
1289
+
1290
+ if (affectedMetrics.size === 0) {
1291
+ return {
1292
+ minimumMetricSet,
1293
+ conclusion: report.conclusion || null,
1294
+ };
1295
+ }
1296
+
1297
+ const inconsistentCodes = inconsistentMetrics.map(key => (
1298
+ metricDetails[key]?.code || key.toUpperCase()
1299
+ ));
1300
+ const missingCodes = detailDowngrades
1301
+ .filter(item => item.status === 'missing')
1302
+ .map(item => metricDetails[item.key]?.code || item.key.toUpperCase());
1303
+ const provisionalCodes = detailDowngrades
1304
+ .filter(item => item.status === 'provisional')
1305
+ .map(item => metricDetails[item.key]?.code || item.key.toUpperCase());
1306
+ const trustStatements = [];
1307
+ if (inconsistentCodes.length > 0) {
1308
+ trustStatements.push(`核心指标 ${inconsistentCodes.join('、')} 的计算明细与结果不一致,已从可信范围移除`);
1309
+ }
1310
+ if (missingCodes.length > 0) {
1311
+ trustStatements.push(`核心指标 ${missingCodes.join('、')} 的报告结果缺失,已从可信范围移除`);
1312
+ }
1313
+ if (provisionalCodes.length > 0) {
1314
+ trustStatements.push(`核心指标 ${provisionalCodes.join('、')} 的计算明细不完整,已降为临时结果`);
1315
+ }
1316
+ const trustStatement = trustStatements.join(';');
1317
+ const existingConclusion = report.conclusion || {};
1318
+ const conclusionParts = [
1319
+ existingConclusion.task_statement,
1320
+ trustStatement,
1321
+ existingConclusion.alert_statement,
1322
+ ].filter(Boolean);
1323
+ const conclusion = {
1324
+ ...existingConclusion,
1325
+ status: 'attention',
1326
+ text: `${conclusionParts.join(';')}。`,
1327
+ trust_statement: trustStatement,
1328
+ inconsistent_metrics: inconsistentMetrics,
1329
+ detail_downgraded_metrics: detailDowngrades.map(item => item.key),
1330
+ };
1331
+
1332
+ return { minimumMetricSet, conclusion };
1333
+ }
1334
+
1335
+ function reconcileSchemeCompliance(schemeCompliance, minimumMetricSet, metricDetails) {
1336
+ if (!schemeCompliance
1337
+ || typeof schemeCompliance !== 'object'
1338
+ || !Array.isArray(schemeCompliance.items)) {
1339
+ return schemeCompliance || null;
1340
+ }
1341
+ const total = hasFiniteMetricNumber(minimumMetricSet?.numeric_total)
1342
+ && Number(minimumMetricSet.numeric_total) > 0
1343
+ ? Number(minimumMetricSet.numeric_total)
1344
+ : (minimumMetricSet?.minimum_metrics?.length || 5);
1345
+ const numericAvailable = Number(minimumMetricSet?.numeric_available) || 0;
1346
+ const trustedAvailable = Number(minimumMetricSet?.trusted_available) || 0;
1347
+ const minimumImplemented = numericAvailable === total
1348
+ && trustedAvailable === total
1349
+ && Number(minimumMetricSet?.inconsistent || 0) === 0;
1350
+ const q3TrustStatus = metricDetails?.q3?.trust?.status || null;
1351
+ const independentReviewImplemented = ['trusted', 'verified'].includes(q3TrustStatus);
1352
+ const items = schemeCompliance.items.map((item) => {
1353
+ if (item.requirement === 'minimum-verifiable-metrics') {
1354
+ return {
1355
+ ...item,
1356
+ status: minimumImplemented ? 'implemented' : 'provisional',
1357
+ evidence: `数值覆盖 ${numericAvailable}/${total};可信覆盖 ${trustedAvailable}/${total}`,
1358
+ };
1359
+ }
1360
+ if (item.requirement === 'independent-reviewer') {
1361
+ return {
1362
+ ...item,
1363
+ status: independentReviewImplemented ? 'implemented' : 'provisional',
1364
+ evidence: independentReviewImplemented
1365
+ ? 'Q3 已有完整的独立子代理身份与会话证据'
1366
+ : 'Q3 尚未形成完整的独立子代理身份与会话证据',
1367
+ };
1368
+ }
1369
+ return item;
1370
+ });
1371
+ const counts = {};
1372
+ for (const item of items) counts[item.status] = (counts[item.status] || 0) + 1;
1373
+ return {
1374
+ ...schemeCompliance,
1375
+ items,
1376
+ counts,
1377
+ };
1378
+ }
1379
+
412
1380
  function buildChangeReportModel(report, events = []) {
413
1381
  if (!report || report.level !== 'change' || !report.change) return report;
414
1382
  const scopedEvents = scopedEventsForChange(events, report.change);
@@ -422,6 +1390,13 @@ function buildChangeReportModel(report, events = []) {
422
1390
  tasks: existingSummary.tasks || normalizeTaskSummary(report),
423
1391
  evidence: existingSummary.evidence || normalizeEvidenceSummary(report),
424
1392
  };
1393
+ const metricDetails = report.metric_details || buildMetricDetails(report);
1394
+ const consistency = propagateMetricConsistency(report, metricDetails);
1395
+ const schemeCompliance = reconcileSchemeCompliance(
1396
+ report.scheme_compliance,
1397
+ consistency.minimumMetricSet,
1398
+ metricDetails,
1399
+ );
425
1400
  return {
426
1401
  ...report,
427
1402
  report_scope: {
@@ -435,6 +1410,10 @@ function buildChangeReportModel(report, events = []) {
435
1410
  cross_change_data_included: false,
436
1411
  },
437
1412
  metric_provenance: report.metric_provenance || report.metric_evidence || null,
1413
+ metric_details: metricDetails,
1414
+ minimum_metric_set: consistency.minimumMetricSet,
1415
+ scheme_compliance: schemeCompliance,
1416
+ conclusion: consistency.conclusion,
438
1417
  change_summary: changeSummary,
439
1418
  changed_files: changedFiles,
440
1419
  warning_dispositions: buildWarningDispositions(report),