kld-sdd 2.6.13 → 2.6.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -12
- package/kld-sdd-guide.html +4 -5
- package/lib/init.js +14 -11
- package/package.json +2 -2
- package/skywalk-sdd/index.cjs +936 -109
- package/skywalk-sdd/metrics-v3.cjs +103 -15
- package/skywalk-sdd/ontology/archive-package.cjs +6 -0
- package/skywalk-sdd/ontology/identity-index.cjs +9 -2
- package/skywalk-sdd/ontology/ontology-paths.cjs +73 -0
- package/skywalk-sdd/ontology/runtime.cjs +32 -22
- package/skywalk-sdd/ontology/structural-identity.cjs +11 -2
- package/skywalk-sdd/ontology/traceability-validator.cjs +16 -8
- package/skywalk-sdd/ontology/working-artifacts.cjs +2 -1
- package/skywalk-sdd/reporting/change-report-markdown.cjs +294 -0
- package/skywalk-sdd/reporting/change-report-model.cjs +452 -0
- package/skywalk-sdd/reporting/change-report-renderer.cjs +349 -0
- package/skywalk-sdd/reporting/change-report-view-model.cjs +340 -0
- package/skywalk-sdd/runtime-metadata.cjs +21 -0
- package/templates/skills/kld-sdd/opsx-apply/reference.md +4 -2
- package/templates/skills/kld-sdd/opsx-archive/SKILL.md +1 -1
- package/templates/skills/kld-sdd/opsx-archive/checklist.md +1 -1
- package/templates/skills/kld-sdd/opsx-check/SKILL.md +2 -2
- package/templates/skills/kld-sdd/opsx-check/checklist.md +1 -1
- package/templates/skills/kld-sdd/opsx-kb-ingest/SKILL.md +14 -0
- package/templates/skills/kld-sdd/opsx-ontology-query/SKILL.md +31 -0
- package/templates/skills/kld-sdd/opsx-propose/SKILL.md +6 -6
- package/templates/skills/kld-sdd/opsx-propose/reference.md +6 -7
- package/templates/skills/kld-sdd/opsx-spec/SKILL.md +1 -1
- package/templates/skills/kld-sdd/tdd-core/reference.md +3 -1
- package/templates/skills/kld-sdd/tdd-rules/rules/test-skeleton-telemetry.md +1 -1
package/skywalk-sdd/index.cjs
CHANGED
|
@@ -2622,8 +2622,14 @@ function computeConformanceMetrics(events) {
|
|
|
2622
2622
|
}
|
|
2623
2623
|
|
|
2624
2624
|
function getAiAdoptionReview(event) {
|
|
2625
|
-
// Q4:
|
|
2626
|
-
const
|
|
2625
|
+
// Q4: 兼容四 key + 历史顶层 details.ai_diff(无嵌套 review 外壳)
|
|
2626
|
+
const nested = event?.details?.ai_adoption
|
|
2627
|
+
|| event?.details?.ai_code_adoption
|
|
2628
|
+
|| event?.details?.ai_adoption_review;
|
|
2629
|
+
const topLevelAiDiff = event?.details?.ai_diff && typeof event.details.ai_diff === 'object'
|
|
2630
|
+
? event.details.ai_diff
|
|
2631
|
+
: null;
|
|
2632
|
+
const review = nested || (topLevelAiDiff ? { ai_diff: topLevelAiDiff } : null);
|
|
2627
2633
|
if (!review) return null;
|
|
2628
2634
|
|
|
2629
2635
|
const retainedLines = Number.isFinite(review.retained_lines) ? review.retained_lines : null;
|
|
@@ -2645,6 +2651,11 @@ function getAiAdoptionReview(event) {
|
|
|
2645
2651
|
else adoptionLevel = 'rewritten';
|
|
2646
2652
|
}
|
|
2647
2653
|
|
|
2654
|
+
const aiDiff = review.ai_diff && typeof review.ai_diff === 'object'
|
|
2655
|
+
? review.ai_diff
|
|
2656
|
+
: (topLevelAiDiff || {});
|
|
2657
|
+
const legacyTopLevelShape = Boolean(topLevelAiDiff) && !nested;
|
|
2658
|
+
|
|
2648
2659
|
return {
|
|
2649
2660
|
base_git_sha: review.base_git_sha || null,
|
|
2650
2661
|
ai_git_sha: review.ai_git_sha || null,
|
|
@@ -2655,9 +2666,10 @@ function getAiAdoptionReview(event) {
|
|
|
2655
2666
|
retained_lines: retainedLines,
|
|
2656
2667
|
rewritten_lines: rewrittenLines,
|
|
2657
2668
|
deleted_lines: deletedLines,
|
|
2658
|
-
ai_diff:
|
|
2669
|
+
ai_diff: aiDiff,
|
|
2659
2670
|
final_diff: review.final_diff || {},
|
|
2660
2671
|
notes: review.notes || '',
|
|
2672
|
+
legacy_shape: legacyTopLevelShape || undefined,
|
|
2661
2673
|
};
|
|
2662
2674
|
}
|
|
2663
2675
|
|
|
@@ -2666,7 +2678,8 @@ function getAiAdoptionEvents(events) {
|
|
|
2666
2678
|
return e.type === 'ai_adoption_review'
|
|
2667
2679
|
|| Boolean(e.details?.ai_adoption)
|
|
2668
2680
|
|| Boolean(e.details?.ai_code_adoption)
|
|
2669
|
-
|| Boolean(e.details?.ai_adoption_review)
|
|
2681
|
+
|| Boolean(e.details?.ai_adoption_review)
|
|
2682
|
+
|| Boolean(e.details?.ai_diff);
|
|
2670
2683
|
});
|
|
2671
2684
|
}
|
|
2672
2685
|
|
|
@@ -3218,16 +3231,6 @@ function computeSinglePdfMvpMetrics(events, options = {}) {
|
|
|
3218
3231
|
change_type: changeType,
|
|
3219
3232
|
change_name: options.change || null,
|
|
3220
3233
|
v3,
|
|
3221
|
-
legacy_metrics: {
|
|
3222
|
-
e1_lead_time_ms: leadTime,
|
|
3223
|
-
e4_ai_code_first_pass_rate: aiFirstPassMetrics.e4_ai_code_first_pass_rate,
|
|
3224
|
-
p1_spec_iteration_count: specIterationCount,
|
|
3225
|
-
p2_ai_code_adoption_rate: aiAdoptionMetrics.p2_ai_code_adoption_rate,
|
|
3226
|
-
q3_build_first_pass_rate: firstBuildSuccess == null ? null : (firstBuildSuccess ? 1 : 0),
|
|
3227
|
-
q5_cross_doc_consistency_score: consistencyScore,
|
|
3228
|
-
deprecated: true,
|
|
3229
|
-
notes: '旧版指标,保留用于兼容。新版主指标见 v3 字段(E1=场景交付效率 ms/场景,Q3=规约质量评分 0-100)。',
|
|
3230
|
-
},
|
|
3231
3234
|
};
|
|
3232
3235
|
}
|
|
3233
3236
|
|
|
@@ -3499,7 +3502,13 @@ function reportStatusLabel(status) {
|
|
|
3499
3502
|
|
|
3500
3503
|
function renderExecutiveReportMarkdown(report) {
|
|
3501
3504
|
if (report?.level === 'change' && report.change) {
|
|
3502
|
-
report
|
|
3505
|
+
if (!report.change_summary || !report.artifacts?.files) {
|
|
3506
|
+
report = require('./reporting/change-report-model.cjs').buildChangeReportModel(report);
|
|
3507
|
+
}
|
|
3508
|
+
const changeMarkdown = require('./reporting/change-report-markdown.cjs');
|
|
3509
|
+
if (typeof changeMarkdown.renderChangeReportMarkdown === 'function') {
|
|
3510
|
+
return changeMarkdown.renderChangeReportMarkdown(report);
|
|
3511
|
+
}
|
|
3503
3512
|
}
|
|
3504
3513
|
const metrics = report.metrics;
|
|
3505
3514
|
const health = report.doctor || {};
|
|
@@ -3672,9 +3681,12 @@ function renderExecutiveReportMarkdown(report) {
|
|
|
3672
3681
|
`- 归档目录:${archive?.archive_path || 'null'}`,
|
|
3673
3682
|
`- 归档清单:${archive?.manifest_path || 'null'}`,
|
|
3674
3683
|
`- 最终报告:${archive?.report_path || 'null'}`,
|
|
3675
|
-
`-
|
|
3676
|
-
|
|
3677
|
-
|
|
3684
|
+
`- 主任务完成:${taskCompletion?.primary_tasks
|
|
3685
|
+
? `${taskCompletion.primary_tasks.completed}/${taskCompletion.primary_tasks.total}`
|
|
3686
|
+
: 'null'}`,
|
|
3687
|
+
`- 全部 checkbox 完成:${taskCompletion?.completed ?? 'null'}/${taskCompletion?.total ?? 'null'}(含验收子项;分母≠主任务数)`,
|
|
3688
|
+
`- 未勾选 checkbox:${taskCompletion?.incomplete ?? 'null'}`,
|
|
3689
|
+
`- 验收证据待确认:${taskCompletion?.acceptance_evidence?.unconfirmed ?? 'null'}/${taskCompletion?.acceptance_evidence?.total ?? 'null'}`,
|
|
3678
3690
|
'',
|
|
3679
3691
|
'## 说明',
|
|
3680
3692
|
'- `null` 表示当前还没有采集到对应事件或该指标暂不适用。',
|
|
@@ -4219,10 +4231,32 @@ tr:last-child td,tr:last-child th{border-bottom:none}
|
|
|
4219
4231
|
</body></html>`;
|
|
4220
4232
|
}
|
|
4221
4233
|
|
|
4234
|
+
function sortedUniqueFileList(files) {
|
|
4235
|
+
return [...new Set((files || []).filter(file => typeof file === 'string' && file.trim()).map(file => file.trim()))]
|
|
4236
|
+
.sort((a, b) => a.localeCompare(b));
|
|
4237
|
+
}
|
|
4238
|
+
|
|
4239
|
+
function fileListSymmetricDiff(left, right) {
|
|
4240
|
+
const leftSet = new Set(left || []);
|
|
4241
|
+
const rightSet = new Set(right || []);
|
|
4242
|
+
return {
|
|
4243
|
+
only_in_task_events: [...leftSet].filter(file => !rightSet.has(file)).sort((a, b) => a.localeCompare(b)),
|
|
4244
|
+
only_in_final_output: [...rightSet].filter(file => !leftSet.has(file)).sort((a, b) => a.localeCompare(b)),
|
|
4245
|
+
};
|
|
4246
|
+
}
|
|
4247
|
+
|
|
4222
4248
|
// ── U1 报告维度提取辅助:从 events/metrics/doctor/archive 提炼 5 维度,数据缺失诚实标注 _available: false ──
|
|
4223
4249
|
function extractChangedFiles(events, projectRoot) {
|
|
4224
|
-
const adoptionEvents = events
|
|
4225
|
-
const
|
|
4250
|
+
const adoptionEvents = getAiAdoptionEvents(events);
|
|
4251
|
+
const finalAdoptionEvents = adoptionEvents.filter((event) => {
|
|
4252
|
+
const review = getAiAdoptionReview(event);
|
|
4253
|
+
return review?.review_status === 'final'
|
|
4254
|
+
|| event.status === 'final'
|
|
4255
|
+
|| review?.ai_diff?.snapshot_kind === 'final';
|
|
4256
|
+
});
|
|
4257
|
+
const latest = latestByTimestamp(
|
|
4258
|
+
finalAdoptionEvents.length > 0 ? finalAdoptionEvents : adoptionEvents,
|
|
4259
|
+
);
|
|
4226
4260
|
const review = latest ? getAiAdoptionReview(latest) : null;
|
|
4227
4261
|
const aiDiff = review?.ai_diff || null;
|
|
4228
4262
|
const taskUpdateFiles = new Set();
|
|
@@ -4243,7 +4277,10 @@ function extractChangedFiles(events, projectRoot) {
|
|
|
4243
4277
|
: [],
|
|
4244
4278
|
);
|
|
4245
4279
|
if (finalOutputFiles.size === 0 && latest?.details) {
|
|
4246
|
-
const rawReview = latest.details.ai_adoption
|
|
4280
|
+
const rawReview = latest.details.ai_adoption
|
|
4281
|
+
|| latest.details.ai_adoption_review
|
|
4282
|
+
|| latest.details.ai_code_adoption
|
|
4283
|
+
|| {};
|
|
4247
4284
|
for (const assertion of rawReview.assertions || []) {
|
|
4248
4285
|
for (const file of assertion.files || []) {
|
|
4249
4286
|
if (typeof file === 'string' && file.trim()) finalOutputFiles.add(file.trim());
|
|
@@ -4251,12 +4288,17 @@ function extractChangedFiles(events, projectRoot) {
|
|
|
4251
4288
|
}
|
|
4252
4289
|
}
|
|
4253
4290
|
|
|
4254
|
-
const
|
|
4291
|
+
const taskEventFileList = sortedUniqueFileList([...taskUpdateFiles]);
|
|
4292
|
+
const finalOutputFileList = sortedUniqueFileList([...finalOutputFiles]);
|
|
4293
|
+
const taskUpdateFileCount = taskEventFileList.length;
|
|
4255
4294
|
const finalOutputFileCount = Number.isFinite(aiDiff?.files_changed)
|
|
4256
4295
|
? aiDiff.files_changed
|
|
4257
|
-
: (
|
|
4258
|
-
const snapshotKind = aiDiff?.snapshot_kind
|
|
4259
|
-
|
|
4296
|
+
: (finalOutputFileList.length > 0 ? finalOutputFileList.length : null);
|
|
4297
|
+
const snapshotKind = aiDiff?.snapshot_kind
|
|
4298
|
+
|| ((review?.review_status === 'final' || latest?.status === 'final') ? 'final' : null);
|
|
4299
|
+
const finalOutputStatus = snapshotKind === 'final'
|
|
4300
|
+
? 'final'
|
|
4301
|
+
: (finalOutputFileCount == null ? 'unavailable' : 'provisional');
|
|
4260
4302
|
const addedLines = Number.isFinite(aiDiff?.added_lines)
|
|
4261
4303
|
? aiDiff.added_lines
|
|
4262
4304
|
: (Number.isFinite(review?.final_diff?.added_lines) ? review.final_diff.added_lines : null);
|
|
@@ -4264,11 +4306,11 @@ function extractChangedFiles(events, projectRoot) {
|
|
|
4264
4306
|
? aiDiff.deleted_lines
|
|
4265
4307
|
: (Number.isFinite(review?.final_diff?.deleted_lines) ? review.final_diff.deleted_lines : null);
|
|
4266
4308
|
let lineCountEstimate = null;
|
|
4267
|
-
if (addedLines == null && review?.ai_git_sha == null &&
|
|
4309
|
+
if (addedLines == null && review?.ai_git_sha == null && finalOutputFileList.length > 0 && projectRoot) {
|
|
4268
4310
|
let lines = 0;
|
|
4269
4311
|
let countedFiles = 0;
|
|
4270
4312
|
const root = path.resolve(projectRoot);
|
|
4271
|
-
for (const relativePath of
|
|
4313
|
+
for (const relativePath of finalOutputFileList) {
|
|
4272
4314
|
try {
|
|
4273
4315
|
const absolutePath = path.isAbsolute(relativePath)
|
|
4274
4316
|
? path.resolve(relativePath)
|
|
@@ -4289,29 +4331,56 @@ function extractChangedFiles(events, projectRoot) {
|
|
|
4289
4331
|
};
|
|
4290
4332
|
}
|
|
4291
4333
|
}
|
|
4292
|
-
|
|
4334
|
+
// Prefer final AI snapshot for public files_changed when available.
|
|
4335
|
+
const filesChanged = finalOutputStatus === 'final' && finalOutputFileCount != null
|
|
4336
|
+
? finalOutputFileCount
|
|
4337
|
+
: (taskUpdateFileCount > 0 ? taskUpdateFileCount : finalOutputFileCount);
|
|
4293
4338
|
const countsDiffer = taskUpdateFileCount > 0
|
|
4294
4339
|
&& finalOutputFileCount != null
|
|
4295
4340
|
&& taskUpdateFileCount !== finalOutputFileCount;
|
|
4296
|
-
const
|
|
4297
|
-
|
|
4298
|
-
|
|
4341
|
+
const listDiff = fileListSymmetricDiff(taskEventFileList, finalOutputFileList);
|
|
4342
|
+
const listsDiffer = taskUpdateFileCount > 0
|
|
4343
|
+
&& finalOutputFileList.length > 0
|
|
4344
|
+
&& (listDiff.only_in_task_events.length > 0 || listDiff.only_in_final_output.length > 0);
|
|
4345
|
+
let reconciliationStatus = 'unavailable';
|
|
4346
|
+
let reconciliationNote = '没有可用的文件变更证据。';
|
|
4347
|
+
if (taskUpdateFileCount === 0 && finalOutputFileCount == null) {
|
|
4348
|
+
reconciliationStatus = 'unavailable';
|
|
4349
|
+
reconciliationNote = '没有可用的文件变更证据。';
|
|
4350
|
+
} else if (taskUpdateFileCount === 0 || finalOutputFileCount == null) {
|
|
4351
|
+
reconciliationStatus = 'unavailable';
|
|
4352
|
+
reconciliationNote = finalOutputFileCount == null
|
|
4353
|
+
? '最终输出文件快照不可用,无法与任务事件对账。'
|
|
4354
|
+
: '任务事件未记录文件清单;仅展示最终输出快照。';
|
|
4355
|
+
} else if (countsDiffer) {
|
|
4356
|
+
reconciliationStatus = 'count-mismatch';
|
|
4357
|
+
reconciliationNote = `任务事件累计涉及 ${taskUpdateFileCount} 个文件;最终输出快照包含 ${finalOutputFileCount} 个文件。两者用途不同,不应合并为一个口径。`;
|
|
4358
|
+
} else if (listsDiffer) {
|
|
4359
|
+
reconciliationStatus = 'list-mismatch';
|
|
4360
|
+
reconciliationNote = `文件数量均为 ${taskUpdateFileCount},但清单不一致(任务独有 ${listDiff.only_in_task_events.length},最终独有 ${listDiff.only_in_final_output.length})。`;
|
|
4361
|
+
} else {
|
|
4362
|
+
reconciliationStatus = 'consistent';
|
|
4363
|
+
reconciliationNote = '任务事件与最终输出文件口径一致。';
|
|
4364
|
+
}
|
|
4299
4365
|
return {
|
|
4300
4366
|
_available: filesChanged != null,
|
|
4301
4367
|
files_changed: filesChanged,
|
|
4302
4368
|
added_lines: addedLines,
|
|
4303
4369
|
deleted_lines: deletedLines,
|
|
4304
|
-
files:
|
|
4305
|
-
task_event_files: taskUpdateFileCount > 0 ?
|
|
4370
|
+
files: finalOutputFileList.length > 0 ? finalOutputFileList : null,
|
|
4371
|
+
task_event_files: taskUpdateFileCount > 0 ? taskEventFileList : null,
|
|
4306
4372
|
task_event_files_changed: taskUpdateFileCount > 0 ? taskUpdateFileCount : null,
|
|
4307
|
-
final_output_files:
|
|
4373
|
+
final_output_files: finalOutputFileList.length > 0 ? finalOutputFileList : null,
|
|
4308
4374
|
final_output_files_changed: finalOutputFileCount,
|
|
4309
4375
|
final_output_status: finalOutputStatus,
|
|
4310
4376
|
diff_source: snapshotKind === 'final' ? 'ai-diff-final' : (aiDiff ? 'ai-diff-provisional' : 'task-events'),
|
|
4311
4377
|
line_count_estimate: lineCountEstimate,
|
|
4378
|
+
reconciliation_status: reconciliationStatus,
|
|
4312
4379
|
reconciliation_note: reconciliationNote,
|
|
4313
|
-
|
|
4314
|
-
|
|
4380
|
+
only_in_task_events: listDiff.only_in_task_events,
|
|
4381
|
+
only_in_final_output: listDiff.only_in_final_output,
|
|
4382
|
+
ai_diff_files_changed: (countsDiffer || listsDiffer) ? finalOutputFileCount : null,
|
|
4383
|
+
files_diff_note: (countsDiffer || listsDiffer) ? reconciliationNote : null,
|
|
4315
4384
|
};
|
|
4316
4385
|
}
|
|
4317
4386
|
|
|
@@ -4564,9 +4633,101 @@ function validateMaturityEvidenceDetails(details) {
|
|
|
4564
4633
|
};
|
|
4565
4634
|
}
|
|
4566
4635
|
|
|
4636
|
+
function scopeEventsForChangeReport(events, change) {
|
|
4637
|
+
const input = Array.isArray(events) ? events : [];
|
|
4638
|
+
if (!change) {
|
|
4639
|
+
return {
|
|
4640
|
+
events: input,
|
|
4641
|
+
audit: {
|
|
4642
|
+
kind: 'project',
|
|
4643
|
+
change: null,
|
|
4644
|
+
input_event_count: input.length,
|
|
4645
|
+
included_event_count: input.length,
|
|
4646
|
+
excluded_event_count: 0,
|
|
4647
|
+
cross_change_data_included: false,
|
|
4648
|
+
},
|
|
4649
|
+
};
|
|
4650
|
+
}
|
|
4651
|
+
const scoped = input
|
|
4652
|
+
.filter((event) => (
|
|
4653
|
+
event?.change === change
|
|
4654
|
+
|| event?.attribution?.target_change === change
|
|
4655
|
+
))
|
|
4656
|
+
.map((event) => {
|
|
4657
|
+
if (event?.change === change) return event;
|
|
4658
|
+
return {
|
|
4659
|
+
...event,
|
|
4660
|
+
change,
|
|
4661
|
+
attribution: {
|
|
4662
|
+
...event.attribution,
|
|
4663
|
+
source_change: event.attribution?.source_change || event.change || null,
|
|
4664
|
+
target_change: change,
|
|
4665
|
+
},
|
|
4666
|
+
};
|
|
4667
|
+
});
|
|
4668
|
+
return {
|
|
4669
|
+
events: scoped,
|
|
4670
|
+
audit: {
|
|
4671
|
+
kind: 'single-change',
|
|
4672
|
+
change,
|
|
4673
|
+
input_event_count: input.length,
|
|
4674
|
+
included_event_count: scoped.length,
|
|
4675
|
+
excluded_event_count: input.length - scoped.length,
|
|
4676
|
+
cross_change_data_included: false,
|
|
4677
|
+
},
|
|
4678
|
+
};
|
|
4679
|
+
}
|
|
4680
|
+
|
|
4681
|
+
function tddPairFinding(pair) {
|
|
4682
|
+
const definitions = {
|
|
4683
|
+
'red-did-not-fail': {
|
|
4684
|
+
code: 'TDD_PAIR_RED_DID_NOT_FAIL',
|
|
4685
|
+
expected: 'RED 测试必须以非零退出码失败,并至少包含 1 个失败用例。',
|
|
4686
|
+
actual: '已记录 RED 事件,但测试没有形成有效失败。',
|
|
4687
|
+
root_cause: 'RED 事件的 valid_red、exit_code 或 failed 计数不满足失败测试约束。',
|
|
4688
|
+
impact: '该 TDD 对不能证明实现前测试曾有效拦截缺失行为。',
|
|
4689
|
+
remediation: '修正 RED 用例并重新记录失败结果,再执行对应 GREEN 或回归测试。',
|
|
4690
|
+
},
|
|
4691
|
+
'red-missing': {
|
|
4692
|
+
code: 'TDD_PAIR_RED_MISSING',
|
|
4693
|
+
expected: '每个 TDD 对都应先存在可定位的有效 RED 事件。',
|
|
4694
|
+
actual: '未找到该 TDD 对对应的 RED 事件。',
|
|
4695
|
+
root_cause: '任务与事件中没有可关联到该 pair 的 RED 证据。',
|
|
4696
|
+
impact: '无法证明测试先于实现并曾经失败。',
|
|
4697
|
+
remediation: '补充并记录该 pair 的 RED 失败测试;不能事后伪造历史证据。',
|
|
4698
|
+
},
|
|
4699
|
+
'green-missing': {
|
|
4700
|
+
code: 'TDD_PAIR_GREEN_MISSING',
|
|
4701
|
+
expected: '有效 RED 之后应存在覆盖对应任务的 GREEN 或回归成功事件。',
|
|
4702
|
+
actual: '存在有效 RED,但没有找到其后的成功 GREEN/回归事件。',
|
|
4703
|
+
root_cause: '后续成功测试缺失、时间顺序错误,或未覆盖对应 GREEN 任务。',
|
|
4704
|
+
impact: '无法证明该 TDD 对已经由失败转为通过。',
|
|
4705
|
+
remediation: '执行对应 GREEN/回归测试并记录覆盖任务与成功结果。',
|
|
4706
|
+
},
|
|
4707
|
+
};
|
|
4708
|
+
const detail = definitions[pair.status] || {
|
|
4709
|
+
code: 'TDD_PAIR_UNCLASSIFIED',
|
|
4710
|
+
expected: 'TDD 对应具备可判定的 RED→GREEN 证据。',
|
|
4711
|
+
actual: `该 TDD 对状态为 ${pair.status || 'unclassified'}。`,
|
|
4712
|
+
root_cause: '现有任务或测试事件不足以完成配对。',
|
|
4713
|
+
impact: '该 TDD 对的完整性不可判定。',
|
|
4714
|
+
remediation: '补充 pair、任务和测试事件之间的明确关联。',
|
|
4715
|
+
};
|
|
4716
|
+
return {
|
|
4717
|
+
...detail,
|
|
4718
|
+
severity: 'warning',
|
|
4719
|
+
verdict: pair.status,
|
|
4720
|
+
pair_id: pair.pair_id,
|
|
4721
|
+
task_id: pair.red_task_id || pair.green_task_id || null,
|
|
4722
|
+
message: `${pair.pair_id}:${detail.actual}`,
|
|
4723
|
+
};
|
|
4724
|
+
}
|
|
4725
|
+
|
|
4567
4726
|
function buildReport(projectRoot, events, options = {}) {
|
|
4727
|
+
const scopedReport = scopeEventsForChangeReport(events, options.change);
|
|
4728
|
+
events = scopedReport.events;
|
|
4568
4729
|
const archiveEvent = latestByTimestamp(events.filter(e => {
|
|
4569
|
-
return e.command === 'archive' && e.type === 'stage_end'
|
|
4730
|
+
return e.command === 'archive' && e.type === 'stage_end';
|
|
4570
4731
|
}));
|
|
4571
4732
|
const gitCommitTimeline = resolveGitCommitTimeline(
|
|
4572
4733
|
events,
|
|
@@ -4618,18 +4779,46 @@ function buildReport(projectRoot, events, options = {}) {
|
|
|
4618
4779
|
});
|
|
4619
4780
|
} catch { maturity = null; }
|
|
4620
4781
|
|
|
4621
|
-
const archiveTaskCompletion =
|
|
4782
|
+
const archiveTaskCompletion = options.taskModel
|
|
4783
|
+
|| archiveEvent?.details?.archive_result?.task_completion
|
|
4784
|
+
|| null;
|
|
4622
4785
|
const applyEvidenceAudit = options.change && archiveTaskCompletion
|
|
4623
4786
|
? buildApplyEvidenceAudit(options.change, archiveTaskCompletion, events, { mode: 'report-only' })
|
|
4624
4787
|
: null;
|
|
4788
|
+
const taskEvidenceAudit = options.change && archiveTaskCompletion
|
|
4789
|
+
? buildTaskEvidenceAudit(options.change, archiveTaskCompletion, events, { mode: 'report-only' })
|
|
4790
|
+
: null;
|
|
4791
|
+
const tddPairAudit = options.change && archiveTaskCompletion
|
|
4792
|
+
? buildTddPairAudit(options.change, archiveTaskCompletion, events, { mode: 'report-only' })
|
|
4793
|
+
: null;
|
|
4794
|
+
if (applyEvidenceAudit && tddPairAudit) {
|
|
4795
|
+
applyEvidenceAudit.tdd = {
|
|
4796
|
+
required: tddPairAudit.required,
|
|
4797
|
+
red_green_complete: tddPairAudit.complete,
|
|
4798
|
+
};
|
|
4799
|
+
}
|
|
4625
4800
|
const baseEvidenceAlerts = metricsV3.buildEvidenceAlerts(events);
|
|
4626
|
-
const
|
|
4627
|
-
|
|
4801
|
+
const evidenceFindings = [
|
|
4802
|
+
...(applyEvidenceAudit?.findings || []),
|
|
4803
|
+
...((tddPairAudit?.pairs || [])
|
|
4804
|
+
.filter((pair) => pair.status !== 'complete')
|
|
4805
|
+
.map(tddPairFinding)),
|
|
4806
|
+
];
|
|
4807
|
+
const auditAlerts = evidenceFindings.map((finding) => ({
|
|
4808
|
+
key: `${finding.code}:${finding.pair_id || finding.task_id || options.change || 'change'}`,
|
|
4628
4809
|
category: 'evidence',
|
|
4629
4810
|
warning: finding.code,
|
|
4630
|
-
subject: finding.task_id || options.change || null,
|
|
4811
|
+
subject: finding.pair_id || finding.task_id || options.change || null,
|
|
4812
|
+
task_id: finding.task_id || null,
|
|
4813
|
+
pair_id: finding.pair_id || null,
|
|
4631
4814
|
message: finding.message,
|
|
4632
4815
|
severity: finding.severity || 'warning',
|
|
4816
|
+
verdict: finding.verdict || 'evidence-gap',
|
|
4817
|
+
expected: finding.expected || null,
|
|
4818
|
+
actual: finding.actual || finding.message || null,
|
|
4819
|
+
root_cause: finding.root_cause || null,
|
|
4820
|
+
impact: finding.impact || null,
|
|
4821
|
+
remediation: finding.remediation || null,
|
|
4633
4822
|
status: 'open',
|
|
4634
4823
|
occurrences: 1,
|
|
4635
4824
|
opened_by_event_id: null,
|
|
@@ -4738,6 +4927,7 @@ function buildReport(projectRoot, events, options = {}) {
|
|
|
4738
4927
|
active_links: [],
|
|
4739
4928
|
unassigned_candidate_event_ids: [],
|
|
4740
4929
|
},
|
|
4930
|
+
report_scope: scopedReport.audit,
|
|
4741
4931
|
conclusion: {
|
|
4742
4932
|
status: evidenceAlerts.open.length > 0 || trustRate < 1 || primaryTasks?.has_incomplete ? 'attention' : 'trusted',
|
|
4743
4933
|
text: `${taskStatement};${trustStatement};${alertStatement}。`,
|
|
@@ -4746,9 +4936,9 @@ function buildReport(projectRoot, events, options = {}) {
|
|
|
4746
4936
|
alert_statement: alertStatement,
|
|
4747
4937
|
},
|
|
4748
4938
|
metrics,
|
|
4749
|
-
legacy_metrics: metrics.legacy_metrics || null,
|
|
4750
4939
|
doctor,
|
|
4751
|
-
archive_result: archiveEvent?.details?.archive_result
|
|
4940
|
+
archive_result: archiveEvent?.details?.archive_result
|
|
4941
|
+
|| (options.taskModel ? { task_completion: options.taskModel } : null),
|
|
4752
4942
|
changed_files: extractChangedFiles(events, projectRoot),
|
|
4753
4943
|
timeline: extractTimeline(events),
|
|
4754
4944
|
stage_timeline: extractStageTimeline(events),
|
|
@@ -4765,12 +4955,115 @@ function buildReport(projectRoot, events, options = {}) {
|
|
|
4765
4955
|
evidence_alerts: evidenceAlerts,
|
|
4766
4956
|
check_warning_dispositions: checkWarningDispositions,
|
|
4767
4957
|
apply_evidence_audit: applyEvidenceAudit,
|
|
4958
|
+
task_evidence_audit: taskEvidenceAudit,
|
|
4959
|
+
tdd_pair_audit: tddPairAudit,
|
|
4768
4960
|
scheme_compliance: schemeCompliance,
|
|
4769
4961
|
maturity,
|
|
4770
4962
|
};
|
|
4771
4963
|
return require('./reporting/change-report-model.cjs').buildChangeReportModel(baseReport, events);
|
|
4772
4964
|
}
|
|
4773
4965
|
|
|
4966
|
+
function reportArtifactPaths(projectRoot, archiveResult = {}, explicitPaths = []) {
|
|
4967
|
+
const declared = [
|
|
4968
|
+
archiveResult.report_path,
|
|
4969
|
+
archiveResult.report_html_path,
|
|
4970
|
+
archiveResult.report_json_path,
|
|
4971
|
+
...explicitPaths,
|
|
4972
|
+
].filter(Boolean).map((filePath) => (
|
|
4973
|
+
path.isAbsolute(filePath) ? filePath : path.resolve(projectRoot, filePath)
|
|
4974
|
+
));
|
|
4975
|
+
return [...new Set(declared)].filter((filePath) => ['.md', '.html', '.json'].includes(
|
|
4976
|
+
path.extname(filePath).toLowerCase(),
|
|
4977
|
+
));
|
|
4978
|
+
}
|
|
4979
|
+
|
|
4980
|
+
function renderReportArtifact(report, filePath) {
|
|
4981
|
+
const extension = path.extname(filePath).toLowerCase();
|
|
4982
|
+
if (extension === '.json') return JSON.stringify(report, null, 2);
|
|
4983
|
+
if (extension === '.html') return renderExecutiveReportHtml(report);
|
|
4984
|
+
return renderExecutiveReportMarkdown(report);
|
|
4985
|
+
}
|
|
4986
|
+
|
|
4987
|
+
function artifactSizeVector(projectRoot, report) {
|
|
4988
|
+
const required = [
|
|
4989
|
+
'markdown_report',
|
|
4990
|
+
'html_report',
|
|
4991
|
+
'json_report',
|
|
4992
|
+
'execution_log',
|
|
4993
|
+
'manifest',
|
|
4994
|
+
'archive_zip',
|
|
4995
|
+
];
|
|
4996
|
+
const embedded = {};
|
|
4997
|
+
const actual = {};
|
|
4998
|
+
for (const name of required) {
|
|
4999
|
+
const artifact = report.artifacts?.files?.[name];
|
|
5000
|
+
if (!artifact?.path) {
|
|
5001
|
+
throw new Error(`E_ARTIFACT_INVENTORY_INCOMPLETE: ${name} 缺少最终路径`);
|
|
5002
|
+
}
|
|
5003
|
+
const absolutePath = path.isAbsolute(artifact.path)
|
|
5004
|
+
? artifact.path
|
|
5005
|
+
: path.resolve(projectRoot, artifact.path);
|
|
5006
|
+
if (!fs.existsSync(absolutePath) || !fs.statSync(absolutePath).isFile()) {
|
|
5007
|
+
throw new Error(`E_ARTIFACT_INVENTORY_INCOMPLETE: ${name} 未落盘 (${artifact.path})`);
|
|
5008
|
+
}
|
|
5009
|
+
embedded[name] = artifact.size_bytes;
|
|
5010
|
+
actual[name] = fs.statSync(absolutePath).size;
|
|
5011
|
+
}
|
|
5012
|
+
return { embedded, actual };
|
|
5013
|
+
}
|
|
5014
|
+
|
|
5015
|
+
function sameArtifactSizeVector(left, right) {
|
|
5016
|
+
return Object.keys(left).every(name => left[name] === right[name]);
|
|
5017
|
+
}
|
|
5018
|
+
|
|
5019
|
+
function finalizeArtifactInventory(options = {}) {
|
|
5020
|
+
const projectRoot = options.projectRoot;
|
|
5021
|
+
const change = options.change;
|
|
5022
|
+
const archiveResult = options.archiveResult || {};
|
|
5023
|
+
const dataDir = options.dataDir || getDataDir(projectRoot);
|
|
5024
|
+
const paths = reportArtifactPaths(projectRoot, archiveResult, options.reportPaths || []);
|
|
5025
|
+
if (paths.length !== 3) {
|
|
5026
|
+
throw new Error(`E_ARTIFACT_INVENTORY_INCOMPLETE: 需要 md/html/json 三份报告,实际 ${paths.length} 份`);
|
|
5027
|
+
}
|
|
5028
|
+
const archiveDirAbs = path.isAbsolute(archiveResult.archive_path)
|
|
5029
|
+
? archiveResult.archive_path
|
|
5030
|
+
: path.resolve(projectRoot, archiveResult.archive_path);
|
|
5031
|
+
let previousActual = null;
|
|
5032
|
+
for (let round = 1; round <= 5; round += 1) {
|
|
5033
|
+
const report = buildReport(projectRoot, readEvents(dataDir, change), {
|
|
5034
|
+
level: options.level || 'change',
|
|
5035
|
+
change,
|
|
5036
|
+
capability: options.capability,
|
|
5037
|
+
formats: ['md', 'html', 'json'],
|
|
5038
|
+
});
|
|
5039
|
+
for (const filePath of paths) {
|
|
5040
|
+
atomicWriteText(filePath, `${renderReportArtifact(report, filePath)}\n`);
|
|
5041
|
+
}
|
|
5042
|
+
ensureArchiveManifest(projectRoot, change, archiveDirAbs, {
|
|
5043
|
+
reason: archiveResult.reason || '',
|
|
5044
|
+
method: archiveResult.method || 'skywalk-full-spec-archive',
|
|
5045
|
+
archivePath: archiveDirAbs,
|
|
5046
|
+
archivedAt: archiveResult.created_at || archiveResult.archived_at,
|
|
5047
|
+
copiedSpecs: archiveResult.copied_specs,
|
|
5048
|
+
finalizeAfterReports: true,
|
|
5049
|
+
runIdentity: options.runIdentity,
|
|
5050
|
+
});
|
|
5051
|
+
const vector = artifactSizeVector(projectRoot, report);
|
|
5052
|
+
const embeddedMatchesDisk = sameArtifactSizeVector(vector.embedded, vector.actual);
|
|
5053
|
+
const actualMatchesPrevious = previousActual
|
|
5054
|
+
&& sameArtifactSizeVector(previousActual, vector.actual);
|
|
5055
|
+
if (embeddedMatchesDisk && actualMatchesPrevious) {
|
|
5056
|
+
return {
|
|
5057
|
+
stable: true,
|
|
5058
|
+
rounds: round,
|
|
5059
|
+
sizes: vector.actual,
|
|
5060
|
+
};
|
|
5061
|
+
}
|
|
5062
|
+
previousActual = vector.actual;
|
|
5063
|
+
}
|
|
5064
|
+
throw new Error('E_ARTIFACT_INVENTORY_NOT_STABLE: 六类产物大小在 5 轮内未收敛');
|
|
5065
|
+
}
|
|
5066
|
+
|
|
4774
5067
|
function filterEventsByDate(events, dateFrom, dateTo) {
|
|
4775
5068
|
if (!dateFrom && !dateTo) return events;
|
|
4776
5069
|
return events.filter(e => {
|
|
@@ -5078,15 +5371,29 @@ function cmdEnd(args, options = {}) {
|
|
|
5078
5371
|
const archiveDirAbs = path.isAbsolute(archiveResult.archive_path)
|
|
5079
5372
|
? archiveResult.archive_path
|
|
5080
5373
|
: path.resolve(projectRoot, archiveResult.archive_path);
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
5374
|
+
const existingReportPaths = reportArtifactPaths(projectRoot, archiveResult);
|
|
5375
|
+
if (existingReportPaths.length === 3) {
|
|
5376
|
+
finalizeArtifactInventory({
|
|
5377
|
+
projectRoot,
|
|
5378
|
+
dataDir,
|
|
5379
|
+
change,
|
|
5380
|
+
archiveResult,
|
|
5381
|
+
reportPaths: existingReportPaths,
|
|
5382
|
+
level: args['report-level'] || 'change',
|
|
5383
|
+
capability: args.capability || args['capability-name'] || startEvent?.capability,
|
|
5384
|
+
runIdentity: latestExisting.event_id,
|
|
5385
|
+
});
|
|
5386
|
+
} else {
|
|
5387
|
+
ensureArchiveManifest(projectRoot, change, archiveDirAbs, {
|
|
5388
|
+
reason: archiveResult.reason || '',
|
|
5389
|
+
method: archiveResult.method || 'skywalk-full-spec-archive',
|
|
5390
|
+
archivePath: archiveDirAbs,
|
|
5391
|
+
archivedAt: archiveResult.created_at || archiveResult.archived_at,
|
|
5392
|
+
copiedSpecs: archiveResult.copied_specs,
|
|
5393
|
+
finalizeAfterReports: true,
|
|
5394
|
+
runIdentity: latestExisting.event_id,
|
|
5395
|
+
});
|
|
5396
|
+
}
|
|
5090
5397
|
}
|
|
5091
5398
|
const output = {
|
|
5092
5399
|
event_id: eventId,
|
|
@@ -5339,13 +5646,14 @@ function cmdEnd(args, options = {}) {
|
|
|
5339
5646
|
const archiveDirAbs = path.isAbsolute(finalArchiveResult.archive_path)
|
|
5340
5647
|
? finalArchiveResult.archive_path
|
|
5341
5648
|
: path.resolve(projectRoot, finalArchiveResult.archive_path);
|
|
5342
|
-
|
|
5343
|
-
|
|
5344
|
-
|
|
5345
|
-
|
|
5346
|
-
|
|
5347
|
-
|
|
5348
|
-
|
|
5649
|
+
finalizeArtifactInventory({
|
|
5650
|
+
projectRoot,
|
|
5651
|
+
dataDir,
|
|
5652
|
+
change,
|
|
5653
|
+
archiveResult: finalArchiveResult,
|
|
5654
|
+
reportPaths: writtenReportPaths,
|
|
5655
|
+
level: args['report-level'] || 'change',
|
|
5656
|
+
capability: args.capability || args['capability-name'] || startEvent?.capability,
|
|
5349
5657
|
runIdentity: event.event_id,
|
|
5350
5658
|
});
|
|
5351
5659
|
}
|
|
@@ -5468,6 +5776,153 @@ function isValidCompletionTestResults(testResults) {
|
|
|
5468
5776
|
return true;
|
|
5469
5777
|
}
|
|
5470
5778
|
|
|
5779
|
+
/**
|
|
5780
|
+
* Pure task↔test reference resolver.
|
|
5781
|
+
* Accepts mutually exclusive test_event_id or test_run_id.
|
|
5782
|
+
* Does not throw; callers decide whether to fail strict writes.
|
|
5783
|
+
*/
|
|
5784
|
+
function resolveTaskVerificationReference(events, input = {}) {
|
|
5785
|
+
const taskId = input.task_id || input.taskId || null;
|
|
5786
|
+
const change = input.change || null;
|
|
5787
|
+
const inputEventId = input.test_event_id || null;
|
|
5788
|
+
const inputRunId = input.test_run_id || null;
|
|
5789
|
+
const verificationNotApplicable = input.verification_not_applicable === true;
|
|
5790
|
+
const verificationReason = String(
|
|
5791
|
+
input.verification_not_applicable_reason
|
|
5792
|
+
|| input.verification_reason
|
|
5793
|
+
|| '',
|
|
5794
|
+
).trim();
|
|
5795
|
+
|
|
5796
|
+
const base = {
|
|
5797
|
+
status: 'missing',
|
|
5798
|
+
resolved_event_id: null,
|
|
5799
|
+
input_event_id: inputEventId,
|
|
5800
|
+
input_run_id: inputRunId,
|
|
5801
|
+
covered_task_ids: [],
|
|
5802
|
+
reason_code: null,
|
|
5803
|
+
};
|
|
5804
|
+
|
|
5805
|
+
if (verificationNotApplicable) {
|
|
5806
|
+
if (!verificationReason) {
|
|
5807
|
+
return {
|
|
5808
|
+
...base,
|
|
5809
|
+
status: 'invalid',
|
|
5810
|
+
reason_code: 'TASK_VERIFICATION_REASON_REQUIRED',
|
|
5811
|
+
};
|
|
5812
|
+
}
|
|
5813
|
+
return {
|
|
5814
|
+
...base,
|
|
5815
|
+
status: 'waived',
|
|
5816
|
+
reason_code: 'VERIFICATION_NOT_APPLICABLE',
|
|
5817
|
+
};
|
|
5818
|
+
}
|
|
5819
|
+
|
|
5820
|
+
if (inputEventId && inputRunId) {
|
|
5821
|
+
return {
|
|
5822
|
+
...base,
|
|
5823
|
+
status: 'invalid',
|
|
5824
|
+
reason_code: 'TASK_TEST_REFERENCE_MUTEX',
|
|
5825
|
+
};
|
|
5826
|
+
}
|
|
5827
|
+
|
|
5828
|
+
if (!inputEventId && !inputRunId) {
|
|
5829
|
+
return {
|
|
5830
|
+
...base,
|
|
5831
|
+
status: 'missing',
|
|
5832
|
+
reason_code: 'TASK_TEST_REFERENCE_MISSING',
|
|
5833
|
+
};
|
|
5834
|
+
}
|
|
5835
|
+
|
|
5836
|
+
const scoped = (events || []).filter((event) => {
|
|
5837
|
+
if (!change) return true;
|
|
5838
|
+
return event.change === change || event.attribution?.target_change === change;
|
|
5839
|
+
});
|
|
5840
|
+
|
|
5841
|
+
function evaluateCandidate(event) {
|
|
5842
|
+
if (!event) {
|
|
5843
|
+
return { ...base, status: 'invalid', reason_code: 'TASK_TEST_REFERENCE_MISSING' };
|
|
5844
|
+
}
|
|
5845
|
+
if (change
|
|
5846
|
+
&& event.change
|
|
5847
|
+
&& event.change !== change
|
|
5848
|
+
&& event.attribution?.target_change !== change) {
|
|
5849
|
+
return { ...base, status: 'invalid', reason_code: 'TASK_TEST_REFERENCE_CROSS_CHANGE' };
|
|
5850
|
+
}
|
|
5851
|
+
if (event.type !== 'test_result') {
|
|
5852
|
+
return { ...base, status: 'invalid', reason_code: 'TASK_TEST_REFERENCE_NOT_TEST' };
|
|
5853
|
+
}
|
|
5854
|
+
const testResults = event.details?.test_results || {};
|
|
5855
|
+
const covered = collectVerifiedTaskIds(event);
|
|
5856
|
+
if (testResults.evidence_tier !== 'strict') {
|
|
5857
|
+
return {
|
|
5858
|
+
...base,
|
|
5859
|
+
status: 'invalid',
|
|
5860
|
+
resolved_event_id: event.event_id || null,
|
|
5861
|
+
covered_task_ids: covered,
|
|
5862
|
+
reason_code: 'TASK_TEST_REFERENCE_NON_STRICT',
|
|
5863
|
+
};
|
|
5864
|
+
}
|
|
5865
|
+
if (!isValidCompletionTestResults(testResults)) {
|
|
5866
|
+
return {
|
|
5867
|
+
...base,
|
|
5868
|
+
status: 'invalid',
|
|
5869
|
+
resolved_event_id: event.event_id || null,
|
|
5870
|
+
covered_task_ids: covered,
|
|
5871
|
+
reason_code: 'TASK_TEST_REFERENCE_INVALID',
|
|
5872
|
+
};
|
|
5873
|
+
}
|
|
5874
|
+
if (taskId && !covered.includes(taskId)) {
|
|
5875
|
+
return {
|
|
5876
|
+
...base,
|
|
5877
|
+
status: 'invalid',
|
|
5878
|
+
resolved_event_id: event.event_id || null,
|
|
5879
|
+
covered_task_ids: covered,
|
|
5880
|
+
reason_code: 'E_TEST_IDENTITY_MISMATCH',
|
|
5881
|
+
};
|
|
5882
|
+
}
|
|
5883
|
+
return {
|
|
5884
|
+
...base,
|
|
5885
|
+
status: 'verified',
|
|
5886
|
+
resolved_event_id: event.event_id || null,
|
|
5887
|
+
covered_task_ids: covered,
|
|
5888
|
+
reason_code: null,
|
|
5889
|
+
};
|
|
5890
|
+
}
|
|
5891
|
+
|
|
5892
|
+
if (inputEventId) {
|
|
5893
|
+
const referenced = scoped.find(event => event.event_id === inputEventId);
|
|
5894
|
+
return evaluateCandidate(referenced);
|
|
5895
|
+
}
|
|
5896
|
+
|
|
5897
|
+
const candidates = scoped.filter((event) => {
|
|
5898
|
+
if (event.type !== 'test_result') return false;
|
|
5899
|
+
if (event.run_id !== inputRunId) return false;
|
|
5900
|
+
const testResults = event.details?.test_results || {};
|
|
5901
|
+
if (testResults.evidence_tier !== 'strict') return false;
|
|
5902
|
+
if (!isValidCompletionTestResults(testResults)) return false;
|
|
5903
|
+
if (taskId && !collectVerifiedTaskIds(event).includes(taskId)) return false;
|
|
5904
|
+
return true;
|
|
5905
|
+
});
|
|
5906
|
+
|
|
5907
|
+
if (candidates.length === 0) {
|
|
5908
|
+
const anySameRun = scoped.filter(event => event.type === 'test_result' && event.run_id === inputRunId);
|
|
5909
|
+
if (anySameRun.length === 0) {
|
|
5910
|
+
return { ...base, status: 'invalid', reason_code: 'TASK_TEST_REFERENCE_MISSING' };
|
|
5911
|
+
}
|
|
5912
|
+
return evaluateCandidate(anySameRun[0]);
|
|
5913
|
+
}
|
|
5914
|
+
if (candidates.length > 1) {
|
|
5915
|
+
return {
|
|
5916
|
+
...base,
|
|
5917
|
+
status: 'ambiguous',
|
|
5918
|
+
candidate_event_ids: candidates.map(event => event.event_id).filter(Boolean),
|
|
5919
|
+
covered_task_ids: [...new Set(candidates.flatMap(collectVerifiedTaskIds))],
|
|
5920
|
+
reason_code: 'TASK_TEST_REFERENCE_AMBIGUOUS',
|
|
5921
|
+
};
|
|
5922
|
+
}
|
|
5923
|
+
return evaluateCandidate(candidates[0]);
|
|
5924
|
+
}
|
|
5925
|
+
|
|
5471
5926
|
function normalizeStrictProcessNote(args, details) {
|
|
5472
5927
|
const input = details?.process_note && typeof details.process_note === 'object'
|
|
5473
5928
|
? details.process_note
|
|
@@ -5514,6 +5969,7 @@ function normalizeStrictTaskUpdate(args, details, dataDir, change) {
|
|
|
5514
5969
|
'run_id',
|
|
5515
5970
|
);
|
|
5516
5971
|
const testEventId = input?.test_event_id || details?.test_event_id || null;
|
|
5972
|
+
const testRunId = input?.test_run_id || details?.test_run_id || null;
|
|
5517
5973
|
const verificationNotApplicable = input?.verification_not_applicable === true
|
|
5518
5974
|
|| details?.verification_not_applicable === true;
|
|
5519
5975
|
const verificationReason = String(
|
|
@@ -5524,33 +5980,35 @@ function normalizeStrictTaskUpdate(args, details, dataDir, change) {
|
|
|
5524
5980
|
|| '',
|
|
5525
5981
|
).trim();
|
|
5526
5982
|
|
|
5983
|
+
let resolvedTestEventId = testEventId;
|
|
5984
|
+
let resolvedTestRunId = testRunId || undefined;
|
|
5985
|
+
|
|
5527
5986
|
if (status === 'completed') {
|
|
5528
|
-
|
|
5529
|
-
|
|
5530
|
-
|
|
5531
|
-
|
|
5532
|
-
|
|
5533
|
-
|
|
5534
|
-
|
|
5535
|
-
|
|
5536
|
-
|
|
5537
|
-
|
|
5538
|
-
|
|
5539
|
-
|
|
5540
|
-
|
|
5541
|
-
|
|
5542
|
-
|
|
5543
|
-
const verifiedTaskIds = new Set([
|
|
5544
|
-
referenced.task_id,
|
|
5545
|
-
testResults.task_id,
|
|
5546
|
-
...(Array.isArray(testResults.verified_task_ids) ? testResults.verified_task_ids : []),
|
|
5547
|
-
].filter(Boolean));
|
|
5548
|
-
if (testResults.evidence_tier !== 'strict' || !verifiedTaskIds.has(taskId)) {
|
|
5549
|
-
fail(`E_TEST_IDENTITY_MISMATCH: test_result "${testEventId}" 未严格验证任务 "${taskId}"`);
|
|
5550
|
-
}
|
|
5987
|
+
const events = readEvents(dataDir, change);
|
|
5988
|
+
const resolution = resolveTaskVerificationReference(events, {
|
|
5989
|
+
task_id: taskId,
|
|
5990
|
+
change,
|
|
5991
|
+
test_event_id: testEventId,
|
|
5992
|
+
test_run_id: testRunId,
|
|
5993
|
+
verification_not_applicable: verificationNotApplicable,
|
|
5994
|
+
verification_not_applicable_reason: verificationReason,
|
|
5995
|
+
});
|
|
5996
|
+
|
|
5997
|
+
if (resolution.status === 'waived') {
|
|
5998
|
+
// ok
|
|
5999
|
+
} else if (resolution.status === 'verified') {
|
|
6000
|
+
resolvedTestEventId = resolution.resolved_event_id;
|
|
6001
|
+
if (testRunId) resolvedTestRunId = testRunId;
|
|
5551
6002
|
if (typeof input?.tdd_required !== 'boolean') {
|
|
5552
6003
|
fail('TASK_TDD_REQUIRED_MISSING: completed 任务引用测试时必须明确 tdd_required=true/false');
|
|
5553
6004
|
}
|
|
6005
|
+
} else if (resolution.status === 'ambiguous') {
|
|
6006
|
+
fail(`TASK_TEST_REFERENCE_AMBIGUOUS: test_run_id "${testRunId}" 对应多个严格测试候选,拒绝猜测`);
|
|
6007
|
+
} else if (resolution.status === 'missing') {
|
|
6008
|
+
fail('TASK_TEST_REFERENCE_MISSING: completed 任务必须引用 test_event_id 或唯一 test_run_id,或声明 verification_not_applicable=true 并说明原因');
|
|
6009
|
+
} else {
|
|
6010
|
+
const code = resolution.reason_code || 'TASK_TEST_REFERENCE_INVALID';
|
|
6011
|
+
fail(`${code}: 测试引用无法验证任务 "${taskId}"`);
|
|
5554
6012
|
}
|
|
5555
6013
|
}
|
|
5556
6014
|
|
|
@@ -5564,7 +6022,8 @@ function normalizeStrictTaskUpdate(args, details, dataDir, change) {
|
|
|
5564
6022
|
...(details.task_update || {}),
|
|
5565
6023
|
task_id: taskId,
|
|
5566
6024
|
status,
|
|
5567
|
-
test_event_id:
|
|
6025
|
+
test_event_id: resolvedTestEventId,
|
|
6026
|
+
test_run_id: resolvedTestRunId,
|
|
5568
6027
|
tdd_required: typeof input?.tdd_required === 'boolean' ? input.tdd_required : undefined,
|
|
5569
6028
|
test_requirement: input?.test_requirement
|
|
5570
6029
|
|| (verificationNotApplicable ? 'not_applicable' : 'required'),
|
|
@@ -5594,6 +6053,325 @@ function eventIdempotencyFingerprint(event) {
|
|
|
5594
6053
|
});
|
|
5595
6054
|
}
|
|
5596
6055
|
|
|
6056
|
+
function collectVerifiedTaskIds(event) {
|
|
6057
|
+
const testResults = event?.details?.test_results || {};
|
|
6058
|
+
return [...new Set([
|
|
6059
|
+
event?.task_id,
|
|
6060
|
+
testResults.task_id,
|
|
6061
|
+
...(Array.isArray(testResults.verified_task_ids) ? testResults.verified_task_ids : []),
|
|
6062
|
+
].filter(Boolean))];
|
|
6063
|
+
}
|
|
6064
|
+
|
|
6065
|
+
function isStrictCoveringTest(event, taskId) {
|
|
6066
|
+
const testResults = event?.details?.test_results || {};
|
|
6067
|
+
if (testResults.evidence_tier !== 'strict') return false;
|
|
6068
|
+
if (!isValidCompletionTestResults(testResults)) return false;
|
|
6069
|
+
return new Set(collectVerifiedTaskIds(event)).has(taskId);
|
|
6070
|
+
}
|
|
6071
|
+
|
|
6072
|
+
/**
|
|
6073
|
+
* Four-way task evidence audit (document / completion event / direct backlink / strict coverage).
|
|
6074
|
+
* Does not treat invalid direct backlinks as "no tests".
|
|
6075
|
+
*/
|
|
6076
|
+
function buildTaskEvidenceAudit(change, taskModel, events = [], options = {}) {
|
|
6077
|
+
const completedTasks = (taskModel?.primary_tasks?.tasks || []).filter(task => task.completed);
|
|
6078
|
+
const latestTaskUpdates = new Map();
|
|
6079
|
+
const testEvents = new Map();
|
|
6080
|
+
const strictByTask = new Map();
|
|
6081
|
+
|
|
6082
|
+
for (const event of events) {
|
|
6083
|
+
if (event.change && change && event.change !== change) continue;
|
|
6084
|
+
if (event.type === 'task_update'
|
|
6085
|
+
&& (event.status === 'completed' || event.details?.task_update?.status === 'completed')) {
|
|
6086
|
+
const existing = latestTaskUpdates.get(event.task_id);
|
|
6087
|
+
if (!existing || timestampMs(event) > timestampMs(existing)) {
|
|
6088
|
+
latestTaskUpdates.set(event.task_id, event);
|
|
6089
|
+
}
|
|
6090
|
+
continue;
|
|
6091
|
+
}
|
|
6092
|
+
if (event.type === 'test_result') {
|
|
6093
|
+
testEvents.set(event.event_id, event);
|
|
6094
|
+
for (const verifiedTaskId of collectVerifiedTaskIds(event)) {
|
|
6095
|
+
if (!isStrictCoveringTest(event, verifiedTaskId)) continue;
|
|
6096
|
+
const related = strictByTask.get(verifiedTaskId) || [];
|
|
6097
|
+
related.push(event);
|
|
6098
|
+
strictByTask.set(verifiedTaskId, related);
|
|
6099
|
+
}
|
|
6100
|
+
}
|
|
6101
|
+
}
|
|
6102
|
+
|
|
6103
|
+
let withUpdate = 0;
|
|
6104
|
+
let directVerified = 0;
|
|
6105
|
+
let strictVerified = 0;
|
|
6106
|
+
let waived = 0;
|
|
6107
|
+
const tasks = [];
|
|
6108
|
+
|
|
6109
|
+
for (const task of completedTasks) {
|
|
6110
|
+
const update = latestTaskUpdates.get(task.task_id);
|
|
6111
|
+
const updateDetails = update?.details?.task_update || update?.details || {};
|
|
6112
|
+
const notApplicable = updateDetails.verification_not_applicable === true
|
|
6113
|
+
&& Boolean(String(updateDetails.verification_not_applicable_reason || '').trim());
|
|
6114
|
+
const testEventId = updateDetails.test_event_id || null;
|
|
6115
|
+
const testEvent = testEventId ? testEvents.get(testEventId) : null;
|
|
6116
|
+
const directOk = Boolean(testEvent) && isStrictCoveringTest(testEvent, task.task_id);
|
|
6117
|
+
const strictOk = (strictByTask.get(task.task_id) || []).length > 0;
|
|
6118
|
+
|
|
6119
|
+
if (update) withUpdate += 1;
|
|
6120
|
+
if (directOk) directVerified += 1;
|
|
6121
|
+
if (strictOk) strictVerified += 1;
|
|
6122
|
+
if (notApplicable) waived += 1;
|
|
6123
|
+
|
|
6124
|
+
let backlinkStatus = 'missing';
|
|
6125
|
+
if (notApplicable) backlinkStatus = 'waived';
|
|
6126
|
+
else if (!testEventId) backlinkStatus = 'missing';
|
|
6127
|
+
else if (!testEvent) backlinkStatus = 'invalid';
|
|
6128
|
+
else if (!directOk) backlinkStatus = 'invalid';
|
|
6129
|
+
else backlinkStatus = 'verified';
|
|
6130
|
+
|
|
6131
|
+
tasks.push({
|
|
6132
|
+
task_id: task.task_id,
|
|
6133
|
+
has_completion_event: Boolean(update),
|
|
6134
|
+
direct_backlink_status: backlinkStatus,
|
|
6135
|
+
test_event_id: testEventId,
|
|
6136
|
+
strict_covered: strictOk,
|
|
6137
|
+
verification_not_applicable: notApplicable,
|
|
6138
|
+
});
|
|
6139
|
+
}
|
|
6140
|
+
|
|
6141
|
+
return {
|
|
6142
|
+
mode: options.mode === 'report-only' ? 'report-only' : 'strict',
|
|
6143
|
+
document_completion: {
|
|
6144
|
+
completed: completedTasks.length,
|
|
6145
|
+
total: taskModel?.primary_tasks?.total ?? completedTasks.length,
|
|
6146
|
+
source: 'tasks-document',
|
|
6147
|
+
},
|
|
6148
|
+
completion_event_coverage: {
|
|
6149
|
+
verified: withUpdate,
|
|
6150
|
+
total: completedTasks.length,
|
|
6151
|
+
},
|
|
6152
|
+
direct_test_backlink: {
|
|
6153
|
+
verified: directVerified,
|
|
6154
|
+
total: completedTasks.length,
|
|
6155
|
+
},
|
|
6156
|
+
strict_test_coverage: {
|
|
6157
|
+
verified: strictVerified,
|
|
6158
|
+
total: completedTasks.length,
|
|
6159
|
+
},
|
|
6160
|
+
verification_waivers: {
|
|
6161
|
+
count: waived,
|
|
6162
|
+
total: completedTasks.length,
|
|
6163
|
+
},
|
|
6164
|
+
tasks,
|
|
6165
|
+
};
|
|
6166
|
+
}
|
|
6167
|
+
|
|
6168
|
+
function extractTddPairNumber(text) {
|
|
6169
|
+
if (!text) return null;
|
|
6170
|
+
const red = String(text).match(/\bRED-(\d+)\b/i) || String(text).match(/red(\d+)/i);
|
|
6171
|
+
if (red) return { role: 'red', pair: Number(red[1]) };
|
|
6172
|
+
const green = String(text).match(/\bGREEN-(\d+)\b/i) || String(text).match(/green(\d+)/i);
|
|
6173
|
+
if (green) return { role: 'green', pair: Number(green[1]) };
|
|
6174
|
+
return null;
|
|
6175
|
+
}
|
|
6176
|
+
|
|
6177
|
+
function buildTddPairDefinitions(taskModel, events = [], options = {}) {
|
|
6178
|
+
if (Array.isArray(options.pair_definitions) && options.pair_definitions.length > 0) {
|
|
6179
|
+
return options.pair_definitions.map((item) => ({
|
|
6180
|
+
pair_id: item.pair_id || `pair-${item.pair_number}`,
|
|
6181
|
+
pair_number: Number(item.pair_number),
|
|
6182
|
+
red_task_id: item.red_task_id || null,
|
|
6183
|
+
green_task_id: item.green_task_id || null,
|
|
6184
|
+
}));
|
|
6185
|
+
}
|
|
6186
|
+
|
|
6187
|
+
const byNumber = new Map();
|
|
6188
|
+
const ensure = (num) => {
|
|
6189
|
+
if (!byNumber.has(num)) {
|
|
6190
|
+
byNumber.set(num, {
|
|
6191
|
+
pair_id: `pair-${num}`,
|
|
6192
|
+
pair_number: num,
|
|
6193
|
+
red_task_id: null,
|
|
6194
|
+
green_task_id: null,
|
|
6195
|
+
});
|
|
6196
|
+
}
|
|
6197
|
+
return byNumber.get(num);
|
|
6198
|
+
};
|
|
6199
|
+
|
|
6200
|
+
for (const task of (taskModel?.primary_tasks?.tasks || [])) {
|
|
6201
|
+
const hint = extractTddPairNumber(task.title || task.summary || task.task_id);
|
|
6202
|
+
if (!hint || !Number.isFinite(hint.pair)) continue;
|
|
6203
|
+
const entry = ensure(hint.pair);
|
|
6204
|
+
if (hint.role === 'red') entry.red_task_id = task.task_id;
|
|
6205
|
+
if (hint.role === 'green') entry.green_task_id = task.task_id;
|
|
6206
|
+
}
|
|
6207
|
+
|
|
6208
|
+
for (const event of events) {
|
|
6209
|
+
if (event.type !== 'test_result') continue;
|
|
6210
|
+
const explicit = event.details?.test_results?.tdd_pair_id
|
|
6211
|
+
|| event.details?.tdd_pair_id;
|
|
6212
|
+
if (explicit) {
|
|
6213
|
+
const numMatch = String(explicit).match(/(\d+)/);
|
|
6214
|
+
const num = numMatch ? Number(numMatch[1]) : null;
|
|
6215
|
+
if (!num) continue;
|
|
6216
|
+
const entry = ensure(num);
|
|
6217
|
+
const role = event.details?.test_results?.tdd_role
|
|
6218
|
+
|| event.details?.test_results?.tdd_phase;
|
|
6219
|
+
if (role === 'red') entry.red_task_id = entry.red_task_id || event.task_id;
|
|
6220
|
+
if (role === 'green' || role === 'regression') {
|
|
6221
|
+
entry.green_task_id = entry.green_task_id || event.task_id;
|
|
6222
|
+
}
|
|
6223
|
+
continue;
|
|
6224
|
+
}
|
|
6225
|
+
const fromRun = extractTddPairNumber(event.run_id);
|
|
6226
|
+
const fromSummary = extractTddPairNumber(event.summary);
|
|
6227
|
+
const hint = fromRun || fromSummary || extractTddPairNumber(event.details?.test_results?.tdd_phase);
|
|
6228
|
+
if (!hint) continue;
|
|
6229
|
+
const entry = ensure(hint.pair);
|
|
6230
|
+
const phase = event.details?.test_results?.tdd_phase;
|
|
6231
|
+
if (hint.role === 'red' || phase === 'red') {
|
|
6232
|
+
entry.red_task_id = entry.red_task_id || event.task_id;
|
|
6233
|
+
}
|
|
6234
|
+
if (hint.role === 'green' || phase === 'green' || phase === 'regression') {
|
|
6235
|
+
// Prefer dedicated green task over regression umbrella task when already set.
|
|
6236
|
+
if (!entry.green_task_id || phase === 'green') {
|
|
6237
|
+
if (phase === 'green' || hint.role === 'green') {
|
|
6238
|
+
entry.green_task_id = entry.green_task_id || event.task_id;
|
|
6239
|
+
}
|
|
6240
|
+
}
|
|
6241
|
+
}
|
|
6242
|
+
}
|
|
6243
|
+
|
|
6244
|
+
// 兼容没有 RED-n/GREEN-n 编号、但 task_update 明确声明 tdd_required 的
|
|
6245
|
+
// 同任务链。仅在完全没有显式/可推导 pair 时启用,避免与编号 pair 重复计数。
|
|
6246
|
+
if (byNumber.size === 0) {
|
|
6247
|
+
const implicitTaskIds = [...new Set(events
|
|
6248
|
+
.filter(event => (
|
|
6249
|
+
event.type === 'task_update'
|
|
6250
|
+
&& event.task_id
|
|
6251
|
+
&& event.details?.task_update?.tdd_required === true
|
|
6252
|
+
))
|
|
6253
|
+
.map(event => event.task_id))];
|
|
6254
|
+
return implicitTaskIds.map((taskId, index) => ({
|
|
6255
|
+
pair_id: `task-pair:${taskId}`,
|
|
6256
|
+
pair_number: index + 1,
|
|
6257
|
+
red_task_id: taskId,
|
|
6258
|
+
green_task_id: taskId,
|
|
6259
|
+
}));
|
|
6260
|
+
}
|
|
6261
|
+
|
|
6262
|
+
return [...byNumber.values()].sort((a, b) => a.pair_number - b.pair_number);
|
|
6263
|
+
}
|
|
6264
|
+
|
|
6265
|
+
/**
|
|
6266
|
+
* Pair-level TDD audit. Historical fallback uses RED-n/GREEN-n labels and run_ids.
|
|
6267
|
+
*/
|
|
6268
|
+
function buildTddPairAudit(change, taskModel, events = [], options = {}) {
|
|
6269
|
+
const scopedEvents = events.filter(event => (
|
|
6270
|
+
!change
|
|
6271
|
+
|| event.change === change
|
|
6272
|
+
|| event.attribution?.target_change === change
|
|
6273
|
+
));
|
|
6274
|
+
const definitions = buildTddPairDefinitions(taskModel, scopedEvents, options);
|
|
6275
|
+
const testsByTask = new Map();
|
|
6276
|
+
|
|
6277
|
+
for (const event of scopedEvents) {
|
|
6278
|
+
if (event.type !== 'test_result') continue;
|
|
6279
|
+
for (const taskId of collectVerifiedTaskIds(event)) {
|
|
6280
|
+
const related = testsByTask.get(taskId) || [];
|
|
6281
|
+
related.push(event);
|
|
6282
|
+
testsByTask.set(taskId, related);
|
|
6283
|
+
}
|
|
6284
|
+
if (event.task_id) {
|
|
6285
|
+
const related = testsByTask.get(event.task_id) || [];
|
|
6286
|
+
if (!related.includes(event)) related.push(event);
|
|
6287
|
+
testsByTask.set(event.task_id, related);
|
|
6288
|
+
}
|
|
6289
|
+
}
|
|
6290
|
+
for (const related of testsByTask.values()) {
|
|
6291
|
+
related.sort((a, b) => {
|
|
6292
|
+
const delta = timestampMs(a) - timestampMs(b);
|
|
6293
|
+
if (delta !== 0) return delta;
|
|
6294
|
+
return String(a.event_id || '').localeCompare(String(b.event_id || ''));
|
|
6295
|
+
});
|
|
6296
|
+
}
|
|
6297
|
+
|
|
6298
|
+
const pairs = [];
|
|
6299
|
+
let complete = 0;
|
|
6300
|
+
|
|
6301
|
+
for (const def of definitions) {
|
|
6302
|
+
const redEvents = (testsByTask.get(def.red_task_id) || [])
|
|
6303
|
+
.filter(event => event.details?.test_results?.tdd_phase === 'red');
|
|
6304
|
+
const greenOrRegression = [
|
|
6305
|
+
...(testsByTask.get(def.green_task_id) || []),
|
|
6306
|
+
...(testsByTask.get(def.red_task_id) || []),
|
|
6307
|
+
].filter((event, index, arr) => arr.indexOf(event) === index)
|
|
6308
|
+
.filter(event => ['green', 'regression'].includes(event.details?.test_results?.tdd_phase)
|
|
6309
|
+
&& event.details?.test_results?.exit_code === 0);
|
|
6310
|
+
|
|
6311
|
+
let status = 'unclassified';
|
|
6312
|
+
if (redEvents.length === 0 && def.green_task_id) {
|
|
6313
|
+
const hasGreenSide = greenOrRegression.some(event => collectVerifiedTaskIds(event).includes(def.green_task_id)
|
|
6314
|
+
|| event.task_id === def.green_task_id);
|
|
6315
|
+
status = hasGreenSide || def.red_task_id ? 'red-missing' : 'unclassified';
|
|
6316
|
+
if (redEvents.length === 0) status = 'red-missing';
|
|
6317
|
+
}
|
|
6318
|
+
|
|
6319
|
+
if (redEvents.length > 0) {
|
|
6320
|
+
const firstRed = redEvents.find((event) => {
|
|
6321
|
+
const results = event.details?.test_results || {};
|
|
6322
|
+
return results.valid_red === true
|
|
6323
|
+
&& results.exit_code !== 0
|
|
6324
|
+
&& Number(results.failed || 0) > 0;
|
|
6325
|
+
});
|
|
6326
|
+
if (!firstRed) {
|
|
6327
|
+
status = 'red-did-not-fail';
|
|
6328
|
+
} else {
|
|
6329
|
+
const redTs = timestampMs(firstRed);
|
|
6330
|
+
const greenAfter = greenOrRegression.find(event => (
|
|
6331
|
+
timestampMs(event) > redTs
|
|
6332
|
+
&& (
|
|
6333
|
+
!def.green_task_id
|
|
6334
|
+
|| collectVerifiedTaskIds(event).includes(def.green_task_id)
|
|
6335
|
+
|| event.task_id === def.green_task_id
|
|
6336
|
+
|| event.details?.test_results?.tdd_phase === 'green'
|
|
6337
|
+
)
|
|
6338
|
+
));
|
|
6339
|
+
// Accept later regression that covers the green task id.
|
|
6340
|
+
const regressionCover = greenOrRegression.find(event => (
|
|
6341
|
+
timestampMs(event) > redTs
|
|
6342
|
+
&& def.green_task_id
|
|
6343
|
+
&& collectVerifiedTaskIds(event).includes(def.green_task_id)
|
|
6344
|
+
));
|
|
6345
|
+
if (greenAfter || regressionCover) {
|
|
6346
|
+
status = 'complete';
|
|
6347
|
+
complete += 1;
|
|
6348
|
+
} else {
|
|
6349
|
+
status = 'green-missing';
|
|
6350
|
+
}
|
|
6351
|
+
}
|
|
6352
|
+
} else if (!def.red_task_id && def.green_task_id) {
|
|
6353
|
+
status = 'red-missing';
|
|
6354
|
+
} else if (def.red_task_id && redEvents.length === 0) {
|
|
6355
|
+
status = 'red-missing';
|
|
6356
|
+
}
|
|
6357
|
+
|
|
6358
|
+
pairs.push({
|
|
6359
|
+
pair_id: def.pair_id,
|
|
6360
|
+
pair_number: def.pair_number,
|
|
6361
|
+
red_task_id: def.red_task_id,
|
|
6362
|
+
green_task_id: def.green_task_id,
|
|
6363
|
+
status,
|
|
6364
|
+
});
|
|
6365
|
+
}
|
|
6366
|
+
|
|
6367
|
+
return {
|
|
6368
|
+
mode: options.mode === 'report-only' ? 'report-only' : 'strict',
|
|
6369
|
+
required: pairs.length,
|
|
6370
|
+
complete,
|
|
6371
|
+
pairs,
|
|
6372
|
+
};
|
|
6373
|
+
}
|
|
6374
|
+
|
|
5597
6375
|
function buildApplyEvidenceAudit(change, taskModel, events = [], options = {}) {
|
|
5598
6376
|
const strict = options.mode !== 'report-only';
|
|
5599
6377
|
const completedTasks = (taskModel?.primary_tasks?.tasks || []).filter(task => task.completed);
|
|
@@ -5644,6 +6422,12 @@ function buildApplyEvidenceAudit(change, taskModel, events = [], options = {}) {
|
|
|
5644
6422
|
severity: strict ? 'error' : 'warning',
|
|
5645
6423
|
task_id: task.task_id,
|
|
5646
6424
|
message: '主任务已标完成,但缺少 completed task_update 事件',
|
|
6425
|
+
verdict: 'missing-task-update',
|
|
6426
|
+
expected: '已完成主任务应存在同一变更下的 completed task_update 事件。',
|
|
6427
|
+
actual: '任务文档标记已完成,但事件流中没有对应 completed task_update。',
|
|
6428
|
+
root_cause: '任务完成状态只写入了 tasks 文档,没有同步记录结构化 task_update 事件。',
|
|
6429
|
+
impact: '任务完成状态无法与测试事件建立直接追溯链。',
|
|
6430
|
+
remediation: '为该任务补录真实 completed task_update;若历史证据不存在,应明确标记为无法追溯。',
|
|
5647
6431
|
});
|
|
5648
6432
|
continue;
|
|
5649
6433
|
}
|
|
@@ -5662,39 +6446,52 @@ function buildApplyEvidenceAudit(change, taskModel, events = [], options = {}) {
|
|
|
5662
6446
|
&& testResults.evidence_tier === 'strict'
|
|
5663
6447
|
&& verifiedTaskIds.has(task.task_id)
|
|
5664
6448
|
&& isValidCompletionTestResults(testResults);
|
|
6449
|
+
const strictCovered = (testsByTask.get(task.task_id) || [])
|
|
6450
|
+
.some(event => isStrictCoveringTest(event, task.task_id));
|
|
5665
6451
|
if (validTest || notApplicable) {
|
|
5666
6452
|
withTestEvidence += 1;
|
|
6453
|
+
} else if (strictCovered) {
|
|
6454
|
+
findings.push({
|
|
6455
|
+
code: 'TASK_TEST_LINKAGE_DEFECT',
|
|
6456
|
+
severity: 'warning',
|
|
6457
|
+
task_id: task.task_id,
|
|
6458
|
+
message: '任务存在严格测试覆盖,但 completed task_update 的直接引用无效或缺失(链路缺陷,不是缺测试)',
|
|
6459
|
+
verdict: 'linkage-defect',
|
|
6460
|
+
expected: 'completed task_update 应通过 test_event_id 精确引用覆盖该任务的严格成功测试。',
|
|
6461
|
+
actual: '存在严格测试覆盖,但 task_update 没有有效的直接测试引用。',
|
|
6462
|
+
root_cause: 'task_update 的 test_event_id 缺失、无效,或引用事件没有明确覆盖当前任务。',
|
|
6463
|
+
impact: '测试本身存在,但任务到测试的直接审计链不完整。',
|
|
6464
|
+
remediation: '修正 task_update 的 test_event_id,使其指向覆盖当前任务的 strict 成功测试事件。',
|
|
6465
|
+
});
|
|
5667
6466
|
} else {
|
|
5668
6467
|
findings.push({
|
|
5669
6468
|
code: 'TASK_TEST_EVIDENCE_MISSING',
|
|
5670
6469
|
severity: strict ? 'error' : 'warning',
|
|
5671
6470
|
task_id: task.task_id,
|
|
5672
6471
|
message: 'completed task_update 没有有效测试引用或不适用理由',
|
|
6472
|
+
verdict: 'missing-test-evidence',
|
|
6473
|
+
expected: 'completed task_update 应引用严格成功测试,或提供明确且非空的不适用理由。',
|
|
6474
|
+
actual: '没有有效直接引用,也没有发现可间接覆盖该任务的严格成功测试。',
|
|
6475
|
+
root_cause: '任务完成事件缺少测试证据,且未声明可审计的不适用理由。',
|
|
6476
|
+
impact: '无法验证该任务完成状态是否经过测试。',
|
|
6477
|
+
remediation: '执行覆盖该任务的严格测试并记录引用;确实不适用时填写具体原因。',
|
|
5673
6478
|
});
|
|
5674
6479
|
}
|
|
5675
6480
|
|
|
5676
6481
|
if (updateDetails.tdd_required === true) {
|
|
5677
6482
|
tddRequired += 1;
|
|
5678
|
-
const related = testsByTask.get(task.task_id) || [];
|
|
5679
|
-
const redIndex = related.findIndex(event => event.details?.test_results?.valid_red === true);
|
|
5680
|
-
const greenIndex = related.findIndex((event, index) => (
|
|
5681
|
-
index > redIndex
|
|
5682
|
-
&& event.details?.test_results?.exit_code === 0
|
|
5683
|
-
&& ['green', 'regression'].includes(event.details?.test_results?.tdd_phase)
|
|
5684
|
-
));
|
|
5685
|
-
if (redIndex >= 0 && greenIndex > redIndex) {
|
|
5686
|
-
redGreenComplete += 1;
|
|
5687
|
-
} else {
|
|
5688
|
-
findings.push({
|
|
5689
|
-
code: 'TDD_RED_GREEN_INCOMPLETE',
|
|
5690
|
-
severity: strict ? 'error' : 'warning',
|
|
5691
|
-
task_id: task.task_id,
|
|
5692
|
-
message: 'TDD 任务缺少有效 RED→GREEN 顺序证据',
|
|
5693
|
-
});
|
|
5694
|
-
}
|
|
5695
6483
|
}
|
|
5696
6484
|
}
|
|
5697
6485
|
|
|
6486
|
+
const pairAudit = buildTddPairAudit(change, taskModel, events, options);
|
|
6487
|
+
tddRequired = pairAudit.required;
|
|
6488
|
+
redGreenComplete = pairAudit.complete;
|
|
6489
|
+
if (strict) {
|
|
6490
|
+
findings.push(...pairAudit.pairs
|
|
6491
|
+
.filter(pair => pair.status !== 'complete')
|
|
6492
|
+
.map(pair => ({ ...tddPairFinding(pair), severity: 'error' })));
|
|
6493
|
+
}
|
|
6494
|
+
|
|
5698
6495
|
return {
|
|
5699
6496
|
mode: strict ? 'strict' : 'report-only',
|
|
5700
6497
|
status: findings.some(finding => finding.severity === 'error') ? 'blocked' : 'pass',
|
|
@@ -6788,15 +7585,40 @@ function cmdArchiveDocs(args) {
|
|
|
6788
7585
|
}
|
|
6789
7586
|
|
|
6790
7587
|
const archiveDir = path.resolve(result.archive_path);
|
|
6791
|
-
|
|
6792
|
-
|
|
6793
|
-
|
|
6794
|
-
|
|
6795
|
-
|
|
6796
|
-
|
|
6797
|
-
|
|
6798
|
-
|
|
6799
|
-
|
|
7588
|
+
let finalPackage;
|
|
7589
|
+
if (stageEnd) {
|
|
7590
|
+
// cmdEnd 已完成报告/Manifest/ZIP 的收敛封装;此处只读取终态,禁止再次
|
|
7591
|
+
// 物化导致报告中记录的 Manifest/ZIP 大小失真。
|
|
7592
|
+
const finalEvent = readAllEventEndsById(
|
|
7593
|
+
getDataDir(projectRoot),
|
|
7594
|
+
stageEnd.event_id,
|
|
7595
|
+
changeName,
|
|
7596
|
+
).sort((a, b) => timestampMs(b) - timestampMs(a))[0];
|
|
7597
|
+
const finalArchiveResult = finalEvent?.details?.archive_result || {};
|
|
7598
|
+
const manifestPath = path.isAbsolute(finalArchiveResult.manifest_path || '')
|
|
7599
|
+
? finalArchiveResult.manifest_path
|
|
7600
|
+
: path.resolve(projectRoot, finalArchiveResult.manifest_path || path.join(archiveDir, 'archive-manifest.json'));
|
|
7601
|
+
const packagePath = path.isAbsolute(finalArchiveResult.package_path || '')
|
|
7602
|
+
? finalArchiveResult.package_path
|
|
7603
|
+
: path.resolve(projectRoot, finalArchiveResult.package_path || `${archiveDir}.zip`);
|
|
7604
|
+
finalPackage = {
|
|
7605
|
+
manifest: JSON.parse(fs.readFileSync(manifestPath, 'utf8')),
|
|
7606
|
+
manifest_path: manifestPath,
|
|
7607
|
+
canonical_facts_path: path.resolve(archiveDir, 'canonical-facts.json'),
|
|
7608
|
+
conversion_report_path: path.resolve(archiveDir, 'conversion-report.json'),
|
|
7609
|
+
package_path: packagePath,
|
|
7610
|
+
};
|
|
7611
|
+
} else {
|
|
7612
|
+
finalPackage = ensureArchiveManifest(projectRoot, changeName, archiveDir, {
|
|
7613
|
+
reason: result.reason || args.reason || '',
|
|
7614
|
+
method: result.method || 'skywalk-full-spec-archive',
|
|
7615
|
+
archivePath: archiveDir,
|
|
7616
|
+
archivedAt: result.created_at || result.archived_at,
|
|
7617
|
+
copiedSpecs: result.copied_specs,
|
|
7618
|
+
finalizeAfterReports: true,
|
|
7619
|
+
runIdentity: `archive-docs:${changeName}`,
|
|
7620
|
+
});
|
|
7621
|
+
}
|
|
6800
7622
|
result = {
|
|
6801
7623
|
...result,
|
|
6802
7624
|
schema_version: finalPackage.manifest.schema_version,
|
|
@@ -6994,7 +7816,12 @@ module.exports = {
|
|
|
6994
7816
|
formatDuration,
|
|
6995
7817
|
formatReworkReasons,
|
|
6996
7818
|
buildReport,
|
|
7819
|
+
finalizeArtifactInventory,
|
|
6997
7820
|
buildApplyEvidenceAudit,
|
|
7821
|
+
buildTaskEvidenceAudit,
|
|
7822
|
+
buildTddPairAudit,
|
|
7823
|
+
resolveTaskVerificationReference,
|
|
7824
|
+
extractChangedFiles,
|
|
6998
7825
|
computeGitDocumentMetrics,
|
|
6999
7826
|
getCheckResults,
|
|
7000
7827
|
getBuildResults,
|