@zhuan-ai/zhuanspec 2.15.7 → 2.16.2
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/dist/cli/hooks.d.ts +13 -1
- package/dist/cli/hooks.js +75 -2
- package/dist/cli/index.js +111 -4
- package/dist/commands/accuracy.js +5 -1
- package/dist/commands/knowledge.d.ts +21 -0
- package/dist/commands/knowledge.js +80 -0
- package/dist/commands/progress.d.ts +78 -0
- package/dist/commands/progress.js +349 -3
- package/dist/core/archive.js +28 -0
- package/dist/core/configurators/codex.d.ts +55 -1
- package/dist/core/configurators/codex.js +234 -2
- package/dist/core/corrections/select-candidates.d.ts +58 -0
- package/dist/core/corrections/select-candidates.js +357 -0
- package/dist/core/hooks/collect-knowledge.js +80 -5
- package/dist/core/hooks/deviation-check.d.ts +26 -1
- package/dist/core/hooks/deviation-check.js +32 -184
- package/dist/core/hooks/knowledge-index.d.ts +84 -0
- package/dist/core/hooks/knowledge-index.js +270 -0
- package/dist/core/hooks/post-archive.js +25 -13
- package/dist/core/hooks/record-progress.d.ts +64 -1
- package/dist/core/hooks/record-progress.js +224 -54
- package/dist/core/hooks/summarize.js +107 -20
- package/dist/core/hooks/user-input-hook.d.ts +38 -2
- package/dist/core/hooks/user-input-hook.js +346 -21
- package/dist/core/init.d.ts +16 -0
- package/dist/core/init.js +208 -11
- package/dist/core/metrics/code-accuracy.d.ts +152 -3
- package/dist/core/metrics/code-accuracy.js +323 -19
- package/dist/core/templates/agents-root-stub.d.ts +1 -1
- package/dist/core/templates/agents-root-stub.js +1 -0
- package/dist/core/templates/agents-template.d.ts +1 -1
- package/dist/core/templates/agents-template.js +27 -25
- package/dist/core/templates/codex-hooks-template.d.ts +37 -0
- package/dist/core/templates/codex-hooks-template.js +33 -2
- package/dist/core/templates/slash-command-templates.js +208 -66
- package/dist/core/templates/tasks-template.js +66 -14
- package/dist/core/update.js +17 -0
- package/dist/core/validation/strict-rules.d.ts +28 -0
- package/dist/core/validation/strict-rules.js +284 -0
- package/dist/utils/hook-merge.d.ts +6 -0
- package/dist/utils/hook-merge.js +33 -2
- package/dist/utils/phase-utils.js +5 -0
- package/package.json +22 -20
|
@@ -3,6 +3,7 @@ import { FileSystemUtils } from '../../utils/file-system.js';
|
|
|
3
3
|
import { readdirSync, rmSync } from 'fs';
|
|
4
4
|
import chalk from 'chalk';
|
|
5
5
|
import { getBeijingTime, getBeijingTimeForFilename } from './record-progress.js';
|
|
6
|
+
import { scanKnowledgeEntries, rebuildIndexContent, } from './knowledge-index.js';
|
|
6
7
|
export async function postArchiveHook(options) {
|
|
7
8
|
const output = await runPostArchiveHook(options.archived || '');
|
|
8
9
|
if (options.json) {
|
|
@@ -19,21 +20,11 @@ async function runPostArchiveHook(archivedChange) {
|
|
|
19
20
|
const repoRoot = process.cwd();
|
|
20
21
|
const zhuanspecDir = path.join(repoRoot, 'zhuanspec');
|
|
21
22
|
const knowledgeDir = path.join(zhuanspecDir, 'knowledge');
|
|
22
|
-
// 1. Update archive log
|
|
23
|
-
console.log(chalk.gray(' → 更新知识索引...'));
|
|
24
|
-
await FileSystemUtils.createDirectory(knowledgeDir);
|
|
25
23
|
const indexPath = path.join(knowledgeDir, 'index.md');
|
|
26
|
-
|
|
27
|
-
if (await FileSystemUtils.fileExists(indexPath)) {
|
|
28
|
-
const existing = await FileSystemUtils.readFile(indexPath);
|
|
29
|
-
await FileSystemUtils.writeFile(indexPath, `${existing.trimEnd()}\n${line}\n`);
|
|
30
|
-
}
|
|
31
|
-
else {
|
|
32
|
-
await FileSystemUtils.writeFile(indexPath, `# Knowledge Index\n\n## Archive Log\n${line}\n`);
|
|
33
|
-
}
|
|
34
|
-
// 2. Collect change knowledge (extract best practices, implicit conventions)
|
|
24
|
+
// 1. Collect change knowledge (extract best practices, implicit conventions)
|
|
35
25
|
// 注意:知识库只保留三类目录 troubleshooting / best-practices / implicit-conventions,
|
|
36
26
|
// 原“决策类”内容合并进 best-practices;踩坑类走 zhuanspec:knowledge skill 单独沉淀。
|
|
27
|
+
await FileSystemUtils.createDirectory(knowledgeDir);
|
|
37
28
|
console.log(chalk.gray(' → 提取设计文档中的知识记录...'));
|
|
38
29
|
const knowledgeResult = await collectChangeKnowledge(zhuanspecDir, archivedChange, knowledgeDir);
|
|
39
30
|
if (knowledgeResult.bestPractices > 0) {
|
|
@@ -42,7 +33,28 @@ async function runPostArchiveHook(archivedChange) {
|
|
|
42
33
|
if (knowledgeResult.implicitConventions > 0) {
|
|
43
34
|
console.log(chalk.gray(` - 隐式约定:${knowledgeResult.implicitConventions} 条`));
|
|
44
35
|
}
|
|
45
|
-
//
|
|
36
|
+
// 2. Reindex 三分区(修复:archive 前的 refreshBusinessTemplateFromRemote 会用远端 index.md
|
|
37
|
+
// 覆盖本地,导致 review/knowledge skill 期间 appendIndexEntry 的登记行丢失。
|
|
38
|
+
// 这里基于本地 troubleshooting/ best-practices/ implicit-conventions/ 实际文件重建三分区,
|
|
39
|
+
// 保留远端拉来的 ## Archive Log 既有内容。)
|
|
40
|
+
console.log(chalk.gray(' → 重建知识索引(三分区 + Archive Log)...'));
|
|
41
|
+
const scan = await scanKnowledgeEntries(knowledgeDir);
|
|
42
|
+
const existingIndex = (await FileSystemUtils.fileExists(indexPath))
|
|
43
|
+
? await FileSystemUtils.readFile(indexPath)
|
|
44
|
+
: null;
|
|
45
|
+
const rebuilt = rebuildIndexContent(scan, existingIndex);
|
|
46
|
+
await FileSystemUtils.writeFile(indexPath, rebuilt);
|
|
47
|
+
console.log(chalk.gray(` - troubleshooting=${scan.entries.filter((e) => e.category === 'troubleshooting').length}, ` +
|
|
48
|
+
`best-practices=${scan.entries.filter((e) => e.category === 'best-practices').length}, ` +
|
|
49
|
+
`implicit-conventions=${scan.entries.filter((e) => e.category === 'implicit-conventions').length}`));
|
|
50
|
+
if (scan.warnings.length > 0) {
|
|
51
|
+
console.log(chalk.yellow(` ⚠ Front Matter 不完整 ${scan.warnings.length} 条,建议补齐关键词/适用场景`));
|
|
52
|
+
}
|
|
53
|
+
// 3. Append 本次 archive log 行
|
|
54
|
+
const archiveLogLine = `- ${getBeijingTime()} archived: ${archivedChange}`;
|
|
55
|
+
const afterReindex = await FileSystemUtils.readFile(indexPath);
|
|
56
|
+
await FileSystemUtils.writeFile(indexPath, `${afterReindex.trimEnd()}\n${archiveLogLine}\n`);
|
|
57
|
+
// 4. Cleanup temp files
|
|
46
58
|
console.log(chalk.gray(' → 清理临时文件...'));
|
|
47
59
|
const tempFilesCleaned = await cleanupTempFiles(zhuanspecDir);
|
|
48
60
|
if (tempFilesCleaned > 0) {
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* 3. Track tool calls for metrics
|
|
8
8
|
*/
|
|
9
9
|
import { Phase } from '../../utils/phase-utils.js';
|
|
10
|
+
import type { PhaseAccuracyEntry, TrackedKind } from '../metrics/code-accuracy.js';
|
|
10
11
|
/**
|
|
11
12
|
* Atomic write JSON file: write to temp file first, then rename
|
|
12
13
|
* This prevents half-written files during crashes or concurrent writes
|
|
@@ -147,6 +148,7 @@ export interface PhaseDurationRecord {
|
|
|
147
148
|
phase: Phase;
|
|
148
149
|
startedAt: string;
|
|
149
150
|
endedAt?: string;
|
|
151
|
+
lastUpdatedAt?: string;
|
|
150
152
|
durationMs: number;
|
|
151
153
|
taskCount: number;
|
|
152
154
|
completedTaskCount: number;
|
|
@@ -174,7 +176,11 @@ export interface ProposalChangeRecord {
|
|
|
174
176
|
relatedDeviationId?: string;
|
|
175
177
|
}
|
|
176
178
|
/**
|
|
177
|
-
*
|
|
179
|
+
* 用户纠偏事件记录
|
|
180
|
+
*
|
|
181
|
+
* v2.15.16 Sprint 3 起:.correction-log 已下线,纠偏事件由 user-input-hook
|
|
182
|
+
* 写入 user_inputs.json 的 correctionSignal / postBaseline / resolution 字段统一管理。
|
|
183
|
+
* 此类型仅保留给 progress.ts resolve-correction 命令的老 change 兼容路径使用。
|
|
178
184
|
*/
|
|
179
185
|
export interface CorrectionRecord {
|
|
180
186
|
ts: string;
|
|
@@ -197,8 +203,18 @@ export interface AccuracyData {
|
|
|
197
203
|
originalRate: number;
|
|
198
204
|
overriddenRate: number;
|
|
199
205
|
reason?: string;
|
|
206
|
+
/** 纠正前的 raw 快照(完整保留,供事后差异分析;历史数据可能缺失) */
|
|
207
|
+
originalRateRaw?: number;
|
|
208
|
+
originalAiTotalLines?: number;
|
|
209
|
+
originalUserCorrectionLines?: number;
|
|
200
210
|
}>;
|
|
201
211
|
correctionEdits?: CorrectionEditRecord[];
|
|
212
|
+
/**
|
|
213
|
+
* 分阶段准确率桶(Task 3)。
|
|
214
|
+
* 顶层 aiLinesAdded / userCorrectionLines / aiTotalLines = 各 phase 桶求和(全生命周期聚合)。
|
|
215
|
+
* 纠偏明细不放在桶内;按 phase 查明细:correctionEdits.filter(e => e.phase === X)。
|
|
216
|
+
*/
|
|
217
|
+
phaseAccuracy?: PhaseAccuracyEntry[];
|
|
202
218
|
}
|
|
203
219
|
export interface CorrectionEditRecord {
|
|
204
220
|
filePath: string;
|
|
@@ -207,6 +223,17 @@ export interface CorrectionEditRecord {
|
|
|
207
223
|
strategy: 'L1' | 'L2' | 'L3' | 'correction-context';
|
|
208
224
|
agentType?: string;
|
|
209
225
|
timestamp: string;
|
|
226
|
+
/** 该 edit 发生在哪个 phase(供按 phase 查明细时过滤用) */
|
|
227
|
+
phase?: string;
|
|
228
|
+
/** 本 edit 所属的文件白名单类别 */
|
|
229
|
+
trackedKind?: TrackedKind;
|
|
230
|
+
/** 纠偏触发源头信息(Task 6),用于 prompt ↔ edit 联关查询 */
|
|
231
|
+
triggeredBy?: {
|
|
232
|
+
source: 'correction-keyword' | 'post-baseline-prompt' | 'pending-correction-marker';
|
|
233
|
+
promptId?: string;
|
|
234
|
+
promptIndex?: number;
|
|
235
|
+
promptSnippet?: string;
|
|
236
|
+
};
|
|
210
237
|
}
|
|
211
238
|
/**
|
|
212
239
|
* 纠偏上下文标记 — 替代 accuracy snapshot 的精准归因机制。
|
|
@@ -227,6 +254,41 @@ export interface CorrectionContext {
|
|
|
227
254
|
path: 'A' | 'B' | 'C' | 'D';
|
|
228
255
|
expiresAt: string;
|
|
229
256
|
phase: string;
|
|
257
|
+
/**
|
|
258
|
+
* 纠偏触发来源:
|
|
259
|
+
* - 'correction-keyword' 用户 prompt 命中纠偏关键词 → deviation-check → resolve-correction CLI
|
|
260
|
+
* - 'pending-correction-marker' .pending-correction 文件兑底(并行竞态兼容)
|
|
261
|
+
* - 'post-baseline-prompt' phaseBaselines 打标后的 UserPromptSubmit(Task 0)
|
|
262
|
+
*/
|
|
263
|
+
source?: 'correction-keyword' | 'post-baseline-prompt' | 'pending-correction-marker';
|
|
264
|
+
/** 当前 phase 内的 user prompt 序号(1-based),仅 source='post-baseline-prompt' 有意义 */
|
|
265
|
+
promptIndex?: number;
|
|
266
|
+
/** 关联的 user_inputs.json.inputId,用于查明细(Task 6) */
|
|
267
|
+
promptId?: string;
|
|
268
|
+
/** prompt 文本前 120 字符摘要,冗余存一份避免频繁 join user_inputs.json */
|
|
269
|
+
promptSnippet?: string;
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Phase baseline 打标(Task 0)——“首版完成标识”。
|
|
273
|
+
*
|
|
274
|
+
* 语义:某 phase 的 markedAt 写入后,该 phase 首版已完成,
|
|
275
|
+
* 之后的 user prompt 触发修改才算纠偏。
|
|
276
|
+
*
|
|
277
|
+
* 打标路径(三路径 OR,先到先得,幂等):
|
|
278
|
+
* - auto-file-complete:Stop hook 中核心产出文件齐备
|
|
279
|
+
* - auto-stop:phase 切换时兜底打标(既然都切下一 phase 了,上一 phase 视为“首版完成”)
|
|
280
|
+
* - manual-cli:`zhuanspec progress mark-baseline` 手动打标
|
|
281
|
+
*/
|
|
282
|
+
export interface PhaseBaselineEntry {
|
|
283
|
+
markedAt: string;
|
|
284
|
+
markedBy: 'auto-file-complete' | 'auto-stop' | 'manual-cli';
|
|
285
|
+
firstStopAt?: string;
|
|
286
|
+
}
|
|
287
|
+
export interface PhaseBaselines {
|
|
288
|
+
techDesign?: PhaseBaselineEntry;
|
|
289
|
+
propose?: PhaseBaselineEntry;
|
|
290
|
+
apply?: PhaseBaselineEntry;
|
|
291
|
+
review?: PhaseBaselineEntry;
|
|
230
292
|
}
|
|
231
293
|
export interface ProgressData {
|
|
232
294
|
changeId: string;
|
|
@@ -271,6 +333,7 @@ export interface ProgressData {
|
|
|
271
333
|
lastEvent?: string;
|
|
272
334
|
accuracy?: AccuracyData;
|
|
273
335
|
correctionContext?: CorrectionContext;
|
|
336
|
+
phaseBaselines?: PhaseBaselines;
|
|
274
337
|
}
|
|
275
338
|
export declare function recordProgressHook(options: RecordProgressOptions): Promise<void>;
|
|
276
339
|
/**
|
|
@@ -11,7 +11,7 @@ import { promises as fsPromises, existsSync, readFileSync, unlinkSync } from 'fs
|
|
|
11
11
|
import { FileSystemUtils } from '../../utils/file-system.js';
|
|
12
12
|
import { PHASE_ORDER } from '../../utils/phase-utils.js';
|
|
13
13
|
import { resolveZhuanSpecRoot } from '../../utils/resolve-root.js';
|
|
14
|
-
import { isCodeFile } from '../metrics/code-accuracy.js';
|
|
14
|
+
import { isCodeFile, isTrackedFileForPhase, getOrCreatePhaseBucket, recomputeAccuracyRate, persistAccuracyJson, appendAccuracyDebugLog } from '../metrics/code-accuracy.js';
|
|
15
15
|
import { detectHookHost, sanitizeCodexEnvelope } from '../../utils/hook-host.js';
|
|
16
16
|
const fs = fsPromises;
|
|
17
17
|
/**
|
|
@@ -112,6 +112,7 @@ export async function recoverProgressJsonForWrite(filePath) {
|
|
|
112
112
|
phaseDurations: [{
|
|
113
113
|
phase: recoveredPhase,
|
|
114
114
|
startedAt: '',
|
|
115
|
+
lastUpdatedAt: '',
|
|
115
116
|
durationMs: 0,
|
|
116
117
|
taskCount: 0,
|
|
117
118
|
completedTaskCount: 0,
|
|
@@ -502,6 +503,37 @@ async function runRecordProgress(filePath, toolName, success, stdinData) {
|
|
|
502
503
|
timestamp: getBeijingTime(),
|
|
503
504
|
triggeredBy: currentTask || 'manual',
|
|
504
505
|
});
|
|
506
|
+
// === Task 0 路径 2:phase 切换时给上一 phase 补 baseline(兜底打标)===
|
|
507
|
+
// previousPhase ∈ {techDesign, propose, apply, review} 才打标;idle/archive 排除
|
|
508
|
+
const BASELINE_PHASES = ['techDesign', 'propose', 'apply', 'review'];
|
|
509
|
+
if (BASELINE_PHASES.includes(previousPhase)) {
|
|
510
|
+
progress.phaseBaselines = progress.phaseBaselines || {};
|
|
511
|
+
const key = previousPhase;
|
|
512
|
+
const existedBefore = !!progress.phaseBaselines[key]?.markedAt;
|
|
513
|
+
if (!existedBefore) {
|
|
514
|
+
progress.phaseBaselines[key] = {
|
|
515
|
+
markedAt: getBeijingTime(),
|
|
516
|
+
markedBy: 'auto-stop',
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
// 埋点:baseline.mark
|
|
520
|
+
appendAccuracyDebugLog(changeDir, {
|
|
521
|
+
type: 'baseline.mark',
|
|
522
|
+
phase: key,
|
|
523
|
+
trigger: 'auto-stop',
|
|
524
|
+
changeId,
|
|
525
|
+
input: { toPhase: phase, previousPhase: key },
|
|
526
|
+
output: { markedBy: 'auto-stop', existed: existedBefore },
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
// === 跨 phase 清理 correctionContext:
|
|
530
|
+
// 旧 phase 的 active 不应泄漏到新 phase,新 phase 是否进入纠偏态
|
|
531
|
+
// 由新 phase 的 phaseBaselines + UserPromptSubmit 重新判定
|
|
532
|
+
if (progress.correctionContext
|
|
533
|
+
&& progress.correctionContext.active
|
|
534
|
+
&& progress.correctionContext.phase !== phase) {
|
|
535
|
+
progress.correctionContext.active = false;
|
|
536
|
+
}
|
|
505
537
|
}
|
|
506
538
|
progress.currentNode = process.env.ZHUANSPEC_CURRENT_NODE || phase;
|
|
507
539
|
progress.currentTask = currentTask || progress.currentTask;
|
|
@@ -610,7 +642,20 @@ async function runRecordProgress(filePath, toolName, success, stdinData) {
|
|
|
610
642
|
contentSource = toolInputEdits.length > 0 ? 'edits[]' : 'none';
|
|
611
643
|
}
|
|
612
644
|
let estimatedLines = 0;
|
|
613
|
-
|
|
645
|
+
// === P0 兜底:post-baseline-fallback ===
|
|
646
|
+
// 正常链路:UserPromptSubmit 的 user-input hook 会在 phaseBaseline 已打标时写入
|
|
647
|
+
// correctionContext.active=true。但若 .claude/settings.json 未挂 user-input hook
|
|
648
|
+
// (老版本模板),correctionContext 会保持 null,导致后续 Write 误入 linesAdded。
|
|
649
|
+
// 本兜底:只要当前 phase 的 phaseBaseline 已打标,就视同进入纠正上下文。
|
|
650
|
+
// 语义:Stop 打 baseline 代表该 phase 首版齐备,之后的同 phase Write 绝大多数
|
|
651
|
+
// 由后续 user prompt 驱动(AI 不会主动在无 prompt 情况下继续大改),归为 user 纠正。
|
|
652
|
+
const _phaseKeyForBaseline = progress.phase;
|
|
653
|
+
const _baselinePhases = ['techDesign', 'propose', 'apply', 'review'];
|
|
654
|
+
const baselineMarkedForPhase = _baselinePhases.includes(_phaseKeyForBaseline)
|
|
655
|
+
&& !!progress.phaseBaselines?.[_phaseKeyForBaseline]?.markedAt;
|
|
656
|
+
const ccActiveRaw = !!(progress.correctionContext?.active);
|
|
657
|
+
const inCorrectionContext = ccActiveRaw || pendingExists || baselineMarkedForPhase;
|
|
658
|
+
const correctionFallbackReason = (!ccActiveRaw && !pendingExists && baselineMarkedForPhase) ? 'post-baseline-fallback' : 'none';
|
|
614
659
|
// === 调试日志:在 changeDir 下写 .correction-debug.log,方便排查“为什么没记录上” ===
|
|
615
660
|
const correctionDebugLog = (branch, addedTo, delta) => {
|
|
616
661
|
try {
|
|
@@ -628,6 +673,8 @@ async function runRecordProgress(filePath, toolName, success, stdinData) {
|
|
|
628
673
|
contentSource,
|
|
629
674
|
changeLines,
|
|
630
675
|
pendingExists,
|
|
676
|
+
baselineMarkedForPhase,
|
|
677
|
+
fallbackReason: correctionFallbackReason,
|
|
631
678
|
correctionContext: progress.correctionContext
|
|
632
679
|
? { active: ccActive, path: ccPath, expiresAt: ccExpiresAt, phase: ccPhase, currentPhase: progress.phase }
|
|
633
680
|
: null,
|
|
@@ -646,46 +693,175 @@ async function runRecordProgress(filePath, toolName, success, stdinData) {
|
|
|
646
693
|
// 日志失败不阻断主流程
|
|
647
694
|
}
|
|
648
695
|
};
|
|
649
|
-
if (changeLines > 0 && filePath
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
696
|
+
if (changeLines > 0 && filePath) {
|
|
697
|
+
// Task 3.3:按 phase 选白名单(techDesign/propose/apply/review),而非笼统用 isCodeFile
|
|
698
|
+
const track = isTrackedFileForPhase(filePath, progress.phase, changeDir);
|
|
699
|
+
if (!track.tracked) {
|
|
700
|
+
correctionDebugLog(`skipped/out-of-phase-whitelist`, 'skipped', 0);
|
|
701
|
+
appendAccuracyDebugLog(changeDir, {
|
|
702
|
+
type: 'skip.out-of-phase-whitelist',
|
|
703
|
+
phase: progress.phase,
|
|
704
|
+
trigger: 'hook',
|
|
705
|
+
changeId,
|
|
706
|
+
skipReason: 'out-of-phase-whitelist',
|
|
707
|
+
input: { tool: toolName, file: filePath, changeLines },
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
else {
|
|
711
|
+
estimatedLines = changeLines;
|
|
712
|
+
const trackedKind = track.kind;
|
|
713
|
+
const trackedPhase = progress.phase;
|
|
714
|
+
// 确保 accuracy 结构存在
|
|
653
715
|
if (!progress.accuracy) {
|
|
654
716
|
progress.accuracy = {
|
|
655
717
|
aiLinesAdded: 0, aiLinesModified: 0, aiTotalLines: 0,
|
|
656
718
|
userCorrectionLines: 0, accuracyRate: 1.0,
|
|
657
|
-
correctionEdits: [],
|
|
719
|
+
correctionEdits: [], phaseAccuracy: [],
|
|
658
720
|
};
|
|
659
721
|
}
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
722
|
+
const bucket = getOrCreatePhaseBucket(progress, trackedPhase, trackedKind);
|
|
723
|
+
if (inCorrectionContext) {
|
|
724
|
+
// 工具白名单守卫:techDesign / propose 阶段的纠偏统计只接受增量编辑(Edit/MultiEdit),
|
|
725
|
+
// 禁止 Write。原因:tech-spec.md / proposal.md 通常是整文件 Write 产出,一旦被误判为纠偏,
|
|
726
|
+
// 会把整份文件行数当成 userCorrectionLines 记入,导致分子被严重放大、accuracyRate 失真。
|
|
727
|
+
// apply / review 阶段不受此限制(apply 可 Write 新增代码文件,review-report.md 也常由 Write 生成)。
|
|
728
|
+
if ((trackedPhase === 'techDesign' || trackedPhase === 'propose') && toolName === 'Write') {
|
|
729
|
+
correctionDebugLog(`skipped/write-blocked-${trackedPhase}-correction`, 'skipped', 0);
|
|
730
|
+
appendAccuracyDebugLog(changeDir, {
|
|
731
|
+
type: `skip.write-blocked-${trackedPhase}-correction`,
|
|
732
|
+
phase: trackedPhase,
|
|
733
|
+
kind: trackedKind,
|
|
734
|
+
trigger: 'hook',
|
|
735
|
+
changeId,
|
|
736
|
+
skipReason: `write-blocked-${trackedPhase}-correction`,
|
|
737
|
+
input: { tool: toolName, file: filePath, changeLines },
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
else {
|
|
741
|
+
// 纠偏上下文:桶 + 顶层 userCorrectionLines 同步累加
|
|
742
|
+
bucket.userCorrectionLines += changeLines;
|
|
743
|
+
bucket.lastUpdatedAt = timestamp;
|
|
744
|
+
progress.accuracy.userCorrectionLines = (progress.accuracy.userCorrectionLines || 0) + changeLines;
|
|
745
|
+
progress.accuracy.correctionEdits = progress.accuracy.correctionEdits || [];
|
|
746
|
+
// 提取纠偏触发源头信息(Task 6)
|
|
747
|
+
// post-baseline-fallback 兜底命中时,语义等价于 user-input hook 写入的 'post-baseline-prompt'。
|
|
748
|
+
const ccSource = progress.correctionContext?.source
|
|
749
|
+
|| (pendingExists
|
|
750
|
+
? 'pending-correction-marker'
|
|
751
|
+
: (correctionFallbackReason === 'post-baseline-fallback'
|
|
752
|
+
? 'post-baseline-prompt'
|
|
753
|
+
: 'correction-keyword'));
|
|
754
|
+
// Task 6:若 correctionContext 无 prompt 元数据(典型于 pending-correction-marker 兑底路径),
|
|
755
|
+
// 从 .pending-correction marker JSON 中回填 promptId / phaseInputIndex / promptSnippet。
|
|
756
|
+
let fallbackPromptId;
|
|
757
|
+
let fallbackPromptIndex;
|
|
758
|
+
let fallbackPromptSnippet;
|
|
759
|
+
if (!progress.correctionContext?.promptId && pendingExists) {
|
|
760
|
+
try {
|
|
761
|
+
const markerRaw = readFileSync(pendingMarkerPath, 'utf-8');
|
|
762
|
+
if (markerRaw) {
|
|
763
|
+
const markerData = JSON.parse(markerRaw);
|
|
764
|
+
fallbackPromptId = markerData.promptId;
|
|
765
|
+
fallbackPromptIndex = markerData.phaseInputIndex;
|
|
766
|
+
fallbackPromptSnippet = markerData.promptSnippet;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
catch {
|
|
770
|
+
// marker 解析失败不阻断主流程
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
const promptId = progress.correctionContext?.promptId ?? fallbackPromptId;
|
|
774
|
+
const promptIndex = progress.correctionContext?.promptIndex ?? fallbackPromptIndex;
|
|
775
|
+
const promptSnippet = progress.correctionContext?.promptSnippet ?? fallbackPromptSnippet;
|
|
776
|
+
progress.accuracy.correctionEdits.push({
|
|
777
|
+
filePath,
|
|
778
|
+
editLines: changeLines,
|
|
779
|
+
source: 'user',
|
|
780
|
+
strategy: 'correction-context',
|
|
781
|
+
agentType: progress.correctionContext?.path,
|
|
782
|
+
timestamp,
|
|
783
|
+
phase: trackedPhase,
|
|
784
|
+
trackedKind,
|
|
785
|
+
triggeredBy: {
|
|
786
|
+
source: ccSource,
|
|
787
|
+
promptId,
|
|
788
|
+
promptIndex,
|
|
789
|
+
promptSnippet,
|
|
790
|
+
},
|
|
791
|
+
});
|
|
792
|
+
// 记录 phase 维度 promptId(去重)
|
|
793
|
+
if (promptId && !bucket.correctionPromptIds.includes(promptId)) {
|
|
794
|
+
bucket.correctionPromptIds.push(promptId);
|
|
795
|
+
}
|
|
796
|
+
correctionDebugLog(`${toolName}/correction`, 'userCorrectionLines', changeLines);
|
|
797
|
+
appendAccuracyDebugLog(changeDir, {
|
|
798
|
+
type: 'correction.record',
|
|
799
|
+
phase: trackedPhase,
|
|
800
|
+
kind: trackedKind,
|
|
801
|
+
trigger: 'hook',
|
|
802
|
+
changeId,
|
|
803
|
+
input: {
|
|
804
|
+
tool: toolName,
|
|
805
|
+
file: filePath,
|
|
806
|
+
changeLines,
|
|
807
|
+
contentSource,
|
|
808
|
+
ccSource,
|
|
809
|
+
promptId,
|
|
810
|
+
promptIndex,
|
|
811
|
+
},
|
|
812
|
+
output: {
|
|
813
|
+
bucketCorrection: bucket.userCorrectionLines,
|
|
814
|
+
topCorrection: progress.accuracy.userCorrectionLines,
|
|
815
|
+
promptIdsCount: bucket.correctionPromptIds.length,
|
|
816
|
+
},
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
else {
|
|
821
|
+
// 正常 AI 产出:桶 + 顶层 aiLinesAdded/aiTotalLines 同步累加,同时顶层 progress.linesAdded 仍需累加以兼容旧逻辑
|
|
822
|
+
bucket.aiLinesAdded += changeLines;
|
|
823
|
+
bucket.aiTotalLines += changeLines;
|
|
824
|
+
bucket.lastUpdatedAt = timestamp;
|
|
825
|
+
progress.accuracy.aiLinesAdded = (progress.accuracy.aiLinesAdded || 0) + changeLines;
|
|
826
|
+
progress.accuracy.aiTotalLines = (progress.accuracy.aiTotalLines || 0) + changeLines;
|
|
827
|
+
progress.linesAdded += changeLines;
|
|
828
|
+
correctionDebugLog(`${toolName}/normal`, 'linesAdded', changeLines);
|
|
829
|
+
appendAccuracyDebugLog(changeDir, {
|
|
830
|
+
type: 'ai.record',
|
|
831
|
+
phase: trackedPhase,
|
|
832
|
+
kind: trackedKind,
|
|
833
|
+
trigger: 'hook',
|
|
834
|
+
changeId,
|
|
835
|
+
input: { tool: toolName, file: filePath, changeLines, contentSource },
|
|
836
|
+
output: {
|
|
837
|
+
bucketAiTotal: bucket.aiTotalLines,
|
|
838
|
+
topAiTotal: progress.accuracy.aiTotalLines,
|
|
839
|
+
},
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
// 累加后重算 accuracyRate(顶层 + 各桶)
|
|
843
|
+
recomputeAccuracyRate(progress, { changeDir, trigger: 'hook' });
|
|
844
|
+
// 统一同步 accuracy.json
|
|
845
|
+
await persistAccuracyJson(changeDir, progress);
|
|
676
846
|
}
|
|
677
847
|
}
|
|
678
848
|
else {
|
|
679
|
-
//
|
|
849
|
+
// 未进入计量分支:记录原因(字段缺失 / 非写入工具)
|
|
680
850
|
if (toolName === 'Write' || toolName === 'Edit' || toolName === 'MultiEdit') {
|
|
681
851
|
const reason = !filePath
|
|
682
852
|
? 'no-file-path'
|
|
683
|
-
:
|
|
684
|
-
?
|
|
685
|
-
:
|
|
686
|
-
? `empty-${contentSource}`
|
|
687
|
-
: 'unknown';
|
|
853
|
+
: changeLines === 0
|
|
854
|
+
? `empty-${contentSource}`
|
|
855
|
+
: 'unknown';
|
|
688
856
|
correctionDebugLog(`skipped/${reason}`, 'skipped', 0);
|
|
857
|
+
appendAccuracyDebugLog(changeDir, {
|
|
858
|
+
type: `skip.${reason}`,
|
|
859
|
+
phase: progress.phase,
|
|
860
|
+
trigger: 'hook',
|
|
861
|
+
changeId,
|
|
862
|
+
skipReason: reason,
|
|
863
|
+
input: { tool: toolName, file: filePath || null, changeLines, contentSource },
|
|
864
|
+
});
|
|
689
865
|
}
|
|
690
866
|
}
|
|
691
867
|
const tokenUsageFromEnv = Number(process.env.ZHUANSPEC_TOKEN_USAGE || 0);
|
|
@@ -730,6 +906,15 @@ async function runRecordProgress(filePath, toolName, success, stdinData) {
|
|
|
730
906
|
progress.stats.durationMs.review = elapsedMs;
|
|
731
907
|
if (phase === 'archive')
|
|
732
908
|
progress.stats.durationMs.archive = elapsedMs;
|
|
909
|
+
// v2.15.16:同步刷新活跃段的 phaseDurations 记录,进行中也能从单条直接读出
|
|
910
|
+
// 实时 durationMs / lastUpdatedAt。endedAt 仍仅在 phase 切换时填。
|
|
911
|
+
const activeEntry = [...progress.phaseDurations]
|
|
912
|
+
.reverse()
|
|
913
|
+
.find(pd => pd.phase === phase && !pd.endedAt);
|
|
914
|
+
if (activeEntry) {
|
|
915
|
+
activeEntry.durationMs = elapsedMs;
|
|
916
|
+
activeEntry.lastUpdatedAt = getBeijingTime();
|
|
917
|
+
}
|
|
733
918
|
}
|
|
734
919
|
}
|
|
735
920
|
// === Auto-count tasks from tasks.md ===
|
|
@@ -756,31 +941,10 @@ async function runRecordProgress(filePath, toolName, success, stdinData) {
|
|
|
756
941
|
// Ignore tasks.md read errors
|
|
757
942
|
}
|
|
758
943
|
}
|
|
759
|
-
//
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
const content = readFileSync(correctionLogPath, 'utf-8');
|
|
764
|
-
const parsed = [];
|
|
765
|
-
for (const line of content.split('\n')) {
|
|
766
|
-
const trimmed = line.trim();
|
|
767
|
-
if (!trimmed)
|
|
768
|
-
continue;
|
|
769
|
-
try {
|
|
770
|
-
parsed.push(JSON.parse(trimmed));
|
|
771
|
-
}
|
|
772
|
-
catch {
|
|
773
|
-
// skip malformed line
|
|
774
|
-
}
|
|
775
|
-
}
|
|
776
|
-
if (parsed.length > 0) {
|
|
777
|
-
progress.corrections = parsed;
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
catch {
|
|
782
|
-
// ignore correction log read errors
|
|
783
|
-
}
|
|
944
|
+
// v2.15.16 Sprint 3:.correction-log → progress.corrections 的合并逻辑已下线。
|
|
945
|
+
// 纠偏明细现由 user-input-hook 写入 user_inputs.json(correctionSignal / postBaseline /
|
|
946
|
+
// resolution 字段),review 步骤 0 与 knowledge skill 模式 B 均直接读 user_inputs.json,
|
|
947
|
+
// 不再依赖 progress.corrections。
|
|
784
948
|
// Write progress file atomically
|
|
785
949
|
await atomicWriteJson(progressPath, progress);
|
|
786
950
|
// Also update tool_calls.json separately for detailed tracking
|
|
@@ -933,6 +1097,7 @@ function createNewProgress(changeId) {
|
|
|
933
1097
|
phaseDurations: [{
|
|
934
1098
|
phase: initialPhase,
|
|
935
1099
|
startedAt: getBeijingTime(),
|
|
1100
|
+
lastUpdatedAt: getBeijingTime(),
|
|
936
1101
|
durationMs: 0,
|
|
937
1102
|
taskCount: 0,
|
|
938
1103
|
completedTaskCount: 0,
|
|
@@ -1006,6 +1171,7 @@ export async function initializeProgress(changeId, initialPhase = 'propose') {
|
|
|
1006
1171
|
const prevPhaseDuration = progress.phaseDurations.find(pd => pd.phase === previousPhase && !pd.endedAt);
|
|
1007
1172
|
if (prevPhaseDuration) {
|
|
1008
1173
|
prevPhaseDuration.endedAt = timestamp;
|
|
1174
|
+
prevPhaseDuration.lastUpdatedAt = timestamp;
|
|
1009
1175
|
const startTime = new Date(prevPhaseDuration.startedAt.replace(' ', 'T')).getTime();
|
|
1010
1176
|
const endTime = new Date(timestamp.replace(' ', 'T')).getTime();
|
|
1011
1177
|
const ms = Number.isFinite(startTime) && endTime > startTime ? endTime - startTime : 0;
|
|
@@ -1027,6 +1193,7 @@ export async function initializeProgress(changeId, initialPhase = 'propose') {
|
|
|
1027
1193
|
phase: previousPhase,
|
|
1028
1194
|
startedAt: inferredStart,
|
|
1029
1195
|
endedAt: timestamp,
|
|
1196
|
+
lastUpdatedAt: timestamp,
|
|
1030
1197
|
durationMs: ms,
|
|
1031
1198
|
taskCount: 0,
|
|
1032
1199
|
completedTaskCount: 0,
|
|
@@ -1040,6 +1207,7 @@ export async function initializeProgress(changeId, initialPhase = 'propose') {
|
|
|
1040
1207
|
progress.phaseDurations.push({
|
|
1041
1208
|
phase: initialPhase,
|
|
1042
1209
|
startedAt: timestamp,
|
|
1210
|
+
lastUpdatedAt: timestamp,
|
|
1043
1211
|
durationMs: 0,
|
|
1044
1212
|
taskCount: 0,
|
|
1045
1213
|
completedTaskCount: 0,
|
|
@@ -1117,6 +1285,7 @@ function seedInitialPhase(progress, initialPhase, timestamp) {
|
|
|
1117
1285
|
for (const entry of progress.phaseDurations) {
|
|
1118
1286
|
if (!entry.endedAt && entry.phase !== initialPhase) {
|
|
1119
1287
|
entry.endedAt = timestamp;
|
|
1288
|
+
entry.lastUpdatedAt = timestamp;
|
|
1120
1289
|
const startTime = new Date((entry.startedAt || timestamp).replace(' ', 'T')).getTime();
|
|
1121
1290
|
const endTime = new Date(timestamp.replace(' ', 'T')).getTime();
|
|
1122
1291
|
entry.durationMs = Number.isFinite(startTime) && endTime > startTime ? endTime - startTime : 0;
|
|
@@ -1125,6 +1294,7 @@ function seedInitialPhase(progress, initialPhase, timestamp) {
|
|
|
1125
1294
|
progress.phaseDurations.push({
|
|
1126
1295
|
phase: initialPhase,
|
|
1127
1296
|
startedAt: timestamp,
|
|
1297
|
+
lastUpdatedAt: timestamp,
|
|
1128
1298
|
durationMs: 0,
|
|
1129
1299
|
taskCount: 0,
|
|
1130
1300
|
completedTaskCount: 0,
|