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.
- package/bin/kld-sdd-init.js +10 -0
- package/kld-sdd-guide.html +22 -0
- package/lib/device-auth-cli.js +130 -0
- package/lib/device-auth.js +345 -0
- package/lib/init-account-binding.js +108 -0
- package/lib/init.js +238 -55
- package/lib/skills-bundle.js +20 -1
- package/lib/tool-profiles.js +8 -0
- package/package.json +7 -3
- package/skywalk-sdd/apply-worktree-finish.cjs +2 -23
- package/skywalk-sdd/context-client.cjs +38 -87
- package/skywalk-sdd/index.cjs +860 -132
- package/skywalk-sdd/kb-sync-identity.cjs +780 -0
- package/skywalk-sdd/kb-upload.cjs +505 -0
- package/skywalk-sdd/lib/shared.cjs +811 -0
- package/skywalk-sdd/lib/usage-contract.cjs +276 -0
- package/skywalk-sdd/lib/usage-reporter.cjs +354 -0
- package/skywalk-sdd/lib/user-config.cjs +157 -0
- package/skywalk-sdd/metrics-v3.cjs +138 -8
- package/skywalk-sdd/ontology/archive-package.cjs +19 -34
- package/skywalk-sdd/ontology/change-lock.cjs +3 -7
- package/skywalk-sdd/ontology/external-key.cjs +18 -4
- package/skywalk-sdd/ontology/id.cjs +26 -5
- package/skywalk-sdd/ontology/identity-index.cjs +3 -7
- package/skywalk-sdd/ontology/resolve-spec-root.cjs +20 -6
- package/skywalk-sdd/ontology/runtime.cjs +16 -12
- package/skywalk-sdd/ontology/traceability-validator.cjs +7 -4
- package/skywalk-sdd/reporting/change-report-markdown.cjs +137 -19
- package/skywalk-sdd/reporting/change-report-model.cjs +993 -14
- package/skywalk-sdd/reporting/change-report-renderer.cjs +106 -41
- package/skywalk-sdd/reporting/change-report-view-model.cjs +272 -43
- package/skywalk-sdd/reporting/core-metric-definitions.cjs +192 -0
- package/skywalk-sdd/spec-root.cjs +31 -0
- package/templates/git-hooks/pre-commit-consistency-check.cjs +271 -116
- package/templates/git-hooks/pre-push-consistency-check.cjs +252 -123
- package/templates/hooks/codebuddy/hooks/hook-gate-core.cjs +327 -0
- package/templates/hooks/codebuddy/hooks/sdd-apply-test-gate.cjs +54 -9
- package/templates/hooks/codebuddy/hooks/sdd-mid-checkpoint.cjs +63 -6
- package/templates/hooks/codebuddy/hooks/sdd-tdd-rhythm-gate.cjs +113 -65
- package/templates/openspec/proposal.md +7 -3
- package/templates/openspec/spec.md +3 -3
- package/templates/skills/kld-sdd/opsx-apply/SKILL.md +8 -6
- package/templates/skills/kld-sdd/opsx-apply/checklist.md +2 -0
- package/templates/skills/kld-sdd/opsx-apply/reference.md +29 -7
- package/templates/skills/kld-sdd/opsx-archive/SKILL.md +6 -5
- package/templates/skills/kld-sdd/opsx-check/SKILL.md +49 -17
- package/templates/skills/kld-sdd/opsx-check/checklist.md +4 -2
- package/templates/skills/kld-sdd/opsx-consistency-check/SKILL.md +187 -257
- package/templates/skills/kld-sdd/opsx-consistency-check/reference.md +129 -0
- package/templates/skills/kld-sdd/opsx-design/SKILL.md +2 -2
- package/templates/skills/kld-sdd/opsx-explore/SKILL.md +2 -2
- package/templates/skills/kld-sdd/opsx-kb-config/SKILL.md +185 -0
- package/templates/skills/kld-sdd/opsx-kb-config/reference.md +127 -0
- package/templates/skills/kld-sdd/opsx-kb-ingest/SKILL.md +218 -53
- package/templates/skills/kld-sdd/opsx-kb-ingest/reference.md +51 -9
- package/templates/skills/kld-sdd/opsx-ontology-query/SKILL.md +12 -50
- package/templates/skills/kld-sdd/opsx-ontology-query/phase-3-postchange.md +2 -2
- package/templates/skills/kld-sdd/opsx-ontology-query/reference.md +1 -1
- package/templates/skills/kld-sdd/opsx-propose/SKILL.md +35 -23
- package/templates/skills/kld-sdd/opsx-propose/checklist.md +2 -0
- package/templates/skills/kld-sdd/opsx-propose/reference.md +22 -17
- package/templates/skills/kld-sdd/opsx-rules/SKILL.md +2 -2
- package/templates/skills/kld-sdd/opsx-spec/SKILL.md +19 -15
- package/templates/skills/kld-sdd/opsx-spec/checklist.md +2 -0
- package/templates/skills/kld-sdd/opsx-task/SKILL.md +2 -4
- package/templates/skills/kld-sdd/opsx-test/SKILL.md +2 -2
- package/templates/skills/kld-sdd/tdd-core/reference.md +1 -1
- package/templates/skills/kld-sdd/tdd-rules/rules/test-skeleton-telemetry.md +1 -1
- package/templates/skills/kld-sdd/opsx-kb-ingest/state.example.json +0 -7
- 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
|
-
({
|
|
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: '
|
|
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'}
|
|
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:
|
|
271
|
+
status: statusForDisposition(item.disposition),
|
|
240
272
|
last_seen_event_id: item.evidence_event_id,
|
|
241
273
|
})),
|
|
242
274
|
];
|
|
243
|
-
const
|
|
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
|
|
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
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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 =
|
|
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),
|