kld-sdd 2.6.16 → 2.6.17
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/lib/init.js +65 -6
- package/lib/skills-bundle.js +20 -1
- package/lib/tool-profiles.js +8 -0
- package/package.json +5 -1
- package/skywalk-sdd/apply-worktree-finish.cjs +2 -23
- package/skywalk-sdd/context-client.cjs +38 -87
- package/skywalk-sdd/index.cjs +719 -108
- package/skywalk-sdd/kb-sync-identity.cjs +780 -0
- package/skywalk-sdd/kb-upload.cjs +505 -0
- package/skywalk-sdd/lib/shared.cjs +663 -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/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 +48 -16
- package/templates/skills/kld-sdd/opsx-check/checklist.md +4 -2
- 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 +164 -0
- package/templates/skills/kld-sdd/opsx-kb-config/reference.md +116 -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,
|
|
@@ -1905,7 +1887,9 @@ function summarizeStageExecutions(events) {
|
|
|
1905
1887
|
start,
|
|
1906
1888
|
end,
|
|
1907
1889
|
result: end?.result || null,
|
|
1908
|
-
duration_ms: Number.isFinite(end?.duration_ms)
|
|
1890
|
+
duration_ms: Number.isFinite(end?.duration_ms) && end.duration_ms >= 0
|
|
1891
|
+
? end.duration_ms
|
|
1892
|
+
: null,
|
|
1909
1893
|
canonical: false,
|
|
1910
1894
|
rework_reason: null,
|
|
1911
1895
|
};
|
|
@@ -1935,7 +1919,7 @@ function summarizeStageExecutions(events) {
|
|
|
1935
1919
|
if (canonical) {
|
|
1936
1920
|
canonical.canonical = true;
|
|
1937
1921
|
canonicalAttempts.push(canonical);
|
|
1938
|
-
effectiveStageDurationMs += canonical.duration_ms;
|
|
1922
|
+
effectiveStageDurationMs += Number.isFinite(canonical.duration_ms) ? canonical.duration_ms : 0;
|
|
1939
1923
|
}
|
|
1940
1924
|
|
|
1941
1925
|
for (const attempt of groupAttempts) {
|
|
@@ -1967,8 +1951,9 @@ function summarizeStageExecutions(events) {
|
|
|
1967
1951
|
if (attempt.end) {
|
|
1968
1952
|
completedReworkAttempts += 1;
|
|
1969
1953
|
bucket.completed_rework_attempts += 1;
|
|
1970
|
-
|
|
1971
|
-
|
|
1954
|
+
const reworkDurationMs = Number.isFinite(attempt.duration_ms) ? attempt.duration_ms : 0;
|
|
1955
|
+
bucket.rework_duration_ms += reworkDurationMs;
|
|
1956
|
+
reworkStageDurationMs += reworkDurationMs;
|
|
1972
1957
|
} else {
|
|
1973
1958
|
supersededOpenStages += 1;
|
|
1974
1959
|
bucket.superseded_open_stages += 1;
|
|
@@ -2250,9 +2235,14 @@ function getCheckResults(event) {
|
|
|
2250
2235
|
categories: results.categories || {},
|
|
2251
2236
|
// V3 Q3: reviewer 元信息,用于评估 reviewer_independence/score_status(缺失时保持 null,不影响旧字段)
|
|
2252
2237
|
reviewer: results.reviewer || null,
|
|
2238
|
+
review_execution_mode: results.review_execution_mode || null,
|
|
2239
|
+
reviewer_agent_id: results.reviewer_agent_id || null,
|
|
2253
2240
|
review_session_id: results.review_session_id || null,
|
|
2241
|
+
reviewer_session_id: results.reviewer_session_id || null,
|
|
2254
2242
|
author_session_id: results.author_session_id || null,
|
|
2255
2243
|
reviewer_independence: results.reviewer_independence || null,
|
|
2244
|
+
fallback_reason_code: results.fallback_reason_code || null,
|
|
2245
|
+
fallback_reason: results.fallback_reason || null,
|
|
2256
2246
|
warning_items: Array.isArray(results.warning_items) ? results.warning_items : [],
|
|
2257
2247
|
warning_dispositions: Array.isArray(results.warning_dispositions) ? results.warning_dispositions : [],
|
|
2258
2248
|
};
|
|
@@ -2967,7 +2957,20 @@ function computeSinglePdfMvpMetrics(events, options = {}) {
|
|
|
2967
2957
|
? new Date(archiveEnd.timestamp).getTime() - new Date(proposeStart.timestamp).getTime()
|
|
2968
2958
|
: null;
|
|
2969
2959
|
|
|
2970
|
-
|
|
2960
|
+
// E1 只在全部纳入加总的已结束阶段都有合法耗时时发布:
|
|
2961
|
+
// 没有配对记录或任一耗时缺失都代表“没有足够证据”,不能解释为真实耗时为 0。
|
|
2962
|
+
const e1DurationAttempts = [
|
|
2963
|
+
...executionSummary.canonicalAttempts,
|
|
2964
|
+
...executionSummary.reworkAttempts.filter(attempt => attempt.end),
|
|
2965
|
+
];
|
|
2966
|
+
const hasCompleteE1DurationEvidence = e1DurationAttempts.length > 0
|
|
2967
|
+
&& e1DurationAttempts.every(attempt => (
|
|
2968
|
+
Number.isFinite(attempt.end?.duration_ms)
|
|
2969
|
+
&& attempt.end.duration_ms >= 0
|
|
2970
|
+
));
|
|
2971
|
+
const totalStageDuration = hasCompleteE1DurationEvidence
|
|
2972
|
+
? executionSummary.summary.total_stage_duration_ms
|
|
2973
|
+
: null;
|
|
2971
2974
|
const codingDuration = sumDurations(ends, ['apply']);
|
|
2972
2975
|
const specDuration = sumDurations(ends, ['propose', 'spec', 'design', 'task']);
|
|
2973
2976
|
const reworkCodingDuration = sumAttemptDurations(executionSummary.reworkAttempts, ['apply']);
|
|
@@ -3020,6 +3023,26 @@ function computeSinglePdfMvpMetrics(events, options = {}) {
|
|
|
3020
3023
|
&& timestampMs(attempt.end) <= timestampMs(firstApply)
|
|
3021
3024
|
))
|
|
3022
3025
|
: null;
|
|
3026
|
+
const qualifyingCheckAttempts = firstApply
|
|
3027
|
+
? executionSummary.attempts.filter(attempt => (
|
|
3028
|
+
attempt.command === 'check'
|
|
3029
|
+
&& attempt.end
|
|
3030
|
+
&& ['success', 'partial'].includes(attempt.result)
|
|
3031
|
+
&& timestampMs(attempt.end) <= timestampMs(firstApply)
|
|
3032
|
+
))
|
|
3033
|
+
: [];
|
|
3034
|
+
const p4Evidence = {
|
|
3035
|
+
first_apply_started_at: firstApply?.timestamp || null,
|
|
3036
|
+
first_apply_event_id: firstApply?.event_id || null,
|
|
3037
|
+
qualifying_check_count: qualifyingCheckAttempts.length,
|
|
3038
|
+
qualifying_checks: qualifyingCheckAttempts.map(attempt => ({
|
|
3039
|
+
result: attempt.result,
|
|
3040
|
+
started_at: attempt.start?.timestamp || null,
|
|
3041
|
+
ended_at: attempt.end?.timestamp || null,
|
|
3042
|
+
start_event_id: attempt.start?.event_id || null,
|
|
3043
|
+
end_event_id: attempt.end?.event_id || null,
|
|
3044
|
+
})),
|
|
3045
|
+
};
|
|
3023
3046
|
const latestCheckWithFixRate = latestByTimestamp(checkEvents.filter(e => {
|
|
3024
3047
|
const results = getCheckResults(e);
|
|
3025
3048
|
return results?.total > 0 && results.fixed_before_apply != null;
|
|
@@ -3049,8 +3072,13 @@ function computeSinglePdfMvpMetrics(events, options = {}) {
|
|
|
3049
3072
|
reviewer: latestCheckResultsV3?.reviewer || null,
|
|
3050
3073
|
author: formalApplyAttempt?.start?.session_id || formalApplyAttempt?.start?.agent_type || null,
|
|
3051
3074
|
reviewer_independence: latestCheckResultsV3?.reviewer_independence || null,
|
|
3075
|
+
review_execution_mode: latestCheckResultsV3?.review_execution_mode || null,
|
|
3076
|
+
reviewer_agent_id: latestCheckResultsV3?.reviewer_agent_id || null,
|
|
3052
3077
|
review_session_id: latestCheckResultsV3?.review_session_id || null,
|
|
3078
|
+
reviewer_session_id: latestCheckResultsV3?.reviewer_session_id || null,
|
|
3053
3079
|
author_session_id: latestCheckResultsV3?.author_session_id || formalApplyAttempt?.start?.session_id || null,
|
|
3080
|
+
fallback_reason_code: latestCheckResultsV3?.fallback_reason_code || null,
|
|
3081
|
+
fallback_reason: latestCheckResultsV3?.fallback_reason || null,
|
|
3054
3082
|
};
|
|
3055
3083
|
const q3Result = metricsV3.computeSpecQualityScore(latestCheckResultsV3, checkMeta);
|
|
3056
3084
|
|
|
@@ -3074,11 +3102,21 @@ function computeSinglePdfMvpMetrics(events, options = {}) {
|
|
|
3074
3102
|
completed_stages: completedStages,
|
|
3075
3103
|
missing: missingRequiredStages,
|
|
3076
3104
|
};
|
|
3077
|
-
const pHTelemetryHealthScore =
|
|
3078
|
-
?
|
|
3079
|
-
:
|
|
3105
|
+
const pHTelemetryHealthScore = measurementEvents.length === 0
|
|
3106
|
+
? null
|
|
3107
|
+
: (Number.isFinite(telemetryHealth.telemetry_health_score_percent)
|
|
3108
|
+
? telemetryHealth.telemetry_health_score_percent
|
|
3109
|
+
: (Number.isFinite(telemetryHealth.telemetry_health_score)
|
|
3110
|
+
? metricsV3.round(telemetryHealth.telemetry_health_score * 100, 2)
|
|
3111
|
+
: null));
|
|
3080
3112
|
|
|
3081
|
-
const
|
|
3113
|
+
const compactedReworkSummary = compactReworkSummary(executionSummary.summary, reworkReasons);
|
|
3114
|
+
const reworkSummary = executionSummary.attempts.length === 0
|
|
3115
|
+
? {
|
|
3116
|
+
...compactedReworkSummary,
|
|
3117
|
+
total_rework_attempts: null,
|
|
3118
|
+
}
|
|
3119
|
+
: compactedReworkSummary;
|
|
3082
3120
|
|
|
3083
3121
|
const v3Context = buildV3ChangeContext(scopedEvents, executionSummary, {
|
|
3084
3122
|
change_name: options.change || null,
|
|
@@ -3117,6 +3155,7 @@ function computeSinglePdfMvpMetrics(events, options = {}) {
|
|
|
3117
3155
|
: (completeMeasurementJourney ? 'trusted' : 'provisional'),
|
|
3118
3156
|
reason: e1AvailabilityReason || incompleteJourneyReason,
|
|
3119
3157
|
source_event_count: measurementEvents.length,
|
|
3158
|
+
event_ids: measurementEvents.map(event => event.event_id).filter(Boolean),
|
|
3120
3159
|
},
|
|
3121
3160
|
q3: {
|
|
3122
3161
|
status: v3.quality.q3_score_status === 'verified'
|
|
@@ -3125,6 +3164,7 @@ function computeSinglePdfMvpMetrics(events, options = {}) {
|
|
|
3125
3164
|
: (v3.quality.q3_availability ? 'provisional' : 'missing'),
|
|
3126
3165
|
reason: v3.quality.q3_availability_reason,
|
|
3127
3166
|
source_event_count: checkEvents.length,
|
|
3167
|
+
event_ids: checkEvents.map(event => event.event_id).filter(Boolean),
|
|
3128
3168
|
},
|
|
3129
3169
|
p4: {
|
|
3130
3170
|
status: !firstApply
|
|
@@ -3132,6 +3172,10 @@ function computeSinglePdfMvpMetrics(events, options = {}) {
|
|
|
3132
3172
|
: (completeMeasurementJourney ? 'trusted' : 'provisional'),
|
|
3133
3173
|
reason: firstApply ? incompleteJourneyReason : 'missing-apply-stage',
|
|
3134
3174
|
source_event_count: executionSummary.attempts.filter(attempt => attempt.command === 'check').length,
|
|
3175
|
+
event_ids: [
|
|
3176
|
+
p4Evidence.first_apply_event_id,
|
|
3177
|
+
...p4Evidence.qualifying_checks.flatMap(item => [item.start_event_id, item.end_event_id]),
|
|
3178
|
+
].filter(Boolean),
|
|
3135
3179
|
},
|
|
3136
3180
|
p_r: {
|
|
3137
3181
|
status: executionSummary.attempts.length === 0
|
|
@@ -3139,6 +3183,9 @@ function computeSinglePdfMvpMetrics(events, options = {}) {
|
|
|
3139
3183
|
: (completeMeasurementJourney ? 'trusted' : 'provisional'),
|
|
3140
3184
|
reason: executionSummary.attempts.length > 0 ? incompleteJourneyReason : 'missing-stage-events',
|
|
3141
3185
|
source_event_count: executionSummary.attempts.length,
|
|
3186
|
+
event_ids: executionSummary.attempts
|
|
3187
|
+
.flatMap(attempt => [attempt.start?.event_id, attempt.end?.event_id])
|
|
3188
|
+
.filter(Boolean),
|
|
3142
3189
|
},
|
|
3143
3190
|
p_h: {
|
|
3144
3191
|
status: measurementEvents.length === 0
|
|
@@ -3146,6 +3193,7 @@ function computeSinglePdfMvpMetrics(events, options = {}) {
|
|
|
3146
3193
|
: (completeMeasurementJourney ? 'trusted' : 'provisional'),
|
|
3147
3194
|
reason: measurementEvents.length > 0 ? incompleteJourneyReason : 'missing-stage-events',
|
|
3148
3195
|
source_event_count: measurementEvents.length,
|
|
3196
|
+
event_ids: measurementEvents.map(event => event.event_id).filter(Boolean),
|
|
3149
3197
|
},
|
|
3150
3198
|
};
|
|
3151
3199
|
|
|
@@ -3167,7 +3215,7 @@ function computeSinglePdfMvpMetrics(events, options = {}) {
|
|
|
3167
3215
|
? Math.max(0, leadTime - executionSummary.summary.effective_stage_duration_ms)
|
|
3168
3216
|
: null,
|
|
3169
3217
|
rework_stage_duration_ms: executionSummary.summary.rework_stage_duration_ms,
|
|
3170
|
-
total_stage_duration_ms:
|
|
3218
|
+
total_stage_duration_ms: totalStageDuration,
|
|
3171
3219
|
measurement_stage_duration_ms: totalStageDuration,
|
|
3172
3220
|
e2_coding_time_ratio_including_rework: executionSummary.summary.total_stage_duration_ms > 0
|
|
3173
3221
|
? roundMetric((codingDuration + reworkCodingDuration) / executionSummary.summary.total_stage_duration_ms)
|
|
@@ -3199,6 +3247,11 @@ function computeSinglePdfMvpMetrics(events, options = {}) {
|
|
|
3199
3247
|
q3_score_status: v3.quality.q3_score_status,
|
|
3200
3248
|
q3_reviewer_independence: v3.quality.q3_reviewer_independence,
|
|
3201
3249
|
q3_category_scores: v3.quality.q3_category_scores,
|
|
3250
|
+
q3_total: v3.quality.q3_total,
|
|
3251
|
+
q3_errors: v3.quality.q3_errors,
|
|
3252
|
+
q3_warnings: v3.quality.q3_warnings,
|
|
3253
|
+
q3_suggestions: v3.quality.q3_suggestions,
|
|
3254
|
+
q3_review: v3.quality.q3_review,
|
|
3202
3255
|
q3_availability: v3.quality.q3_availability,
|
|
3203
3256
|
// Q1/Q4 上游信号未就绪时用 pending-upstream 标注,不把 null 误转成 0
|
|
3204
3257
|
q1_status: v3.quality.q1_spec_conformance.status,
|
|
@@ -3211,6 +3264,7 @@ function computeSinglePdfMvpMetrics(events, options = {}) {
|
|
|
3211
3264
|
p2_ai_code_adoption_level: aiAdoptionMetrics.adoption_level,
|
|
3212
3265
|
ai_adoption_counts: aiAdoptionMetrics.adoption_counts,
|
|
3213
3266
|
p4_quality_gate_enforcement_rate: qualityGateRate,
|
|
3267
|
+
p4_evidence: p4Evidence,
|
|
3214
3268
|
p4b_check_fixed_rate: fixRate,
|
|
3215
3269
|
git_document_metrics: gitDocumentMetrics,
|
|
3216
3270
|
rework_summary: reworkSummary,
|
|
@@ -3354,9 +3408,11 @@ function computePdfMvpMetrics(events, options = {}) {
|
|
|
3354
3408
|
const health = computeTelemetryHealthMetrics(events);
|
|
3355
3409
|
return {
|
|
3356
3410
|
...health,
|
|
3357
|
-
p_h_telemetry_health_score: Number.isFinite(health.
|
|
3358
|
-
?
|
|
3359
|
-
:
|
|
3411
|
+
p_h_telemetry_health_score: Number.isFinite(health.telemetry_health_score_percent)
|
|
3412
|
+
? health.telemetry_health_score_percent
|
|
3413
|
+
: (Number.isFinite(health.telemetry_health_score)
|
|
3414
|
+
? metricsV3.round(health.telemetry_health_score * 100, 2)
|
|
3415
|
+
: null),
|
|
3360
3416
|
};
|
|
3361
3417
|
})(),
|
|
3362
3418
|
by_change: byChange,
|
|
@@ -4247,6 +4303,17 @@ function fileListSymmetricDiff(left, right) {
|
|
|
4247
4303
|
|
|
4248
4304
|
// ── U1 报告维度提取辅助:从 events/metrics/doctor/archive 提炼 5 维度,数据缺失诚实标注 _available: false ──
|
|
4249
4305
|
function extractChangedFiles(events, projectRoot) {
|
|
4306
|
+
const snapshotEvents = events.filter(event => (
|
|
4307
|
+
event?.type === 'final_output_snapshot'
|
|
4308
|
+
&& event.details?.final_output_snapshot
|
|
4309
|
+
&& typeof event.details.final_output_snapshot === 'object'
|
|
4310
|
+
));
|
|
4311
|
+
const latestSnapshotEvent = latestByTimestamp(snapshotEvents);
|
|
4312
|
+
const snapshotValidation = latestSnapshotEvent
|
|
4313
|
+
? validateFinalOutputSnapshot(latestSnapshotEvent.details.final_output_snapshot)
|
|
4314
|
+
: null;
|
|
4315
|
+
const finalSnapshot = snapshotValidation?.ok ? snapshotValidation.snapshot : null;
|
|
4316
|
+
const invalidFinalSnapshot = Boolean(latestSnapshotEvent && !snapshotValidation?.ok);
|
|
4250
4317
|
const adoptionEvents = getAiAdoptionEvents(events);
|
|
4251
4318
|
const finalAdoptionEvents = adoptionEvents.filter((event) => {
|
|
4252
4319
|
const review = getAiAdoptionReview(event);
|
|
@@ -4258,25 +4325,47 @@ function extractChangedFiles(events, projectRoot) {
|
|
|
4258
4325
|
finalAdoptionEvents.length > 0 ? finalAdoptionEvents : adoptionEvents,
|
|
4259
4326
|
);
|
|
4260
4327
|
const review = latest ? getAiAdoptionReview(latest) : null;
|
|
4261
|
-
const aiDiff = review?.ai_diff || null;
|
|
4328
|
+
const aiDiff = latestSnapshotEvent ? null : (review?.ai_diff || null);
|
|
4262
4329
|
const taskUpdateFiles = new Set();
|
|
4330
|
+
const taskEventIds = [];
|
|
4331
|
+
const taskIds = [];
|
|
4332
|
+
const noFileChangeReasons = [];
|
|
4333
|
+
let taskEventLatestAt = null;
|
|
4263
4334
|
for (const e of events) {
|
|
4264
4335
|
if (e?.type !== 'task_update') continue;
|
|
4265
|
-
const
|
|
4336
|
+
const taskUpdate = e.details?.task_update || {};
|
|
4337
|
+
const filesField = taskUpdate.files
|
|
4338
|
+
|| e.details?.files
|
|
4266
4339
|
|| e.details?.test_results?.files
|
|
4267
4340
|
|| (Array.isArray(e.details?.files_changed) ? e.details.files_changed : null);
|
|
4268
|
-
|
|
4341
|
+
const reason = taskUpdate.no_file_change_reason || e.details?.no_file_change_reason;
|
|
4342
|
+
if (typeof reason === 'string' && reason.trim()) {
|
|
4343
|
+
noFileChangeReasons.push({
|
|
4344
|
+
task_id: e.task_id || taskUpdate.task_id || null,
|
|
4345
|
+
event_id: e.event_id || null,
|
|
4346
|
+
reason: reason.trim(),
|
|
4347
|
+
});
|
|
4348
|
+
}
|
|
4349
|
+
if (Array.isArray(filesField) && filesField.length > 0) {
|
|
4350
|
+
if (e.event_id) taskEventIds.push(e.event_id);
|
|
4351
|
+
const taskId = e.task_id || taskUpdate.task_id;
|
|
4352
|
+
if (taskId) taskIds.push(taskId);
|
|
4353
|
+
if (!taskEventLatestAt || timestampMs(e) > Date.parse(taskEventLatestAt)) {
|
|
4354
|
+
taskEventLatestAt = e.timestamp || null;
|
|
4355
|
+
}
|
|
4269
4356
|
for (const f of filesField) {
|
|
4270
4357
|
if (typeof f === 'string' && f.trim()) taskUpdateFiles.add(f.trim());
|
|
4271
4358
|
}
|
|
4272
4359
|
}
|
|
4273
4360
|
}
|
|
4274
4361
|
const finalOutputFiles = new Set(
|
|
4275
|
-
Array.isArray(
|
|
4276
|
-
?
|
|
4277
|
-
:
|
|
4362
|
+
Array.isArray(finalSnapshot?.files)
|
|
4363
|
+
? finalSnapshot.files.filter(file => typeof file === 'string' && file.trim()).map(file => file.trim())
|
|
4364
|
+
: (Array.isArray(aiDiff?.files)
|
|
4365
|
+
? aiDiff.files.filter(file => typeof file === 'string' && file.trim()).map(file => file.trim())
|
|
4366
|
+
: [])
|
|
4278
4367
|
);
|
|
4279
|
-
if (finalOutputFiles.size === 0 && latest?.details) {
|
|
4368
|
+
if (!finalSnapshot && finalOutputFiles.size === 0 && latest?.details) {
|
|
4280
4369
|
const rawReview = latest.details.ai_adoption
|
|
4281
4370
|
|| latest.details.ai_adoption_review
|
|
4282
4371
|
|| latest.details.ai_code_adoption
|
|
@@ -4290,23 +4379,40 @@ function extractChangedFiles(events, projectRoot) {
|
|
|
4290
4379
|
|
|
4291
4380
|
const taskEventFileList = sortedUniqueFileList([...taskUpdateFiles]);
|
|
4292
4381
|
const finalOutputFileList = sortedUniqueFileList([...finalOutputFiles]);
|
|
4293
|
-
const
|
|
4294
|
-
const
|
|
4295
|
-
|
|
4296
|
-
|
|
4297
|
-
|
|
4298
|
-
|
|
4382
|
+
const taskEvidenceAvailable = taskEventFileList.length > 0 || noFileChangeReasons.length > 0;
|
|
4383
|
+
const taskUpdateFileCount = taskEvidenceAvailable ? taskEventFileList.length : null;
|
|
4384
|
+
const finalOutputFileCount = finalSnapshot
|
|
4385
|
+
? (Number.isFinite(finalSnapshot.files_changed)
|
|
4386
|
+
? finalSnapshot.files_changed
|
|
4387
|
+
: finalOutputFileList.length)
|
|
4388
|
+
: (Number.isFinite(aiDiff?.files_changed)
|
|
4389
|
+
? aiDiff.files_changed
|
|
4390
|
+
: (finalOutputFileList.length > 0 ? finalOutputFileList.length : null));
|
|
4391
|
+
const snapshotKind = finalSnapshot
|
|
4392
|
+
? finalSnapshot.snapshot_kind
|
|
4393
|
+
: (latestSnapshotEvent
|
|
4394
|
+
? null
|
|
4395
|
+
: (aiDiff?.snapshot_kind
|
|
4396
|
+
|| ((review?.review_status === 'final' || latest?.status === 'final') ? 'final' : null)));
|
|
4299
4397
|
const finalOutputStatus = snapshotKind === 'final'
|
|
4300
4398
|
? 'final'
|
|
4301
4399
|
: (finalOutputFileCount == null ? 'unavailable' : 'provisional');
|
|
4302
|
-
const addedLines =
|
|
4303
|
-
?
|
|
4304
|
-
: (Number.isFinite(
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
|
|
4400
|
+
const addedLines = finalSnapshot
|
|
4401
|
+
? (Number.isFinite(finalSnapshot.added_lines) ? finalSnapshot.added_lines : null)
|
|
4402
|
+
: (Number.isFinite(aiDiff?.added_lines)
|
|
4403
|
+
? aiDiff.added_lines
|
|
4404
|
+
: (Number.isFinite(review?.final_diff?.added_lines) ? review.final_diff.added_lines : null));
|
|
4405
|
+
const deletedLines = finalSnapshot
|
|
4406
|
+
? (Number.isFinite(finalSnapshot.deleted_lines) ? finalSnapshot.deleted_lines : null)
|
|
4407
|
+
: (Number.isFinite(aiDiff?.deleted_lines)
|
|
4408
|
+
? aiDiff.deleted_lines
|
|
4409
|
+
: (Number.isFinite(review?.final_diff?.deleted_lines) ? review.final_diff.deleted_lines : null));
|
|
4308
4410
|
let lineCountEstimate = null;
|
|
4309
|
-
if (
|
|
4411
|
+
if (!finalSnapshot
|
|
4412
|
+
&& addedLines == null
|
|
4413
|
+
&& review?.ai_git_sha == null
|
|
4414
|
+
&& finalOutputFileList.length > 0
|
|
4415
|
+
&& projectRoot) {
|
|
4310
4416
|
let lines = 0;
|
|
4311
4417
|
let countedFiles = 0;
|
|
4312
4418
|
const root = path.resolve(projectRoot);
|
|
@@ -4331,27 +4437,26 @@ function extractChangedFiles(events, projectRoot) {
|
|
|
4331
4437
|
};
|
|
4332
4438
|
}
|
|
4333
4439
|
}
|
|
4334
|
-
//
|
|
4335
|
-
const filesChanged = finalOutputStatus === 'final'
|
|
4336
|
-
|
|
4337
|
-
: (taskUpdateFileCount > 0 ? taskUpdateFileCount : finalOutputFileCount);
|
|
4338
|
-
const countsDiffer = taskUpdateFileCount > 0
|
|
4440
|
+
// 对外的 files_changed 只代表最终实际交付;任务事件文件始终使用独立字段。
|
|
4441
|
+
const filesChanged = finalOutputStatus === 'final' ? finalOutputFileCount : null;
|
|
4442
|
+
const countsDiffer = taskUpdateFileCount != null
|
|
4339
4443
|
&& finalOutputFileCount != null
|
|
4340
4444
|
&& taskUpdateFileCount !== finalOutputFileCount;
|
|
4341
4445
|
const listDiff = fileListSymmetricDiff(taskEventFileList, finalOutputFileList);
|
|
4342
|
-
const listsDiffer = taskUpdateFileCount
|
|
4343
|
-
&&
|
|
4446
|
+
const listsDiffer = taskUpdateFileCount != null
|
|
4447
|
+
&& finalOutputFileCount != null
|
|
4344
4448
|
&& (listDiff.only_in_task_events.length > 0 || listDiff.only_in_final_output.length > 0);
|
|
4345
4449
|
let reconciliationStatus = 'unavailable';
|
|
4346
4450
|
let reconciliationNote = '没有可用的文件变更证据。';
|
|
4347
|
-
if (taskUpdateFileCount
|
|
4451
|
+
if (taskUpdateFileCount == null && finalOutputFileCount == null) {
|
|
4348
4452
|
reconciliationStatus = 'unavailable';
|
|
4349
4453
|
reconciliationNote = '没有可用的文件变更证据。';
|
|
4350
|
-
} else if (
|
|
4351
|
-
reconciliationStatus = '
|
|
4352
|
-
reconciliationNote =
|
|
4353
|
-
|
|
4354
|
-
|
|
4454
|
+
} else if (finalOutputFileCount == null) {
|
|
4455
|
+
reconciliationStatus = 'final-snapshot-missing';
|
|
4456
|
+
reconciliationNote = '最终输出文件快照不可用,无法与任务事件对账。';
|
|
4457
|
+
} else if (taskUpdateFileCount == null) {
|
|
4458
|
+
reconciliationStatus = 'task-evidence-missing';
|
|
4459
|
+
reconciliationNote = '任务事件未记录文件清单或无文件原因;仅展示最终输出快照。';
|
|
4355
4460
|
} else if (countsDiffer) {
|
|
4356
4461
|
reconciliationStatus = 'count-mismatch';
|
|
4357
4462
|
reconciliationNote = `任务事件累计涉及 ${taskUpdateFileCount} 个文件;最终输出快照包含 ${finalOutputFileCount} 个文件。两者用途不同,不应合并为一个口径。`;
|
|
@@ -4363,17 +4468,37 @@ function extractChangedFiles(events, projectRoot) {
|
|
|
4363
4468
|
reconciliationNote = '任务事件与最终输出文件口径一致。';
|
|
4364
4469
|
}
|
|
4365
4470
|
return {
|
|
4366
|
-
_available: filesChanged != null,
|
|
4471
|
+
_available: filesChanged != null || taskUpdateFileCount != null,
|
|
4367
4472
|
files_changed: filesChanged,
|
|
4368
4473
|
added_lines: addedLines,
|
|
4369
4474
|
deleted_lines: deletedLines,
|
|
4370
4475
|
files: finalOutputFileList.length > 0 ? finalOutputFileList : null,
|
|
4371
|
-
task_event_files: taskUpdateFileCount
|
|
4372
|
-
task_event_files_changed: taskUpdateFileCount
|
|
4476
|
+
task_event_files: taskUpdateFileCount != null ? taskEventFileList : null,
|
|
4477
|
+
task_event_files_changed: taskUpdateFileCount,
|
|
4478
|
+
task_event_source: 'task_update.files',
|
|
4479
|
+
task_event_task_ids: [...new Set(taskIds)],
|
|
4480
|
+
task_event_ids: [...new Set(taskEventIds)],
|
|
4481
|
+
task_event_latest_at: taskEventLatestAt,
|
|
4482
|
+
no_file_change_reasons: noFileChangeReasons,
|
|
4373
4483
|
final_output_files: finalOutputFileList.length > 0 ? finalOutputFileList : null,
|
|
4374
4484
|
final_output_files_changed: finalOutputFileCount,
|
|
4375
4485
|
final_output_status: finalOutputStatus,
|
|
4376
|
-
|
|
4486
|
+
final_output_source: finalSnapshot
|
|
4487
|
+
? 'final_output_snapshot.files'
|
|
4488
|
+
: (snapshotKind === 'final' ? 'ai-diff-final(旧事件兼容)' : null),
|
|
4489
|
+
final_output_event_id: latestSnapshotEvent?.event_id || latest?.event_id || null,
|
|
4490
|
+
final_output_captured_at: latestSnapshotEvent?.timestamp || latest?.timestamp || null,
|
|
4491
|
+
final_output_no_output_reason: finalSnapshot?.no_output_reason || null,
|
|
4492
|
+
final_output_vcs_mode: finalSnapshot?.vcs_mode || null,
|
|
4493
|
+
final_output_validation_status: invalidFinalSnapshot ? 'invalid' : (finalSnapshot ? 'valid' : 'unavailable'),
|
|
4494
|
+
final_output_validation_reason: invalidFinalSnapshot
|
|
4495
|
+
? `${snapshotValidation.code}: ${snapshotValidation.message}`
|
|
4496
|
+
: null,
|
|
4497
|
+
diff_source: finalSnapshot
|
|
4498
|
+
? 'final-output-snapshot'
|
|
4499
|
+
: (invalidFinalSnapshot
|
|
4500
|
+
? 'invalid-final-output-snapshot'
|
|
4501
|
+
: (snapshotKind === 'final' ? 'ai-diff-final' : (aiDiff ? 'ai-diff-provisional' : 'task-events'))),
|
|
4377
4502
|
line_count_estimate: lineCountEstimate,
|
|
4378
4503
|
reconciliation_status: reconciliationStatus,
|
|
4379
4504
|
reconciliation_note: reconciliationNote,
|
|
@@ -4434,6 +4559,8 @@ function extractStageTimeline(events) {
|
|
|
4434
4559
|
return {
|
|
4435
4560
|
stage: cmd,
|
|
4436
4561
|
round,
|
|
4562
|
+
start_event_id: start.event_id || null,
|
|
4563
|
+
end_event_id: end?.event_id || null,
|
|
4437
4564
|
start_ts: start.timestamp || null,
|
|
4438
4565
|
end_ts: end?.timestamp || null,
|
|
4439
4566
|
duration_ms: end && Number.isFinite(end.duration_ms) ? end.duration_ms : null,
|
|
@@ -5141,11 +5268,14 @@ function computeDoctorReport(events, options = {}) {
|
|
|
5141
5268
|
outdatedSchemaEvents.length * 0.06 +
|
|
5142
5269
|
missingChangeEvents.length * 0.04 +
|
|
5143
5270
|
missingCapabilityEvents.length * 0.01;
|
|
5144
|
-
const
|
|
5145
|
-
|
|
5271
|
+
const telemetryHealth = metricsV3.computeTelemetryHealthScore({
|
|
5272
|
+
weighted_issue_value: penalty,
|
|
5273
|
+
stage_event_count: stageEvents.length,
|
|
5274
|
+
});
|
|
5146
5275
|
|
|
5147
5276
|
return {
|
|
5148
|
-
telemetry_health_score:
|
|
5277
|
+
telemetry_health_score: telemetryHealth.ratio,
|
|
5278
|
+
telemetry_health_score_percent: telemetryHealth.score,
|
|
5149
5279
|
matched_stage_rate: matchedStageRate == null ? null : Math.round(matchedStageRate * 100) / 100,
|
|
5150
5280
|
total_events: scopedEvents.length,
|
|
5151
5281
|
stage_events: stageEvents.length,
|
|
@@ -5173,19 +5303,8 @@ function computeDoctorReport(events, options = {}) {
|
|
|
5173
5303
|
|
|
5174
5304
|
// ── CLI 参数解析 ──────────────────────────────────────
|
|
5175
5305
|
|
|
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
|
-
}
|
|
5306
|
+
// parseArgs 已迁移至 lib/shared.cjs(统一 5 份实现)
|
|
5307
|
+
// 本文件顶部已通过 require 引入,此处保留 const 别名以兼容内部引用
|
|
5189
5308
|
|
|
5190
5309
|
// ── CLI 命令实现 ─────────────────────────────────────
|
|
5191
5310
|
|
|
@@ -5348,7 +5467,21 @@ function cmdEnd(args, options = {}) {
|
|
|
5348
5467
|
fail('E_CHANGE_REQUIRED: 无法解析 start 的 end 必须显式提供 --change');
|
|
5349
5468
|
}
|
|
5350
5469
|
if (startEvent && requestedChange && startEvent.change !== requestedChange) {
|
|
5351
|
-
|
|
5470
|
+
// BUGFIX: 允许 start 使用占位 change(如 pending)时 end 指定真实 change-key 自动迁移
|
|
5471
|
+
// 场景:propose 阶段初期不知道最终 change-key,start 使用 --change=pending 占位,
|
|
5472
|
+
// 后期生成 change-key 后 end 使用真实名称。此时应允许并自动迁移事件文件。
|
|
5473
|
+
const PLACEHOLDER_CHANGES = new Set(['pending']);
|
|
5474
|
+
if (PLACEHOLDER_CHANGES.has(startEvent.change)) {
|
|
5475
|
+
// 将 start 事件复制到新的 change 文件下,保持 event_id 连续性
|
|
5476
|
+
if (process.env.SDD_DEBUG_TELEMETRY) {
|
|
5477
|
+
console.error(`[telemetry] migrating start event from placeholder "${startEvent.change}" → "${requestedChange}" (event_id=${eventId})`);
|
|
5478
|
+
}
|
|
5479
|
+
appendEvent(dataDir, requestedChange, startEvent, { lockHeld: false });
|
|
5480
|
+
// 更新内存中的 startEvent.change 供后续使用
|
|
5481
|
+
startEvent = { ...startEvent, change: requestedChange };
|
|
5482
|
+
} else {
|
|
5483
|
+
fail(`E_CHANGE_CONFLICT: end 指定 change "${requestedChange}",但 active stage 属于 "${startEvent.change}"`);
|
|
5484
|
+
}
|
|
5352
5485
|
}
|
|
5353
5486
|
// stage_end 幂等性保护:同一 event_id 已存在 stage_end 时,结果相同则跳过,不同则更新(archive 命令只要存在即跳过)
|
|
5354
5487
|
const command = startEvent?.command || args.command || 'unknown';
|
|
@@ -5693,6 +5826,426 @@ function strictInteger(value, code, fieldName, minimum = 0) {
|
|
|
5693
5826
|
return number;
|
|
5694
5827
|
}
|
|
5695
5828
|
|
|
5829
|
+
function strictStructuredInteger(value, code, fieldName, minimum = 0) {
|
|
5830
|
+
if (!Number.isInteger(value) || value < minimum) {
|
|
5831
|
+
fail(`${code}: ${fieldName} 必须是大于等于 ${minimum} 的整数`);
|
|
5832
|
+
}
|
|
5833
|
+
return value;
|
|
5834
|
+
}
|
|
5835
|
+
|
|
5836
|
+
const CHECK_FALLBACK_REASON_CODES = new Set([
|
|
5837
|
+
'SUBAGENT_CAPABILITY_UNAVAILABLE',
|
|
5838
|
+
'SUBAGENT_PERMISSION_DENIED',
|
|
5839
|
+
'SUBAGENT_START_FAILED',
|
|
5840
|
+
'SUBAGENT_TIMEOUT',
|
|
5841
|
+
'SUBAGENT_RESULT_INVALID',
|
|
5842
|
+
]);
|
|
5843
|
+
|
|
5844
|
+
const CHECK_IDENTITY_PLACEHOLDERS = new Set([
|
|
5845
|
+
'unknown',
|
|
5846
|
+
'none',
|
|
5847
|
+
'null',
|
|
5848
|
+
'n/a',
|
|
5849
|
+
'na',
|
|
5850
|
+
'未记录',
|
|
5851
|
+
'未知',
|
|
5852
|
+
]);
|
|
5853
|
+
|
|
5854
|
+
function isMeaningfulCheckIdentity(value) {
|
|
5855
|
+
const normalized = String(value || '').trim();
|
|
5856
|
+
if (!normalized) return false;
|
|
5857
|
+
if (CHECK_IDENTITY_PLACEHOLDERS.has(normalized.toLowerCase())) return false;
|
|
5858
|
+
if (/^<[^>]+>$/.test(normalized) || /^\[[^\]]+\]$/.test(normalized)) return false;
|
|
5859
|
+
return true;
|
|
5860
|
+
}
|
|
5861
|
+
|
|
5862
|
+
function normalizeRepositoryRelativeFiles(files, {
|
|
5863
|
+
requiredCode,
|
|
5864
|
+
invalidCode,
|
|
5865
|
+
fieldName,
|
|
5866
|
+
} = {}) {
|
|
5867
|
+
if (!Array.isArray(files)) {
|
|
5868
|
+
const code = requiredCode || invalidCode || 'REPOSITORY_FILE_PATH_INVALID';
|
|
5869
|
+
throw new Error(`${code}: ${fieldName || 'files'} 必须是数组`);
|
|
5870
|
+
}
|
|
5871
|
+
if (files.length === 0) {
|
|
5872
|
+
if (requiredCode) {
|
|
5873
|
+
throw new Error(`${requiredCode}: ${fieldName || 'files'} 必须是非空数组`);
|
|
5874
|
+
}
|
|
5875
|
+
return [];
|
|
5876
|
+
}
|
|
5877
|
+
const normalized = [];
|
|
5878
|
+
for (const value of files) {
|
|
5879
|
+
if (typeof value !== 'string') {
|
|
5880
|
+
throw new Error(`${invalidCode || 'REPOSITORY_FILE_PATH_INVALID'}: ${fieldName || 'files'} 只能包含字符串路径`);
|
|
5881
|
+
}
|
|
5882
|
+
const raw = value.trim().replace(/\\/g, '/');
|
|
5883
|
+
const segments = raw.split('/');
|
|
5884
|
+
if (!raw
|
|
5885
|
+
|| path.isAbsolute(raw)
|
|
5886
|
+
|| /^[A-Za-z]:/.test(raw)
|
|
5887
|
+
|| segments.includes('..')
|
|
5888
|
+
|| segments.includes('.')
|
|
5889
|
+
|| raw.startsWith('/')) {
|
|
5890
|
+
throw new Error(`${invalidCode || 'REPOSITORY_FILE_PATH_INVALID'}: 非法仓库相对路径 "${raw || String(value)}"`);
|
|
5891
|
+
}
|
|
5892
|
+
normalized.push(raw);
|
|
5893
|
+
}
|
|
5894
|
+
return [...new Set(normalized)].sort();
|
|
5895
|
+
}
|
|
5896
|
+
|
|
5897
|
+
function normalizeStrictCheckResult(details, result) {
|
|
5898
|
+
const input = details?.check_results;
|
|
5899
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
5900
|
+
fail('CHECK_RESULT_REQUIRED: strict check_result 缺少 details.check_results');
|
|
5901
|
+
}
|
|
5902
|
+
const total = strictStructuredInteger(
|
|
5903
|
+
input.total,
|
|
5904
|
+
'CHECK_RESULT_TOTAL_REQUIRED',
|
|
5905
|
+
'check_results.total',
|
|
5906
|
+
1,
|
|
5907
|
+
);
|
|
5908
|
+
const errors = strictStructuredInteger(
|
|
5909
|
+
input.errors,
|
|
5910
|
+
'CHECK_RESULT_ERRORS_REQUIRED',
|
|
5911
|
+
'check_results.errors',
|
|
5912
|
+
);
|
|
5913
|
+
if (errors > total) {
|
|
5914
|
+
fail('CHECK_RESULT_ERRORS_INVALID: check_results.errors 不能大于 check_results.total');
|
|
5915
|
+
}
|
|
5916
|
+
const warnings = strictStructuredInteger(
|
|
5917
|
+
input.warnings,
|
|
5918
|
+
'CHECK_RESULT_WARNINGS_REQUIRED',
|
|
5919
|
+
'check_results.warnings',
|
|
5920
|
+
);
|
|
5921
|
+
const suggestions = strictStructuredInteger(
|
|
5922
|
+
input.suggestions,
|
|
5923
|
+
'CHECK_RESULT_SUGGESTIONS_REQUIRED',
|
|
5924
|
+
'check_results.suggestions',
|
|
5925
|
+
);
|
|
5926
|
+
const fixedBeforeApply = strictStructuredInteger(
|
|
5927
|
+
input.fixed_before_apply,
|
|
5928
|
+
'CHECK_RESULT_FIXED_REQUIRED',
|
|
5929
|
+
'check_results.fixed_before_apply',
|
|
5930
|
+
);
|
|
5931
|
+
if (fixedBeforeApply > total) {
|
|
5932
|
+
fail('CHECK_RESULT_FIXED_INVALID: fixed_before_apply 不能大于 check_results.total');
|
|
5933
|
+
}
|
|
5934
|
+
const categoryNames = [
|
|
5935
|
+
'completeness',
|
|
5936
|
+
'consistency',
|
|
5937
|
+
'executability',
|
|
5938
|
+
'tdd_compliance',
|
|
5939
|
+
];
|
|
5940
|
+
const categoryInput = input.categories;
|
|
5941
|
+
if (!categoryInput || typeof categoryInput !== 'object' || Array.isArray(categoryInput)) {
|
|
5942
|
+
fail('CHECK_RESULT_CATEGORIES_REQUIRED: strict check_result 缺少 check_results.categories');
|
|
5943
|
+
}
|
|
5944
|
+
const categories = {};
|
|
5945
|
+
let categoryTotalSum = 0;
|
|
5946
|
+
let categoryErrorSum = 0;
|
|
5947
|
+
for (const categoryName of categoryNames) {
|
|
5948
|
+
const category = categoryInput[categoryName];
|
|
5949
|
+
if (!category || typeof category !== 'object' || Array.isArray(category)) {
|
|
5950
|
+
fail(`CHECK_RESULT_CATEGORY_REQUIRED: check_results.categories.${categoryName} 必须是对象`);
|
|
5951
|
+
}
|
|
5952
|
+
const categoryTotal = strictStructuredInteger(
|
|
5953
|
+
category.total,
|
|
5954
|
+
'CHECK_RESULT_CATEGORY_TOTAL_REQUIRED',
|
|
5955
|
+
`check_results.categories.${categoryName}.total`,
|
|
5956
|
+
);
|
|
5957
|
+
const categoryPassed = strictStructuredInteger(
|
|
5958
|
+
category.passed,
|
|
5959
|
+
'CHECK_RESULT_CATEGORY_PASSED_REQUIRED',
|
|
5960
|
+
`check_results.categories.${categoryName}.passed`,
|
|
5961
|
+
);
|
|
5962
|
+
if (categoryPassed > categoryTotal) {
|
|
5963
|
+
fail(`CHECK_RESULT_CATEGORY_INVALID: ${categoryName}.passed 不能大于 total`);
|
|
5964
|
+
}
|
|
5965
|
+
categories[categoryName] = {
|
|
5966
|
+
...category,
|
|
5967
|
+
passed: categoryPassed,
|
|
5968
|
+
total: categoryTotal,
|
|
5969
|
+
};
|
|
5970
|
+
categoryTotalSum += categoryTotal;
|
|
5971
|
+
categoryErrorSum += categoryTotal - categoryPassed;
|
|
5972
|
+
}
|
|
5973
|
+
if (categoryTotalSum !== total) {
|
|
5974
|
+
fail('CHECK_RESULT_CATEGORY_TOTAL_MISMATCH: 四类 categories.total 合计必须等于 check_results.total');
|
|
5975
|
+
}
|
|
5976
|
+
if (categoryErrorSum !== errors) {
|
|
5977
|
+
fail('CHECK_RESULT_CATEGORY_ERRORS_MISMATCH: 四类未通过数合计必须等于 check_results.errors');
|
|
5978
|
+
}
|
|
5979
|
+
const taskInput = input.task_completion;
|
|
5980
|
+
if (!taskInput || typeof taskInput !== 'object' || Array.isArray(taskInput)) {
|
|
5981
|
+
fail('CHECK_RESULT_TASK_COMPLETION_REQUIRED: strict check_result 缺少 check_results.task_completion');
|
|
5982
|
+
}
|
|
5983
|
+
const taskCompleted = strictStructuredInteger(
|
|
5984
|
+
taskInput.completed,
|
|
5985
|
+
'CHECK_RESULT_TASK_COMPLETED_REQUIRED',
|
|
5986
|
+
'check_results.task_completion.completed',
|
|
5987
|
+
);
|
|
5988
|
+
const taskIncomplete = strictStructuredInteger(
|
|
5989
|
+
taskInput.incomplete,
|
|
5990
|
+
'CHECK_RESULT_TASK_INCOMPLETE_REQUIRED',
|
|
5991
|
+
'check_results.task_completion.incomplete',
|
|
5992
|
+
);
|
|
5993
|
+
const taskTotal = strictStructuredInteger(
|
|
5994
|
+
taskInput.total,
|
|
5995
|
+
'CHECK_RESULT_TASK_TOTAL_REQUIRED',
|
|
5996
|
+
'check_results.task_completion.total',
|
|
5997
|
+
);
|
|
5998
|
+
if (taskCompleted + taskIncomplete !== taskTotal) {
|
|
5999
|
+
fail('CHECK_RESULT_TASK_COUNTS_INVALID: completed + incomplete 必须等于 task_completion.total');
|
|
6000
|
+
}
|
|
6001
|
+
if (typeof taskInput.has_incomplete !== 'boolean'
|
|
6002
|
+
|| taskInput.has_incomplete !== (taskIncomplete > 0)) {
|
|
6003
|
+
fail('CHECK_RESULT_TASK_FLAG_INVALID: has_incomplete 必须与 incomplete 是否大于 0 一致');
|
|
6004
|
+
}
|
|
6005
|
+
if (typeof taskInput.checked_for_archive_readiness !== 'boolean') {
|
|
6006
|
+
fail('CHECK_RESULT_ARCHIVE_READINESS_REQUIRED: checked_for_archive_readiness 必须是布尔值');
|
|
6007
|
+
}
|
|
6008
|
+
const normalizedResult = requireStrictString(
|
|
6009
|
+
result,
|
|
6010
|
+
'CHECK_RESULT_STATUS_REQUIRED',
|
|
6011
|
+
'result',
|
|
6012
|
+
);
|
|
6013
|
+
if (!['success', 'partial', 'failure'].includes(normalizedResult)) {
|
|
6014
|
+
fail(`CHECK_RESULT_STATUS_INVALID: 非法 check_result.result "${normalizedResult}"`);
|
|
6015
|
+
}
|
|
6016
|
+
if (taskIncomplete > 0 && normalizedResult === 'success') {
|
|
6017
|
+
fail('CHECK_RESULT_TASK_STATUS_INVALID: 存在未完成任务时 check_result.result 不能是 success');
|
|
6018
|
+
}
|
|
6019
|
+
const warningItems = input.warning_items;
|
|
6020
|
+
if (taskIncomplete > 0 && (
|
|
6021
|
+
!Array.isArray(warningItems)
|
|
6022
|
+
|| !warningItems.some(item => item?.category === 'task_completion')
|
|
6023
|
+
)) {
|
|
6024
|
+
fail('CHECK_RESULT_TASK_WARNING_REQUIRED: 存在未完成任务时 warning_items 必须包含 task_completion 明细');
|
|
6025
|
+
}
|
|
6026
|
+
const taskCompletion = {
|
|
6027
|
+
...taskInput,
|
|
6028
|
+
completed: taskCompleted,
|
|
6029
|
+
incomplete: taskIncomplete,
|
|
6030
|
+
total: taskTotal,
|
|
6031
|
+
has_incomplete: taskInput.has_incomplete,
|
|
6032
|
+
checked_for_archive_readiness: taskInput.checked_for_archive_readiness,
|
|
6033
|
+
};
|
|
6034
|
+
const executionMode = requireStrictString(
|
|
6035
|
+
input.review_execution_mode,
|
|
6036
|
+
'CHECK_REVIEW_EXECUTION_MODE_REQUIRED',
|
|
6037
|
+
'review_execution_mode',
|
|
6038
|
+
);
|
|
6039
|
+
if (!['subagent', 'main-agent-fallback'].includes(executionMode)) {
|
|
6040
|
+
fail(`CHECK_REVIEW_EXECUTION_MODE_INVALID: 非法 review_execution_mode "${executionMode}"`);
|
|
6041
|
+
}
|
|
6042
|
+
const reviewerIndependence = requireStrictString(
|
|
6043
|
+
input.reviewer_independence,
|
|
6044
|
+
'CHECK_REVIEW_INDEPENDENCE_REQUIRED',
|
|
6045
|
+
'reviewer_independence',
|
|
6046
|
+
);
|
|
6047
|
+
if (!['independent-review', 'self-review', 'unknown'].includes(reviewerIndependence)) {
|
|
6048
|
+
fail(`CHECK_REVIEW_INDEPENDENCE_INVALID: 非法 reviewer_independence "${reviewerIndependence}"`);
|
|
6049
|
+
}
|
|
6050
|
+
const reviewSessionId = requireStrictString(
|
|
6051
|
+
input.review_session_id || input.reviewer_session_id,
|
|
6052
|
+
'CHECK_REVIEW_SESSION_REQUIRED',
|
|
6053
|
+
'review_session_id',
|
|
6054
|
+
);
|
|
6055
|
+
const authorSessionId = requireStrictString(
|
|
6056
|
+
input.author_session_id,
|
|
6057
|
+
'CHECK_AUTHOR_SESSION_REQUIRED',
|
|
6058
|
+
'author_session_id',
|
|
6059
|
+
);
|
|
6060
|
+
const parentSessionId = String(input.parent_session_id || '').trim();
|
|
6061
|
+
const fallbackReasonCode = String(input.fallback_reason_code || '').trim();
|
|
6062
|
+
const fallbackReason = String(input.fallback_reason || '').trim();
|
|
6063
|
+
|
|
6064
|
+
if (executionMode === 'subagent') {
|
|
6065
|
+
if (!parentSessionId) {
|
|
6066
|
+
fail('CHECK_PARENT_SESSION_REQUIRED: 子代理评审必须记录 parent_session_id');
|
|
6067
|
+
}
|
|
6068
|
+
if (!isMeaningfulCheckIdentity(input.reviewer_agent_id)
|
|
6069
|
+
|| !isMeaningfulCheckIdentity(parentSessionId)
|
|
6070
|
+
|| !isMeaningfulCheckIdentity(reviewSessionId)
|
|
6071
|
+
|| !isMeaningfulCheckIdentity(authorSessionId)) {
|
|
6072
|
+
fail('CHECK_REVIEW_IDENTITY_INVALID: 子代理评审必须记录真实且非占位的评审者、父会话、评审会话和作者会话身份');
|
|
6073
|
+
}
|
|
6074
|
+
if (reviewerIndependence !== 'independent-review'
|
|
6075
|
+
|| reviewSessionId === authorSessionId
|
|
6076
|
+
|| fallbackReasonCode
|
|
6077
|
+
|| fallbackReason) {
|
|
6078
|
+
fail('CHECK_REVIEW_METADATA_INVALID: 子代理评审必须具有独立评审标记、不同会话、评审者身份且不能带回退原因');
|
|
6079
|
+
}
|
|
6080
|
+
} else {
|
|
6081
|
+
if (!fallbackReasonCode || !fallbackReason) {
|
|
6082
|
+
fail('CHECK_FALLBACK_REASON_REQUIRED: 主 Agent 回退检查必须记录 fallback_reason_code 和 fallback_reason');
|
|
6083
|
+
}
|
|
6084
|
+
if (!CHECK_FALLBACK_REASON_CODES.has(fallbackReasonCode)) {
|
|
6085
|
+
fail(`CHECK_FALLBACK_REASON_INVALID: 非法回退原因 "${fallbackReasonCode}"`);
|
|
6086
|
+
}
|
|
6087
|
+
if (!['self-review', 'unknown'].includes(reviewerIndependence)) {
|
|
6088
|
+
fail('CHECK_REVIEW_METADATA_INVALID: 主 Agent 回退检查只能声明为 self-review 或 unknown');
|
|
6089
|
+
}
|
|
6090
|
+
}
|
|
6091
|
+
|
|
6092
|
+
return {
|
|
6093
|
+
...details,
|
|
6094
|
+
check_results: {
|
|
6095
|
+
...input,
|
|
6096
|
+
total,
|
|
6097
|
+
errors,
|
|
6098
|
+
warnings,
|
|
6099
|
+
suggestions,
|
|
6100
|
+
fixed_before_apply: fixedBeforeApply,
|
|
6101
|
+
categories,
|
|
6102
|
+
task_completion: taskCompletion,
|
|
6103
|
+
review_execution_mode: executionMode,
|
|
6104
|
+
reviewer_independence: reviewerIndependence,
|
|
6105
|
+
reviewer_agent_id: executionMode === 'subagent'
|
|
6106
|
+
? String(input.reviewer_agent_id).trim()
|
|
6107
|
+
: undefined,
|
|
6108
|
+
parent_session_id: parentSessionId || undefined,
|
|
6109
|
+
review_session_id: reviewSessionId,
|
|
6110
|
+
author_session_id: authorSessionId,
|
|
6111
|
+
fallback_reason_code: fallbackReasonCode || undefined,
|
|
6112
|
+
fallback_reason: fallbackReason || undefined,
|
|
6113
|
+
},
|
|
6114
|
+
};
|
|
6115
|
+
}
|
|
6116
|
+
|
|
6117
|
+
function validateFinalOutputSnapshot(input) {
|
|
6118
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
6119
|
+
return {
|
|
6120
|
+
ok: false,
|
|
6121
|
+
code: 'FINAL_OUTPUT_SNAPSHOT_REQUIRED',
|
|
6122
|
+
message: 'strict final_output_snapshot 缺少 details.final_output_snapshot',
|
|
6123
|
+
};
|
|
6124
|
+
}
|
|
6125
|
+
const snapshotKind = typeof input.snapshot_kind === 'string' ? input.snapshot_kind.trim() : '';
|
|
6126
|
+
if (!snapshotKind) {
|
|
6127
|
+
return {
|
|
6128
|
+
ok: false,
|
|
6129
|
+
code: 'FINAL_OUTPUT_SNAPSHOT_KIND_REQUIRED',
|
|
6130
|
+
message: 'strict 事件缺少 final_output_snapshot.snapshot_kind',
|
|
6131
|
+
};
|
|
6132
|
+
}
|
|
6133
|
+
if (snapshotKind !== 'final') {
|
|
6134
|
+
return {
|
|
6135
|
+
ok: false,
|
|
6136
|
+
code: 'FINAL_OUTPUT_SNAPSHOT_KIND_INVALID',
|
|
6137
|
+
message: `snapshot_kind 必须是 final,收到 "${snapshotKind}"`,
|
|
6138
|
+
};
|
|
6139
|
+
}
|
|
6140
|
+
const vcsMode = typeof input.vcs_mode === 'string' ? input.vcs_mode.trim() : '';
|
|
6141
|
+
if (!vcsMode) {
|
|
6142
|
+
return {
|
|
6143
|
+
ok: false,
|
|
6144
|
+
code: 'FINAL_OUTPUT_VCS_MODE_REQUIRED',
|
|
6145
|
+
message: 'strict 事件缺少 final_output_snapshot.vcs_mode',
|
|
6146
|
+
};
|
|
6147
|
+
}
|
|
6148
|
+
if (!['readonly', 'no-git'].includes(vcsMode)) {
|
|
6149
|
+
return {
|
|
6150
|
+
ok: false,
|
|
6151
|
+
code: 'FINAL_OUTPUT_VCS_MODE_INVALID',
|
|
6152
|
+
message: `vcs_mode 必须是 readonly 或 no-git,收到 "${vcsMode}"`,
|
|
6153
|
+
};
|
|
6154
|
+
}
|
|
6155
|
+
if (!Array.isArray(input.files)) {
|
|
6156
|
+
return {
|
|
6157
|
+
ok: false,
|
|
6158
|
+
code: 'FINAL_OUTPUT_PATH_INVALID',
|
|
6159
|
+
message: 'final_output_snapshot.files 必须显式提供数组;没有交付文件时请使用空数组并填写 no_output_reason',
|
|
6160
|
+
};
|
|
6161
|
+
}
|
|
6162
|
+
let files;
|
|
6163
|
+
try {
|
|
6164
|
+
files = normalizeRepositoryRelativeFiles(input.files, {
|
|
6165
|
+
invalidCode: 'FINAL_OUTPUT_PATH_INVALID',
|
|
6166
|
+
fieldName: 'final_output_snapshot.files',
|
|
6167
|
+
});
|
|
6168
|
+
} catch (error) {
|
|
6169
|
+
return {
|
|
6170
|
+
ok: false,
|
|
6171
|
+
code: 'FINAL_OUTPUT_PATH_INVALID',
|
|
6172
|
+
message: String(error?.message || error).replace(/^FINAL_OUTPUT_PATH_INVALID:\s*/, ''),
|
|
6173
|
+
};
|
|
6174
|
+
}
|
|
6175
|
+
const noOutputReason = String(input.no_output_reason || '').trim();
|
|
6176
|
+
if (files.length === 0 && !noOutputReason) {
|
|
6177
|
+
return {
|
|
6178
|
+
ok: false,
|
|
6179
|
+
code: 'FINAL_OUTPUT_FILES_OR_REASON_REQUIRED',
|
|
6180
|
+
message: '最终快照必须提供 files,或说明 no_output_reason',
|
|
6181
|
+
};
|
|
6182
|
+
}
|
|
6183
|
+
if (files.length > 0 && noOutputReason) {
|
|
6184
|
+
return {
|
|
6185
|
+
ok: false,
|
|
6186
|
+
code: 'FINAL_OUTPUT_REASON_CONFLICT',
|
|
6187
|
+
message: '有最终交付文件时不能同时填写 no_output_reason',
|
|
6188
|
+
};
|
|
6189
|
+
}
|
|
6190
|
+
const filesChanged = input.files_changed;
|
|
6191
|
+
if (typeof filesChanged !== 'number'
|
|
6192
|
+
|| !Number.isInteger(filesChanged)
|
|
6193
|
+
|| filesChanged < 0) {
|
|
6194
|
+
return {
|
|
6195
|
+
ok: false,
|
|
6196
|
+
code: 'FINAL_OUTPUT_FILES_CHANGED_INVALID',
|
|
6197
|
+
message: 'final_output_snapshot.files_changed 必须是大于等于 0 的整数',
|
|
6198
|
+
};
|
|
6199
|
+
}
|
|
6200
|
+
if (filesChanged !== files.length) {
|
|
6201
|
+
return {
|
|
6202
|
+
ok: false,
|
|
6203
|
+
code: 'FINAL_OUTPUT_FILE_COUNT_MISMATCH',
|
|
6204
|
+
message: `files_changed=${filesChanged} 与去重后的 files 数量 ${files.length} 不一致`,
|
|
6205
|
+
};
|
|
6206
|
+
}
|
|
6207
|
+
const normalizeLineCount = (fieldName, code) => {
|
|
6208
|
+
if (vcsMode === 'no-git' && (input[fieldName] === undefined || input[fieldName] === null)) {
|
|
6209
|
+
return { ok: true, value: null };
|
|
6210
|
+
}
|
|
6211
|
+
const value = input[fieldName];
|
|
6212
|
+
if (typeof value !== 'number' || !Number.isInteger(value) || value < 0) {
|
|
6213
|
+
return {
|
|
6214
|
+
ok: false,
|
|
6215
|
+
code,
|
|
6216
|
+
message: `final_output_snapshot.${fieldName} 必须是大于等于 0 的整数`,
|
|
6217
|
+
};
|
|
6218
|
+
}
|
|
6219
|
+
return { ok: true, value };
|
|
6220
|
+
};
|
|
6221
|
+
const addedLines = normalizeLineCount('added_lines', 'FINAL_OUTPUT_ADDED_LINES_INVALID');
|
|
6222
|
+
if (!addedLines.ok) return addedLines;
|
|
6223
|
+
const deletedLines = normalizeLineCount('deleted_lines', 'FINAL_OUTPUT_DELETED_LINES_INVALID');
|
|
6224
|
+
if (!deletedLines.ok) return deletedLines;
|
|
6225
|
+
return {
|
|
6226
|
+
ok: true,
|
|
6227
|
+
snapshot: {
|
|
6228
|
+
...input,
|
|
6229
|
+
snapshot_kind: snapshotKind,
|
|
6230
|
+
vcs_mode: vcsMode,
|
|
6231
|
+
files,
|
|
6232
|
+
files_changed: filesChanged,
|
|
6233
|
+
added_lines: addedLines.value,
|
|
6234
|
+
deleted_lines: deletedLines.value,
|
|
6235
|
+
no_output_reason: files.length === 0 ? noOutputReason : undefined,
|
|
6236
|
+
},
|
|
6237
|
+
};
|
|
6238
|
+
}
|
|
6239
|
+
|
|
6240
|
+
function normalizeStrictFinalOutputSnapshot(details) {
|
|
6241
|
+
const validation = validateFinalOutputSnapshot(details?.final_output_snapshot);
|
|
6242
|
+
if (!validation.ok) fail(`${validation.code}: ${validation.message}`);
|
|
6243
|
+
return {
|
|
6244
|
+
...details,
|
|
6245
|
+
final_output_snapshot: validation.snapshot,
|
|
6246
|
+
};
|
|
6247
|
+
}
|
|
6248
|
+
|
|
5696
6249
|
function normalizeStrictTestResult(args, details) {
|
|
5697
6250
|
const input = details?.test_results;
|
|
5698
6251
|
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
@@ -5979,6 +6532,20 @@ function normalizeStrictTaskUpdate(args, details, dataDir, change) {
|
|
|
5979
6532
|
|| details?.verification_reason
|
|
5980
6533
|
|| '',
|
|
5981
6534
|
).trim();
|
|
6535
|
+
const rawTaskFiles = input && Object.hasOwn(input, 'files') ? input.files : [];
|
|
6536
|
+
let taskFiles;
|
|
6537
|
+
try {
|
|
6538
|
+
taskFiles = normalizeRepositoryRelativeFiles(rawTaskFiles, {
|
|
6539
|
+
invalidCode: 'TASK_FILE_PATH_INVALID',
|
|
6540
|
+
fieldName: 'task_update.files',
|
|
6541
|
+
});
|
|
6542
|
+
} catch (error) {
|
|
6543
|
+
fail(String(error?.message || error));
|
|
6544
|
+
}
|
|
6545
|
+
const noFileChangeReason = String(input?.no_file_change_reason || '').trim();
|
|
6546
|
+
if (taskFiles.length === 0 && !noFileChangeReason) {
|
|
6547
|
+
fail('TASK_FILES_OR_REASON_REQUIRED: strict task_update 必须提供 files,或说明 no_file_change_reason');
|
|
6548
|
+
}
|
|
5982
6549
|
|
|
5983
6550
|
let resolvedTestEventId = testEventId;
|
|
5984
6551
|
let resolvedTestRunId = testRunId || undefined;
|
|
@@ -6031,6 +6598,8 @@ function normalizeStrictTaskUpdate(args, details, dataDir, change) {
|
|
|
6031
6598
|
|| (verificationNotApplicable ? verificationReason : undefined),
|
|
6032
6599
|
verification_not_applicable: verificationNotApplicable,
|
|
6033
6600
|
verification_not_applicable_reason: verificationNotApplicable ? verificationReason : undefined,
|
|
6601
|
+
files: taskFiles,
|
|
6602
|
+
no_file_change_reason: taskFiles.length === 0 ? noFileChangeReason : undefined,
|
|
6034
6603
|
},
|
|
6035
6604
|
},
|
|
6036
6605
|
};
|
|
@@ -6624,6 +7193,7 @@ function cmdRecord(args) {
|
|
|
6624
7193
|
'quality_gate_result',
|
|
6625
7194
|
'conformance_review',
|
|
6626
7195
|
'ai_adoption_review',
|
|
7196
|
+
'final_output_snapshot',
|
|
6627
7197
|
'survey_result',
|
|
6628
7198
|
'baseline_record',
|
|
6629
7199
|
'telemetry_warning',
|
|
@@ -6787,6 +7357,12 @@ function cmdRecord(args) {
|
|
|
6787
7357
|
strictTaskId = taskUpdate.taskId;
|
|
6788
7358
|
eventStatus = taskUpdate.status;
|
|
6789
7359
|
details = taskUpdate.details;
|
|
7360
|
+
} else if (strict && type === 'check_result') {
|
|
7361
|
+
strictRunId = requireStrictString(strictRunId, 'EVENT_RUN_ID_REQUIRED', 'run_id');
|
|
7362
|
+
details = normalizeStrictCheckResult(details, args.result);
|
|
7363
|
+
} else if (strict && type === 'final_output_snapshot') {
|
|
7364
|
+
strictRunId = requireStrictString(strictRunId, 'EVENT_RUN_ID_REQUIRED', 'run_id');
|
|
7365
|
+
details = normalizeStrictFinalOutputSnapshot(details);
|
|
6790
7366
|
} else if (strict) {
|
|
6791
7367
|
strictRunId = requireStrictString(strictRunId, 'EVENT_RUN_ID_REQUIRED', 'run_id');
|
|
6792
7368
|
}
|
|
@@ -7521,6 +8097,33 @@ function cmdSemanticObserve(args) {
|
|
|
7521
8097
|
return observer;
|
|
7522
8098
|
}
|
|
7523
8099
|
|
|
8100
|
+
/**
|
|
8101
|
+
* kb-upload: 知识库上传(文件夹 / zip)
|
|
8102
|
+
* 委托给 skywalk-sdd/kb-upload.cjs 执行
|
|
8103
|
+
*/
|
|
8104
|
+
function cmdKbUpload(args) {
|
|
8105
|
+
// 将 args 还原为 --key=value 格式,传递给 kb-upload.cjs
|
|
8106
|
+
const passThrough = [];
|
|
8107
|
+
for (const [key, value] of Object.entries(args)) {
|
|
8108
|
+
if (value === true) {
|
|
8109
|
+
passThrough.push(`--${key}`);
|
|
8110
|
+
} else if (value !== undefined && value !== null && value !== false) {
|
|
8111
|
+
passThrough.push(`--${key}=${value}`);
|
|
8112
|
+
}
|
|
8113
|
+
}
|
|
8114
|
+
|
|
8115
|
+
const scriptPath = path.join(__dirname, 'kb-upload.cjs');
|
|
8116
|
+
try {
|
|
8117
|
+
const output = execFileSync(process.execPath, [scriptPath, ...passThrough], {
|
|
8118
|
+
encoding: 'utf8',
|
|
8119
|
+
stdio: 'inherit',
|
|
8120
|
+
});
|
|
8121
|
+
return output;
|
|
8122
|
+
} catch (err) {
|
|
8123
|
+
process.exitCode = 1;
|
|
8124
|
+
}
|
|
8125
|
+
}
|
|
8126
|
+
|
|
7524
8127
|
/**
|
|
7525
8128
|
* log archive-docs: 真实归档 Simple/Full spec,结束 archive 阶段并生成最终报告
|
|
7526
8129
|
*/
|
|
@@ -7716,6 +8319,9 @@ function main() {
|
|
|
7716
8319
|
case 'check-task':
|
|
7717
8320
|
cmdCheckTask(flags);
|
|
7718
8321
|
break;
|
|
8322
|
+
case 'kb-upload':
|
|
8323
|
+
cmdKbUpload(flags);
|
|
8324
|
+
break;
|
|
7719
8325
|
default:
|
|
7720
8326
|
showHelp();
|
|
7721
8327
|
process.exit(1);
|
|
@@ -7734,6 +8340,8 @@ SDD Telemetry CLI - 流程度量采集工具
|
|
|
7734
8340
|
node skywalk-sdd/log.cjs tasks-status --project=<path> --change=<name> [--require-complete]
|
|
7735
8341
|
node skywalk-sdd/log.cjs check-task --project=<path> --change=<name> --task-id=<id>
|
|
7736
8342
|
node skywalk-sdd/log.cjs archive-docs --project=<path> --change=<name> [--reason=<text>] [--event-id=<id>] [--report-output=<file>]
|
|
8343
|
+
node skywalk-sdd/log.cjs kb-upload --folder=<path> [--minimum-level=propose-spec] [--validate-only] [--project=<spec-root>]
|
|
8344
|
+
node skywalk-sdd/log.cjs kb-upload --package=<zip-path> [--project=<spec-root>]
|
|
7737
8345
|
node skywalk-sdd/log.cjs semantic-identity --delta-state=added
|
|
7738
8346
|
node skywalk-sdd/log.cjs semantic-identity --delta-state=modified --entity-id=<uuid> --predecessor-version=<uuid>
|
|
7739
8347
|
node skywalk-sdd/log.cjs semantic-identity --delta-state=unchanged --entity-id=<uuid> --version-id=<uuid>
|
|
@@ -7752,6 +8360,7 @@ SDD Telemetry CLI - 流程度量采集工具
|
|
|
7752
8360
|
tasks-status 扫描 Full/Simple 模式 tasks.md 勾选状态
|
|
7753
8361
|
check-task 扫描变更目录 tasks.md 并勾选指定 task_id
|
|
7754
8362
|
archive-docs 将 Simple/Full spec 变更真实移动到 openspec/changes/archive/,并可结束 archive 阶段生成报告
|
|
8363
|
+
kb-upload 知识库上传(文件夹自动打包 / zip 直传),支持随时入库
|
|
7755
8364
|
semantic-identity 生成或复用实体/版本 ID(8 位十六进制);无需网络和中央 ID 服务
|
|
7756
8365
|
semantic-scan 只读解析并校验当前 Change
|
|
7757
8366
|
semantic-reconcile 全量对账文件与本地工作态本体实例
|
|
@@ -7775,6 +8384,8 @@ SDD Telemetry CLI - 流程度量采集工具
|
|
|
7775
8384
|
node skywalk-sdd/log.cjs check-task --project=/my/project --change=user-auth --task-id=TASK-01
|
|
7776
8385
|
node skywalk-sdd/log.cjs archive-docs --project=/my/project --change=user-auth --reason="变更已完成实施" --event-id=evt_archive
|
|
7777
8386
|
# archive-docs 最终报告默认生成到 openspec/changes/archive/<日期>-<change>/reports/<change>-report.md;可用 --report-output=<路径> 指定自定义路径
|
|
8387
|
+
node skywalk-sdd/log.cjs kb-upload --folder=openspec/changes/user-auth --minimum-level=propose-spec --project=/my/project
|
|
8388
|
+
node skywalk-sdd/log.cjs kb-upload --package=openspec/changes/archive/2026-07-31-user-auth.zip --project=/my/project
|
|
7778
8389
|
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
8390
|
|
|
7780
8391
|
Apply worktree(主仓库根目录,非 log.cjs 子命令):
|