@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.
@@ -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
- const newLines = toolInputNewString ? toolInputNewString.split('\n').length : 0;
712
- const oldLines = toolInputOldString ? toolInputOldString.split('\n').length : 0;
713
- // max(new, old):体现本次编辑触及的代码量,兼容纯新增/纯删除/替换
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
- const newLines = edit?.new_string ? edit.new_string.split('\n').length : 0;
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
- const bucket = getOrCreatePhaseBucket(progress, trackedPhase, trackedKind);
804
- if (inCorrectionContext) {
805
- // 工具白名单守卫:techDesign / propose 阶段的纠偏统计只接受增量编辑(Edit/MultiEdit),
806
- // 禁止 Write。原因:tech-spec.md / proposal.md 通常是整文件 Write 产出,一旦被误判为纠偏,
807
- // 会把整份文件行数当成 userCorrectionLines 记入,导致分子被严重放大、accuracyRate 失真。
808
- // apply / review 阶段不受此限制(apply 可 Write 新增代码文件,review-report.md 也常由 Write 生成)。
809
- if ((trackedPhase === 'techDesign' || trackedPhase === 'propose') && toolName === 'Write') {
810
- correctionDebugLog(`skipped/write-blocked-${trackedPhase}-correction`, 'skipped', 0);
811
- appendAccuracyDebugLog(changeDir, {
812
- type: `skip.write-blocked-${trackedPhase}-correction`,
813
- phase: trackedPhase,
814
- kind: trackedKind,
815
- trigger: 'hook',
816
- changeId,
817
- skipReason: `write-blocked-${trackedPhase}-correction`,
818
- input: { tool: toolName, file: filePath, changeLines },
819
- });
820
- }
821
- else {
822
- // 纠偏上下文:桶 + 顶层 userCorrectionLines 同步累加
823
- bucket.userCorrectionLines += changeLines;
824
- bucket.lastUpdatedAt = timestamp;
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: trackedPhase,
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
- // 记录 phase 维度 promptId(去重)
874
- if (promptId && !bucket.correctionPromptIds.includes(promptId)) {
875
- bucket.correctionPromptIds.push(promptId);
873
+ // 记录 apply bucket promptId(去重)
874
+ if (promptId && !applyBucket.correctionPromptIds.includes(promptId)) {
875
+ applyBucket.correctionPromptIds.push(promptId);
876
876
  }
877
- correctionDebugLog(`${toolName}/correction`, 'userCorrectionLines', changeLines);
877
+ correctionDebugLog(`${toolName}/review-user-correction→apply`, 'userCorrectionLines', dedupDelta);
878
878
  appendAccuracyDebugLog(changeDir, {
879
879
  type: 'correction.record',
880
- phase: trackedPhase,
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: bucket.userCorrectionLines,
898
+ bucketCorrection: applyBucket.userCorrectionLines,
895
899
  topCorrection: progress.accuracy.userCorrectionLines,
896
- promptIdsCount: bucket.correctionPromptIds.length,
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
- // 正常 AI 产出:桶 + 顶层 aiLinesAdded/aiTotalLines 同步累加,同时顶层 progress.linesAdded 仍需累加以兼容旧逻辑
903
- bucket.aiLinesAdded += changeLines;
904
- bucket.aiTotalLines += changeLines;
905
- bucket.lastUpdatedAt = timestamp;
906
- progress.accuracy.aiLinesAdded = (progress.accuracy.aiLinesAdded || 0) + changeLines;
907
- progress.accuracy.aiTotalLines = (progress.accuracy.aiTotalLines || 0) + changeLines;
908
- progress.linesAdded += changeLines;
909
- correctionDebugLog(`${toolName}/normal`, 'linesAdded', changeLines);
910
- appendAccuracyDebugLog(changeDir, {
911
- type: 'ai.record',
912
- phase: trackedPhase,
913
- kind: trackedKind,
914
- trigger: 'hook',
915
- changeId,
916
- input: { tool: toolName, file: filePath, changeLines, contentSource },
917
- output: {
918
- bucketAiTotal: bucket.aiTotalLines,
919
- topAiTotal: progress.accuracy.aiTotalLines,
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",
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
+ }