@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
package/dist/core/init.d.ts
CHANGED
|
@@ -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', '
|
|
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:
|
|
854
|
-
dotClaude:
|
|
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
|
|
1271
|
-
statusMessage: '
|
|
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
|
-
|
|
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] = [...
|
|
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
|
|
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 {
|