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
package/skywalk-sdd/index.cjs
CHANGED
|
@@ -17,6 +17,16 @@ const { resolveProjectArg } = require('./ontology/resolve-spec-root.cjs');
|
|
|
17
17
|
const crypto = require('crypto');
|
|
18
18
|
const { execFileSync } = require('child_process');
|
|
19
19
|
const metricsV3 = require('./metrics-v3.cjs');
|
|
20
|
+
const {
|
|
21
|
+
parseArgs,
|
|
22
|
+
ensureDir: sharedEnsureDir,
|
|
23
|
+
writeTextAtomic,
|
|
24
|
+
copyDirRecursive,
|
|
25
|
+
readJson: sharedReadJson,
|
|
26
|
+
stripBom,
|
|
27
|
+
today: sharedToday,
|
|
28
|
+
nowISO: sharedNowISO,
|
|
29
|
+
} = require('./lib/shared.cjs');
|
|
20
30
|
|
|
21
31
|
const SCHEMA_VERSION = 2;
|
|
22
32
|
|
|
@@ -58,11 +68,8 @@ const MEASUREMENT_STAGES = ['propose', 'spec', 'design', 'task', 'check', 'apply
|
|
|
58
68
|
const AUXILIARY_STAGES = ['test', 'explore'];
|
|
59
69
|
|
|
60
70
|
// ── 工具函数 ──────────────────────────────────────────
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
fs.mkdirSync(dirPath, { recursive: true });
|
|
64
|
-
}
|
|
65
|
-
}
|
|
71
|
+
// ensureDir 已迁移至 lib/shared.cjs
|
|
72
|
+
const ensureDir = sharedEnsureDir;
|
|
66
73
|
|
|
67
74
|
function generateEventId() {
|
|
68
75
|
return 'evt_' + crypto.randomBytes(6).toString('hex');
|
|
@@ -99,7 +106,7 @@ function readJsonFile(filePath, projectRoot) {
|
|
|
99
106
|
const resolved = path.isAbsolute(filePath)
|
|
100
107
|
? filePath
|
|
101
108
|
: path.resolve(projectRoot || process.cwd(), filePath);
|
|
102
|
-
return
|
|
109
|
+
return sharedReadJson(resolved);
|
|
103
110
|
}
|
|
104
111
|
|
|
105
112
|
function parseJsonOption(args, inlineKey, fileKey, projectRoot, fallback = {}) {
|
|
@@ -152,25 +159,8 @@ function safeChangeName(name) {
|
|
|
152
159
|
return name.replace(/[^a-zA-Z0-9_-]/g, '-').replace(/^-+|-+$/g, '') || 'general';
|
|
153
160
|
}
|
|
154
161
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
const tempPath = `${filePath}.${process.pid}.${crypto.randomBytes(4).toString('hex')}.tmp`;
|
|
158
|
-
let fd;
|
|
159
|
-
try {
|
|
160
|
-
fd = fs.openSync(tempPath, 'w');
|
|
161
|
-
fs.writeFileSync(fd, content, 'utf8');
|
|
162
|
-
fs.fsyncSync(fd);
|
|
163
|
-
fs.closeSync(fd);
|
|
164
|
-
fd = null;
|
|
165
|
-
fs.renameSync(tempPath, filePath);
|
|
166
|
-
} catch (error) {
|
|
167
|
-
if (fd != null) {
|
|
168
|
-
try { fs.closeSync(fd); } catch {}
|
|
169
|
-
}
|
|
170
|
-
try { fs.rmSync(tempPath, { force: true }); } catch {}
|
|
171
|
-
throw error;
|
|
172
|
-
}
|
|
173
|
-
}
|
|
162
|
+
// atomicWriteText 已迁移至 lib/shared.cjs
|
|
163
|
+
const atomicWriteText = writeTextAtomic;
|
|
174
164
|
|
|
175
165
|
function requireValidChangeName(name) {
|
|
176
166
|
if (typeof name === 'string'
|
|
@@ -320,19 +310,8 @@ function walkFiles(dirPath, predicate, files = []) {
|
|
|
320
310
|
return files;
|
|
321
311
|
}
|
|
322
312
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
for (const entry of fs.readdirSync(sourceDir, { withFileTypes: true })) {
|
|
326
|
-
const sourcePath = path.join(sourceDir, entry.name);
|
|
327
|
-
const targetPath = path.join(targetDir, entry.name);
|
|
328
|
-
if (entry.isDirectory()) {
|
|
329
|
-
copyDirSync(sourcePath, targetPath);
|
|
330
|
-
} else {
|
|
331
|
-
ensureDir(path.dirname(targetPath));
|
|
332
|
-
fs.copyFileSync(sourcePath, targetPath);
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
}
|
|
313
|
+
// copyDirSync 已迁移至 lib/shared.cjs(copyDirRecursive)
|
|
314
|
+
const copyDirSync = copyDirRecursive;
|
|
336
315
|
|
|
337
316
|
function nextAvailableDir(baseDir, preferredName) {
|
|
338
317
|
let candidate = path.join(baseDir, preferredName);
|
|
@@ -849,9 +828,10 @@ function countSpecScenarios(projectRoot, changeName) {
|
|
|
849
828
|
seen.add(abs);
|
|
850
829
|
let text = '';
|
|
851
830
|
try { text = fs.readFileSync(abs, 'utf8'); } catch { text = ''; }
|
|
852
|
-
const
|
|
831
|
+
const scenarios = metricsV3.extractScenariosInMarkdown(text);
|
|
832
|
+
const count = scenarios.length;
|
|
853
833
|
total += count;
|
|
854
|
-
files.push({ path: toProjectRelative(projectRoot, abs), count });
|
|
834
|
+
files.push({ path: toProjectRelative(projectRoot, abs), count, scenarios });
|
|
855
835
|
}
|
|
856
836
|
return { scenario_count: total, files, source, root: toProjectRelative(projectRoot, root) };
|
|
857
837
|
}
|
|
@@ -893,7 +873,9 @@ function buildV3ChangeContext(events, executionSummary, options = {}) {
|
|
|
893
873
|
return {
|
|
894
874
|
change_name: options.change_name || null,
|
|
895
875
|
change_type: options.change_type || 'unknown',
|
|
896
|
-
total_stage_duration_ms: options
|
|
876
|
+
total_stage_duration_ms: Object.prototype.hasOwnProperty.call(options, 'total_stage_duration_ms')
|
|
877
|
+
? options.total_stage_duration_ms
|
|
878
|
+
: (summary.total_stage_duration_ms ?? null),
|
|
897
879
|
canonical_stage_duration_ms: options.canonical_stage_duration_ms ?? summary.effective_stage_duration_ms ?? null,
|
|
898
880
|
rework_stage_duration_ms: options.rework_stage_duration_ms ?? summary.rework_stage_duration_ms ?? null,
|
|
899
881
|
scenario_count: options.scenario_count ?? 0,
|
|
@@ -1393,30 +1375,35 @@ function appendEventUnlocked(dataDir, changeName, event) {
|
|
|
1393
1375
|
appendMarkdownLog(dataDir, changeName, event);
|
|
1394
1376
|
}
|
|
1395
1377
|
|
|
1396
|
-
/** 追加一行 JSONL 到事件文件;同一 change 的事实写入和日志投影共享跨进程锁。
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
}
|
|
1378
|
+
/** 追加一行 JSONL 到事件文件;同一 change 的事实写入和日志投影共享跨进程锁。
|
|
1379
|
+
* kld-T06:Progress Runtime 已删除,appendEvent 仅负责本地持久化,不再做事件富化、
|
|
1380
|
+
* agent 拉起、metadata refresh 或 artifact snapshot。
|
|
1381
|
+
*/
|
|
1382
|
+
function appendEvent(dataDir, changeName, event, options = {}) {
|
|
1383
|
+
if (options.lockHeld) {
|
|
1384
|
+
appendEventUnlocked(dataDir, changeName, event);
|
|
1385
|
+
return { idempotent: false, event };
|
|
1386
|
+
}
|
|
1387
|
+
const result = withEventWriteLock(dataDir, changeName, () => {
|
|
1388
|
+
if (event?.type === 'stage_end') {
|
|
1389
|
+
const existing = readAllEventEndsById(dataDir, event.event_id, changeName);
|
|
1390
|
+
if (existing.length > 0) {
|
|
1391
|
+
const latest = existing.sort((a, b) => timestampMs(b) - timestampMs(a))[0];
|
|
1392
|
+
if (latest.result !== event.result) {
|
|
1393
|
+
const error = new Error(
|
|
1394
|
+
`E_STAGE_END_CONFLICT: event_id "${event.event_id}" 已记录 result=${latest.result},不能改为 ${event.result}`,
|
|
1395
|
+
);
|
|
1396
|
+
error.code = 'E_STAGE_END_CONFLICT';
|
|
1397
|
+
throw error;
|
|
1398
|
+
}
|
|
1399
|
+
return { idempotent: true, event: latest };
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
appendEventUnlocked(dataDir, changeName, event);
|
|
1403
|
+
return { idempotent: false, event };
|
|
1404
|
+
});
|
|
1405
|
+
return result;
|
|
1406
|
+
}
|
|
1420
1407
|
|
|
1421
1408
|
/** 把 ISO 时间戳格式化为本地时区 `YYYY-MM-DD HH:mm:ss (UTC±HH:MM)`。
|
|
1422
1409
|
* execution-log 面向人读,统一用本地时区 + 显式偏移标注,避免 UTC 裸值与用户体感差 8 小时(N9)。 */
|
|
@@ -1905,7 +1892,9 @@ function summarizeStageExecutions(events) {
|
|
|
1905
1892
|
start,
|
|
1906
1893
|
end,
|
|
1907
1894
|
result: end?.result || null,
|
|
1908
|
-
duration_ms: Number.isFinite(end?.duration_ms)
|
|
1895
|
+
duration_ms: Number.isFinite(end?.duration_ms) && end.duration_ms >= 0
|
|
1896
|
+
? end.duration_ms
|
|
1897
|
+
: null,
|
|
1909
1898
|
canonical: false,
|
|
1910
1899
|
rework_reason: null,
|
|
1911
1900
|
};
|
|
@@ -1935,7 +1924,7 @@ function summarizeStageExecutions(events) {
|
|
|
1935
1924
|
if (canonical) {
|
|
1936
1925
|
canonical.canonical = true;
|
|
1937
1926
|
canonicalAttempts.push(canonical);
|
|
1938
|
-
effectiveStageDurationMs += canonical.duration_ms;
|
|
1927
|
+
effectiveStageDurationMs += Number.isFinite(canonical.duration_ms) ? canonical.duration_ms : 0;
|
|
1939
1928
|
}
|
|
1940
1929
|
|
|
1941
1930
|
for (const attempt of groupAttempts) {
|
|
@@ -1967,8 +1956,9 @@ function summarizeStageExecutions(events) {
|
|
|
1967
1956
|
if (attempt.end) {
|
|
1968
1957
|
completedReworkAttempts += 1;
|
|
1969
1958
|
bucket.completed_rework_attempts += 1;
|
|
1970
|
-
|
|
1971
|
-
|
|
1959
|
+
const reworkDurationMs = Number.isFinite(attempt.duration_ms) ? attempt.duration_ms : 0;
|
|
1960
|
+
bucket.rework_duration_ms += reworkDurationMs;
|
|
1961
|
+
reworkStageDurationMs += reworkDurationMs;
|
|
1972
1962
|
} else {
|
|
1973
1963
|
supersededOpenStages += 1;
|
|
1974
1964
|
bucket.superseded_open_stages += 1;
|
|
@@ -2250,9 +2240,14 @@ function getCheckResults(event) {
|
|
|
2250
2240
|
categories: results.categories || {},
|
|
2251
2241
|
// V3 Q3: reviewer 元信息,用于评估 reviewer_independence/score_status(缺失时保持 null,不影响旧字段)
|
|
2252
2242
|
reviewer: results.reviewer || null,
|
|
2243
|
+
review_execution_mode: results.review_execution_mode || null,
|
|
2244
|
+
reviewer_agent_id: results.reviewer_agent_id || null,
|
|
2253
2245
|
review_session_id: results.review_session_id || null,
|
|
2246
|
+
reviewer_session_id: results.reviewer_session_id || null,
|
|
2254
2247
|
author_session_id: results.author_session_id || null,
|
|
2255
2248
|
reviewer_independence: results.reviewer_independence || null,
|
|
2249
|
+
fallback_reason_code: results.fallback_reason_code || null,
|
|
2250
|
+
fallback_reason: results.fallback_reason || null,
|
|
2256
2251
|
warning_items: Array.isArray(results.warning_items) ? results.warning_items : [],
|
|
2257
2252
|
warning_dispositions: Array.isArray(results.warning_dispositions) ? results.warning_dispositions : [],
|
|
2258
2253
|
};
|
|
@@ -2967,7 +2962,20 @@ function computeSinglePdfMvpMetrics(events, options = {}) {
|
|
|
2967
2962
|
? new Date(archiveEnd.timestamp).getTime() - new Date(proposeStart.timestamp).getTime()
|
|
2968
2963
|
: null;
|
|
2969
2964
|
|
|
2970
|
-
|
|
2965
|
+
// E1 只在全部纳入加总的已结束阶段都有合法耗时时发布:
|
|
2966
|
+
// 没有配对记录或任一耗时缺失都代表“没有足够证据”,不能解释为真实耗时为 0。
|
|
2967
|
+
const e1DurationAttempts = [
|
|
2968
|
+
...executionSummary.canonicalAttempts,
|
|
2969
|
+
...executionSummary.reworkAttempts.filter(attempt => attempt.end),
|
|
2970
|
+
];
|
|
2971
|
+
const hasCompleteE1DurationEvidence = e1DurationAttempts.length > 0
|
|
2972
|
+
&& e1DurationAttempts.every(attempt => (
|
|
2973
|
+
Number.isFinite(attempt.end?.duration_ms)
|
|
2974
|
+
&& attempt.end.duration_ms >= 0
|
|
2975
|
+
));
|
|
2976
|
+
const totalStageDuration = hasCompleteE1DurationEvidence
|
|
2977
|
+
? executionSummary.summary.total_stage_duration_ms
|
|
2978
|
+
: null;
|
|
2971
2979
|
const codingDuration = sumDurations(ends, ['apply']);
|
|
2972
2980
|
const specDuration = sumDurations(ends, ['propose', 'spec', 'design', 'task']);
|
|
2973
2981
|
const reworkCodingDuration = sumAttemptDurations(executionSummary.reworkAttempts, ['apply']);
|
|
@@ -3020,6 +3028,26 @@ function computeSinglePdfMvpMetrics(events, options = {}) {
|
|
|
3020
3028
|
&& timestampMs(attempt.end) <= timestampMs(firstApply)
|
|
3021
3029
|
))
|
|
3022
3030
|
: null;
|
|
3031
|
+
const qualifyingCheckAttempts = firstApply
|
|
3032
|
+
? executionSummary.attempts.filter(attempt => (
|
|
3033
|
+
attempt.command === 'check'
|
|
3034
|
+
&& attempt.end
|
|
3035
|
+
&& ['success', 'partial'].includes(attempt.result)
|
|
3036
|
+
&& timestampMs(attempt.end) <= timestampMs(firstApply)
|
|
3037
|
+
))
|
|
3038
|
+
: [];
|
|
3039
|
+
const p4Evidence = {
|
|
3040
|
+
first_apply_started_at: firstApply?.timestamp || null,
|
|
3041
|
+
first_apply_event_id: firstApply?.event_id || null,
|
|
3042
|
+
qualifying_check_count: qualifyingCheckAttempts.length,
|
|
3043
|
+
qualifying_checks: qualifyingCheckAttempts.map(attempt => ({
|
|
3044
|
+
result: attempt.result,
|
|
3045
|
+
started_at: attempt.start?.timestamp || null,
|
|
3046
|
+
ended_at: attempt.end?.timestamp || null,
|
|
3047
|
+
start_event_id: attempt.start?.event_id || null,
|
|
3048
|
+
end_event_id: attempt.end?.event_id || null,
|
|
3049
|
+
})),
|
|
3050
|
+
};
|
|
3023
3051
|
const latestCheckWithFixRate = latestByTimestamp(checkEvents.filter(e => {
|
|
3024
3052
|
const results = getCheckResults(e);
|
|
3025
3053
|
return results?.total > 0 && results.fixed_before_apply != null;
|
|
@@ -3049,8 +3077,13 @@ function computeSinglePdfMvpMetrics(events, options = {}) {
|
|
|
3049
3077
|
reviewer: latestCheckResultsV3?.reviewer || null,
|
|
3050
3078
|
author: formalApplyAttempt?.start?.session_id || formalApplyAttempt?.start?.agent_type || null,
|
|
3051
3079
|
reviewer_independence: latestCheckResultsV3?.reviewer_independence || null,
|
|
3080
|
+
review_execution_mode: latestCheckResultsV3?.review_execution_mode || null,
|
|
3081
|
+
reviewer_agent_id: latestCheckResultsV3?.reviewer_agent_id || null,
|
|
3052
3082
|
review_session_id: latestCheckResultsV3?.review_session_id || null,
|
|
3083
|
+
reviewer_session_id: latestCheckResultsV3?.reviewer_session_id || null,
|
|
3053
3084
|
author_session_id: latestCheckResultsV3?.author_session_id || formalApplyAttempt?.start?.session_id || null,
|
|
3085
|
+
fallback_reason_code: latestCheckResultsV3?.fallback_reason_code || null,
|
|
3086
|
+
fallback_reason: latestCheckResultsV3?.fallback_reason || null,
|
|
3054
3087
|
};
|
|
3055
3088
|
const q3Result = metricsV3.computeSpecQualityScore(latestCheckResultsV3, checkMeta);
|
|
3056
3089
|
|
|
@@ -3074,11 +3107,21 @@ function computeSinglePdfMvpMetrics(events, options = {}) {
|
|
|
3074
3107
|
completed_stages: completedStages,
|
|
3075
3108
|
missing: missingRequiredStages,
|
|
3076
3109
|
};
|
|
3077
|
-
const pHTelemetryHealthScore =
|
|
3078
|
-
?
|
|
3079
|
-
:
|
|
3110
|
+
const pHTelemetryHealthScore = measurementEvents.length === 0
|
|
3111
|
+
? null
|
|
3112
|
+
: (Number.isFinite(telemetryHealth.telemetry_health_score_percent)
|
|
3113
|
+
? telemetryHealth.telemetry_health_score_percent
|
|
3114
|
+
: (Number.isFinite(telemetryHealth.telemetry_health_score)
|
|
3115
|
+
? metricsV3.round(telemetryHealth.telemetry_health_score * 100, 2)
|
|
3116
|
+
: null));
|
|
3080
3117
|
|
|
3081
|
-
const
|
|
3118
|
+
const compactedReworkSummary = compactReworkSummary(executionSummary.summary, reworkReasons);
|
|
3119
|
+
const reworkSummary = executionSummary.attempts.length === 0
|
|
3120
|
+
? {
|
|
3121
|
+
...compactedReworkSummary,
|
|
3122
|
+
total_rework_attempts: null,
|
|
3123
|
+
}
|
|
3124
|
+
: compactedReworkSummary;
|
|
3082
3125
|
|
|
3083
3126
|
const v3Context = buildV3ChangeContext(scopedEvents, executionSummary, {
|
|
3084
3127
|
change_name: options.change || null,
|
|
@@ -3117,6 +3160,7 @@ function computeSinglePdfMvpMetrics(events, options = {}) {
|
|
|
3117
3160
|
: (completeMeasurementJourney ? 'trusted' : 'provisional'),
|
|
3118
3161
|
reason: e1AvailabilityReason || incompleteJourneyReason,
|
|
3119
3162
|
source_event_count: measurementEvents.length,
|
|
3163
|
+
event_ids: measurementEvents.map(event => event.event_id).filter(Boolean),
|
|
3120
3164
|
},
|
|
3121
3165
|
q3: {
|
|
3122
3166
|
status: v3.quality.q3_score_status === 'verified'
|
|
@@ -3125,6 +3169,7 @@ function computeSinglePdfMvpMetrics(events, options = {}) {
|
|
|
3125
3169
|
: (v3.quality.q3_availability ? 'provisional' : 'missing'),
|
|
3126
3170
|
reason: v3.quality.q3_availability_reason,
|
|
3127
3171
|
source_event_count: checkEvents.length,
|
|
3172
|
+
event_ids: checkEvents.map(event => event.event_id).filter(Boolean),
|
|
3128
3173
|
},
|
|
3129
3174
|
p4: {
|
|
3130
3175
|
status: !firstApply
|
|
@@ -3132,6 +3177,10 @@ function computeSinglePdfMvpMetrics(events, options = {}) {
|
|
|
3132
3177
|
: (completeMeasurementJourney ? 'trusted' : 'provisional'),
|
|
3133
3178
|
reason: firstApply ? incompleteJourneyReason : 'missing-apply-stage',
|
|
3134
3179
|
source_event_count: executionSummary.attempts.filter(attempt => attempt.command === 'check').length,
|
|
3180
|
+
event_ids: [
|
|
3181
|
+
p4Evidence.first_apply_event_id,
|
|
3182
|
+
...p4Evidence.qualifying_checks.flatMap(item => [item.start_event_id, item.end_event_id]),
|
|
3183
|
+
].filter(Boolean),
|
|
3135
3184
|
},
|
|
3136
3185
|
p_r: {
|
|
3137
3186
|
status: executionSummary.attempts.length === 0
|
|
@@ -3139,6 +3188,9 @@ function computeSinglePdfMvpMetrics(events, options = {}) {
|
|
|
3139
3188
|
: (completeMeasurementJourney ? 'trusted' : 'provisional'),
|
|
3140
3189
|
reason: executionSummary.attempts.length > 0 ? incompleteJourneyReason : 'missing-stage-events',
|
|
3141
3190
|
source_event_count: executionSummary.attempts.length,
|
|
3191
|
+
event_ids: executionSummary.attempts
|
|
3192
|
+
.flatMap(attempt => [attempt.start?.event_id, attempt.end?.event_id])
|
|
3193
|
+
.filter(Boolean),
|
|
3142
3194
|
},
|
|
3143
3195
|
p_h: {
|
|
3144
3196
|
status: measurementEvents.length === 0
|
|
@@ -3146,6 +3198,7 @@ function computeSinglePdfMvpMetrics(events, options = {}) {
|
|
|
3146
3198
|
: (completeMeasurementJourney ? 'trusted' : 'provisional'),
|
|
3147
3199
|
reason: measurementEvents.length > 0 ? incompleteJourneyReason : 'missing-stage-events',
|
|
3148
3200
|
source_event_count: measurementEvents.length,
|
|
3201
|
+
event_ids: measurementEvents.map(event => event.event_id).filter(Boolean),
|
|
3149
3202
|
},
|
|
3150
3203
|
};
|
|
3151
3204
|
|
|
@@ -3167,7 +3220,7 @@ function computeSinglePdfMvpMetrics(events, options = {}) {
|
|
|
3167
3220
|
? Math.max(0, leadTime - executionSummary.summary.effective_stage_duration_ms)
|
|
3168
3221
|
: null,
|
|
3169
3222
|
rework_stage_duration_ms: executionSummary.summary.rework_stage_duration_ms,
|
|
3170
|
-
total_stage_duration_ms:
|
|
3223
|
+
total_stage_duration_ms: totalStageDuration,
|
|
3171
3224
|
measurement_stage_duration_ms: totalStageDuration,
|
|
3172
3225
|
e2_coding_time_ratio_including_rework: executionSummary.summary.total_stage_duration_ms > 0
|
|
3173
3226
|
? roundMetric((codingDuration + reworkCodingDuration) / executionSummary.summary.total_stage_duration_ms)
|
|
@@ -3199,6 +3252,11 @@ function computeSinglePdfMvpMetrics(events, options = {}) {
|
|
|
3199
3252
|
q3_score_status: v3.quality.q3_score_status,
|
|
3200
3253
|
q3_reviewer_independence: v3.quality.q3_reviewer_independence,
|
|
3201
3254
|
q3_category_scores: v3.quality.q3_category_scores,
|
|
3255
|
+
q3_total: v3.quality.q3_total,
|
|
3256
|
+
q3_errors: v3.quality.q3_errors,
|
|
3257
|
+
q3_warnings: v3.quality.q3_warnings,
|
|
3258
|
+
q3_suggestions: v3.quality.q3_suggestions,
|
|
3259
|
+
q3_review: v3.quality.q3_review,
|
|
3202
3260
|
q3_availability: v3.quality.q3_availability,
|
|
3203
3261
|
// Q1/Q4 上游信号未就绪时用 pending-upstream 标注,不把 null 误转成 0
|
|
3204
3262
|
q1_status: v3.quality.q1_spec_conformance.status,
|
|
@@ -3211,6 +3269,7 @@ function computeSinglePdfMvpMetrics(events, options = {}) {
|
|
|
3211
3269
|
p2_ai_code_adoption_level: aiAdoptionMetrics.adoption_level,
|
|
3212
3270
|
ai_adoption_counts: aiAdoptionMetrics.adoption_counts,
|
|
3213
3271
|
p4_quality_gate_enforcement_rate: qualityGateRate,
|
|
3272
|
+
p4_evidence: p4Evidence,
|
|
3214
3273
|
p4b_check_fixed_rate: fixRate,
|
|
3215
3274
|
git_document_metrics: gitDocumentMetrics,
|
|
3216
3275
|
rework_summary: reworkSummary,
|
|
@@ -3354,9 +3413,11 @@ function computePdfMvpMetrics(events, options = {}) {
|
|
|
3354
3413
|
const health = computeTelemetryHealthMetrics(events);
|
|
3355
3414
|
return {
|
|
3356
3415
|
...health,
|
|
3357
|
-
p_h_telemetry_health_score: Number.isFinite(health.
|
|
3358
|
-
?
|
|
3359
|
-
:
|
|
3416
|
+
p_h_telemetry_health_score: Number.isFinite(health.telemetry_health_score_percent)
|
|
3417
|
+
? health.telemetry_health_score_percent
|
|
3418
|
+
: (Number.isFinite(health.telemetry_health_score)
|
|
3419
|
+
? metricsV3.round(health.telemetry_health_score * 100, 2)
|
|
3420
|
+
: null),
|
|
3360
3421
|
};
|
|
3361
3422
|
})(),
|
|
3362
3423
|
by_change: byChange,
|
|
@@ -4247,6 +4308,17 @@ function fileListSymmetricDiff(left, right) {
|
|
|
4247
4308
|
|
|
4248
4309
|
// ── U1 报告维度提取辅助:从 events/metrics/doctor/archive 提炼 5 维度,数据缺失诚实标注 _available: false ──
|
|
4249
4310
|
function extractChangedFiles(events, projectRoot) {
|
|
4311
|
+
const snapshotEvents = events.filter(event => (
|
|
4312
|
+
event?.type === 'final_output_snapshot'
|
|
4313
|
+
&& event.details?.final_output_snapshot
|
|
4314
|
+
&& typeof event.details.final_output_snapshot === 'object'
|
|
4315
|
+
));
|
|
4316
|
+
const latestSnapshotEvent = latestByTimestamp(snapshotEvents);
|
|
4317
|
+
const snapshotValidation = latestSnapshotEvent
|
|
4318
|
+
? validateFinalOutputSnapshot(latestSnapshotEvent.details.final_output_snapshot)
|
|
4319
|
+
: null;
|
|
4320
|
+
const finalSnapshot = snapshotValidation?.ok ? snapshotValidation.snapshot : null;
|
|
4321
|
+
const invalidFinalSnapshot = Boolean(latestSnapshotEvent && !snapshotValidation?.ok);
|
|
4250
4322
|
const adoptionEvents = getAiAdoptionEvents(events);
|
|
4251
4323
|
const finalAdoptionEvents = adoptionEvents.filter((event) => {
|
|
4252
4324
|
const review = getAiAdoptionReview(event);
|
|
@@ -4258,25 +4330,47 @@ function extractChangedFiles(events, projectRoot) {
|
|
|
4258
4330
|
finalAdoptionEvents.length > 0 ? finalAdoptionEvents : adoptionEvents,
|
|
4259
4331
|
);
|
|
4260
4332
|
const review = latest ? getAiAdoptionReview(latest) : null;
|
|
4261
|
-
const aiDiff = review?.ai_diff || null;
|
|
4333
|
+
const aiDiff = latestSnapshotEvent ? null : (review?.ai_diff || null);
|
|
4262
4334
|
const taskUpdateFiles = new Set();
|
|
4335
|
+
const taskEventIds = [];
|
|
4336
|
+
const taskIds = [];
|
|
4337
|
+
const noFileChangeReasons = [];
|
|
4338
|
+
let taskEventLatestAt = null;
|
|
4263
4339
|
for (const e of events) {
|
|
4264
4340
|
if (e?.type !== 'task_update') continue;
|
|
4265
|
-
const
|
|
4341
|
+
const taskUpdate = e.details?.task_update || {};
|
|
4342
|
+
const filesField = taskUpdate.files
|
|
4343
|
+
|| e.details?.files
|
|
4266
4344
|
|| e.details?.test_results?.files
|
|
4267
4345
|
|| (Array.isArray(e.details?.files_changed) ? e.details.files_changed : null);
|
|
4268
|
-
|
|
4346
|
+
const reason = taskUpdate.no_file_change_reason || e.details?.no_file_change_reason;
|
|
4347
|
+
if (typeof reason === 'string' && reason.trim()) {
|
|
4348
|
+
noFileChangeReasons.push({
|
|
4349
|
+
task_id: e.task_id || taskUpdate.task_id || null,
|
|
4350
|
+
event_id: e.event_id || null,
|
|
4351
|
+
reason: reason.trim(),
|
|
4352
|
+
});
|
|
4353
|
+
}
|
|
4354
|
+
if (Array.isArray(filesField) && filesField.length > 0) {
|
|
4355
|
+
if (e.event_id) taskEventIds.push(e.event_id);
|
|
4356
|
+
const taskId = e.task_id || taskUpdate.task_id;
|
|
4357
|
+
if (taskId) taskIds.push(taskId);
|
|
4358
|
+
if (!taskEventLatestAt || timestampMs(e) > Date.parse(taskEventLatestAt)) {
|
|
4359
|
+
taskEventLatestAt = e.timestamp || null;
|
|
4360
|
+
}
|
|
4269
4361
|
for (const f of filesField) {
|
|
4270
4362
|
if (typeof f === 'string' && f.trim()) taskUpdateFiles.add(f.trim());
|
|
4271
4363
|
}
|
|
4272
4364
|
}
|
|
4273
4365
|
}
|
|
4274
4366
|
const finalOutputFiles = new Set(
|
|
4275
|
-
Array.isArray(
|
|
4276
|
-
?
|
|
4277
|
-
:
|
|
4367
|
+
Array.isArray(finalSnapshot?.files)
|
|
4368
|
+
? finalSnapshot.files.filter(file => typeof file === 'string' && file.trim()).map(file => file.trim())
|
|
4369
|
+
: (Array.isArray(aiDiff?.files)
|
|
4370
|
+
? aiDiff.files.filter(file => typeof file === 'string' && file.trim()).map(file => file.trim())
|
|
4371
|
+
: [])
|
|
4278
4372
|
);
|
|
4279
|
-
if (finalOutputFiles.size === 0 && latest?.details) {
|
|
4373
|
+
if (!finalSnapshot && finalOutputFiles.size === 0 && latest?.details) {
|
|
4280
4374
|
const rawReview = latest.details.ai_adoption
|
|
4281
4375
|
|| latest.details.ai_adoption_review
|
|
4282
4376
|
|| latest.details.ai_code_adoption
|
|
@@ -4290,23 +4384,40 @@ function extractChangedFiles(events, projectRoot) {
|
|
|
4290
4384
|
|
|
4291
4385
|
const taskEventFileList = sortedUniqueFileList([...taskUpdateFiles]);
|
|
4292
4386
|
const finalOutputFileList = sortedUniqueFileList([...finalOutputFiles]);
|
|
4293
|
-
const
|
|
4294
|
-
const
|
|
4295
|
-
|
|
4296
|
-
|
|
4297
|
-
|
|
4298
|
-
|
|
4387
|
+
const taskEvidenceAvailable = taskEventFileList.length > 0 || noFileChangeReasons.length > 0;
|
|
4388
|
+
const taskUpdateFileCount = taskEvidenceAvailable ? taskEventFileList.length : null;
|
|
4389
|
+
const finalOutputFileCount = finalSnapshot
|
|
4390
|
+
? (Number.isFinite(finalSnapshot.files_changed)
|
|
4391
|
+
? finalSnapshot.files_changed
|
|
4392
|
+
: finalOutputFileList.length)
|
|
4393
|
+
: (Number.isFinite(aiDiff?.files_changed)
|
|
4394
|
+
? aiDiff.files_changed
|
|
4395
|
+
: (finalOutputFileList.length > 0 ? finalOutputFileList.length : null));
|
|
4396
|
+
const snapshotKind = finalSnapshot
|
|
4397
|
+
? finalSnapshot.snapshot_kind
|
|
4398
|
+
: (latestSnapshotEvent
|
|
4399
|
+
? null
|
|
4400
|
+
: (aiDiff?.snapshot_kind
|
|
4401
|
+
|| ((review?.review_status === 'final' || latest?.status === 'final') ? 'final' : null)));
|
|
4299
4402
|
const finalOutputStatus = snapshotKind === 'final'
|
|
4300
4403
|
? 'final'
|
|
4301
4404
|
: (finalOutputFileCount == null ? 'unavailable' : 'provisional');
|
|
4302
|
-
const addedLines =
|
|
4303
|
-
?
|
|
4304
|
-
: (Number.isFinite(
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
|
|
4405
|
+
const addedLines = finalSnapshot
|
|
4406
|
+
? (Number.isFinite(finalSnapshot.added_lines) ? finalSnapshot.added_lines : null)
|
|
4407
|
+
: (Number.isFinite(aiDiff?.added_lines)
|
|
4408
|
+
? aiDiff.added_lines
|
|
4409
|
+
: (Number.isFinite(review?.final_diff?.added_lines) ? review.final_diff.added_lines : null));
|
|
4410
|
+
const deletedLines = finalSnapshot
|
|
4411
|
+
? (Number.isFinite(finalSnapshot.deleted_lines) ? finalSnapshot.deleted_lines : null)
|
|
4412
|
+
: (Number.isFinite(aiDiff?.deleted_lines)
|
|
4413
|
+
? aiDiff.deleted_lines
|
|
4414
|
+
: (Number.isFinite(review?.final_diff?.deleted_lines) ? review.final_diff.deleted_lines : null));
|
|
4308
4415
|
let lineCountEstimate = null;
|
|
4309
|
-
if (
|
|
4416
|
+
if (!finalSnapshot
|
|
4417
|
+
&& addedLines == null
|
|
4418
|
+
&& review?.ai_git_sha == null
|
|
4419
|
+
&& finalOutputFileList.length > 0
|
|
4420
|
+
&& projectRoot) {
|
|
4310
4421
|
let lines = 0;
|
|
4311
4422
|
let countedFiles = 0;
|
|
4312
4423
|
const root = path.resolve(projectRoot);
|
|
@@ -4331,27 +4442,26 @@ function extractChangedFiles(events, projectRoot) {
|
|
|
4331
4442
|
};
|
|
4332
4443
|
}
|
|
4333
4444
|
}
|
|
4334
|
-
//
|
|
4335
|
-
const filesChanged = finalOutputStatus === 'final'
|
|
4336
|
-
|
|
4337
|
-
: (taskUpdateFileCount > 0 ? taskUpdateFileCount : finalOutputFileCount);
|
|
4338
|
-
const countsDiffer = taskUpdateFileCount > 0
|
|
4445
|
+
// 对外的 files_changed 只代表最终实际交付;任务事件文件始终使用独立字段。
|
|
4446
|
+
const filesChanged = finalOutputStatus === 'final' ? finalOutputFileCount : null;
|
|
4447
|
+
const countsDiffer = taskUpdateFileCount != null
|
|
4339
4448
|
&& finalOutputFileCount != null
|
|
4340
4449
|
&& taskUpdateFileCount !== finalOutputFileCount;
|
|
4341
4450
|
const listDiff = fileListSymmetricDiff(taskEventFileList, finalOutputFileList);
|
|
4342
|
-
const listsDiffer = taskUpdateFileCount
|
|
4343
|
-
&&
|
|
4451
|
+
const listsDiffer = taskUpdateFileCount != null
|
|
4452
|
+
&& finalOutputFileCount != null
|
|
4344
4453
|
&& (listDiff.only_in_task_events.length > 0 || listDiff.only_in_final_output.length > 0);
|
|
4345
4454
|
let reconciliationStatus = 'unavailable';
|
|
4346
4455
|
let reconciliationNote = '没有可用的文件变更证据。';
|
|
4347
|
-
if (taskUpdateFileCount
|
|
4456
|
+
if (taskUpdateFileCount == null && finalOutputFileCount == null) {
|
|
4348
4457
|
reconciliationStatus = 'unavailable';
|
|
4349
4458
|
reconciliationNote = '没有可用的文件变更证据。';
|
|
4350
|
-
} else if (
|
|
4351
|
-
reconciliationStatus = '
|
|
4352
|
-
reconciliationNote =
|
|
4353
|
-
|
|
4354
|
-
|
|
4459
|
+
} else if (finalOutputFileCount == null) {
|
|
4460
|
+
reconciliationStatus = 'final-snapshot-missing';
|
|
4461
|
+
reconciliationNote = '最终输出文件快照不可用,无法与任务事件对账。';
|
|
4462
|
+
} else if (taskUpdateFileCount == null) {
|
|
4463
|
+
reconciliationStatus = 'task-evidence-missing';
|
|
4464
|
+
reconciliationNote = '任务事件未记录文件清单或无文件原因;仅展示最终输出快照。';
|
|
4355
4465
|
} else if (countsDiffer) {
|
|
4356
4466
|
reconciliationStatus = 'count-mismatch';
|
|
4357
4467
|
reconciliationNote = `任务事件累计涉及 ${taskUpdateFileCount} 个文件;最终输出快照包含 ${finalOutputFileCount} 个文件。两者用途不同,不应合并为一个口径。`;
|
|
@@ -4363,17 +4473,37 @@ function extractChangedFiles(events, projectRoot) {
|
|
|
4363
4473
|
reconciliationNote = '任务事件与最终输出文件口径一致。';
|
|
4364
4474
|
}
|
|
4365
4475
|
return {
|
|
4366
|
-
_available: filesChanged != null,
|
|
4476
|
+
_available: filesChanged != null || taskUpdateFileCount != null,
|
|
4367
4477
|
files_changed: filesChanged,
|
|
4368
4478
|
added_lines: addedLines,
|
|
4369
4479
|
deleted_lines: deletedLines,
|
|
4370
4480
|
files: finalOutputFileList.length > 0 ? finalOutputFileList : null,
|
|
4371
|
-
task_event_files: taskUpdateFileCount
|
|
4372
|
-
task_event_files_changed: taskUpdateFileCount
|
|
4481
|
+
task_event_files: taskUpdateFileCount != null ? taskEventFileList : null,
|
|
4482
|
+
task_event_files_changed: taskUpdateFileCount,
|
|
4483
|
+
task_event_source: 'task_update.files',
|
|
4484
|
+
task_event_task_ids: [...new Set(taskIds)],
|
|
4485
|
+
task_event_ids: [...new Set(taskEventIds)],
|
|
4486
|
+
task_event_latest_at: taskEventLatestAt,
|
|
4487
|
+
no_file_change_reasons: noFileChangeReasons,
|
|
4373
4488
|
final_output_files: finalOutputFileList.length > 0 ? finalOutputFileList : null,
|
|
4374
4489
|
final_output_files_changed: finalOutputFileCount,
|
|
4375
4490
|
final_output_status: finalOutputStatus,
|
|
4376
|
-
|
|
4491
|
+
final_output_source: finalSnapshot
|
|
4492
|
+
? 'final_output_snapshot.files'
|
|
4493
|
+
: (snapshotKind === 'final' ? 'ai-diff-final(旧事件兼容)' : null),
|
|
4494
|
+
final_output_event_id: latestSnapshotEvent?.event_id || latest?.event_id || null,
|
|
4495
|
+
final_output_captured_at: latestSnapshotEvent?.timestamp || latest?.timestamp || null,
|
|
4496
|
+
final_output_no_output_reason: finalSnapshot?.no_output_reason || null,
|
|
4497
|
+
final_output_vcs_mode: finalSnapshot?.vcs_mode || null,
|
|
4498
|
+
final_output_validation_status: invalidFinalSnapshot ? 'invalid' : (finalSnapshot ? 'valid' : 'unavailable'),
|
|
4499
|
+
final_output_validation_reason: invalidFinalSnapshot
|
|
4500
|
+
? `${snapshotValidation.code}: ${snapshotValidation.message}`
|
|
4501
|
+
: null,
|
|
4502
|
+
diff_source: finalSnapshot
|
|
4503
|
+
? 'final-output-snapshot'
|
|
4504
|
+
: (invalidFinalSnapshot
|
|
4505
|
+
? 'invalid-final-output-snapshot'
|
|
4506
|
+
: (snapshotKind === 'final' ? 'ai-diff-final' : (aiDiff ? 'ai-diff-provisional' : 'task-events'))),
|
|
4377
4507
|
line_count_estimate: lineCountEstimate,
|
|
4378
4508
|
reconciliation_status: reconciliationStatus,
|
|
4379
4509
|
reconciliation_note: reconciliationNote,
|
|
@@ -4434,6 +4564,8 @@ function extractStageTimeline(events) {
|
|
|
4434
4564
|
return {
|
|
4435
4565
|
stage: cmd,
|
|
4436
4566
|
round,
|
|
4567
|
+
start_event_id: start.event_id || null,
|
|
4568
|
+
end_event_id: end?.event_id || null,
|
|
4437
4569
|
start_ts: start.timestamp || null,
|
|
4438
4570
|
end_ts: end?.timestamp || null,
|
|
4439
4571
|
duration_ms: end && Number.isFinite(end.duration_ms) ? end.duration_ms : null,
|
|
@@ -5141,11 +5273,14 @@ function computeDoctorReport(events, options = {}) {
|
|
|
5141
5273
|
outdatedSchemaEvents.length * 0.06 +
|
|
5142
5274
|
missingChangeEvents.length * 0.04 +
|
|
5143
5275
|
missingCapabilityEvents.length * 0.01;
|
|
5144
|
-
const
|
|
5145
|
-
|
|
5276
|
+
const telemetryHealth = metricsV3.computeTelemetryHealthScore({
|
|
5277
|
+
weighted_issue_value: penalty,
|
|
5278
|
+
stage_event_count: stageEvents.length,
|
|
5279
|
+
});
|
|
5146
5280
|
|
|
5147
5281
|
return {
|
|
5148
|
-
telemetry_health_score:
|
|
5282
|
+
telemetry_health_score: telemetryHealth.ratio,
|
|
5283
|
+
telemetry_health_score_percent: telemetryHealth.score,
|
|
5149
5284
|
matched_stage_rate: matchedStageRate == null ? null : Math.round(matchedStageRate * 100) / 100,
|
|
5150
5285
|
total_events: scopedEvents.length,
|
|
5151
5286
|
stage_events: stageEvents.length,
|
|
@@ -5173,19 +5308,8 @@ function computeDoctorReport(events, options = {}) {
|
|
|
5173
5308
|
|
|
5174
5309
|
// ── CLI 参数解析 ──────────────────────────────────────
|
|
5175
5310
|
|
|
5176
|
-
|
|
5177
|
-
|
|
5178
|
-
const args = {};
|
|
5179
|
-
for (const arg of argv) {
|
|
5180
|
-
const match = arg.match(/^--([a-zA-Z_-]+)=(.*)$/);
|
|
5181
|
-
if (match) {
|
|
5182
|
-
args[match[1]] = match[2];
|
|
5183
|
-
} else if (arg.match(/^--([a-zA-Z_-]+)$/)) {
|
|
5184
|
-
args[arg.slice(2)] = true;
|
|
5185
|
-
}
|
|
5186
|
-
}
|
|
5187
|
-
return args;
|
|
5188
|
-
}
|
|
5311
|
+
// parseArgs 已迁移至 lib/shared.cjs(统一 5 份实现)
|
|
5312
|
+
// 本文件顶部已通过 require 引入,此处保留 const 别名以兼容内部引用
|
|
5189
5313
|
|
|
5190
5314
|
// ── CLI 命令实现 ─────────────────────────────────────
|
|
5191
5315
|
|
|
@@ -5266,6 +5390,20 @@ function cmdStart(args) {
|
|
|
5266
5390
|
appendEvent(dataDir, event.change, event);
|
|
5267
5391
|
writeActiveStage(projectRoot, event);
|
|
5268
5392
|
|
|
5393
|
+
// kld-T05: 七值命令成功写入 stage_start 后,异步上报使用事件。
|
|
5394
|
+
// test/explore 与失败路径不上报;上报失败不影响本地事件与退出码。
|
|
5395
|
+
const usagePromise = (() => {
|
|
5396
|
+
try {
|
|
5397
|
+
const { mapCommandToUsageSkill } = require('./lib/usage-contract.cjs');
|
|
5398
|
+
const skill = mapCommandToUsageSkill(command);
|
|
5399
|
+
if (!skill) return null;
|
|
5400
|
+
const { reportUsageOnce } = require('./lib/usage-reporter.cjs');
|
|
5401
|
+
return reportUsageOnce({ skill, startedAt: timestamp }).catch(() => null);
|
|
5402
|
+
} catch {
|
|
5403
|
+
return null;
|
|
5404
|
+
}
|
|
5405
|
+
})();
|
|
5406
|
+
|
|
5269
5407
|
// 输出 JSON,方便 AI Agent 解析 event_id
|
|
5270
5408
|
const output = {
|
|
5271
5409
|
event_id: eventId,
|
|
@@ -5273,6 +5411,9 @@ function cmdStart(args) {
|
|
|
5273
5411
|
message: `SDD ${command} 阶段开始记录(change: ${event.change})`,
|
|
5274
5412
|
};
|
|
5275
5413
|
console.log(JSON.stringify(output, null, 2));
|
|
5414
|
+
// 返回 usage 上报 promise(可能为 null),便于测试或调用方 await;
|
|
5415
|
+
// 现有 main() 不读返回值,行为兼容。
|
|
5416
|
+
return usagePromise;
|
|
5276
5417
|
}
|
|
5277
5418
|
|
|
5278
5419
|
const ONTOLOGY_AUTHORING_STAGES = new Set(['propose', 'spec', 'design', 'task', 'check']);
|
|
@@ -5348,7 +5489,21 @@ function cmdEnd(args, options = {}) {
|
|
|
5348
5489
|
fail('E_CHANGE_REQUIRED: 无法解析 start 的 end 必须显式提供 --change');
|
|
5349
5490
|
}
|
|
5350
5491
|
if (startEvent && requestedChange && startEvent.change !== requestedChange) {
|
|
5351
|
-
|
|
5492
|
+
// BUGFIX: 允许 start 使用占位 change(如 pending)时 end 指定真实 change-key 自动迁移
|
|
5493
|
+
// 场景:propose 阶段初期不知道最终 change-key,start 使用 --change=pending 占位,
|
|
5494
|
+
// 后期生成 change-key 后 end 使用真实名称。此时应允许并自动迁移事件文件。
|
|
5495
|
+
const PLACEHOLDER_CHANGES = new Set(['pending']);
|
|
5496
|
+
if (PLACEHOLDER_CHANGES.has(startEvent.change)) {
|
|
5497
|
+
// 将 start 事件复制到新的 change 文件下,保持 event_id 连续性
|
|
5498
|
+
if (process.env.SDD_DEBUG_TELEMETRY) {
|
|
5499
|
+
console.error(`[telemetry] migrating start event from placeholder "${startEvent.change}" → "${requestedChange}" (event_id=${eventId})`);
|
|
5500
|
+
}
|
|
5501
|
+
appendEvent(dataDir, requestedChange, startEvent, { lockHeld: false });
|
|
5502
|
+
// 更新内存中的 startEvent.change 供后续使用
|
|
5503
|
+
startEvent = { ...startEvent, change: requestedChange };
|
|
5504
|
+
} else {
|
|
5505
|
+
fail(`E_CHANGE_CONFLICT: end 指定 change "${requestedChange}",但 active stage 属于 "${startEvent.change}"`);
|
|
5506
|
+
}
|
|
5352
5507
|
}
|
|
5353
5508
|
// stage_end 幂等性保护:同一 event_id 已存在 stage_end 时,结果相同则跳过,不同则更新(archive 命令只要存在即跳过)
|
|
5354
5509
|
const command = startEvent?.command || args.command || 'unknown';
|
|
@@ -5605,6 +5760,9 @@ function cmdEnd(args, options = {}) {
|
|
|
5605
5760
|
...event.details.archive_result,
|
|
5606
5761
|
...actualPaths,
|
|
5607
5762
|
};
|
|
5763
|
+
if (event.details.archive_result.archive_id && !event.archive_id) {
|
|
5764
|
+
event.archive_id = event.details.archive_result.archive_id;
|
|
5765
|
+
}
|
|
5608
5766
|
}
|
|
5609
5767
|
|
|
5610
5768
|
appendEvent(dataDir, event.change, event);
|
|
@@ -5612,6 +5770,37 @@ function cmdEnd(args, options = {}) {
|
|
|
5612
5770
|
clearActiveStage(projectRoot, startEvent);
|
|
5613
5771
|
}
|
|
5614
5772
|
|
|
5773
|
+
// Archive stage_end is remapped to archive_linked on the wire. Also emit an explicit
|
|
5774
|
+
// run_ended so monitors that only listen for terminal kinds still close the run.
|
|
5775
|
+
if (command === 'archive' && (result === 'success' || result === 'partial' || result === 'failed' || result === 'failure')) {
|
|
5776
|
+
try {
|
|
5777
|
+
const terminalResult = result === 'failure' ? 'failed' : result;
|
|
5778
|
+
appendEvent(dataDir, event.change, cleanOptionalFields({
|
|
5779
|
+
schema_version: SCHEMA_VERSION,
|
|
5780
|
+
event_id: generateEventId(),
|
|
5781
|
+
type: 'stage_end',
|
|
5782
|
+
event_kind: 'run_ended',
|
|
5783
|
+
source: event.source,
|
|
5784
|
+
command: 'archive',
|
|
5785
|
+
stage: 'archive',
|
|
5786
|
+
change: event.change,
|
|
5787
|
+
agent_type: event.agent_type,
|
|
5788
|
+
project_root: projectRoot,
|
|
5789
|
+
session_id: event.session_id,
|
|
5790
|
+
git_sha: event.git_sha,
|
|
5791
|
+
timestamp: nowISO(),
|
|
5792
|
+
result: terminalResult,
|
|
5793
|
+
summary: event.summary || '变更归档完成,运行已结束',
|
|
5794
|
+
details: {
|
|
5795
|
+
terminal_of: event.event_id,
|
|
5796
|
+
archive_id: event.details?.archive_result?.archive_id,
|
|
5797
|
+
},
|
|
5798
|
+
}));
|
|
5799
|
+
} catch (err) {
|
|
5800
|
+
try { console.error(`[telemetry] run_ended 补发失败(不阻塞): ${err.message}`); } catch {}
|
|
5801
|
+
}
|
|
5802
|
+
}
|
|
5803
|
+
|
|
5615
5804
|
// 终态摘要必须基于已经落库的 archive stage_end。
|
|
5616
5805
|
if (command === 'archive') {
|
|
5617
5806
|
try { appendChangeSummary(projectRoot, change, readEvents(dataDir, change)); } catch (err) {
|
|
@@ -5693,6 +5882,484 @@ function strictInteger(value, code, fieldName, minimum = 0) {
|
|
|
5693
5882
|
return number;
|
|
5694
5883
|
}
|
|
5695
5884
|
|
|
5885
|
+
function strictStructuredInteger(value, code, fieldName, minimum = 0) {
|
|
5886
|
+
if (!Number.isInteger(value) || value < minimum) {
|
|
5887
|
+
fail(`${code}: ${fieldName} 必须是大于等于 ${minimum} 的整数`);
|
|
5888
|
+
}
|
|
5889
|
+
return value;
|
|
5890
|
+
}
|
|
5891
|
+
|
|
5892
|
+
const CHECK_FALLBACK_REASON_CODES = new Set([
|
|
5893
|
+
'SUBAGENT_CAPABILITY_UNAVAILABLE',
|
|
5894
|
+
'SUBAGENT_PERMISSION_DENIED',
|
|
5895
|
+
'SUBAGENT_START_FAILED',
|
|
5896
|
+
'SUBAGENT_TIMEOUT',
|
|
5897
|
+
'SUBAGENT_RESULT_INVALID',
|
|
5898
|
+
]);
|
|
5899
|
+
|
|
5900
|
+
const CHECK_IDENTITY_PLACEHOLDERS = new Set([
|
|
5901
|
+
'unknown',
|
|
5902
|
+
'none',
|
|
5903
|
+
'null',
|
|
5904
|
+
'n/a',
|
|
5905
|
+
'na',
|
|
5906
|
+
'未记录',
|
|
5907
|
+
'未知',
|
|
5908
|
+
]);
|
|
5909
|
+
|
|
5910
|
+
function isMeaningfulCheckIdentity(value) {
|
|
5911
|
+
const normalized = String(value || '').trim();
|
|
5912
|
+
if (!normalized) return false;
|
|
5913
|
+
if (CHECK_IDENTITY_PLACEHOLDERS.has(normalized.toLowerCase())) return false;
|
|
5914
|
+
if (/^<[^>]+>$/.test(normalized) || /^\[[^\]]+\]$/.test(normalized)) return false;
|
|
5915
|
+
return true;
|
|
5916
|
+
}
|
|
5917
|
+
|
|
5918
|
+
function normalizeRepositoryRelativeFiles(files, {
|
|
5919
|
+
requiredCode,
|
|
5920
|
+
invalidCode,
|
|
5921
|
+
fieldName,
|
|
5922
|
+
} = {}) {
|
|
5923
|
+
if (!Array.isArray(files)) {
|
|
5924
|
+
const code = requiredCode || invalidCode || 'REPOSITORY_FILE_PATH_INVALID';
|
|
5925
|
+
throw new Error(`${code}: ${fieldName || 'files'} 必须是数组`);
|
|
5926
|
+
}
|
|
5927
|
+
if (files.length === 0) {
|
|
5928
|
+
if (requiredCode) {
|
|
5929
|
+
throw new Error(`${requiredCode}: ${fieldName || 'files'} 必须是非空数组`);
|
|
5930
|
+
}
|
|
5931
|
+
return [];
|
|
5932
|
+
}
|
|
5933
|
+
const normalized = [];
|
|
5934
|
+
for (const value of files) {
|
|
5935
|
+
if (typeof value !== 'string') {
|
|
5936
|
+
throw new Error(`${invalidCode || 'REPOSITORY_FILE_PATH_INVALID'}: ${fieldName || 'files'} 只能包含字符串路径`);
|
|
5937
|
+
}
|
|
5938
|
+
const raw = value.trim().replace(/\\/g, '/');
|
|
5939
|
+
const segments = raw.split('/');
|
|
5940
|
+
if (!raw
|
|
5941
|
+
|| path.isAbsolute(raw)
|
|
5942
|
+
|| /^[A-Za-z]:/.test(raw)
|
|
5943
|
+
|| segments.includes('..')
|
|
5944
|
+
|| segments.includes('.')
|
|
5945
|
+
|| raw.startsWith('/')) {
|
|
5946
|
+
throw new Error(`${invalidCode || 'REPOSITORY_FILE_PATH_INVALID'}: 非法仓库相对路径 "${raw || String(value)}"`);
|
|
5947
|
+
}
|
|
5948
|
+
normalized.push(raw);
|
|
5949
|
+
}
|
|
5950
|
+
return [...new Set(normalized)].sort();
|
|
5951
|
+
}
|
|
5952
|
+
|
|
5953
|
+
function normalizeStrictCheckResult(details, result) {
|
|
5954
|
+
const input = details?.check_results;
|
|
5955
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
5956
|
+
fail('CHECK_RESULT_REQUIRED: strict check_result 缺少 details.check_results');
|
|
5957
|
+
}
|
|
5958
|
+
const total = strictStructuredInteger(
|
|
5959
|
+
input.total,
|
|
5960
|
+
'CHECK_RESULT_TOTAL_REQUIRED',
|
|
5961
|
+
'check_results.total',
|
|
5962
|
+
1,
|
|
5963
|
+
);
|
|
5964
|
+
const errors = strictStructuredInteger(
|
|
5965
|
+
input.errors,
|
|
5966
|
+
'CHECK_RESULT_ERRORS_REQUIRED',
|
|
5967
|
+
'check_results.errors',
|
|
5968
|
+
);
|
|
5969
|
+
if (errors > total) {
|
|
5970
|
+
fail('CHECK_RESULT_ERRORS_INVALID: check_results.errors 不能大于 check_results.total');
|
|
5971
|
+
}
|
|
5972
|
+
const warnings = strictStructuredInteger(
|
|
5973
|
+
input.warnings,
|
|
5974
|
+
'CHECK_RESULT_WARNINGS_REQUIRED',
|
|
5975
|
+
'check_results.warnings',
|
|
5976
|
+
);
|
|
5977
|
+
const suggestions = strictStructuredInteger(
|
|
5978
|
+
input.suggestions,
|
|
5979
|
+
'CHECK_RESULT_SUGGESTIONS_REQUIRED',
|
|
5980
|
+
'check_results.suggestions',
|
|
5981
|
+
);
|
|
5982
|
+
const fixedBeforeApply = strictStructuredInteger(
|
|
5983
|
+
input.fixed_before_apply,
|
|
5984
|
+
'CHECK_RESULT_FIXED_REQUIRED',
|
|
5985
|
+
'check_results.fixed_before_apply',
|
|
5986
|
+
);
|
|
5987
|
+
if (fixedBeforeApply > total) {
|
|
5988
|
+
fail('CHECK_RESULT_FIXED_INVALID: fixed_before_apply 不能大于 check_results.total');
|
|
5989
|
+
}
|
|
5990
|
+
const categoryNames = [
|
|
5991
|
+
'completeness',
|
|
5992
|
+
'consistency',
|
|
5993
|
+
'executability',
|
|
5994
|
+
'tdd_compliance',
|
|
5995
|
+
];
|
|
5996
|
+
const categoryInput = input.categories;
|
|
5997
|
+
if (!categoryInput || typeof categoryInput !== 'object' || Array.isArray(categoryInput)) {
|
|
5998
|
+
fail('CHECK_RESULT_CATEGORIES_REQUIRED: strict check_result 缺少 check_results.categories');
|
|
5999
|
+
}
|
|
6000
|
+
const categories = {};
|
|
6001
|
+
let categoryTotalSum = 0;
|
|
6002
|
+
let categoryErrorSum = 0;
|
|
6003
|
+
for (const categoryName of categoryNames) {
|
|
6004
|
+
const category = categoryInput[categoryName];
|
|
6005
|
+
if (!category || typeof category !== 'object' || Array.isArray(category)) {
|
|
6006
|
+
fail(`CHECK_RESULT_CATEGORY_REQUIRED: check_results.categories.${categoryName} 必须是对象`);
|
|
6007
|
+
}
|
|
6008
|
+
const categoryTotal = strictStructuredInteger(
|
|
6009
|
+
category.total,
|
|
6010
|
+
'CHECK_RESULT_CATEGORY_TOTAL_REQUIRED',
|
|
6011
|
+
`check_results.categories.${categoryName}.total`,
|
|
6012
|
+
);
|
|
6013
|
+
const categoryPassed = strictStructuredInteger(
|
|
6014
|
+
category.passed,
|
|
6015
|
+
'CHECK_RESULT_CATEGORY_PASSED_REQUIRED',
|
|
6016
|
+
`check_results.categories.${categoryName}.passed`,
|
|
6017
|
+
);
|
|
6018
|
+
if (categoryPassed > categoryTotal) {
|
|
6019
|
+
fail(`CHECK_RESULT_CATEGORY_INVALID: ${categoryName}.passed 不能大于 total`);
|
|
6020
|
+
}
|
|
6021
|
+
categories[categoryName] = {
|
|
6022
|
+
...category,
|
|
6023
|
+
passed: categoryPassed,
|
|
6024
|
+
total: categoryTotal,
|
|
6025
|
+
};
|
|
6026
|
+
categoryTotalSum += categoryTotal;
|
|
6027
|
+
categoryErrorSum += categoryTotal - categoryPassed;
|
|
6028
|
+
}
|
|
6029
|
+
if (categoryTotalSum !== total) {
|
|
6030
|
+
fail('CHECK_RESULT_CATEGORY_TOTAL_MISMATCH: 四类 categories.total 合计必须等于 check_results.total');
|
|
6031
|
+
}
|
|
6032
|
+
if (categoryErrorSum !== errors) {
|
|
6033
|
+
fail('CHECK_RESULT_CATEGORY_ERRORS_MISMATCH: 四类未通过数合计必须等于 check_results.errors');
|
|
6034
|
+
}
|
|
6035
|
+
const taskInput = input.task_completion;
|
|
6036
|
+
if (!taskInput || typeof taskInput !== 'object' || Array.isArray(taskInput)) {
|
|
6037
|
+
fail('CHECK_RESULT_TASK_COMPLETION_REQUIRED: strict check_result 缺少 check_results.task_completion');
|
|
6038
|
+
}
|
|
6039
|
+
const taskCompleted = strictStructuredInteger(
|
|
6040
|
+
taskInput.completed,
|
|
6041
|
+
'CHECK_RESULT_TASK_COMPLETED_REQUIRED',
|
|
6042
|
+
'check_results.task_completion.completed',
|
|
6043
|
+
);
|
|
6044
|
+
const taskIncomplete = strictStructuredInteger(
|
|
6045
|
+
taskInput.incomplete,
|
|
6046
|
+
'CHECK_RESULT_TASK_INCOMPLETE_REQUIRED',
|
|
6047
|
+
'check_results.task_completion.incomplete',
|
|
6048
|
+
);
|
|
6049
|
+
const taskTotal = strictStructuredInteger(
|
|
6050
|
+
taskInput.total,
|
|
6051
|
+
'CHECK_RESULT_TASK_TOTAL_REQUIRED',
|
|
6052
|
+
'check_results.task_completion.total',
|
|
6053
|
+
);
|
|
6054
|
+
if (taskCompleted + taskIncomplete !== taskTotal) {
|
|
6055
|
+
fail('CHECK_RESULT_TASK_COUNTS_INVALID: completed + incomplete 必须等于 task_completion.total');
|
|
6056
|
+
}
|
|
6057
|
+
if (typeof taskInput.has_incomplete !== 'boolean'
|
|
6058
|
+
|| taskInput.has_incomplete !== (taskIncomplete > 0)) {
|
|
6059
|
+
fail('CHECK_RESULT_TASK_FLAG_INVALID: has_incomplete 必须与 incomplete 是否大于 0 一致');
|
|
6060
|
+
}
|
|
6061
|
+
if (typeof taskInput.checked_for_archive_readiness !== 'boolean') {
|
|
6062
|
+
fail('CHECK_RESULT_ARCHIVE_READINESS_REQUIRED: checked_for_archive_readiness 必须是布尔值');
|
|
6063
|
+
}
|
|
6064
|
+
const normalizedResult = requireStrictString(
|
|
6065
|
+
result,
|
|
6066
|
+
'CHECK_RESULT_STATUS_REQUIRED',
|
|
6067
|
+
'result',
|
|
6068
|
+
);
|
|
6069
|
+
if (!['success', 'partial', 'failure'].includes(normalizedResult)) {
|
|
6070
|
+
fail(`CHECK_RESULT_STATUS_INVALID: 非法 check_result.result "${normalizedResult}"`);
|
|
6071
|
+
}
|
|
6072
|
+
if (taskIncomplete > 0 && normalizedResult === 'success') {
|
|
6073
|
+
fail('CHECK_RESULT_TASK_STATUS_INVALID: 存在未完成任务时 check_result.result 不能是 success');
|
|
6074
|
+
}
|
|
6075
|
+
const warningItems = input.warning_items;
|
|
6076
|
+
if (taskIncomplete > 0 && (
|
|
6077
|
+
!Array.isArray(warningItems)
|
|
6078
|
+
|| !warningItems.some(item => item?.category === 'task_completion')
|
|
6079
|
+
)) {
|
|
6080
|
+
fail('CHECK_RESULT_TASK_WARNING_REQUIRED: 存在未完成任务时 warning_items 必须包含 task_completion 明细');
|
|
6081
|
+
}
|
|
6082
|
+
const taskCompletion = {
|
|
6083
|
+
...taskInput,
|
|
6084
|
+
completed: taskCompleted,
|
|
6085
|
+
incomplete: taskIncomplete,
|
|
6086
|
+
total: taskTotal,
|
|
6087
|
+
has_incomplete: taskInput.has_incomplete,
|
|
6088
|
+
checked_for_archive_readiness: taskInput.checked_for_archive_readiness,
|
|
6089
|
+
};
|
|
6090
|
+
const executionMode = requireStrictString(
|
|
6091
|
+
input.review_execution_mode,
|
|
6092
|
+
'CHECK_REVIEW_EXECUTION_MODE_REQUIRED',
|
|
6093
|
+
'review_execution_mode',
|
|
6094
|
+
);
|
|
6095
|
+
if (!['subagent', 'main-agent-fallback'].includes(executionMode)) {
|
|
6096
|
+
fail(`CHECK_REVIEW_EXECUTION_MODE_INVALID: 非法 review_execution_mode "${executionMode}"`);
|
|
6097
|
+
}
|
|
6098
|
+
const reviewerIndependence = requireStrictString(
|
|
6099
|
+
input.reviewer_independence,
|
|
6100
|
+
'CHECK_REVIEW_INDEPENDENCE_REQUIRED',
|
|
6101
|
+
'reviewer_independence',
|
|
6102
|
+
);
|
|
6103
|
+
if (!['independent-review', 'self-review', 'unknown'].includes(reviewerIndependence)) {
|
|
6104
|
+
fail(`CHECK_REVIEW_INDEPENDENCE_INVALID: 非法 reviewer_independence "${reviewerIndependence}"`);
|
|
6105
|
+
}
|
|
6106
|
+
const reviewSessionId = requireStrictString(
|
|
6107
|
+
input.review_session_id || input.reviewer_session_id,
|
|
6108
|
+
'CHECK_REVIEW_SESSION_REQUIRED',
|
|
6109
|
+
'review_session_id',
|
|
6110
|
+
);
|
|
6111
|
+
const authorSessionId = requireStrictString(
|
|
6112
|
+
input.author_session_id,
|
|
6113
|
+
'CHECK_AUTHOR_SESSION_REQUIRED',
|
|
6114
|
+
'author_session_id',
|
|
6115
|
+
);
|
|
6116
|
+
const parentSessionId = String(input.parent_session_id || '').trim();
|
|
6117
|
+
const fallbackReasonCode = String(input.fallback_reason_code || '').trim();
|
|
6118
|
+
const fallbackReason = String(input.fallback_reason || '').trim();
|
|
6119
|
+
|
|
6120
|
+
if (executionMode === 'subagent') {
|
|
6121
|
+
if (!parentSessionId) {
|
|
6122
|
+
fail('CHECK_PARENT_SESSION_REQUIRED: 子代理评审必须记录 parent_session_id');
|
|
6123
|
+
}
|
|
6124
|
+
if (!isMeaningfulCheckIdentity(input.reviewer_agent_id)
|
|
6125
|
+
|| !isMeaningfulCheckIdentity(parentSessionId)
|
|
6126
|
+
|| !isMeaningfulCheckIdentity(reviewSessionId)
|
|
6127
|
+
|| !isMeaningfulCheckIdentity(authorSessionId)) {
|
|
6128
|
+
fail('CHECK_REVIEW_IDENTITY_INVALID: 子代理评审必须记录真实且非占位的评审者、父会话、评审会话和作者会话身份');
|
|
6129
|
+
}
|
|
6130
|
+
if (reviewerIndependence !== 'independent-review'
|
|
6131
|
+
|| reviewSessionId === authorSessionId
|
|
6132
|
+
|| fallbackReasonCode
|
|
6133
|
+
|| fallbackReason) {
|
|
6134
|
+
fail('CHECK_REVIEW_METADATA_INVALID: 子代理评审必须具有独立评审标记、不同会话、评审者身份且不能带回退原因');
|
|
6135
|
+
}
|
|
6136
|
+
} else {
|
|
6137
|
+
if (!fallbackReasonCode || !fallbackReason) {
|
|
6138
|
+
fail('CHECK_FALLBACK_REASON_REQUIRED: 主 Agent 回退检查必须记录 fallback_reason_code 和 fallback_reason');
|
|
6139
|
+
}
|
|
6140
|
+
if (!CHECK_FALLBACK_REASON_CODES.has(fallbackReasonCode)) {
|
|
6141
|
+
fail(`CHECK_FALLBACK_REASON_INVALID: 非法回退原因 "${fallbackReasonCode}"`);
|
|
6142
|
+
}
|
|
6143
|
+
if (!['self-review', 'unknown'].includes(reviewerIndependence)) {
|
|
6144
|
+
fail('CHECK_REVIEW_METADATA_INVALID: 主 Agent 回退检查只能声明为 self-review 或 unknown');
|
|
6145
|
+
}
|
|
6146
|
+
}
|
|
6147
|
+
|
|
6148
|
+
return {
|
|
6149
|
+
...details,
|
|
6150
|
+
check_results: {
|
|
6151
|
+
...input,
|
|
6152
|
+
total,
|
|
6153
|
+
errors,
|
|
6154
|
+
warnings,
|
|
6155
|
+
suggestions,
|
|
6156
|
+
fixed_before_apply: fixedBeforeApply,
|
|
6157
|
+
categories,
|
|
6158
|
+
task_completion: taskCompletion,
|
|
6159
|
+
review_execution_mode: executionMode,
|
|
6160
|
+
reviewer_independence: reviewerIndependence,
|
|
6161
|
+
reviewer_agent_id: executionMode === 'subagent'
|
|
6162
|
+
? String(input.reviewer_agent_id).trim()
|
|
6163
|
+
: undefined,
|
|
6164
|
+
parent_session_id: parentSessionId || undefined,
|
|
6165
|
+
review_session_id: reviewSessionId,
|
|
6166
|
+
author_session_id: authorSessionId,
|
|
6167
|
+
fallback_reason_code: fallbackReasonCode || undefined,
|
|
6168
|
+
fallback_reason: fallbackReason || undefined,
|
|
6169
|
+
},
|
|
6170
|
+
};
|
|
6171
|
+
}
|
|
6172
|
+
|
|
6173
|
+
function validateFinalOutputSnapshot(input) {
|
|
6174
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
6175
|
+
return {
|
|
6176
|
+
ok: false,
|
|
6177
|
+
code: 'FINAL_OUTPUT_SNAPSHOT_REQUIRED',
|
|
6178
|
+
message: 'strict final_output_snapshot 缺少 details.final_output_snapshot',
|
|
6179
|
+
};
|
|
6180
|
+
}
|
|
6181
|
+
const snapshotKind = typeof input.snapshot_kind === 'string' ? input.snapshot_kind.trim() : '';
|
|
6182
|
+
if (!snapshotKind) {
|
|
6183
|
+
return {
|
|
6184
|
+
ok: false,
|
|
6185
|
+
code: 'FINAL_OUTPUT_SNAPSHOT_KIND_REQUIRED',
|
|
6186
|
+
message: 'strict 事件缺少 final_output_snapshot.snapshot_kind',
|
|
6187
|
+
};
|
|
6188
|
+
}
|
|
6189
|
+
if (snapshotKind !== 'final') {
|
|
6190
|
+
return {
|
|
6191
|
+
ok: false,
|
|
6192
|
+
code: 'FINAL_OUTPUT_SNAPSHOT_KIND_INVALID',
|
|
6193
|
+
message: `snapshot_kind 必须是 final,收到 "${snapshotKind}"`,
|
|
6194
|
+
};
|
|
6195
|
+
}
|
|
6196
|
+
const vcsMode = typeof input.vcs_mode === 'string' ? input.vcs_mode.trim() : '';
|
|
6197
|
+
if (!vcsMode) {
|
|
6198
|
+
return {
|
|
6199
|
+
ok: false,
|
|
6200
|
+
code: 'FINAL_OUTPUT_VCS_MODE_REQUIRED',
|
|
6201
|
+
message: 'strict 事件缺少 final_output_snapshot.vcs_mode',
|
|
6202
|
+
};
|
|
6203
|
+
}
|
|
6204
|
+
if (!['readonly', 'no-git'].includes(vcsMode)) {
|
|
6205
|
+
return {
|
|
6206
|
+
ok: false,
|
|
6207
|
+
code: 'FINAL_OUTPUT_VCS_MODE_INVALID',
|
|
6208
|
+
message: `vcs_mode 必须是 readonly 或 no-git,收到 "${vcsMode}"`,
|
|
6209
|
+
};
|
|
6210
|
+
}
|
|
6211
|
+
if (!Array.isArray(input.files)) {
|
|
6212
|
+
return {
|
|
6213
|
+
ok: false,
|
|
6214
|
+
code: 'FINAL_OUTPUT_PATH_INVALID',
|
|
6215
|
+
message: 'final_output_snapshot.files 必须显式提供数组;没有交付文件时请使用空数组并填写 no_output_reason',
|
|
6216
|
+
};
|
|
6217
|
+
}
|
|
6218
|
+
let files;
|
|
6219
|
+
try {
|
|
6220
|
+
files = normalizeRepositoryRelativeFiles(input.files, {
|
|
6221
|
+
invalidCode: 'FINAL_OUTPUT_PATH_INVALID',
|
|
6222
|
+
fieldName: 'final_output_snapshot.files',
|
|
6223
|
+
});
|
|
6224
|
+
} catch (error) {
|
|
6225
|
+
return {
|
|
6226
|
+
ok: false,
|
|
6227
|
+
code: 'FINAL_OUTPUT_PATH_INVALID',
|
|
6228
|
+
message: String(error?.message || error).replace(/^FINAL_OUTPUT_PATH_INVALID:\s*/, ''),
|
|
6229
|
+
};
|
|
6230
|
+
}
|
|
6231
|
+
const noOutputReason = String(input.no_output_reason || '').trim();
|
|
6232
|
+
if (files.length === 0 && !noOutputReason) {
|
|
6233
|
+
return {
|
|
6234
|
+
ok: false,
|
|
6235
|
+
code: 'FINAL_OUTPUT_FILES_OR_REASON_REQUIRED',
|
|
6236
|
+
message: '最终快照必须提供 files,或说明 no_output_reason',
|
|
6237
|
+
};
|
|
6238
|
+
}
|
|
6239
|
+
if (files.length > 0 && noOutputReason) {
|
|
6240
|
+
return {
|
|
6241
|
+
ok: false,
|
|
6242
|
+
code: 'FINAL_OUTPUT_REASON_CONFLICT',
|
|
6243
|
+
message: '有最终交付文件时不能同时填写 no_output_reason',
|
|
6244
|
+
};
|
|
6245
|
+
}
|
|
6246
|
+
const filesChanged = input.files_changed;
|
|
6247
|
+
if (typeof filesChanged !== 'number'
|
|
6248
|
+
|| !Number.isInteger(filesChanged)
|
|
6249
|
+
|| filesChanged < 0) {
|
|
6250
|
+
return {
|
|
6251
|
+
ok: false,
|
|
6252
|
+
code: 'FINAL_OUTPUT_FILES_CHANGED_INVALID',
|
|
6253
|
+
message: 'final_output_snapshot.files_changed 必须是大于等于 0 的整数',
|
|
6254
|
+
};
|
|
6255
|
+
}
|
|
6256
|
+
if (filesChanged !== files.length) {
|
|
6257
|
+
return {
|
|
6258
|
+
ok: false,
|
|
6259
|
+
code: 'FINAL_OUTPUT_FILE_COUNT_MISMATCH',
|
|
6260
|
+
message: `files_changed=${filesChanged} 与去重后的 files 数量 ${files.length} 不一致`,
|
|
6261
|
+
};
|
|
6262
|
+
}
|
|
6263
|
+
const normalizeLineCount = (fieldName, code) => {
|
|
6264
|
+
if (vcsMode === 'no-git' && (input[fieldName] === undefined || input[fieldName] === null)) {
|
|
6265
|
+
return { ok: true, value: null };
|
|
6266
|
+
}
|
|
6267
|
+
const value = input[fieldName];
|
|
6268
|
+
if (typeof value !== 'number' || !Number.isInteger(value) || value < 0) {
|
|
6269
|
+
return {
|
|
6270
|
+
ok: false,
|
|
6271
|
+
code,
|
|
6272
|
+
message: `final_output_snapshot.${fieldName} 必须是大于等于 0 的整数`,
|
|
6273
|
+
};
|
|
6274
|
+
}
|
|
6275
|
+
return { ok: true, value };
|
|
6276
|
+
};
|
|
6277
|
+
const addedLines = normalizeLineCount('added_lines', 'FINAL_OUTPUT_ADDED_LINES_INVALID');
|
|
6278
|
+
if (!addedLines.ok) return addedLines;
|
|
6279
|
+
const deletedLines = normalizeLineCount('deleted_lines', 'FINAL_OUTPUT_DELETED_LINES_INVALID');
|
|
6280
|
+
if (!deletedLines.ok) return deletedLines;
|
|
6281
|
+
return {
|
|
6282
|
+
ok: true,
|
|
6283
|
+
snapshot: {
|
|
6284
|
+
...input,
|
|
6285
|
+
snapshot_kind: snapshotKind,
|
|
6286
|
+
vcs_mode: vcsMode,
|
|
6287
|
+
files,
|
|
6288
|
+
files_changed: filesChanged,
|
|
6289
|
+
added_lines: addedLines.value,
|
|
6290
|
+
deleted_lines: deletedLines.value,
|
|
6291
|
+
no_output_reason: files.length === 0 ? noOutputReason : undefined,
|
|
6292
|
+
},
|
|
6293
|
+
};
|
|
6294
|
+
}
|
|
6295
|
+
|
|
6296
|
+
function normalizeStrictFinalOutputSnapshot(details) {
|
|
6297
|
+
const validation = validateFinalOutputSnapshot(details?.final_output_snapshot);
|
|
6298
|
+
if (!validation.ok) fail(`${validation.code}: ${validation.message}`);
|
|
6299
|
+
return {
|
|
6300
|
+
...details,
|
|
6301
|
+
final_output_snapshot: validation.snapshot,
|
|
6302
|
+
};
|
|
6303
|
+
}
|
|
6304
|
+
|
|
6305
|
+
/**
|
|
6306
|
+
* Best-effort per-file byte sizes for apply delivery snapshots.
|
|
6307
|
+
* Looks under projectRoot first, then its parent (split specs/code workspace).
|
|
6308
|
+
* Never fails the record path — missing files simply omit size entries.
|
|
6309
|
+
*/
|
|
6310
|
+
function attachFinalOutputFileSizes(details, projectRoot) {
|
|
6311
|
+
const snapshot = details?.final_output_snapshot;
|
|
6312
|
+
if (!snapshot || !Array.isArray(snapshot.files) || snapshot.files.length === 0) {
|
|
6313
|
+
return details;
|
|
6314
|
+
}
|
|
6315
|
+
const existing = (snapshot.file_sizes && typeof snapshot.file_sizes === 'object'
|
|
6316
|
+
&& !Array.isArray(snapshot.file_sizes))
|
|
6317
|
+
? { ...snapshot.file_sizes }
|
|
6318
|
+
: {};
|
|
6319
|
+
const roots = [];
|
|
6320
|
+
const pushRoot = (candidate) => {
|
|
6321
|
+
if (!candidate || typeof candidate !== 'string') return;
|
|
6322
|
+
const resolved = path.resolve(candidate);
|
|
6323
|
+
if (!roots.some((item) => item.toLowerCase?.() === resolved.toLowerCase() || item === resolved)) {
|
|
6324
|
+
roots.push(resolved);
|
|
6325
|
+
}
|
|
6326
|
+
};
|
|
6327
|
+
pushRoot(projectRoot);
|
|
6328
|
+
if (projectRoot) pushRoot(path.dirname(projectRoot));
|
|
6329
|
+
|
|
6330
|
+
let changed = false;
|
|
6331
|
+
for (const relative of snapshot.files) {
|
|
6332
|
+
const rel = String(relative || '').trim().replace(/\\/g, '/');
|
|
6333
|
+
if (!rel || Number.isInteger(existing[rel])) continue;
|
|
6334
|
+
for (const root of roots) {
|
|
6335
|
+
try {
|
|
6336
|
+
const absolute = path.resolve(root, ...rel.split('/'));
|
|
6337
|
+
const rootKey = process.platform === 'win32' ? root.toLowerCase() : root;
|
|
6338
|
+
const absKey = process.platform === 'win32' ? absolute.toLowerCase() : absolute;
|
|
6339
|
+
const prefix = rootKey.endsWith(path.sep) ? rootKey : `${rootKey}${path.sep}`;
|
|
6340
|
+
if (absKey !== rootKey && !absKey.startsWith(prefix)) continue;
|
|
6341
|
+
if (!fs.existsSync(absolute) || !fs.statSync(absolute).isFile()) continue;
|
|
6342
|
+
const bytes = fs.statSync(absolute).size;
|
|
6343
|
+
if (Number.isInteger(bytes) && bytes >= 0) {
|
|
6344
|
+
existing[rel] = bytes;
|
|
6345
|
+
changed = true;
|
|
6346
|
+
break;
|
|
6347
|
+
}
|
|
6348
|
+
} catch {
|
|
6349
|
+
// try next root
|
|
6350
|
+
}
|
|
6351
|
+
}
|
|
6352
|
+
}
|
|
6353
|
+
if (!changed && Object.keys(existing).length === 0) return details;
|
|
6354
|
+
return {
|
|
6355
|
+
...details,
|
|
6356
|
+
final_output_snapshot: {
|
|
6357
|
+
...snapshot,
|
|
6358
|
+
file_sizes: existing,
|
|
6359
|
+
},
|
|
6360
|
+
};
|
|
6361
|
+
}
|
|
6362
|
+
|
|
5696
6363
|
function normalizeStrictTestResult(args, details) {
|
|
5697
6364
|
const input = details?.test_results;
|
|
5698
6365
|
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
@@ -5979,6 +6646,20 @@ function normalizeStrictTaskUpdate(args, details, dataDir, change) {
|
|
|
5979
6646
|
|| details?.verification_reason
|
|
5980
6647
|
|| '',
|
|
5981
6648
|
).trim();
|
|
6649
|
+
const rawTaskFiles = input && Object.hasOwn(input, 'files') ? input.files : [];
|
|
6650
|
+
let taskFiles;
|
|
6651
|
+
try {
|
|
6652
|
+
taskFiles = normalizeRepositoryRelativeFiles(rawTaskFiles, {
|
|
6653
|
+
invalidCode: 'TASK_FILE_PATH_INVALID',
|
|
6654
|
+
fieldName: 'task_update.files',
|
|
6655
|
+
});
|
|
6656
|
+
} catch (error) {
|
|
6657
|
+
fail(String(error?.message || error));
|
|
6658
|
+
}
|
|
6659
|
+
const noFileChangeReason = String(input?.no_file_change_reason || '').trim();
|
|
6660
|
+
if (taskFiles.length === 0 && !noFileChangeReason) {
|
|
6661
|
+
fail('TASK_FILES_OR_REASON_REQUIRED: strict task_update 必须提供 files,或说明 no_file_change_reason');
|
|
6662
|
+
}
|
|
5982
6663
|
|
|
5983
6664
|
let resolvedTestEventId = testEventId;
|
|
5984
6665
|
let resolvedTestRunId = testRunId || undefined;
|
|
@@ -6031,6 +6712,8 @@ function normalizeStrictTaskUpdate(args, details, dataDir, change) {
|
|
|
6031
6712
|
|| (verificationNotApplicable ? verificationReason : undefined),
|
|
6032
6713
|
verification_not_applicable: verificationNotApplicable,
|
|
6033
6714
|
verification_not_applicable_reason: verificationNotApplicable ? verificationReason : undefined,
|
|
6715
|
+
files: taskFiles,
|
|
6716
|
+
no_file_change_reason: taskFiles.length === 0 ? noFileChangeReason : undefined,
|
|
6034
6717
|
},
|
|
6035
6718
|
},
|
|
6036
6719
|
};
|
|
@@ -6624,6 +7307,7 @@ function cmdRecord(args) {
|
|
|
6624
7307
|
'quality_gate_result',
|
|
6625
7308
|
'conformance_review',
|
|
6626
7309
|
'ai_adoption_review',
|
|
7310
|
+
'final_output_snapshot',
|
|
6627
7311
|
'survey_result',
|
|
6628
7312
|
'baseline_record',
|
|
6629
7313
|
'telemetry_warning',
|
|
@@ -6787,6 +7471,15 @@ function cmdRecord(args) {
|
|
|
6787
7471
|
strictTaskId = taskUpdate.taskId;
|
|
6788
7472
|
eventStatus = taskUpdate.status;
|
|
6789
7473
|
details = taskUpdate.details;
|
|
7474
|
+
} else if (strict && type === 'check_result') {
|
|
7475
|
+
strictRunId = requireStrictString(strictRunId, 'EVENT_RUN_ID_REQUIRED', 'run_id');
|
|
7476
|
+
details = normalizeStrictCheckResult(details, args.result);
|
|
7477
|
+
} else if (strict && type === 'final_output_snapshot') {
|
|
7478
|
+
strictRunId = requireStrictString(strictRunId, 'EVENT_RUN_ID_REQUIRED', 'run_id');
|
|
7479
|
+
details = attachFinalOutputFileSizes(
|
|
7480
|
+
normalizeStrictFinalOutputSnapshot(details),
|
|
7481
|
+
projectRoot,
|
|
7482
|
+
);
|
|
6790
7483
|
} else if (strict) {
|
|
6791
7484
|
strictRunId = requireStrictString(strictRunId, 'EVENT_RUN_ID_REQUIRED', 'run_id');
|
|
6792
7485
|
}
|
|
@@ -7521,6 +8214,33 @@ function cmdSemanticObserve(args) {
|
|
|
7521
8214
|
return observer;
|
|
7522
8215
|
}
|
|
7523
8216
|
|
|
8217
|
+
/**
|
|
8218
|
+
* kb-upload: 知识库上传(文件夹 / zip)
|
|
8219
|
+
* 委托给 skywalk-sdd/kb-upload.cjs 执行
|
|
8220
|
+
*/
|
|
8221
|
+
function cmdKbUpload(args) {
|
|
8222
|
+
// 将 args 还原为 --key=value 格式,传递给 kb-upload.cjs
|
|
8223
|
+
const passThrough = [];
|
|
8224
|
+
for (const [key, value] of Object.entries(args)) {
|
|
8225
|
+
if (value === true) {
|
|
8226
|
+
passThrough.push(`--${key}`);
|
|
8227
|
+
} else if (value !== undefined && value !== null && value !== false) {
|
|
8228
|
+
passThrough.push(`--${key}=${value}`);
|
|
8229
|
+
}
|
|
8230
|
+
}
|
|
8231
|
+
|
|
8232
|
+
const scriptPath = path.join(__dirname, 'kb-upload.cjs');
|
|
8233
|
+
try {
|
|
8234
|
+
const output = execFileSync(process.execPath, [scriptPath, ...passThrough], {
|
|
8235
|
+
encoding: 'utf8',
|
|
8236
|
+
stdio: 'inherit',
|
|
8237
|
+
});
|
|
8238
|
+
return output;
|
|
8239
|
+
} catch (err) {
|
|
8240
|
+
process.exitCode = 1;
|
|
8241
|
+
}
|
|
8242
|
+
}
|
|
8243
|
+
|
|
7524
8244
|
/**
|
|
7525
8245
|
* log archive-docs: 真实归档 Simple/Full spec,结束 archive 阶段并生成最终报告
|
|
7526
8246
|
*/
|
|
@@ -7716,6 +8436,9 @@ function main() {
|
|
|
7716
8436
|
case 'check-task':
|
|
7717
8437
|
cmdCheckTask(flags);
|
|
7718
8438
|
break;
|
|
8439
|
+
case 'kb-upload':
|
|
8440
|
+
cmdKbUpload(flags);
|
|
8441
|
+
break;
|
|
7719
8442
|
default:
|
|
7720
8443
|
showHelp();
|
|
7721
8444
|
process.exit(1);
|
|
@@ -7734,6 +8457,8 @@ SDD Telemetry CLI - 流程度量采集工具
|
|
|
7734
8457
|
node skywalk-sdd/log.cjs tasks-status --project=<path> --change=<name> [--require-complete]
|
|
7735
8458
|
node skywalk-sdd/log.cjs check-task --project=<path> --change=<name> --task-id=<id>
|
|
7736
8459
|
node skywalk-sdd/log.cjs archive-docs --project=<path> --change=<name> [--reason=<text>] [--event-id=<id>] [--report-output=<file>]
|
|
8460
|
+
node skywalk-sdd/log.cjs kb-upload --folder=<path> [--minimum-level=propose-spec] [--validate-only] [--project=<spec-root>]
|
|
8461
|
+
node skywalk-sdd/log.cjs kb-upload --package=<zip-path> [--project=<spec-root>]
|
|
7737
8462
|
node skywalk-sdd/log.cjs semantic-identity --delta-state=added
|
|
7738
8463
|
node skywalk-sdd/log.cjs semantic-identity --delta-state=modified --entity-id=<uuid> --predecessor-version=<uuid>
|
|
7739
8464
|
node skywalk-sdd/log.cjs semantic-identity --delta-state=unchanged --entity-id=<uuid> --version-id=<uuid>
|
|
@@ -7752,6 +8477,7 @@ SDD Telemetry CLI - 流程度量采集工具
|
|
|
7752
8477
|
tasks-status 扫描 Full/Simple 模式 tasks.md 勾选状态
|
|
7753
8478
|
check-task 扫描变更目录 tasks.md 并勾选指定 task_id
|
|
7754
8479
|
archive-docs 将 Simple/Full spec 变更真实移动到 openspec/changes/archive/,并可结束 archive 阶段生成报告
|
|
8480
|
+
kb-upload 知识库上传(文件夹自动打包 / zip 直传),支持随时入库
|
|
7755
8481
|
semantic-identity 生成或复用实体/版本 ID(8 位十六进制);无需网络和中央 ID 服务
|
|
7756
8482
|
semantic-scan 只读解析并校验当前 Change
|
|
7757
8483
|
semantic-reconcile 全量对账文件与本地工作态本体实例
|
|
@@ -7775,6 +8501,8 @@ SDD Telemetry CLI - 流程度量采集工具
|
|
|
7775
8501
|
node skywalk-sdd/log.cjs check-task --project=/my/project --change=user-auth --task-id=TASK-01
|
|
7776
8502
|
node skywalk-sdd/log.cjs archive-docs --project=/my/project --change=user-auth --reason="变更已完成实施" --event-id=evt_archive
|
|
7777
8503
|
# archive-docs 最终报告默认生成到 openspec/changes/archive/<日期>-<change>/reports/<change>-report.md;可用 --report-output=<路径> 指定自定义路径
|
|
8504
|
+
node skywalk-sdd/log.cjs kb-upload --folder=openspec/changes/user-auth --minimum-level=propose-spec --project=/my/project
|
|
8505
|
+
node skywalk-sdd/log.cjs kb-upload --package=openspec/changes/archive/2026-07-31-user-auth.zip --project=/my/project
|
|
7778
8506
|
node skywalk-sdd/log.cjs record --type=worktree_finish --command=apply --project=/my/project --change=user-auth --capability=user-auth --result=success --summary="merge+remove completed"
|
|
7779
8507
|
|
|
7780
8508
|
Apply worktree(主仓库根目录,非 log.cjs 子命令):
|