@zhuan-ai/zhuanspec 2.16.3 → 2.16.4
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/core/configurators/codex.js +11 -1
- package/dist/core/hooks/record-progress.js +213 -69
- package/dist/core/metrics/code-accuracy.d.ts +76 -0
- package/dist/core/metrics/code-accuracy.js +130 -0
- package/dist/core/templates/codex-hooks-template.d.ts +27 -0
- package/dist/core/templates/codex-hooks-template.js +81 -0
- package/dist/utils/line-diff.d.ts +33 -0
- package/dist/utils/line-diff.js +85 -0
- package/package.json +20 -23
|
@@ -38,7 +38,7 @@ import path from 'path';
|
|
|
38
38
|
import os from 'os';
|
|
39
39
|
import { promises as fs } from 'fs';
|
|
40
40
|
import { FileSystemUtils } from '../../utils/file-system.js';
|
|
41
|
-
import { getCodexHooksTomlBlock, getCodexDefaultHookSlots } from '../templates/codex-hooks-template.js';
|
|
41
|
+
import { getCodexHooksTomlBlock, getCodexDefaultHookSlots, getCodexDefaultHookHashes } from '../templates/codex-hooks-template.js';
|
|
42
42
|
import { getCodexAgentTomls, } from '../templates/codex-agents-template.js';
|
|
43
43
|
import { getCodexSkillFiles } from '../templates/codex-skills-template.js';
|
|
44
44
|
import { CodexSlashCommandConfigurator } from './slash/codex.js';
|
|
@@ -374,6 +374,16 @@ export class CodexConfigurator {
|
|
|
374
374
|
seedBySuffix.set(split.suffix, entry.hash);
|
|
375
375
|
}
|
|
376
376
|
}
|
|
377
|
+
// 2b. Computed hashes take priority over cross-project seeds because
|
|
378
|
+
// seeds from a different source type (e.g. ~/.codex/hooks.json vs
|
|
379
|
+
// .codex/config.toml) may have different hook content and thus wrong
|
|
380
|
+
// hashes. Our computed hashes replicate the exact Codex CLI fingerprint
|
|
381
|
+
// algorithm (NormalizedHookIdentity → canonical JSON → SHA-256) and are
|
|
382
|
+
// guaranteed correct for the current ZhuanSpec default hooks.
|
|
383
|
+
const computedHashes = getCodexDefaultHookHashes();
|
|
384
|
+
for (const [suffix, hash] of computedHashes) {
|
|
385
|
+
seedBySuffix.set(suffix, hash); // always overwrite cross-project seeds
|
|
386
|
+
}
|
|
377
387
|
const seedAvailable = seedBySuffix.size > 0;
|
|
378
388
|
// 3. Plan per-slot writes.
|
|
379
389
|
const written = [];
|
|
@@ -11,8 +11,9 @@ import { promises as fsPromises, existsSync, readFileSync, unlinkSync } from 'fs
|
|
|
11
11
|
import { FileSystemUtils } from '../../utils/file-system.js';
|
|
12
12
|
import { PHASE_ORDER, PHASE_IDLE_THRESHOLD_MS } from '../../utils/phase-utils.js';
|
|
13
13
|
import { resolveZhuanSpecRoot } from '../../utils/resolve-root.js';
|
|
14
|
-
import { isCodeFile, isTrackedFileForPhase, getOrCreatePhaseBucket, recomputeAccuracyRate, persistAccuracyJson, appendAccuracyDebugLog } from '../metrics/code-accuracy.js';
|
|
14
|
+
import { isCodeFile, isTrackedFileForPhase, getOrCreatePhaseBucket, recomputeAccuracyRate, persistAccuracyJson, appendAccuracyDebugLog, isReviewAutoFixActive, isMeaningfulUserCorrection, isTrivialAckPrompt, computePromptFileMaxLines } from '../metrics/code-accuracy.js';
|
|
15
15
|
import { detectHookHost, sanitizeCodexEnvelope } from '../../utils/hook-host.js';
|
|
16
|
+
import { countLineDiff } from '../../utils/line-diff.js';
|
|
16
17
|
const fs = fsPromises;
|
|
17
18
|
/**
|
|
18
19
|
* Atomic write JSON file: write to temp file first, then rename
|
|
@@ -699,6 +700,11 @@ async function runRecordProgress(filePath, toolName, success, stdinData) {
|
|
|
699
700
|
const toolInputOldString = stdinData.tool_input?.old_string;
|
|
700
701
|
const toolInputEdits = stdinData.tool_input?.edits;
|
|
701
702
|
// 按工具类型计算本次变更涉及的行数
|
|
703
|
+
// 口径:
|
|
704
|
+
// Write → 全文行数(无旧本可参考,以完整写入量为准)
|
|
705
|
+
// Edit → countLineDiff(old, new):仅计增+删净改动行(不含上下文锚点)
|
|
706
|
+
// MultiEdit → 多个 edit 的 diff 累加
|
|
707
|
+
// 此口径对 techDesign / propose / apply / review 四阶段统一生效(均走 isTrackedFileForPhase 白名单)
|
|
702
708
|
let changeLines = 0;
|
|
703
709
|
let contentSource = 'none';
|
|
704
710
|
if (toolName === 'Write') {
|
|
@@ -708,17 +714,14 @@ async function runRecordProgress(filePath, toolName, success, stdinData) {
|
|
|
708
714
|
}
|
|
709
715
|
}
|
|
710
716
|
else if (toolName === 'Edit') {
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
changeLines = Math.max(newLines, oldLines);
|
|
717
|
+
// 以前:Math.max(newLines, oldLines) — 含上下文锚点,虚高计数
|
|
718
|
+
// 现在:只计真正变动的行(增+删),跳过未变上下文
|
|
719
|
+
changeLines = countLineDiff(toolInputOldString, toolInputNewString);
|
|
715
720
|
contentSource = (toolInputNewString || toolInputOldString) ? 'new_string' : 'none';
|
|
716
721
|
}
|
|
717
722
|
else if (toolName === 'MultiEdit' && Array.isArray(toolInputEdits)) {
|
|
718
723
|
for (const edit of toolInputEdits) {
|
|
719
|
-
|
|
720
|
-
const oldLines = edit?.old_string ? edit.old_string.split('\n').length : 0;
|
|
721
|
-
changeLines += Math.max(newLines, oldLines);
|
|
724
|
+
changeLines += countLineDiff(edit?.old_string, edit?.new_string);
|
|
722
725
|
}
|
|
723
726
|
contentSource = toolInputEdits.length > 0 ? 'edits[]' : 'none';
|
|
724
727
|
}
|
|
@@ -800,40 +803,30 @@ async function runRecordProgress(filePath, toolName, success, stdinData) {
|
|
|
800
803
|
correctionEdits: [], phaseAccuracy: [],
|
|
801
804
|
};
|
|
802
805
|
}
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
//
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
progress.accuracy.userCorrectionLines = (progress.accuracy.userCorrectionLines || 0) + changeLines;
|
|
826
|
-
progress.accuracy.correctionEdits = progress.accuracy.correctionEdits || [];
|
|
827
|
-
// 提取纠偏触发源头信息(Task 6)
|
|
828
|
-
// post-baseline-fallback 兜底命中时,语义等价于 user-input hook 写入的 'post-baseline-prompt'。
|
|
806
|
+
// === Review 阶段特殊处理 ===
|
|
807
|
+
// 规则:
|
|
808
|
+
// 1. CR/单测 Skill 自动修复 → 不计入准确率(完全跳过)
|
|
809
|
+
// 2. 用户提示词触发的纠正 → 归属到 apply 阶段的准确率桶
|
|
810
|
+
// 3. review 阶段不产生独立的准确率 bucket
|
|
811
|
+
//
|
|
812
|
+
// 用户提示词识别(与 apply 阶段一致):
|
|
813
|
+
// - correction-keyword:命中显式纠偏关键词("不对"、"应该是"等)
|
|
814
|
+
// - post-baseline-prompt + 非琐碎确认:baseline 后的任何用户提示,
|
|
815
|
+
// 只要 promptSnippet 不是"继续"、"好的"等琐碎确认词。
|
|
816
|
+
// 斜杠/感叹号命令(/zhuanspec:、!ls)已在 user-input-hook 过滤。
|
|
817
|
+
// - .pending-correction marker 存在
|
|
818
|
+
//
|
|
819
|
+
// 自动修复 = 最近有 review autofix skill 调用(code-review-expert / generate-mockito-unit-test)
|
|
820
|
+
if (trackedPhase === 'review') {
|
|
821
|
+
// 判断是否为用户提示词触发的纠正
|
|
822
|
+
const meaningfulCC = isMeaningfulUserCorrection(progress.correctionContext ?? undefined);
|
|
823
|
+
const isExplicitUserCorrection = (ccActiveRaw && meaningfulCC) || pendingExists;
|
|
824
|
+
if (isExplicitUserCorrection) {
|
|
825
|
+
// 用户在 review 阶段的纠正 → 归到 apply/code bucket,影响 apply 准确率
|
|
826
|
+
const applyBucket = getOrCreatePhaseBucket(progress, 'apply', trackedKind);
|
|
827
|
+
// 提取纠偏触发源头信息
|
|
829
828
|
const ccSource = progress.correctionContext?.source
|
|
830
|
-
|| (pendingExists
|
|
831
|
-
? 'pending-correction-marker'
|
|
832
|
-
: (correctionFallbackReason === 'post-baseline-fallback'
|
|
833
|
-
? 'post-baseline-prompt'
|
|
834
|
-
: 'correction-keyword'));
|
|
835
|
-
// Task 6:若 correctionContext 无 prompt 元数据(典型于 pending-correction-marker 兑底路径),
|
|
836
|
-
// 从 .pending-correction marker JSON 中回填 promptId / phaseInputIndex / promptSnippet。
|
|
829
|
+
|| (pendingExists ? 'pending-correction-marker' : 'correction-keyword');
|
|
837
830
|
let fallbackPromptId;
|
|
838
831
|
let fallbackPromptIndex;
|
|
839
832
|
let fallbackPromptSnippet;
|
|
@@ -854,6 +847,13 @@ async function runRecordProgress(filePath, toolName, success, stdinData) {
|
|
|
854
847
|
const promptId = progress.correctionContext?.promptId ?? fallbackPromptId;
|
|
855
848
|
const promptIndex = progress.correctionContext?.promptIndex ?? fallbackPromptIndex;
|
|
856
849
|
const promptSnippet = progress.correctionContext?.promptSnippet ?? fallbackPromptSnippet;
|
|
850
|
+
// 去重:同 (promptId + filePath + apply phase) 取历史 max,只累加 delta
|
|
851
|
+
progress.accuracy.correctionEdits = progress.accuracy.correctionEdits || [];
|
|
852
|
+
const prevMax = computePromptFileMaxLines(progress.accuracy.correctionEdits, promptId, filePath, 'apply');
|
|
853
|
+
const dedupDelta = Math.max(0, changeLines - prevMax);
|
|
854
|
+
applyBucket.userCorrectionLines += dedupDelta;
|
|
855
|
+
applyBucket.lastUpdatedAt = timestamp;
|
|
856
|
+
progress.accuracy.userCorrectionLines = (progress.accuracy.userCorrectionLines || 0) + dedupDelta;
|
|
857
857
|
progress.accuracy.correctionEdits.push({
|
|
858
858
|
filePath,
|
|
859
859
|
editLines: changeLines,
|
|
@@ -861,7 +861,7 @@ async function runRecordProgress(filePath, toolName, success, stdinData) {
|
|
|
861
861
|
strategy: 'correction-context',
|
|
862
862
|
agentType: progress.correctionContext?.path,
|
|
863
863
|
timestamp,
|
|
864
|
-
phase:
|
|
864
|
+
phase: 'apply', // 归属到 apply(非 review)
|
|
865
865
|
trackedKind,
|
|
866
866
|
triggeredBy: {
|
|
867
867
|
source: ccSource,
|
|
@@ -870,14 +870,14 @@ async function runRecordProgress(filePath, toolName, success, stdinData) {
|
|
|
870
870
|
promptSnippet,
|
|
871
871
|
},
|
|
872
872
|
});
|
|
873
|
-
// 记录
|
|
874
|
-
if (promptId && !
|
|
875
|
-
|
|
873
|
+
// 记录 apply bucket 的 promptId(去重)
|
|
874
|
+
if (promptId && !applyBucket.correctionPromptIds.includes(promptId)) {
|
|
875
|
+
applyBucket.correctionPromptIds.push(promptId);
|
|
876
876
|
}
|
|
877
|
-
correctionDebugLog(`${toolName}/correction`, 'userCorrectionLines',
|
|
877
|
+
correctionDebugLog(`${toolName}/review-user-correction→apply`, 'userCorrectionLines', dedupDelta);
|
|
878
878
|
appendAccuracyDebugLog(changeDir, {
|
|
879
879
|
type: 'correction.record',
|
|
880
|
-
phase:
|
|
880
|
+
phase: 'apply', // 归属到 apply
|
|
881
881
|
kind: trackedKind,
|
|
882
882
|
trigger: 'hook',
|
|
883
883
|
changeId,
|
|
@@ -886,39 +886,183 @@ async function runRecordProgress(filePath, toolName, success, stdinData) {
|
|
|
886
886
|
file: filePath,
|
|
887
887
|
changeLines,
|
|
888
888
|
contentSource,
|
|
889
|
+
actualPhase: 'review',
|
|
890
|
+
redirectedTo: 'apply',
|
|
889
891
|
ccSource,
|
|
890
892
|
promptId,
|
|
891
893
|
promptIndex,
|
|
894
|
+
prevMax,
|
|
895
|
+
dedupDelta,
|
|
892
896
|
},
|
|
893
897
|
output: {
|
|
894
|
-
bucketCorrection:
|
|
898
|
+
bucketCorrection: applyBucket.userCorrectionLines,
|
|
895
899
|
topCorrection: progress.accuracy.userCorrectionLines,
|
|
896
|
-
promptIdsCount:
|
|
900
|
+
promptIdsCount: applyBucket.correctionPromptIds.length,
|
|
901
|
+
},
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
else {
|
|
905
|
+
// 非用户提示词触发 → review 阶段自动修复、琐碎确认后的编辑、或 baseline fallback
|
|
906
|
+
// 判断是否为 Skill 自动修复(CR/单测)
|
|
907
|
+
const isAutoFix = isReviewAutoFixActive(progress.skillCalls || [], progress.correctionContext ?? undefined);
|
|
908
|
+
const ccSourceRaw = progress.correctionContext?.source;
|
|
909
|
+
const ccPromptSnippet = progress.correctionContext?.promptSnippet;
|
|
910
|
+
const ccTrivialAck = ccSourceRaw === 'post-baseline-prompt'
|
|
911
|
+
&& isTrivialAckPrompt(ccPromptSnippet);
|
|
912
|
+
const skipReason = isAutoFix
|
|
913
|
+
? 'review-skill-autofix'
|
|
914
|
+
: (ccTrivialAck ? 'review-trivial-ack' : 'review-non-correction');
|
|
915
|
+
correctionDebugLog(`skipped/${skipReason}`, 'skipped', 0);
|
|
916
|
+
appendAccuracyDebugLog(changeDir, {
|
|
917
|
+
type: `skip.${skipReason}`,
|
|
918
|
+
phase: trackedPhase,
|
|
919
|
+
kind: trackedKind,
|
|
920
|
+
trigger: 'hook',
|
|
921
|
+
changeId,
|
|
922
|
+
skipReason,
|
|
923
|
+
input: {
|
|
924
|
+
tool: toolName,
|
|
925
|
+
file: filePath,
|
|
926
|
+
changeLines,
|
|
927
|
+
inCorrectionContext,
|
|
928
|
+
baselineMarkedForPhase,
|
|
929
|
+
ccActiveRaw,
|
|
930
|
+
ccSource: ccSourceRaw,
|
|
931
|
+
ccTrivialAck,
|
|
932
|
+
isAutoFix,
|
|
897
933
|
},
|
|
898
934
|
});
|
|
935
|
+
// review 阶段自动修复、琐碎确认、非纠正编辑都完全跳过准确率统计
|
|
899
936
|
}
|
|
900
937
|
}
|
|
901
938
|
else {
|
|
902
|
-
//
|
|
903
|
-
bucket
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
}
|
|
921
|
-
|
|
939
|
+
// === 非 review 阶段:原有逻辑 ===
|
|
940
|
+
const bucket = getOrCreatePhaseBucket(progress, trackedPhase, trackedKind);
|
|
941
|
+
if (inCorrectionContext) {
|
|
942
|
+
// 工具白名单守卫:techDesign / propose 阶段的纠偏统计只接受增量编辑(Edit/MultiEdit),
|
|
943
|
+
// 禁止 Write。原因:tech-spec.md / proposal.md 通常是整文件 Write 产出,一旦被误判为纠偏,
|
|
944
|
+
// 会把整份文件行数当成 userCorrectionLines 记入,导致分子被严重放大、accuracyRate 失真。
|
|
945
|
+
// apply 阶段不受此限制(apply 可 Write 新增代码文件)。
|
|
946
|
+
if ((trackedPhase === 'techDesign' || trackedPhase === 'propose') && toolName === 'Write') {
|
|
947
|
+
correctionDebugLog(`skipped/write-blocked-${trackedPhase}-correction`, 'skipped', 0);
|
|
948
|
+
appendAccuracyDebugLog(changeDir, {
|
|
949
|
+
type: `skip.write-blocked-${trackedPhase}-correction`,
|
|
950
|
+
phase: trackedPhase,
|
|
951
|
+
kind: trackedKind,
|
|
952
|
+
trigger: 'hook',
|
|
953
|
+
changeId,
|
|
954
|
+
skipReason: `write-blocked-${trackedPhase}-correction`,
|
|
955
|
+
input: { tool: toolName, file: filePath, changeLines },
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
else {
|
|
959
|
+
// 纠偏上下文:提取 promptId,同 (promptId+file+phase) 取 max 计算 delta,桶 + 顶层 userCorrectionLines 同步累加 delta
|
|
960
|
+
// 提取纠偏触发源头信息(Task 6)
|
|
961
|
+
// post-baseline-fallback 兜底命中时,语义等价于 user-input hook 写入的 'post-baseline-prompt'。
|
|
962
|
+
const ccSource = progress.correctionContext?.source
|
|
963
|
+
|| (pendingExists
|
|
964
|
+
? 'pending-correction-marker'
|
|
965
|
+
: (correctionFallbackReason === 'post-baseline-fallback'
|
|
966
|
+
? 'post-baseline-prompt'
|
|
967
|
+
: 'correction-keyword'));
|
|
968
|
+
// Task 6:若 correctionContext 无 prompt 元数据(典型于 pending-correction-marker 兑底路径),
|
|
969
|
+
// 从 .pending-correction marker JSON 中回填 promptId / phaseInputIndex / promptSnippet。
|
|
970
|
+
let fallbackPromptId;
|
|
971
|
+
let fallbackPromptIndex;
|
|
972
|
+
let fallbackPromptSnippet;
|
|
973
|
+
if (!progress.correctionContext?.promptId && pendingExists) {
|
|
974
|
+
try {
|
|
975
|
+
const markerRaw = readFileSync(pendingMarkerPath, 'utf-8');
|
|
976
|
+
if (markerRaw) {
|
|
977
|
+
const markerData = JSON.parse(markerRaw);
|
|
978
|
+
fallbackPromptId = markerData.promptId;
|
|
979
|
+
fallbackPromptIndex = markerData.phaseInputIndex;
|
|
980
|
+
fallbackPromptSnippet = markerData.promptSnippet;
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
catch {
|
|
984
|
+
// marker 解析失败不阻断主流程
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
const promptId = progress.correctionContext?.promptId ?? fallbackPromptId;
|
|
988
|
+
const promptIndex = progress.correctionContext?.promptIndex ?? fallbackPromptIndex;
|
|
989
|
+
const promptSnippet = progress.correctionContext?.promptSnippet ?? fallbackPromptSnippet;
|
|
990
|
+
// 去重:同 (promptId + filePath + phase) 取历史 max,只累加 delta。
|
|
991
|
+
// 语义:同一 prompt 中 AI 对同一文件的多次 Edit(重复修正同区域)不重复计数。
|
|
992
|
+
progress.accuracy.correctionEdits = progress.accuracy.correctionEdits || [];
|
|
993
|
+
const prevMax = computePromptFileMaxLines(progress.accuracy.correctionEdits, promptId, filePath, trackedPhase);
|
|
994
|
+
const dedupDelta = Math.max(0, changeLines - prevMax);
|
|
995
|
+
bucket.userCorrectionLines += dedupDelta;
|
|
996
|
+
bucket.lastUpdatedAt = timestamp;
|
|
997
|
+
progress.accuracy.userCorrectionLines = (progress.accuracy.userCorrectionLines || 0) + dedupDelta;
|
|
998
|
+
progress.accuracy.correctionEdits.push({
|
|
999
|
+
filePath,
|
|
1000
|
+
editLines: changeLines,
|
|
1001
|
+
source: 'user',
|
|
1002
|
+
strategy: 'correction-context',
|
|
1003
|
+
agentType: progress.correctionContext?.path,
|
|
1004
|
+
timestamp,
|
|
1005
|
+
phase: trackedPhase,
|
|
1006
|
+
trackedKind,
|
|
1007
|
+
triggeredBy: {
|
|
1008
|
+
source: ccSource,
|
|
1009
|
+
promptId,
|
|
1010
|
+
promptIndex,
|
|
1011
|
+
promptSnippet,
|
|
1012
|
+
},
|
|
1013
|
+
});
|
|
1014
|
+
// 记录 phase 维度 promptId(去重)
|
|
1015
|
+
if (promptId && !bucket.correctionPromptIds.includes(promptId)) {
|
|
1016
|
+
bucket.correctionPromptIds.push(promptId);
|
|
1017
|
+
}
|
|
1018
|
+
correctionDebugLog(`${toolName}/correction`, 'userCorrectionLines', dedupDelta);
|
|
1019
|
+
appendAccuracyDebugLog(changeDir, {
|
|
1020
|
+
type: 'correction.record',
|
|
1021
|
+
phase: trackedPhase,
|
|
1022
|
+
kind: trackedKind,
|
|
1023
|
+
trigger: 'hook',
|
|
1024
|
+
changeId,
|
|
1025
|
+
input: {
|
|
1026
|
+
tool: toolName,
|
|
1027
|
+
file: filePath,
|
|
1028
|
+
changeLines,
|
|
1029
|
+
contentSource,
|
|
1030
|
+
ccSource,
|
|
1031
|
+
promptId,
|
|
1032
|
+
promptIndex,
|
|
1033
|
+
prevMax,
|
|
1034
|
+
dedupDelta,
|
|
1035
|
+
},
|
|
1036
|
+
output: {
|
|
1037
|
+
bucketCorrection: bucket.userCorrectionLines,
|
|
1038
|
+
topCorrection: progress.accuracy.userCorrectionLines,
|
|
1039
|
+
promptIdsCount: bucket.correctionPromptIds.length,
|
|
1040
|
+
},
|
|
1041
|
+
});
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
else {
|
|
1045
|
+
// 正常 AI 产出:桶 + 顶层 aiLinesAdded/aiTotalLines 同步累加,同时顶层 progress.linesAdded 仍需累加以兼容旧逻辑
|
|
1046
|
+
bucket.aiLinesAdded += changeLines;
|
|
1047
|
+
bucket.aiTotalLines += changeLines;
|
|
1048
|
+
bucket.lastUpdatedAt = timestamp;
|
|
1049
|
+
progress.accuracy.aiLinesAdded = (progress.accuracy.aiLinesAdded || 0) + changeLines;
|
|
1050
|
+
progress.accuracy.aiTotalLines = (progress.accuracy.aiTotalLines || 0) + changeLines;
|
|
1051
|
+
progress.linesAdded += changeLines;
|
|
1052
|
+
correctionDebugLog(`${toolName}/normal`, 'linesAdded', changeLines);
|
|
1053
|
+
appendAccuracyDebugLog(changeDir, {
|
|
1054
|
+
type: 'ai.record',
|
|
1055
|
+
phase: trackedPhase,
|
|
1056
|
+
kind: trackedKind,
|
|
1057
|
+
trigger: 'hook',
|
|
1058
|
+
changeId,
|
|
1059
|
+
input: { tool: toolName, file: filePath, changeLines, contentSource },
|
|
1060
|
+
output: {
|
|
1061
|
+
bucketAiTotal: bucket.aiTotalLines,
|
|
1062
|
+
topAiTotal: progress.accuracy.aiTotalLines,
|
|
1063
|
+
},
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
922
1066
|
}
|
|
923
1067
|
// 累加后重算 accuracyRate(顶层 + 各桶)
|
|
924
1068
|
recomputeAccuracyRate(progress, { changeDir, trigger: 'hook' });
|
|
@@ -38,6 +38,66 @@ export declare function appendAccuracyDebugLog(changeDir: string, entry: Accurac
|
|
|
38
38
|
* 判断是否为代码文件(需纳入准确率统计)
|
|
39
39
|
*/
|
|
40
40
|
export declare function isCodeFile(filePath: string): boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Review 阶段自动修复 Skill 列表。
|
|
43
|
+
* 这些 Skill 在 review 阶段触发的代码编辑属于自动修复,
|
|
44
|
+
* 不应计入用户纠正(不影响 apply 阶段准确率)。
|
|
45
|
+
*
|
|
46
|
+
* 匹配规则:skill name 包含列表中任一子串即视为 autofix skill。
|
|
47
|
+
*/
|
|
48
|
+
export declare const REVIEW_AUTOFIX_SKILLS: string[];
|
|
49
|
+
/**
|
|
50
|
+
* 纯确认/招呼类 prompt,不具备纠偏意图,不应触发用户纠正归因。
|
|
51
|
+
* 与 src/core/corrections/select-candidates.ts 中的 TRIVIAL_ACK_SET 保持同步。
|
|
52
|
+
*/
|
|
53
|
+
export declare const TRIVIAL_ACK_WORDS: ReadonlySet<string>;
|
|
54
|
+
/**
|
|
55
|
+
* 判定 prompt 是否为琐碎确认/招呼(如"继续"、"好的"、"ok"等)。
|
|
56
|
+
*
|
|
57
|
+
* 规则:
|
|
58
|
+
* 1. 空字符串视为琐碎;
|
|
59
|
+
* 2. 规范化后命中 TRIVIAL_ACK_WORDS 视为琐碎;
|
|
60
|
+
* 3. 规范化后字符长度 ≤ 2 视为琐碎(如"好。"、"!!")。
|
|
61
|
+
*
|
|
62
|
+
* 注:斜杠/感叹号开头的命令词(/zhuanspec:、!ls 等)已由 user-input-hook
|
|
63
|
+
* 在写入 correctionContext 前过滤,无需在此重复处理。
|
|
64
|
+
*/
|
|
65
|
+
export declare function isTrivialAckPrompt(text: string | null | undefined): boolean;
|
|
66
|
+
/**
|
|
67
|
+
* 判断 correctionContext 是否代表一次有意义的用户纠正(非琐碎确认)。
|
|
68
|
+
*
|
|
69
|
+
* 接受的 source:
|
|
70
|
+
* - correction-keyword:命中显式纠偏关键词,始终视为用户纠正;
|
|
71
|
+
* - post-baseline-prompt:baseline 后任何用户提示,但 promptSnippet
|
|
72
|
+
* 必须不是琐碎确认词(如"继续"、"好的")。
|
|
73
|
+
*
|
|
74
|
+
* 该函数用于 review 阶段判定 "用户提示词触发的纠正",使其覆盖范围与 apply 阶段一致。
|
|
75
|
+
*/
|
|
76
|
+
export declare function isMeaningfulUserCorrection(correctionContext?: {
|
|
77
|
+
active: boolean;
|
|
78
|
+
source?: string;
|
|
79
|
+
promptSnippet?: string;
|
|
80
|
+
} | null): boolean;
|
|
81
|
+
/**
|
|
82
|
+
* 判定当前 review 阶段的编辑是否属于 Skill 自动修复(CR/单测),不应计入准确率。
|
|
83
|
+
*
|
|
84
|
+
* 判定逻辑:取最近一次 review 阶段的 skill 调用,若 skill name 匹配 REVIEW_AUTOFIX_SKILLS
|
|
85
|
+
* 且该 skill 调用晚于最近的有意义用户纠偏上下文(correctionContext.resolvedAt),则视为自动修复。
|
|
86
|
+
*
|
|
87
|
+
* "有意义的用户纠偏" 定义:
|
|
88
|
+
* - source = 'correction-keyword',或
|
|
89
|
+
* - source = 'post-baseline-prompt' 且 promptSnippet 不是琐碎确认词
|
|
90
|
+
*/
|
|
91
|
+
export declare function isReviewAutoFixActive(skillCalls: Array<{
|
|
92
|
+
skill: string;
|
|
93
|
+
triggeredAt: string;
|
|
94
|
+
phase: string;
|
|
95
|
+
}>, correctionContext?: {
|
|
96
|
+
active: boolean;
|
|
97
|
+
resolvedAt?: string;
|
|
98
|
+
source?: string;
|
|
99
|
+
promptSnippet?: string;
|
|
100
|
+
}): boolean;
|
|
41
101
|
export type TrackedPhase = 'techDesign' | 'propose' | 'apply' | 'review';
|
|
42
102
|
export type TrackedKind = 'techSpec' | 'proposalDoc' | 'code';
|
|
43
103
|
/**
|
|
@@ -150,6 +210,22 @@ export declare function snapshotAiBaseline(progress: ProgressData, changeDir: st
|
|
|
150
210
|
* @returns true 表示本次调用创建了新快照,false 表示快照已存在或跳过
|
|
151
211
|
*/
|
|
152
212
|
export declare function ensureAccuracySnapshot(changeDir: string): Promise<boolean>;
|
|
213
|
+
/**
|
|
214
|
+
* 同 (promptId + filePath + phase) 历史 record 中的最大 editLines。
|
|
215
|
+
*
|
|
216
|
+
* 背景:用户一次 prompt 可能触发 AI 多次 Edit(重复修正同一区域)。
|
|
217
|
+
* 直接累加会造成虚高;取 max 反映“最终保留下来的最大一次改动”。
|
|
218
|
+
*
|
|
219
|
+
* promptId 为空时返回 0(冷启动,当前是首条,取 0 后累加全量 changeLines)。
|
|
220
|
+
*/
|
|
221
|
+
export declare function computePromptFileMaxLines(correctionEdits: ReadonlyArray<{
|
|
222
|
+
filePath: string;
|
|
223
|
+
editLines: number;
|
|
224
|
+
phase?: string;
|
|
225
|
+
triggeredBy?: {
|
|
226
|
+
promptId?: string;
|
|
227
|
+
};
|
|
228
|
+
}>, promptId: string | undefined, filePath: string, phase: TrackedPhase): number;
|
|
153
229
|
/**
|
|
154
230
|
* 用户纠正行数累计(legacy——新代码优先用 record-progress.ts 内联的 correctionContext 逻辑)。
|
|
155
231
|
*
|
|
@@ -62,6 +62,110 @@ export function isCodeFile(filePath) {
|
|
|
62
62
|
const ext = path.extname(filePath).toLowerCase();
|
|
63
63
|
return CODE_FILE_EXTENSIONS.has(ext);
|
|
64
64
|
}
|
|
65
|
+
// ============================================================
|
|
66
|
+
// Review 阶段自动修复 Skill 白名单
|
|
67
|
+
// ============================================================
|
|
68
|
+
/**
|
|
69
|
+
* Review 阶段自动修复 Skill 列表。
|
|
70
|
+
* 这些 Skill 在 review 阶段触发的代码编辑属于自动修复,
|
|
71
|
+
* 不应计入用户纠正(不影响 apply 阶段准确率)。
|
|
72
|
+
*
|
|
73
|
+
* 匹配规则:skill name 包含列表中任一子串即视为 autofix skill。
|
|
74
|
+
*/
|
|
75
|
+
export const REVIEW_AUTOFIX_SKILLS = [
|
|
76
|
+
'code-review-expert',
|
|
77
|
+
'generate-mockito-unit-test',
|
|
78
|
+
];
|
|
79
|
+
// ============================================================
|
|
80
|
+
// 琐碎确认词识别(与 select-candidates.ts 保持一致)
|
|
81
|
+
// ============================================================
|
|
82
|
+
/**
|
|
83
|
+
* 纯确认/招呼类 prompt,不具备纠偏意图,不应触发用户纠正归因。
|
|
84
|
+
* 与 src/core/corrections/select-candidates.ts 中的 TRIVIAL_ACK_SET 保持同步。
|
|
85
|
+
*/
|
|
86
|
+
export const TRIVIAL_ACK_WORDS = new Set([
|
|
87
|
+
'是', '是的', '好', '好的', '嗯', '嗯嗯', '对', '行', '可以', '确认',
|
|
88
|
+
'同意', '继续', '没问题', '明白', '了解', '知道了', '收到', '辛苦', '辛苦了',
|
|
89
|
+
'ok', 'okay', 'k', 'yes', 'y', 'yep', 'yeah', 'sure', 'fine', 'cool',
|
|
90
|
+
'go', 'go on', 'plan approved', 'continue', 'thanks', 'thx',
|
|
91
|
+
]);
|
|
92
|
+
/** 规范化文本:小写 + 中英文标点/空白统一压成单空格。 */
|
|
93
|
+
function normalizeAckText(text) {
|
|
94
|
+
return text
|
|
95
|
+
.toLowerCase()
|
|
96
|
+
.replace(/[\s\p{P}\p{S}]+/gu, ' ')
|
|
97
|
+
.trim();
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* 判定 prompt 是否为琐碎确认/招呼(如"继续"、"好的"、"ok"等)。
|
|
101
|
+
*
|
|
102
|
+
* 规则:
|
|
103
|
+
* 1. 空字符串视为琐碎;
|
|
104
|
+
* 2. 规范化后命中 TRIVIAL_ACK_WORDS 视为琐碎;
|
|
105
|
+
* 3. 规范化后字符长度 ≤ 2 视为琐碎(如"好。"、"!!")。
|
|
106
|
+
*
|
|
107
|
+
* 注:斜杠/感叹号开头的命令词(/zhuanspec:、!ls 等)已由 user-input-hook
|
|
108
|
+
* 在写入 correctionContext 前过滤,无需在此重复处理。
|
|
109
|
+
*/
|
|
110
|
+
export function isTrivialAckPrompt(text) {
|
|
111
|
+
if (!text)
|
|
112
|
+
return true;
|
|
113
|
+
const norm = normalizeAckText(text);
|
|
114
|
+
if (norm.length === 0)
|
|
115
|
+
return true;
|
|
116
|
+
if (TRIVIAL_ACK_WORDS.has(norm))
|
|
117
|
+
return true;
|
|
118
|
+
if (norm.length <= 2)
|
|
119
|
+
return true;
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* 判断 correctionContext 是否代表一次有意义的用户纠正(非琐碎确认)。
|
|
124
|
+
*
|
|
125
|
+
* 接受的 source:
|
|
126
|
+
* - correction-keyword:命中显式纠偏关键词,始终视为用户纠正;
|
|
127
|
+
* - post-baseline-prompt:baseline 后任何用户提示,但 promptSnippet
|
|
128
|
+
* 必须不是琐碎确认词(如"继续"、"好的")。
|
|
129
|
+
*
|
|
130
|
+
* 该函数用于 review 阶段判定 "用户提示词触发的纠正",使其覆盖范围与 apply 阶段一致。
|
|
131
|
+
*/
|
|
132
|
+
export function isMeaningfulUserCorrection(correctionContext) {
|
|
133
|
+
if (!correctionContext?.active)
|
|
134
|
+
return false;
|
|
135
|
+
const source = correctionContext.source;
|
|
136
|
+
if (source === 'correction-keyword')
|
|
137
|
+
return true;
|
|
138
|
+
if (source === 'post-baseline-prompt') {
|
|
139
|
+
return !isTrivialAckPrompt(correctionContext.promptSnippet);
|
|
140
|
+
}
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* 判定当前 review 阶段的编辑是否属于 Skill 自动修复(CR/单测),不应计入准确率。
|
|
145
|
+
*
|
|
146
|
+
* 判定逻辑:取最近一次 review 阶段的 skill 调用,若 skill name 匹配 REVIEW_AUTOFIX_SKILLS
|
|
147
|
+
* 且该 skill 调用晚于最近的有意义用户纠偏上下文(correctionContext.resolvedAt),则视为自动修复。
|
|
148
|
+
*
|
|
149
|
+
* "有意义的用户纠偏" 定义:
|
|
150
|
+
* - source = 'correction-keyword',或
|
|
151
|
+
* - source = 'post-baseline-prompt' 且 promptSnippet 不是琐碎确认词
|
|
152
|
+
*/
|
|
153
|
+
export function isReviewAutoFixActive(skillCalls, correctionContext) {
|
|
154
|
+
// 取 review 阶段中匹配 autofix skill 的最近一次调用
|
|
155
|
+
const recentAutofix = [...skillCalls]
|
|
156
|
+
.reverse()
|
|
157
|
+
.find(sc => sc.phase === 'review'
|
|
158
|
+
&& REVIEW_AUTOFIX_SKILLS.some(s => sc.skill.includes(s)));
|
|
159
|
+
if (!recentAutofix)
|
|
160
|
+
return false;
|
|
161
|
+
// 如果存在有意义的用户纠偏上下文且 resolvedAt 晚于 skill 触发时间,说明用户已介入
|
|
162
|
+
if (isMeaningfulUserCorrection(correctionContext)
|
|
163
|
+
&& correctionContext?.resolvedAt
|
|
164
|
+
&& correctionContext.resolvedAt > recentAutofix.triggeredAt) {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
65
169
|
/**
|
|
66
170
|
* 判定 filePath 在当前 phase 下是否属于白名单,并返回文件分类。
|
|
67
171
|
*
|
|
@@ -322,6 +426,32 @@ export async function ensureAccuracySnapshot(changeDir) {
|
|
|
322
426
|
}
|
|
323
427
|
}
|
|
324
428
|
// ============================================================
|
|
429
|
+
// 纠偏去重计算(同 promptId+filePath+phase 取 max)
|
|
430
|
+
// ============================================================
|
|
431
|
+
/**
|
|
432
|
+
* 同 (promptId + filePath + phase) 历史 record 中的最大 editLines。
|
|
433
|
+
*
|
|
434
|
+
* 背景:用户一次 prompt 可能触发 AI 多次 Edit(重复修正同一区域)。
|
|
435
|
+
* 直接累加会造成虚高;取 max 反映“最终保留下来的最大一次改动”。
|
|
436
|
+
*
|
|
437
|
+
* promptId 为空时返回 0(冷启动,当前是首条,取 0 后累加全量 changeLines)。
|
|
438
|
+
*/
|
|
439
|
+
export function computePromptFileMaxLines(correctionEdits, promptId, filePath, phase) {
|
|
440
|
+
if (!promptId || !filePath)
|
|
441
|
+
return 0;
|
|
442
|
+
let maxLines = 0;
|
|
443
|
+
for (const e of correctionEdits) {
|
|
444
|
+
if (e.phase === phase
|
|
445
|
+
&& e.filePath === filePath
|
|
446
|
+
&& e.triggeredBy?.promptId === promptId
|
|
447
|
+
&& typeof e.editLines === 'number'
|
|
448
|
+
&& e.editLines > maxLines) {
|
|
449
|
+
maxLines = e.editLines;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
return maxLines;
|
|
453
|
+
}
|
|
454
|
+
// ============================================================
|
|
325
455
|
// 用户纠正行数累计
|
|
326
456
|
// ============================================================
|
|
327
457
|
/**
|
|
@@ -85,5 +85,32 @@ export interface CodexHookSlot {
|
|
|
85
85
|
* the same event (matching Codex's array-of-tables semantics).
|
|
86
86
|
*/
|
|
87
87
|
export declare function getCodexDefaultHookSlots(): readonly CodexHookSlot[];
|
|
88
|
+
/**
|
|
89
|
+
* Compute the content-trust SHA-256 hash for a hook handler, replicating
|
|
90
|
+
* the algorithm from Codex CLI's `codex-rs/hooks/src/engine/discovery.rs`:
|
|
91
|
+
*
|
|
92
|
+
* Codex hashes a `NormalizedHookIdentity` struct, which is serialized to
|
|
93
|
+
* TOML then converted to canonical JSON. The structure is:
|
|
94
|
+
*
|
|
95
|
+
* { event_name, matcher?, hooks: [{ type, command, async, timeout, statusMessage? }] }
|
|
96
|
+
*
|
|
97
|
+
* Key normalization details:
|
|
98
|
+
* - `timeout` defaults to 600 when absent in source TOML
|
|
99
|
+
* - `async` is always false (we don't support async hooks)
|
|
100
|
+
* - `command_windows` (None) is omitted in TOML serialization
|
|
101
|
+
* - `matcher` is only included when present
|
|
102
|
+
* - Keys are recursively sorted (canonical JSON) then SHA-256 hashed
|
|
103
|
+
*
|
|
104
|
+
* This allows ZhuanSpec to pre-seed `trusted_hash` entries directly without
|
|
105
|
+
* relying on cross-project hash grafting, eliminating the "Hooks need review"
|
|
106
|
+
* prompt on first use or after upgrades.
|
|
107
|
+
*/
|
|
108
|
+
export declare function computeHookContentHash(entry: CodexHookEntry): string;
|
|
109
|
+
/**
|
|
110
|
+
* Get the computed content-trust hashes for all default hook handlers.
|
|
111
|
+
* Returns a map from slot suffix (`<event_snake>:<group>:<handler>`) to
|
|
112
|
+
* the SHA-256 hash string that Codex CLI would compute.
|
|
113
|
+
*/
|
|
114
|
+
export declare function getCodexDefaultHookHashes(): Map<string, string>;
|
|
88
115
|
export {};
|
|
89
116
|
//# sourceMappingURL=codex-hooks-template.d.ts.map
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
* ZhuanSpec markers of `<repo>/.codex/config.toml` so that `zhuanspec update`
|
|
26
26
|
* can regenerate it idempotently without clobbering user-owned config.
|
|
27
27
|
*/
|
|
28
|
+
import { createHash } from 'crypto';
|
|
28
29
|
/**
|
|
29
30
|
* ZhuanSpec default hooks, in logical order. Keep this list in sync with the
|
|
30
31
|
* Claude settings.json generator in init.ts so behavior stays consistent
|
|
@@ -142,4 +143,84 @@ export function getCodexDefaultHookSlots() {
|
|
|
142
143
|
return { event: entry.event, eventSnake, groupIndex, handlerIndex: 0 };
|
|
143
144
|
});
|
|
144
145
|
}
|
|
146
|
+
/**
|
|
147
|
+
* Compute the content-trust SHA-256 hash for a hook handler, replicating
|
|
148
|
+
* the algorithm from Codex CLI's `codex-rs/hooks/src/engine/discovery.rs`:
|
|
149
|
+
*
|
|
150
|
+
* Codex hashes a `NormalizedHookIdentity` struct, which is serialized to
|
|
151
|
+
* TOML then converted to canonical JSON. The structure is:
|
|
152
|
+
*
|
|
153
|
+
* { event_name, matcher?, hooks: [{ type, command, async, timeout, statusMessage? }] }
|
|
154
|
+
*
|
|
155
|
+
* Key normalization details:
|
|
156
|
+
* - `timeout` defaults to 600 when absent in source TOML
|
|
157
|
+
* - `async` is always false (we don't support async hooks)
|
|
158
|
+
* - `command_windows` (None) is omitted in TOML serialization
|
|
159
|
+
* - `matcher` is only included when present
|
|
160
|
+
* - Keys are recursively sorted (canonical JSON) then SHA-256 hashed
|
|
161
|
+
*
|
|
162
|
+
* This allows ZhuanSpec to pre-seed `trusted_hash` entries directly without
|
|
163
|
+
* relying on cross-project hash grafting, eliminating the "Hooks need review"
|
|
164
|
+
* prompt on first use or after upgrades.
|
|
165
|
+
*/
|
|
166
|
+
export function computeHookContentHash(entry) {
|
|
167
|
+
// Build the normalized handler object matching Codex's HookHandlerConfig::Command
|
|
168
|
+
// after normalization (timeout defaults to 600, async always present).
|
|
169
|
+
const handlerObj = {
|
|
170
|
+
type: 'command',
|
|
171
|
+
command: entry.command,
|
|
172
|
+
async: false,
|
|
173
|
+
timeout: 600,
|
|
174
|
+
};
|
|
175
|
+
if (entry.statusMessage) {
|
|
176
|
+
handlerObj.statusMessage = entry.statusMessage;
|
|
177
|
+
}
|
|
178
|
+
// Build the NormalizedHookIdentity: event_name + flattened MatcherGroup
|
|
179
|
+
const identity = {
|
|
180
|
+
event_name: toEventSnake(entry.event),
|
|
181
|
+
hooks: [handlerObj],
|
|
182
|
+
};
|
|
183
|
+
if (entry.matcher) {
|
|
184
|
+
identity.matcher = entry.matcher;
|
|
185
|
+
}
|
|
186
|
+
const canonical = canonicalJson(identity);
|
|
187
|
+
const bytes = Buffer.from(JSON.stringify(canonical), 'utf-8');
|
|
188
|
+
const hex = createHash('sha256').update(bytes).digest('hex');
|
|
189
|
+
return `sha256:${hex}`;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Recursively sort object keys alphabetically to produce canonical JSON,
|
|
193
|
+
* matching Codex CLI's `canonical_json()` in fingerprint.rs.
|
|
194
|
+
*/
|
|
195
|
+
function canonicalJson(value) {
|
|
196
|
+
if (value === null || value === undefined)
|
|
197
|
+
return value;
|
|
198
|
+
if (Array.isArray(value))
|
|
199
|
+
return value.map(canonicalJson);
|
|
200
|
+
if (typeof value === 'object') {
|
|
201
|
+
const obj = value;
|
|
202
|
+
const sorted = {};
|
|
203
|
+
for (const key of Object.keys(obj).sort()) {
|
|
204
|
+
sorted[key] = canonicalJson(obj[key]);
|
|
205
|
+
}
|
|
206
|
+
return sorted;
|
|
207
|
+
}
|
|
208
|
+
return value;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Get the computed content-trust hashes for all default hook handlers.
|
|
212
|
+
* Returns a map from slot suffix (`<event_snake>:<group>:<handler>`) to
|
|
213
|
+
* the SHA-256 hash string that Codex CLI would compute.
|
|
214
|
+
*/
|
|
215
|
+
export function getCodexDefaultHookHashes() {
|
|
216
|
+
const hashes = new Map();
|
|
217
|
+
const slots = getCodexDefaultHookSlots();
|
|
218
|
+
for (let i = 0; i < CODEX_DEFAULT_HOOKS.length; i++) {
|
|
219
|
+
const entry = CODEX_DEFAULT_HOOKS[i];
|
|
220
|
+
const slot = slots[i];
|
|
221
|
+
const suffix = `${slot.eventSnake}:${slot.groupIndex}:${slot.handlerIndex}`;
|
|
222
|
+
hashes.set(suffix, computeHookContentHash(entry));
|
|
223
|
+
}
|
|
224
|
+
return hashes;
|
|
225
|
+
}
|
|
145
226
|
//# sourceMappingURL=codex-hooks-template.js.map
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 行级 diff 计算工具
|
|
3
|
+
*
|
|
4
|
+
* 用途:替代 record-progress.ts 中的 Math.max(newLines, oldLines),
|
|
5
|
+
* 只统计真正发生变化的行数(增 + 删),不计入未修改的上下文锚点。
|
|
6
|
+
*
|
|
7
|
+
* 例:
|
|
8
|
+
* old = "A\nB\nC\nD"
|
|
9
|
+
* new = "A\nX\nC\nD"
|
|
10
|
+
* max(4,4) = 4 ← 旧口径(含上下文)
|
|
11
|
+
* diff = 2 ← 新口径(B 删除 + X 新增)
|
|
12
|
+
*
|
|
13
|
+
* 算法:基于 LCS(Longest Common Subsequence)的行级最小编辑距离。
|
|
14
|
+
* 对小规模编辑(< 1000 行)足够快,且无外部依赖。
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* 计算两段文本的行级净改动行数(添加行数 + 删除行数)。
|
|
18
|
+
*
|
|
19
|
+
* - 仅有 oldText:纯删除,返回旧文本行数
|
|
20
|
+
* - 仅有 newText:纯新增,返回新文本行数
|
|
21
|
+
* - 两者都有:基于 LCS 的最小编辑距离
|
|
22
|
+
*
|
|
23
|
+
* 注意:
|
|
24
|
+
* - 完全相同(无变化)返回 0
|
|
25
|
+
* - 修改一行 = 1 删 + 1 增 = 2
|
|
26
|
+
* - 与 unified diff 的 +/- 行数概念一致
|
|
27
|
+
*
|
|
28
|
+
* @param oldText 旧文本(Edit 的 old_string)
|
|
29
|
+
* @param newText 新文本(Edit 的 new_string)
|
|
30
|
+
* @returns 净改动行数(>= 0)
|
|
31
|
+
*/
|
|
32
|
+
export declare function countLineDiff(oldText: string | undefined | null, newText: string | undefined | null): number;
|
|
33
|
+
//# sourceMappingURL=line-diff.d.ts.map
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 行级 diff 计算工具
|
|
3
|
+
*
|
|
4
|
+
* 用途:替代 record-progress.ts 中的 Math.max(newLines, oldLines),
|
|
5
|
+
* 只统计真正发生变化的行数(增 + 删),不计入未修改的上下文锚点。
|
|
6
|
+
*
|
|
7
|
+
* 例:
|
|
8
|
+
* old = "A\nB\nC\nD"
|
|
9
|
+
* new = "A\nX\nC\nD"
|
|
10
|
+
* max(4,4) = 4 ← 旧口径(含上下文)
|
|
11
|
+
* diff = 2 ← 新口径(B 删除 + X 新增)
|
|
12
|
+
*
|
|
13
|
+
* 算法:基于 LCS(Longest Common Subsequence)的行级最小编辑距离。
|
|
14
|
+
* 对小规模编辑(< 1000 行)足够快,且无外部依赖。
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* 计算两段文本的行级净改动行数(添加行数 + 删除行数)。
|
|
18
|
+
*
|
|
19
|
+
* - 仅有 oldText:纯删除,返回旧文本行数
|
|
20
|
+
* - 仅有 newText:纯新增,返回新文本行数
|
|
21
|
+
* - 两者都有:基于 LCS 的最小编辑距离
|
|
22
|
+
*
|
|
23
|
+
* 注意:
|
|
24
|
+
* - 完全相同(无变化)返回 0
|
|
25
|
+
* - 修改一行 = 1 删 + 1 增 = 2
|
|
26
|
+
* - 与 unified diff 的 +/- 行数概念一致
|
|
27
|
+
*
|
|
28
|
+
* @param oldText 旧文本(Edit 的 old_string)
|
|
29
|
+
* @param newText 新文本(Edit 的 new_string)
|
|
30
|
+
* @returns 净改动行数(>= 0)
|
|
31
|
+
*/
|
|
32
|
+
export function countLineDiff(oldText, newText) {
|
|
33
|
+
const oldStr = oldText ?? '';
|
|
34
|
+
const newStr = newText ?? '';
|
|
35
|
+
if (oldStr === newStr)
|
|
36
|
+
return 0;
|
|
37
|
+
if (!oldStr)
|
|
38
|
+
return newStr.split('\n').length;
|
|
39
|
+
if (!newStr)
|
|
40
|
+
return oldStr.split('\n').length;
|
|
41
|
+
const oldLines = oldStr.split('\n');
|
|
42
|
+
const newLines = newStr.split('\n');
|
|
43
|
+
const lcs = lcsLength(oldLines, newLines);
|
|
44
|
+
// 删除行数 = 旧行 - LCS;新增行数 = 新行 - LCS
|
|
45
|
+
return (oldLines.length - lcs) + (newLines.length - lcs);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* 计算两个字符串数组的 LCS 长度(动态规划,O(n*m) 时间,O(min) 空间)。
|
|
49
|
+
*
|
|
50
|
+
* 内部辅助函数。使用滚动数组避免 O(n*m) 空间开销。
|
|
51
|
+
*
|
|
52
|
+
* @param a 数组 A(旧行)
|
|
53
|
+
* @param b 数组 B(新行)
|
|
54
|
+
* @returns 最长公共子序列长度
|
|
55
|
+
*/
|
|
56
|
+
function lcsLength(a, b) {
|
|
57
|
+
const n = a.length;
|
|
58
|
+
const m = b.length;
|
|
59
|
+
if (n === 0 || m === 0)
|
|
60
|
+
return 0;
|
|
61
|
+
// 让 b 作为短边,节省空间
|
|
62
|
+
const [shortArr, longArr] = m <= n ? [b, a] : [a, b];
|
|
63
|
+
const shortLen = shortArr.length;
|
|
64
|
+
const longLen = longArr.length;
|
|
65
|
+
// 滚动数组:prev / curr,长度 = shortLen + 1
|
|
66
|
+
let prev = new Array(shortLen + 1).fill(0);
|
|
67
|
+
let curr = new Array(shortLen + 1).fill(0);
|
|
68
|
+
for (let i = 1; i <= longLen; i++) {
|
|
69
|
+
for (let j = 1; j <= shortLen; j++) {
|
|
70
|
+
if (longArr[i - 1] === shortArr[j - 1]) {
|
|
71
|
+
curr[j] = prev[j - 1] + 1;
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
curr[j] = Math.max(prev[j], curr[j - 1]);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// 滚动:把 curr 拷给 prev,curr 清零
|
|
78
|
+
const tmp = prev;
|
|
79
|
+
prev = curr;
|
|
80
|
+
curr = tmp;
|
|
81
|
+
curr.fill(0);
|
|
82
|
+
}
|
|
83
|
+
return prev[shortLen];
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=line-diff.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhuan-ai/zhuanspec",
|
|
3
|
-
"version": "2.16.
|
|
3
|
+
"version": "2.16.4",
|
|
4
4
|
"description": "AI-native system for spec-driven development",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"zhuanspec",
|
|
@@ -39,26 +39,6 @@
|
|
|
39
39
|
"!dist/**/__tests__",
|
|
40
40
|
"!dist/**/*.map"
|
|
41
41
|
],
|
|
42
|
-
"scripts": {
|
|
43
|
-
"lint": "eslint src/",
|
|
44
|
-
"build": "node build.js",
|
|
45
|
-
"dev": "tsc --watch",
|
|
46
|
-
"dev:cli": "pnpm build && node bin/zhuanspec.js",
|
|
47
|
-
"test": "vitest run",
|
|
48
|
-
"test:watch": "vitest",
|
|
49
|
-
"test:ui": "vitest --ui",
|
|
50
|
-
"test:coverage": "vitest --coverage",
|
|
51
|
-
"test:postinstall": "node scripts/postinstall.js",
|
|
52
|
-
"prepare": "npm run build",
|
|
53
|
-
"prepublishOnly": "npm run build",
|
|
54
|
-
"postinstall": "node scripts/postinstall.js",
|
|
55
|
-
"check:pack-version": "node scripts/pack-version-check.mjs",
|
|
56
|
-
"diagnose:cursor": "node scripts/diagnose-cursor-commands.js",
|
|
57
|
-
"release": "pnpm run release:ci",
|
|
58
|
-
"release:ci": "pnpm run check:pack-version && pnpm exec changeset publish",
|
|
59
|
-
"release:local": "pnpm exec changeset version && pnpm run check:pack-version && pnpm exec changeset publish",
|
|
60
|
-
"changeset": "changeset"
|
|
61
|
-
},
|
|
62
42
|
"engines": {
|
|
63
43
|
"node": ">=20.19.0"
|
|
64
44
|
},
|
|
@@ -74,12 +54,29 @@
|
|
|
74
54
|
"dependencies": {
|
|
75
55
|
"@inquirer/core": "^10.2.2",
|
|
76
56
|
"@inquirer/prompts": "^7.8.0",
|
|
77
|
-
"@rollup/rollup-darwin-x64": "^4.60.4",
|
|
78
57
|
"chalk": "^5.5.0",
|
|
79
58
|
"commander": "^14.0.0",
|
|
80
59
|
"fast-glob": "^3.3.3",
|
|
81
60
|
"ora": "^8.2.0",
|
|
82
61
|
"yaml": "^2.8.2",
|
|
83
62
|
"zod": "^4.0.17"
|
|
63
|
+
},
|
|
64
|
+
"scripts": {
|
|
65
|
+
"lint": "eslint src/",
|
|
66
|
+
"build": "node build.js",
|
|
67
|
+
"dev": "tsc --watch",
|
|
68
|
+
"dev:cli": "pnpm build && node bin/zhuanspec.js",
|
|
69
|
+
"test": "vitest run",
|
|
70
|
+
"test:watch": "vitest",
|
|
71
|
+
"test:ui": "vitest --ui",
|
|
72
|
+
"test:coverage": "vitest --coverage",
|
|
73
|
+
"test:postinstall": "node scripts/postinstall.js",
|
|
74
|
+
"postinstall": "node scripts/postinstall.js",
|
|
75
|
+
"check:pack-version": "node scripts/pack-version-check.mjs",
|
|
76
|
+
"diagnose:cursor": "node scripts/diagnose-cursor-commands.js",
|
|
77
|
+
"release": "pnpm run release:ci",
|
|
78
|
+
"release:ci": "pnpm run check:pack-version && pnpm exec changeset publish",
|
|
79
|
+
"release:local": "pnpm exec changeset version && pnpm run check:pack-version && pnpm exec changeset publish",
|
|
80
|
+
"changeset": "changeset"
|
|
84
81
|
}
|
|
85
|
-
}
|
|
82
|
+
}
|