@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.
Files changed (43) hide show
  1. package/dist/cli/hooks.d.ts +13 -1
  2. package/dist/cli/hooks.js +75 -2
  3. package/dist/cli/index.js +111 -4
  4. package/dist/commands/accuracy.js +5 -1
  5. package/dist/commands/knowledge.d.ts +21 -0
  6. package/dist/commands/knowledge.js +80 -0
  7. package/dist/commands/progress.d.ts +78 -0
  8. package/dist/commands/progress.js +349 -3
  9. package/dist/core/archive.js +28 -0
  10. package/dist/core/configurators/codex.d.ts +55 -1
  11. package/dist/core/configurators/codex.js +234 -2
  12. package/dist/core/corrections/select-candidates.d.ts +58 -0
  13. package/dist/core/corrections/select-candidates.js +357 -0
  14. package/dist/core/hooks/collect-knowledge.js +80 -5
  15. package/dist/core/hooks/deviation-check.d.ts +26 -1
  16. package/dist/core/hooks/deviation-check.js +32 -184
  17. package/dist/core/hooks/knowledge-index.d.ts +84 -0
  18. package/dist/core/hooks/knowledge-index.js +270 -0
  19. package/dist/core/hooks/post-archive.js +25 -13
  20. package/dist/core/hooks/record-progress.d.ts +64 -1
  21. package/dist/core/hooks/record-progress.js +224 -54
  22. package/dist/core/hooks/summarize.js +107 -20
  23. package/dist/core/hooks/user-input-hook.d.ts +38 -2
  24. package/dist/core/hooks/user-input-hook.js +346 -21
  25. package/dist/core/init.d.ts +16 -0
  26. package/dist/core/init.js +208 -11
  27. package/dist/core/metrics/code-accuracy.d.ts +152 -3
  28. package/dist/core/metrics/code-accuracy.js +323 -19
  29. package/dist/core/templates/agents-root-stub.d.ts +1 -1
  30. package/dist/core/templates/agents-root-stub.js +1 -0
  31. package/dist/core/templates/agents-template.d.ts +1 -1
  32. package/dist/core/templates/agents-template.js +27 -25
  33. package/dist/core/templates/codex-hooks-template.d.ts +37 -0
  34. package/dist/core/templates/codex-hooks-template.js +33 -2
  35. package/dist/core/templates/slash-command-templates.js +208 -66
  36. package/dist/core/templates/tasks-template.js +66 -14
  37. package/dist/core/update.js +17 -0
  38. package/dist/core/validation/strict-rules.d.ts +28 -0
  39. package/dist/core/validation/strict-rules.js +284 -0
  40. package/dist/utils/hook-merge.d.ts +6 -0
  41. package/dist/utils/hook-merge.js +33 -2
  42. package/dist/utils/phase-utils.js +5 -0
  43. package/package.json +22 -20
@@ -21,6 +21,10 @@ type ToolWizardConfig = {
21
21
  initialSelected?: string[];
22
22
  };
23
23
  type ToolSelectionPrompt = (config: ToolWizardConfig) => Promise<string[]>;
24
+ type ClaudeHookSyncSummary = {
25
+ totalAddedHooks: number;
26
+ duplicateHooksSkipped: number;
27
+ };
24
28
  type InitCommandOptions = {
25
29
  prompt?: ToolSelectionPrompt;
26
30
  tools?: string;
@@ -49,6 +53,11 @@ export declare class InitCommand {
49
53
  private fetchRemoteArchitectureFiles;
50
54
  private fetchRemoteArchitectureContent;
51
55
  private cloneRemoteArchitectureRepoToTemp;
56
+ /**
57
+ * Append a JSONL line to ~/.zhuanspec-init-debug.log for offline troubleshooting.
58
+ * Failures are swallowed to avoid breaking init.
59
+ */
60
+ private logInitDebug;
52
61
  private syncBusinessSpecTemplate;
53
62
  private syncBusinessClaudeAssets;
54
63
  private resolveBusinessDirectionRoot;
@@ -72,6 +81,13 @@ export declare class InitCommand {
72
81
  private writeTemplateFiles;
73
82
  private configureAITools;
74
83
  private configureRootAgentsStub;
84
+ /**
85
+ * v2.15.16:对外暴露的 settings.json 同步入口,给 zhuanspec update 复用。
86
+ * 会走 generateClaudeSettings 同样的 merge + 老 group 清理逻辑,
87
+ * 确保老版 settings.json(如还挂着 deviation-check --trigger post-prompt)
88
+ * 在升级后能被过滤掉。
89
+ */
90
+ syncClaudeSettings(projectPath: string): Promise<ClaudeHookSyncSummary>;
75
91
  /**
76
92
  * Generate Claude Code hooks configuration for ZhuanSpec workflow.
77
93
  * Creates .claude/settings.json with hooks for SessionStart, PreToolUse, PostToolUse, and Stop.
package/dist/core/init.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import path from 'path';
2
2
  import os from 'os';
3
- import { cpSync, existsSync, mkdirSync, mkdtempSync, realpathSync, writeFileSync, readdirSync, readFileSync, rmSync, } from 'fs';
3
+ import { cpSync, existsSync, mkdirSync, mkdtempSync, realpathSync, writeFileSync, readdirSync, readFileSync, rmSync, appendFileSync, } from 'fs';
4
4
  import { createPrompt, isBackspaceKey, isDownKey, isEnterKey, isSpaceKey, isUpKey, useKeypress, usePagination, useState, } from '@inquirer/core';
5
5
  import chalk from 'chalk';
6
6
  import ora from 'ora';
@@ -37,6 +37,25 @@ const parseToolLabel = (raw) => {
37
37
  };
38
38
  };
39
39
  const isSelectableChoice = (choice) => choice.selectable;
40
+ /**
41
+ * v2.15.16:判定一个 hook group 是否完全由 ZhuanSpec 管辖。
42
+ * 条件:group.hooks 里所有 entry 的 command 都以 `zhuanspec-hook` 开头。
43
+ * 任何一个非 ZhuanSpec 命令(用户自己挂的)都会让整组保留,避免误伤。
44
+ * 用于 zhuanspec update 时清理老模板挂载、但新模板已删除的 entry(如 Sprint 3 下线的 deviation-check --trigger post-prompt)。
45
+ */
46
+ function isZhuanspecManagedHookGroup(group) {
47
+ if (!group || typeof group !== 'object')
48
+ return false;
49
+ const hooks = group.hooks;
50
+ if (!Array.isArray(hooks) || hooks.length === 0)
51
+ return false;
52
+ return hooks.every((h) => {
53
+ if (!h || typeof h !== 'object')
54
+ return false;
55
+ const command = h.command;
56
+ return typeof command === 'string' && command.trim().startsWith('zhuanspec-hook');
57
+ });
58
+ }
40
59
  const ROOT_STUB_CHOICE_VALUE = '__root_stub__';
41
60
  const OTHER_TOOLS_HEADING_VALUE = '__heading-other__';
42
61
  const LIST_SPACER_VALUE = '__list-spacer__';
@@ -787,15 +806,56 @@ export class InitCommand {
787
806
  }
788
807
  cloneRemoteArchitectureRepoToTemp() {
789
808
  const tempDir = mkdtempSync(path.join(os.tmpdir(), 'zhuanspec-arch-'));
809
+ const startedAt = Date.now();
790
810
  try {
791
- execSync(`git clone --depth 1 --single-branch --branch ${ARCH_REPO_BRANCH} ${ARCH_REPO_URL} "${tempDir}"`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] });
811
+ execSync(`git clone --depth 1 --single-branch --branch ${ARCH_REPO_BRANCH} ${ARCH_REPO_URL} "${tempDir}"`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
812
+ this.logInitDebug({
813
+ scope: 'cloneRemoteArchitectureRepoToTemp',
814
+ result: 'ok',
815
+ repo: ARCH_REPO_URL,
816
+ branch: ARCH_REPO_BRANCH,
817
+ tempDir,
818
+ elapsedMs: Date.now() - startedAt,
819
+ });
792
820
  return tempDir;
793
821
  }
794
- catch {
822
+ catch (err) {
823
+ this.logInitDebug({
824
+ scope: 'cloneRemoteArchitectureRepoToTemp',
825
+ result: 'fail',
826
+ repo: ARCH_REPO_URL,
827
+ branch: ARCH_REPO_BRANCH,
828
+ tempDir,
829
+ elapsedMs: Date.now() - startedAt,
830
+ errorMessage: err?.message ?? String(err),
831
+ stderr: err?.stderr ? String(err.stderr).slice(0, 1000) : undefined,
832
+ stdout: err?.stdout ? String(err.stdout).slice(0, 500) : undefined,
833
+ status: err?.status,
834
+ });
795
835
  rmSync(tempDir, { recursive: true, force: true });
796
836
  return null;
797
837
  }
798
838
  }
839
+ /**
840
+ * Append a JSONL line to ~/.zhuanspec-init-debug.log for offline troubleshooting.
841
+ * Failures are swallowed to avoid breaking init.
842
+ */
843
+ logInitDebug(payload) {
844
+ try {
845
+ const line = JSON.stringify({
846
+ ts: new Date().toISOString(),
847
+ pid: process.pid,
848
+ cwd: process.cwd(),
849
+ businessDirection: this.businessDirection ?? null,
850
+ ...payload,
851
+ });
852
+ const logPath = path.join(os.homedir(), '.zhuanspec-init-debug.log');
853
+ appendFileSync(logPath, line + '\n', 'utf-8');
854
+ }
855
+ catch {
856
+ // ignore
857
+ }
858
+ }
799
859
  syncBusinessSpecTemplate(zhuanspecPath) {
800
860
  if (!this.businessDirection) {
801
861
  return { syncedSpecs: false, syncedChanges: false, syncedKnowledge: false };
@@ -832,10 +892,20 @@ export class InitCommand {
832
892
  dotClaude: { source: 'none', files: { added: [], overwritten: [], skipped: [] } },
833
893
  };
834
894
  if (!this.businessDirection) {
895
+ this.logInitDebug({
896
+ scope: 'syncBusinessClaudeAssets',
897
+ result: 'skip-no-business-direction',
898
+ projectPath,
899
+ });
835
900
  return fallbackResult;
836
901
  }
837
902
  const tempDir = this.cloneRemoteArchitectureRepoToTemp();
838
903
  if (!tempDir) {
904
+ this.logInitDebug({
905
+ scope: 'syncBusinessClaudeAssets',
906
+ result: 'skip-clone-failed',
907
+ projectPath,
908
+ });
839
909
  return fallbackResult;
840
910
  }
841
911
  try {
@@ -849,12 +919,50 @@ export class InitCommand {
849
919
  const resolvedClaudeMdSource = this.resolveClaudeAssetSource(businessClaudeMd, commonClaudeMd);
850
920
  // .claude directory: always from common directory (not business-specific)
851
921
  const commonDotClaude = path.join(commonClaudeRoot, '.claude');
922
+ this.logInitDebug({
923
+ scope: 'syncBusinessClaudeAssets',
924
+ result: 'start-copy',
925
+ projectPath,
926
+ tempDir,
927
+ specsRootExists: existsSync(specsRoot),
928
+ businessRoot,
929
+ commonClaudeRoot,
930
+ commonClaudeRootExists: existsSync(commonClaudeRoot),
931
+ commonDotClaude,
932
+ commonDotClaudeExists: existsSync(commonDotClaude),
933
+ commonDotClaudeHasFiles: existsSync(commonDotClaude)
934
+ ? this.directoryHasAtLeastOneFile(commonDotClaude)
935
+ : false,
936
+ commonDotClaudeTopEntries: existsSync(commonDotClaude)
937
+ ? readdirSync(commonDotClaude)
938
+ : [],
939
+ });
940
+ const claudeMdResult = this.copyClaudeFileIfNeeded(resolvedClaudeMdSource, path.join(projectPath, 'CLAUDE.md'));
941
+ const dotClaudeResult = this.copyClaudeDirectoryFromCommon(commonDotClaude, path.join(projectPath, '.claude'));
942
+ this.logInitDebug({
943
+ scope: 'syncBusinessClaudeAssets',
944
+ result: 'done',
945
+ projectPath,
946
+ claudeMd: claudeMdResult,
947
+ dotClaudeSource: dotClaudeResult.source,
948
+ dotClaudeAdded: dotClaudeResult.files.added.length,
949
+ dotClaudeOverwritten: dotClaudeResult.files.overwritten.length,
950
+ dotClaudeSkipped: dotClaudeResult.files.skipped.length,
951
+ dotClaudeAddedSample: dotClaudeResult.files.added.slice(0, 5),
952
+ });
852
953
  return {
853
- claudeMd: this.copyClaudeFileIfNeeded(resolvedClaudeMdSource, path.join(projectPath, 'CLAUDE.md')),
854
- dotClaude: this.copyClaudeDirectoryFromCommon(commonDotClaude, path.join(projectPath, '.claude')),
954
+ claudeMd: claudeMdResult,
955
+ dotClaude: dotClaudeResult,
855
956
  };
856
957
  }
857
- catch {
958
+ catch (err) {
959
+ this.logInitDebug({
960
+ scope: 'syncBusinessClaudeAssets',
961
+ result: 'exception',
962
+ projectPath,
963
+ errorMessage: err?.message ?? String(err),
964
+ stack: err?.stack ? String(err.stack).slice(0, 500) : undefined,
965
+ });
858
966
  return fallbackResult;
859
967
  }
860
968
  finally {
@@ -972,13 +1080,30 @@ export class InitCommand {
972
1080
  files: { added: [], overwritten: [], skipped: [] },
973
1081
  };
974
1082
  if (!sourceAsset) {
1083
+ this.logInitDebug({
1084
+ scope: 'copyClaudeDirectoryIfNeeded',
1085
+ result: 'skip-no-source-asset',
1086
+ targetPath,
1087
+ });
975
1088
  return emptyResult;
976
1089
  }
977
1090
  if (!existsSync(sourceAsset.path)) {
1091
+ this.logInitDebug({
1092
+ scope: 'copyClaudeDirectoryIfNeeded',
1093
+ result: 'skip-source-not-exists',
1094
+ sourcePath: sourceAsset.path,
1095
+ targetPath,
1096
+ });
978
1097
  return { source: sourceAsset.source, files: { added: [], overwritten: [], skipped: [] } };
979
1098
  }
980
1099
  const repoRoot = this.findGitRootForPath(sourceAsset.path);
981
1100
  if (!repoRoot) {
1101
+ this.logInitDebug({
1102
+ scope: 'copyClaudeDirectoryIfNeeded',
1103
+ branch: 'fallback-fs-no-git-root',
1104
+ sourcePath: sourceAsset.path,
1105
+ targetPath,
1106
+ });
982
1107
  return this.copyClaudeDirectoryByFileSystem(sourceAsset, targetPath);
983
1108
  }
984
1109
  const canonicalRepoRoot = this.safeRealpath(repoRoot);
@@ -989,13 +1114,38 @@ export class InitCommand {
989
1114
  .join('/');
990
1115
  if (relativeSourceDir.startsWith('..') ||
991
1116
  path.isAbsolute(relativeSourceDir)) {
1117
+ this.logInitDebug({
1118
+ scope: 'copyClaudeDirectoryIfNeeded',
1119
+ branch: 'fallback-fs-bad-relative',
1120
+ canonicalRepoRoot,
1121
+ canonicalSourcePath,
1122
+ relativeSourceDir,
1123
+ targetPath,
1124
+ });
992
1125
  return this.copyClaudeDirectoryByFileSystem(sourceAsset, targetPath);
993
1126
  }
994
1127
  const filesResult = this.copyGitTrackedDirectoryFiles(canonicalRepoRoot, relativeSourceDir, targetPath);
995
1128
  const totalFiles = filesResult.added.length + filesResult.overwritten.length + filesResult.skipped.length;
996
1129
  if (totalFiles === 0) {
1130
+ this.logInitDebug({
1131
+ scope: 'copyClaudeDirectoryIfNeeded',
1132
+ branch: 'git-tracked-empty',
1133
+ canonicalRepoRoot,
1134
+ relativeSourceDir,
1135
+ targetPath,
1136
+ });
997
1137
  return { source: sourceAsset.source, files: { added: [], overwritten: [], skipped: [] } };
998
1138
  }
1139
+ this.logInitDebug({
1140
+ scope: 'copyClaudeDirectoryIfNeeded',
1141
+ branch: 'git-tracked-ok',
1142
+ canonicalRepoRoot,
1143
+ relativeSourceDir,
1144
+ targetPath,
1145
+ added: filesResult.added.length,
1146
+ overwritten: filesResult.overwritten.length,
1147
+ skipped: filesResult.skipped.length,
1148
+ });
999
1149
  return {
1000
1150
  source: sourceAsset.source,
1001
1151
  files: filesResult,
@@ -1120,8 +1270,25 @@ export class InitCommand {
1120
1270
  .map((line) => line.trim())
1121
1271
  .filter((line) => line.length > 0);
1122
1272
  if (trackedFiles.length === 0) {
1273
+ this.logInitDebug({
1274
+ scope: 'copyGitTrackedDirectoryFiles',
1275
+ result: 'empty-tracked-files',
1276
+ gitRoot,
1277
+ sourceDirRelativeToGitRoot,
1278
+ targetDir,
1279
+ rawOutputPreview: output.slice(0, 200),
1280
+ });
1123
1281
  return result;
1124
1282
  }
1283
+ this.logInitDebug({
1284
+ scope: 'copyGitTrackedDirectoryFiles',
1285
+ result: 'tracked-files-loaded',
1286
+ gitRoot,
1287
+ sourceDirRelativeToGitRoot,
1288
+ targetDir,
1289
+ trackedCount: trackedFiles.length,
1290
+ sample: trackedFiles.slice(0, 5),
1291
+ });
1125
1292
  // 确保目标目录存在,但不删除已有内容
1126
1293
  mkdirSync(targetDir, { recursive: true });
1127
1294
  trackedFiles.forEach((repoFilePath) => {
@@ -1159,7 +1326,17 @@ export class InitCommand {
1159
1326
  });
1160
1327
  return result;
1161
1328
  }
1162
- catch {
1329
+ catch (err) {
1330
+ this.logInitDebug({
1331
+ scope: 'copyGitTrackedDirectoryFiles',
1332
+ result: 'exception',
1333
+ gitRoot,
1334
+ sourceDirRelativeToGitRoot,
1335
+ targetDir,
1336
+ errorMessage: err?.message ?? String(err),
1337
+ stderr: err?.stderr ? String(err.stderr).slice(0, 500) : undefined,
1338
+ status: err?.status,
1339
+ });
1163
1340
  return result;
1164
1341
  }
1165
1342
  }
@@ -1237,6 +1414,15 @@ export class InitCommand {
1237
1414
  await configurator.configure(projectPath, zhuanspecDir);
1238
1415
  return existed ? 'updated' : 'created';
1239
1416
  }
1417
+ /**
1418
+ * v2.15.16:对外暴露的 settings.json 同步入口,给 zhuanspec update 复用。
1419
+ * 会走 generateClaudeSettings 同样的 merge + 老 group 清理逻辑,
1420
+ * 确保老版 settings.json(如还挂着 deviation-check --trigger post-prompt)
1421
+ * 在升级后能被过滤掉。
1422
+ */
1423
+ async syncClaudeSettings(projectPath) {
1424
+ return this.generateClaudeSettings(projectPath);
1425
+ }
1240
1426
  /**
1241
1427
  * Generate Claude Code hooks configuration for ZhuanSpec workflow.
1242
1428
  * Creates .claude/settings.json with hooks for SessionStart, PreToolUse, PostToolUse, and Stop.
@@ -1267,8 +1453,8 @@ export class InitCommand {
1267
1453
  hooks: [
1268
1454
  {
1269
1455
  type: 'command',
1270
- command: 'zhuanspec-hook deviation-check --trigger post-prompt',
1271
- statusMessage: 'Checking prompt vs proposal consistency...',
1456
+ command: 'zhuanspec-hook user-input',
1457
+ statusMessage: 'Recording user input & correction context...',
1272
1458
  },
1273
1459
  ],
1274
1460
  },
@@ -1381,7 +1567,18 @@ export class InitCommand {
1381
1567
  // Append ZhuanSpec hooks to existing hooks array while de-duplicating.
1382
1568
  const existingArray = existingHooks[hookType];
1383
1569
  const zhuanspecArray = zhuanspecHookConfig;
1384
- const existingSignatures = new Set(existingArray.map((item) => JSON.stringify(item)));
1570
+ // v2.15.16:先剔除所有纯 ZhuanSpec 管辖的老 group,再 append 新模板。
1571
+ // 防止 Sprint 3 下线的 deviation-check --trigger post-prompt 等旧 entry
1572
+ // 因老 settings.json 残留而永久活下来;非 ZhuanSpec entry(用户自己挂的)全量保留。
1573
+ const cleanedExisting = [];
1574
+ for (const item of existingArray) {
1575
+ if (isZhuanspecManagedHookGroup(item)) {
1576
+ // 被清理的老 group,这里静默丢掉(不计入 duplicateHooksSkipped)
1577
+ continue;
1578
+ }
1579
+ cleanedExisting.push(item);
1580
+ }
1581
+ const existingSignatures = new Set(cleanedExisting.map((item) => JSON.stringify(item)));
1385
1582
  const uniqueToAppend = [];
1386
1583
  for (const item of zhuanspecArray) {
1387
1584
  const signature = JSON.stringify(item);
@@ -1393,7 +1590,7 @@ export class InitCommand {
1393
1590
  uniqueToAppend.push(item);
1394
1591
  }
1395
1592
  summary.totalAddedHooks += uniqueToAppend.length;
1396
- existingHooks[hookType] = [...existingArray, ...uniqueToAppend];
1593
+ existingHooks[hookType] = [...cleanedExisting, ...uniqueToAppend];
1397
1594
  }
1398
1595
  else {
1399
1596
  // Create new hook type with ZhuanSpec config
@@ -9,10 +9,121 @@
9
9
  * 3. computeAccuracy() —— 计算准确率
10
10
  */
11
11
  import type { ProgressData } from '../hooks/record-progress.js';
12
+ export interface AccuracyDebugEntry {
13
+ /** 事件类型:baseline.mark / ai.record / correction.record / skip.<reason> / rate.compute / rate.override / persist.ok / persist.fail */
14
+ type: string;
15
+ /** 所属 phase(如有) */
16
+ phase?: string;
17
+ /** 文件分类(如有) */
18
+ kind?: string;
19
+ /** 触发来源:hook / cli / archive / auto */
20
+ trigger?: string;
21
+ /** 输入快照(索引自由) */
22
+ input?: Record<string, unknown>;
23
+ /** 输出快照(索引自由) */
24
+ output?: Record<string, unknown>;
25
+ /** skip.* 时的原因 */
26
+ skipReason?: string;
27
+ /** 耗时(ms) */
28
+ durationMs?: number;
29
+ changeId?: string;
30
+ }
31
+ export declare const ACCURACY_DEBUG_LOG_FILENAME = ".accuracy-debug.log";
32
+ /**
33
+ * 向 changeDir/metrics/.accuracy-debug.log 追写一行 JSONL。
34
+ * 失败不阻断主流程。
35
+ */
36
+ export declare function appendAccuracyDebugLog(changeDir: string, entry: AccuracyDebugEntry): void;
12
37
  /**
13
38
  * 判断是否为代码文件(需纳入准确率统计)
14
39
  */
15
40
  export declare function isCodeFile(filePath: string): boolean;
41
+ export type TrackedPhase = 'techDesign' | 'propose' | 'apply' | 'review';
42
+ export type TrackedKind = 'techSpec' | 'proposalDoc' | 'code';
43
+ /**
44
+ * 分阶段准确率桶。
45
+ * 注:纠偏明细的唯一事实源是顶层 AccuracyData.correctionEdits[](桶不重复存明细)。
46
+ * 按 phase 查明细:progress.accuracy.correctionEdits.filter(e => e.phase === phaseX)。
47
+ */
48
+ export interface PhaseAccuracyEntry {
49
+ phase: TrackedPhase;
50
+ trackedKind: TrackedKind;
51
+ aiLinesAdded: number;
52
+ aiLinesModified: number;
53
+ aiTotalLines: number;
54
+ userCorrectionLines: number;
55
+ accuracyRate: number;
56
+ accuracyRateRaw?: number;
57
+ /** 该 phase 触发纠偏的 promptId 列表(去重、按时间顺序) */
58
+ correctionPromptIds: string[];
59
+ firstRecordedAt?: string;
60
+ lastUpdatedAt?: string;
61
+ /**
62
+ * 用户确认标记(Task 4 阶段切换阻断式确认)。
63
+ * phase-transition-check 读此字段判定是否已确认。
64
+ */
65
+ confirmed?: {
66
+ at: string;
67
+ by: 'manual-cli' | 'auto-archive';
68
+ };
69
+ /**
70
+ * phase 级 override 历史;每次完整保留 raw 快照。
71
+ * 不覆盖,按时间追加,供事后差异分析。
72
+ */
73
+ overrideHistory?: Array<{
74
+ timestamp: string;
75
+ originalRate: number;
76
+ originalRateRaw: number;
77
+ originalAiTotalLines: number;
78
+ originalUserCorrectionLines: number;
79
+ overriddenRate: number;
80
+ reason: string;
81
+ }>;
82
+ }
83
+ /**
84
+ * 判定 filePath 在当前 phase 下是否属于白名单,并返回文件分类。
85
+ *
86
+ * - techDesign → changes/{changeId}/techDesign/** 下所有文件 (kind=techSpec)
87
+ * - propose → changes/{changeId} 根下 proposal.md/tasks.md/design.md/test-cases.md,或 specs/** (kind=proposalDoc)
88
+ * - apply / review → isCodeFile 通用白名单 (kind=code)
89
+ * - idle / archive → 不统计
90
+ */
91
+ export declare function isTrackedFileForPhase(filePath: string, phase: string, changePath: string): {
92
+ tracked: boolean;
93
+ kind?: TrackedKind;
94
+ };
95
+ /**
96
+ * 取/建某 phase+kind 对应的准确率桶。
97
+ */
98
+ export declare function getOrCreatePhaseBucket(progress: ProgressData, phase: TrackedPhase, kind: TrackedKind): PhaseAccuracyEntry;
99
+ /**
100
+ * 重算 accuracy 相关字段。
101
+ * - 顶层 accuracyRateRaw/accuracyRate:基于顶层 aiTotalLines/userCorrectionLines,保留 override 覆写
102
+ * - 各阶段桶 accuracyRate:基于桶自身行数,保留桶级 override。
103
+ *
104
+ * @param opts.changeDir 传入后会向 .accuracy-debug.log 写 rate.compute 事件
105
+ * @param opts.trigger 触发来源标识(hook / cli / archive 等)
106
+ */
107
+ export declare function recomputeAccuracyRate(progress: ProgressData, opts?: {
108
+ changeDir?: string;
109
+ trigger?: string;
110
+ }): void;
111
+ /**
112
+ * 统一入口:将 progress.accuracy 同步到 metrics/accuracy.json。
113
+ * 之前多处手写导致双写漂移,经此函数统一管理。
114
+ */
115
+ export declare function persistAccuracyJson(changeDir: string, progress: ProgressData): Promise<void>;
116
+ /**
117
+ * 计算指定 phase+kind 的桶聚合结果(只读)。
118
+ */
119
+ export declare function computePhaseAccuracy(progress: ProgressData, phase: TrackedPhase, kind?: TrackedKind): (AccuracyResult & {
120
+ phase: TrackedPhase;
121
+ trackedKind?: TrackedKind;
122
+ }) | null;
123
+ /**
124
+ * 列出所有 phase+kind 桶的快照(只读)。
125
+ */
126
+ export declare function listPhaseAccuracy(progress: ProgressData): PhaseAccuracyEntry[];
16
127
  export interface AccuracySnapshotResult {
17
128
  changeId: string;
18
129
  snapshotTimestamp: string;
@@ -21,7 +132,10 @@ export interface AccuracySnapshotResult {
21
132
  aiTotalLines: number;
22
133
  }
23
134
  /**
24
- * 在 Apply 阶段全部任务完成后调用,固化 AI 产出行数基线
135
+ * 在 Apply 阶段全部任务完成后调用,固化 AI 产出行数基线。
136
+ *
137
+ * 幂等语义:保留 existing 中已累积的 userCorrectionLines/overrideHistory/correctionEdits/phaseAccuracy,
138
+ * 仅刷新 snapshotTimestamp 与 AI 行数。写入后调用 recomputeAccuracyRate + persistAccuracyJson。
25
139
  */
26
140
  export declare function snapshotAiBaseline(progress: ProgressData, changeDir: string): Promise<AccuracySnapshotResult>;
27
141
  /**
@@ -72,9 +186,44 @@ export interface OverrideResult {
72
186
  overriddenRate: number;
73
187
  }
74
188
  /**
75
- * 写入手动修正记录
189
+ * 写入顶层准确率手动修正记录。
190
+ *
191
+ * override 时完整保留纠正前的 raw 快照(rate / rateRaw / aiTotal / correction),
192
+ * 永不覆盖,以供事后差异分析。
193
+ */
194
+ export declare function applyOverride(progress: ProgressData, rate: number, opts?: {
195
+ reason?: string;
196
+ changeDir?: string;
197
+ trigger?: string;
198
+ }): OverrideResult;
199
+ export interface PhaseOverrideResult {
200
+ phase: TrackedPhase;
201
+ affectedBuckets: number;
202
+ details: Array<{
203
+ trackedKind: TrackedKind;
204
+ originalRate: number;
205
+ originalRateRaw: number;
206
+ originalAiTotalLines: number;
207
+ originalUserCorrectionLines: number;
208
+ overriddenRate: number;
209
+ }>;
210
+ }
211
+ /**
212
+ * 为某 phase 下的所有桶写入手动 override。
213
+ * 完整保留纠正前的桶级快照,后续可从 overrideHistory 还原。
214
+ */
215
+ export declare function applyPhaseOverride(progress: ProgressData, phase: TrackedPhase, rate: number, reason: string, opts?: {
216
+ changeDir?: string;
217
+ trigger?: string;
218
+ }): PhaseOverrideResult;
219
+ /**
220
+ * 标记某 phase 准确率已被用户确认。phase-transition-check 会读此标记。
76
221
  */
77
- export declare function applyOverride(progress: ProgressData, rate: number): OverrideResult;
222
+ export declare function confirmPhaseAccuracy(progress: ProgressData, phase: TrackedPhase, by?: 'manual-cli' | 'auto-archive', opts?: {
223
+ changeDir?: string;
224
+ }): {
225
+ affectedBuckets: number;
226
+ };
78
227
  export type AgentSource = 'ai' | 'user';
79
228
  export type StrategyLevel = 'L1' | 'L2' | 'L3';
80
229
  export interface SourceDetermination {