@zhuan-ai/zhuanspec 2.15.7 → 2.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/dist/cli/hooks.d.ts +13 -1
  2. package/dist/cli/hooks.js +75 -2
  3. package/dist/cli/index.js +94 -4
  4. package/dist/commands/accuracy.js +5 -1
  5. package/dist/commands/knowledge.d.ts +21 -0
  6. package/dist/commands/knowledge.js +80 -0
  7. package/dist/commands/progress.d.ts +66 -0
  8. package/dist/commands/progress.js +306 -1
  9. package/dist/core/archive.js +28 -0
  10. package/dist/core/configurators/codex.d.ts +55 -1
  11. package/dist/core/configurators/codex.js +234 -2
  12. package/dist/core/hooks/collect-knowledge.js +66 -0
  13. package/dist/core/hooks/deviation-check.d.ts +26 -1
  14. package/dist/core/hooks/deviation-check.js +32 -184
  15. package/dist/core/hooks/knowledge-index.d.ts +84 -0
  16. package/dist/core/hooks/knowledge-index.js +270 -0
  17. package/dist/core/hooks/record-progress.d.ts +63 -1
  18. package/dist/core/hooks/record-progress.js +208 -54
  19. package/dist/core/hooks/summarize.js +90 -2
  20. package/dist/core/hooks/user-input-hook.d.ts +38 -2
  21. package/dist/core/hooks/user-input-hook.js +346 -21
  22. package/dist/core/init.d.ts +11 -0
  23. package/dist/core/init.js +43 -4
  24. package/dist/core/metrics/code-accuracy.d.ts +152 -3
  25. package/dist/core/metrics/code-accuracy.js +323 -19
  26. package/dist/core/templates/agents-root-stub.d.ts +1 -1
  27. package/dist/core/templates/agents-root-stub.js +1 -0
  28. package/dist/core/templates/agents-template.d.ts +1 -1
  29. package/dist/core/templates/agents-template.js +21 -4
  30. package/dist/core/templates/codex-hooks-template.d.ts +37 -0
  31. package/dist/core/templates/codex-hooks-template.js +33 -2
  32. package/dist/core/templates/slash-command-templates.js +144 -28
  33. package/dist/core/templates/tasks-template.js +66 -14
  34. package/dist/core/update.js +17 -0
  35. package/dist/core/validation/strict-rules.d.ts +28 -0
  36. package/dist/core/validation/strict-rules.js +284 -0
  37. package/dist/utils/hook-merge.d.ts +6 -0
  38. package/dist/utils/hook-merge.js +33 -2
  39. package/package.json +1 -1
@@ -7,6 +7,7 @@
7
7
  * 3. Track tool calls for metrics
8
8
  */
9
9
  import { Phase } from '../../utils/phase-utils.js';
10
+ import type { PhaseAccuracyEntry, TrackedKind } from '../metrics/code-accuracy.js';
10
11
  /**
11
12
  * Atomic write JSON file: write to temp file first, then rename
12
13
  * This prevents half-written files during crashes or concurrent writes
@@ -174,7 +175,11 @@ export interface ProposalChangeRecord {
174
175
  relatedDeviationId?: string;
175
176
  }
176
177
  /**
177
- * 用户纠偏事件记录(由 deviation-check 写入 .correction-log)
178
+ * 用户纠偏事件记录
179
+ *
180
+ * v2.15.16 Sprint 3 起:.correction-log 已下线,纠偏事件由 user-input-hook
181
+ * 写入 user_inputs.json 的 correctionSignal / postBaseline / resolution 字段统一管理。
182
+ * 此类型仅保留给 progress.ts resolve-correction 命令的老 change 兼容路径使用。
178
183
  */
179
184
  export interface CorrectionRecord {
180
185
  ts: string;
@@ -197,8 +202,18 @@ export interface AccuracyData {
197
202
  originalRate: number;
198
203
  overriddenRate: number;
199
204
  reason?: string;
205
+ /** 纠正前的 raw 快照(完整保留,供事后差异分析;历史数据可能缺失) */
206
+ originalRateRaw?: number;
207
+ originalAiTotalLines?: number;
208
+ originalUserCorrectionLines?: number;
200
209
  }>;
201
210
  correctionEdits?: CorrectionEditRecord[];
211
+ /**
212
+ * 分阶段准确率桶(Task 3)。
213
+ * 顶层 aiLinesAdded / userCorrectionLines / aiTotalLines = 各 phase 桶求和(全生命周期聚合)。
214
+ * 纠偏明细不放在桶内;按 phase 查明细:correctionEdits.filter(e => e.phase === X)。
215
+ */
216
+ phaseAccuracy?: PhaseAccuracyEntry[];
202
217
  }
203
218
  export interface CorrectionEditRecord {
204
219
  filePath: string;
@@ -207,6 +222,17 @@ export interface CorrectionEditRecord {
207
222
  strategy: 'L1' | 'L2' | 'L3' | 'correction-context';
208
223
  agentType?: string;
209
224
  timestamp: string;
225
+ /** 该 edit 发生在哪个 phase(供按 phase 查明细时过滤用) */
226
+ phase?: string;
227
+ /** 本 edit 所属的文件白名单类别 */
228
+ trackedKind?: TrackedKind;
229
+ /** 纠偏触发源头信息(Task 6),用于 prompt ↔ edit 联关查询 */
230
+ triggeredBy?: {
231
+ source: 'correction-keyword' | 'post-baseline-prompt' | 'pending-correction-marker';
232
+ promptId?: string;
233
+ promptIndex?: number;
234
+ promptSnippet?: string;
235
+ };
210
236
  }
211
237
  /**
212
238
  * 纠偏上下文标记 — 替代 accuracy snapshot 的精准归因机制。
@@ -227,6 +253,41 @@ export interface CorrectionContext {
227
253
  path: 'A' | 'B' | 'C' | 'D';
228
254
  expiresAt: string;
229
255
  phase: string;
256
+ /**
257
+ * 纠偏触发来源:
258
+ * - 'correction-keyword' 用户 prompt 命中纠偏关键词 → deviation-check → resolve-correction CLI
259
+ * - 'pending-correction-marker' .pending-correction 文件兑底(并行竞态兼容)
260
+ * - 'post-baseline-prompt' phaseBaselines 打标后的 UserPromptSubmit(Task 0)
261
+ */
262
+ source?: 'correction-keyword' | 'post-baseline-prompt' | 'pending-correction-marker';
263
+ /** 当前 phase 内的 user prompt 序号(1-based),仅 source='post-baseline-prompt' 有意义 */
264
+ promptIndex?: number;
265
+ /** 关联的 user_inputs.json.inputId,用于查明细(Task 6) */
266
+ promptId?: string;
267
+ /** prompt 文本前 120 字符摘要,冗余存一份避免频繁 join user_inputs.json */
268
+ promptSnippet?: string;
269
+ }
270
+ /**
271
+ * Phase baseline 打标(Task 0)——“首版完成标识”。
272
+ *
273
+ * 语义:某 phase 的 markedAt 写入后,该 phase 首版已完成,
274
+ * 之后的 user prompt 触发修改才算纠偏。
275
+ *
276
+ * 打标路径(三路径 OR,先到先得,幂等):
277
+ * - auto-file-complete:Stop hook 中核心产出文件齐备
278
+ * - auto-stop:phase 切换时兜底打标(既然都切下一 phase 了,上一 phase 视为“首版完成”)
279
+ * - manual-cli:`zhuanspec progress mark-baseline` 手动打标
280
+ */
281
+ export interface PhaseBaselineEntry {
282
+ markedAt: string;
283
+ markedBy: 'auto-file-complete' | 'auto-stop' | 'manual-cli';
284
+ firstStopAt?: string;
285
+ }
286
+ export interface PhaseBaselines {
287
+ techDesign?: PhaseBaselineEntry;
288
+ propose?: PhaseBaselineEntry;
289
+ apply?: PhaseBaselineEntry;
290
+ review?: PhaseBaselineEntry;
230
291
  }
231
292
  export interface ProgressData {
232
293
  changeId: string;
@@ -271,6 +332,7 @@ export interface ProgressData {
271
332
  lastEvent?: string;
272
333
  accuracy?: AccuracyData;
273
334
  correctionContext?: CorrectionContext;
335
+ phaseBaselines?: PhaseBaselines;
274
336
  }
275
337
  export declare function recordProgressHook(options: RecordProgressOptions): Promise<void>;
276
338
  /**
@@ -11,7 +11,7 @@ import { promises as fsPromises, existsSync, readFileSync, unlinkSync } from 'fs
11
11
  import { FileSystemUtils } from '../../utils/file-system.js';
12
12
  import { PHASE_ORDER } from '../../utils/phase-utils.js';
13
13
  import { resolveZhuanSpecRoot } from '../../utils/resolve-root.js';
14
- import { isCodeFile } from '../metrics/code-accuracy.js';
14
+ import { isCodeFile, isTrackedFileForPhase, getOrCreatePhaseBucket, recomputeAccuracyRate, persistAccuracyJson, appendAccuracyDebugLog } from '../metrics/code-accuracy.js';
15
15
  import { detectHookHost, sanitizeCodexEnvelope } from '../../utils/hook-host.js';
16
16
  const fs = fsPromises;
17
17
  /**
@@ -502,6 +502,37 @@ async function runRecordProgress(filePath, toolName, success, stdinData) {
502
502
  timestamp: getBeijingTime(),
503
503
  triggeredBy: currentTask || 'manual',
504
504
  });
505
+ // === Task 0 路径 2:phase 切换时给上一 phase 补 baseline(兜底打标)===
506
+ // previousPhase ∈ {techDesign, propose, apply, review} 才打标;idle/archive 排除
507
+ const BASELINE_PHASES = ['techDesign', 'propose', 'apply', 'review'];
508
+ if (BASELINE_PHASES.includes(previousPhase)) {
509
+ progress.phaseBaselines = progress.phaseBaselines || {};
510
+ const key = previousPhase;
511
+ const existedBefore = !!progress.phaseBaselines[key]?.markedAt;
512
+ if (!existedBefore) {
513
+ progress.phaseBaselines[key] = {
514
+ markedAt: getBeijingTime(),
515
+ markedBy: 'auto-stop',
516
+ };
517
+ }
518
+ // 埋点:baseline.mark
519
+ appendAccuracyDebugLog(changeDir, {
520
+ type: 'baseline.mark',
521
+ phase: key,
522
+ trigger: 'auto-stop',
523
+ changeId,
524
+ input: { toPhase: phase, previousPhase: key },
525
+ output: { markedBy: 'auto-stop', existed: existedBefore },
526
+ });
527
+ }
528
+ // === 跨 phase 清理 correctionContext:
529
+ // 旧 phase 的 active 不应泄漏到新 phase,新 phase 是否进入纠偏态
530
+ // 由新 phase 的 phaseBaselines + UserPromptSubmit 重新判定
531
+ if (progress.correctionContext
532
+ && progress.correctionContext.active
533
+ && progress.correctionContext.phase !== phase) {
534
+ progress.correctionContext.active = false;
535
+ }
505
536
  }
506
537
  progress.currentNode = process.env.ZHUANSPEC_CURRENT_NODE || phase;
507
538
  progress.currentTask = currentTask || progress.currentTask;
@@ -610,7 +641,20 @@ async function runRecordProgress(filePath, toolName, success, stdinData) {
610
641
  contentSource = toolInputEdits.length > 0 ? 'edits[]' : 'none';
611
642
  }
612
643
  let estimatedLines = 0;
613
- const inCorrectionContext = !!(progress.correctionContext?.active) || pendingExists;
644
+ // === P0 兜底:post-baseline-fallback ===
645
+ // 正常链路:UserPromptSubmit 的 user-input hook 会在 phaseBaseline 已打标时写入
646
+ // correctionContext.active=true。但若 .claude/settings.json 未挂 user-input hook
647
+ // (老版本模板),correctionContext 会保持 null,导致后续 Write 误入 linesAdded。
648
+ // 本兜底:只要当前 phase 的 phaseBaseline 已打标,就视同进入纠正上下文。
649
+ // 语义:Stop 打 baseline 代表该 phase 首版齐备,之后的同 phase Write 绝大多数
650
+ // 由后续 user prompt 驱动(AI 不会主动在无 prompt 情况下继续大改),归为 user 纠正。
651
+ const _phaseKeyForBaseline = progress.phase;
652
+ const _baselinePhases = ['techDesign', 'propose', 'apply', 'review'];
653
+ const baselineMarkedForPhase = _baselinePhases.includes(_phaseKeyForBaseline)
654
+ && !!progress.phaseBaselines?.[_phaseKeyForBaseline]?.markedAt;
655
+ const ccActiveRaw = !!(progress.correctionContext?.active);
656
+ const inCorrectionContext = ccActiveRaw || pendingExists || baselineMarkedForPhase;
657
+ const correctionFallbackReason = (!ccActiveRaw && !pendingExists && baselineMarkedForPhase) ? 'post-baseline-fallback' : 'none';
614
658
  // === 调试日志:在 changeDir 下写 .correction-debug.log,方便排查“为什么没记录上” ===
615
659
  const correctionDebugLog = (branch, addedTo, delta) => {
616
660
  try {
@@ -628,6 +672,8 @@ async function runRecordProgress(filePath, toolName, success, stdinData) {
628
672
  contentSource,
629
673
  changeLines,
630
674
  pendingExists,
675
+ baselineMarkedForPhase,
676
+ fallbackReason: correctionFallbackReason,
631
677
  correctionContext: progress.correctionContext
632
678
  ? { active: ccActive, path: ccPath, expiresAt: ccExpiresAt, phase: ccPhase, currentPhase: progress.phase }
633
679
  : null,
@@ -646,46 +692,175 @@ async function runRecordProgress(filePath, toolName, success, stdinData) {
646
692
  // 日志失败不阻断主流程
647
693
  }
648
694
  };
649
- if (changeLines > 0 && filePath && isCodeFile(filePath)) {
650
- estimatedLines = changeLines;
651
- if (inCorrectionContext) {
652
- // 纠偏上下文:行数计入 userCorrectionLines,不进 linesAdded
695
+ if (changeLines > 0 && filePath) {
696
+ // Task 3.3:按 phase 选白名单(techDesign/propose/apply/review),而非笼统用 isCodeFile
697
+ const track = isTrackedFileForPhase(filePath, progress.phase, changeDir);
698
+ if (!track.tracked) {
699
+ correctionDebugLog(`skipped/out-of-phase-whitelist`, 'skipped', 0);
700
+ appendAccuracyDebugLog(changeDir, {
701
+ type: 'skip.out-of-phase-whitelist',
702
+ phase: progress.phase,
703
+ trigger: 'hook',
704
+ changeId,
705
+ skipReason: 'out-of-phase-whitelist',
706
+ input: { tool: toolName, file: filePath, changeLines },
707
+ });
708
+ }
709
+ else {
710
+ estimatedLines = changeLines;
711
+ const trackedKind = track.kind;
712
+ const trackedPhase = progress.phase;
713
+ // 确保 accuracy 结构存在
653
714
  if (!progress.accuracy) {
654
715
  progress.accuracy = {
655
716
  aiLinesAdded: 0, aiLinesModified: 0, aiTotalLines: 0,
656
717
  userCorrectionLines: 0, accuracyRate: 1.0,
657
- correctionEdits: [],
718
+ correctionEdits: [], phaseAccuracy: [],
658
719
  };
659
720
  }
660
- progress.accuracy.userCorrectionLines = (progress.accuracy.userCorrectionLines || 0) + changeLines;
661
- progress.accuracy.correctionEdits = progress.accuracy.correctionEdits || [];
662
- progress.accuracy.correctionEdits.push({
663
- filePath,
664
- editLines: changeLines,
665
- source: 'user',
666
- strategy: 'correction-context',
667
- agentType: progress.correctionContext?.path,
668
- timestamp,
669
- });
670
- correctionDebugLog(`${toolName}/correction`, 'userCorrectionLines', changeLines);
671
- }
672
- else {
673
- // 正常 AI 产出
674
- progress.linesAdded += changeLines;
675
- correctionDebugLog(`${toolName}/normal`, 'linesAdded', changeLines);
721
+ const bucket = getOrCreatePhaseBucket(progress, trackedPhase, trackedKind);
722
+ if (inCorrectionContext) {
723
+ // 工具白名单守卫:techDesign / propose 阶段的纠偏统计只接受增量编辑(Edit/MultiEdit),
724
+ // 禁止 Write。原因:tech-spec.md / proposal.md 通常是整文件 Write 产出,一旦被误判为纠偏,
725
+ // 会把整份文件行数当成 userCorrectionLines 记入,导致分子被严重放大、accuracyRate 失真。
726
+ // apply / review 阶段不受此限制(apply 可 Write 新增代码文件,review-report.md 也常由 Write 生成)。
727
+ if ((trackedPhase === 'techDesign' || trackedPhase === 'propose') && toolName === 'Write') {
728
+ correctionDebugLog(`skipped/write-blocked-${trackedPhase}-correction`, 'skipped', 0);
729
+ appendAccuracyDebugLog(changeDir, {
730
+ type: `skip.write-blocked-${trackedPhase}-correction`,
731
+ phase: trackedPhase,
732
+ kind: trackedKind,
733
+ trigger: 'hook',
734
+ changeId,
735
+ skipReason: `write-blocked-${trackedPhase}-correction`,
736
+ input: { tool: toolName, file: filePath, changeLines },
737
+ });
738
+ }
739
+ else {
740
+ // 纠偏上下文:桶 + 顶层 userCorrectionLines 同步累加
741
+ bucket.userCorrectionLines += changeLines;
742
+ bucket.lastUpdatedAt = timestamp;
743
+ progress.accuracy.userCorrectionLines = (progress.accuracy.userCorrectionLines || 0) + changeLines;
744
+ progress.accuracy.correctionEdits = progress.accuracy.correctionEdits || [];
745
+ // 提取纠偏触发源头信息(Task 6)
746
+ // post-baseline-fallback 兜底命中时,语义等价于 user-input hook 写入的 'post-baseline-prompt'。
747
+ const ccSource = progress.correctionContext?.source
748
+ || (pendingExists
749
+ ? 'pending-correction-marker'
750
+ : (correctionFallbackReason === 'post-baseline-fallback'
751
+ ? 'post-baseline-prompt'
752
+ : 'correction-keyword'));
753
+ // Task 6:若 correctionContext 无 prompt 元数据(典型于 pending-correction-marker 兑底路径),
754
+ // 从 .pending-correction marker JSON 中回填 promptId / phaseInputIndex / promptSnippet。
755
+ let fallbackPromptId;
756
+ let fallbackPromptIndex;
757
+ let fallbackPromptSnippet;
758
+ if (!progress.correctionContext?.promptId && pendingExists) {
759
+ try {
760
+ const markerRaw = readFileSync(pendingMarkerPath, 'utf-8');
761
+ if (markerRaw) {
762
+ const markerData = JSON.parse(markerRaw);
763
+ fallbackPromptId = markerData.promptId;
764
+ fallbackPromptIndex = markerData.phaseInputIndex;
765
+ fallbackPromptSnippet = markerData.promptSnippet;
766
+ }
767
+ }
768
+ catch {
769
+ // marker 解析失败不阻断主流程
770
+ }
771
+ }
772
+ const promptId = progress.correctionContext?.promptId ?? fallbackPromptId;
773
+ const promptIndex = progress.correctionContext?.promptIndex ?? fallbackPromptIndex;
774
+ const promptSnippet = progress.correctionContext?.promptSnippet ?? fallbackPromptSnippet;
775
+ progress.accuracy.correctionEdits.push({
776
+ filePath,
777
+ editLines: changeLines,
778
+ source: 'user',
779
+ strategy: 'correction-context',
780
+ agentType: progress.correctionContext?.path,
781
+ timestamp,
782
+ phase: trackedPhase,
783
+ trackedKind,
784
+ triggeredBy: {
785
+ source: ccSource,
786
+ promptId,
787
+ promptIndex,
788
+ promptSnippet,
789
+ },
790
+ });
791
+ // 记录 phase 维度 promptId(去重)
792
+ if (promptId && !bucket.correctionPromptIds.includes(promptId)) {
793
+ bucket.correctionPromptIds.push(promptId);
794
+ }
795
+ correctionDebugLog(`${toolName}/correction`, 'userCorrectionLines', changeLines);
796
+ appendAccuracyDebugLog(changeDir, {
797
+ type: 'correction.record',
798
+ phase: trackedPhase,
799
+ kind: trackedKind,
800
+ trigger: 'hook',
801
+ changeId,
802
+ input: {
803
+ tool: toolName,
804
+ file: filePath,
805
+ changeLines,
806
+ contentSource,
807
+ ccSource,
808
+ promptId,
809
+ promptIndex,
810
+ },
811
+ output: {
812
+ bucketCorrection: bucket.userCorrectionLines,
813
+ topCorrection: progress.accuracy.userCorrectionLines,
814
+ promptIdsCount: bucket.correctionPromptIds.length,
815
+ },
816
+ });
817
+ }
818
+ }
819
+ else {
820
+ // 正常 AI 产出:桶 + 顶层 aiLinesAdded/aiTotalLines 同步累加,同时顶层 progress.linesAdded 仍需累加以兼容旧逻辑
821
+ bucket.aiLinesAdded += changeLines;
822
+ bucket.aiTotalLines += changeLines;
823
+ bucket.lastUpdatedAt = timestamp;
824
+ progress.accuracy.aiLinesAdded = (progress.accuracy.aiLinesAdded || 0) + changeLines;
825
+ progress.accuracy.aiTotalLines = (progress.accuracy.aiTotalLines || 0) + changeLines;
826
+ progress.linesAdded += changeLines;
827
+ correctionDebugLog(`${toolName}/normal`, 'linesAdded', changeLines);
828
+ appendAccuracyDebugLog(changeDir, {
829
+ type: 'ai.record',
830
+ phase: trackedPhase,
831
+ kind: trackedKind,
832
+ trigger: 'hook',
833
+ changeId,
834
+ input: { tool: toolName, file: filePath, changeLines, contentSource },
835
+ output: {
836
+ bucketAiTotal: bucket.aiTotalLines,
837
+ topAiTotal: progress.accuracy.aiTotalLines,
838
+ },
839
+ });
840
+ }
841
+ // 累加后重算 accuracyRate(顶层 + 各桶)
842
+ recomputeAccuracyRate(progress, { changeDir, trigger: 'hook' });
843
+ // 统一同步 accuracy.json
844
+ await persistAccuracyJson(changeDir, progress);
676
845
  }
677
846
  }
678
847
  else {
679
- // 未进入计量分支:记录原因(非代码文件 / 字段缺失 / 非写入工具)
848
+ // 未进入计量分支:记录原因(字段缺失 / 非写入工具)
680
849
  if (toolName === 'Write' || toolName === 'Edit' || toolName === 'MultiEdit') {
681
850
  const reason = !filePath
682
851
  ? 'no-file-path'
683
- : !isCodeFile(filePath)
684
- ? 'non-code-file'
685
- : changeLines === 0
686
- ? `empty-${contentSource}`
687
- : 'unknown';
852
+ : changeLines === 0
853
+ ? `empty-${contentSource}`
854
+ : 'unknown';
688
855
  correctionDebugLog(`skipped/${reason}`, 'skipped', 0);
856
+ appendAccuracyDebugLog(changeDir, {
857
+ type: `skip.${reason}`,
858
+ phase: progress.phase,
859
+ trigger: 'hook',
860
+ changeId,
861
+ skipReason: reason,
862
+ input: { tool: toolName, file: filePath || null, changeLines, contentSource },
863
+ });
689
864
  }
690
865
  }
691
866
  const tokenUsageFromEnv = Number(process.env.ZHUANSPEC_TOKEN_USAGE || 0);
@@ -756,31 +931,10 @@ async function runRecordProgress(filePath, toolName, success, stdinData) {
756
931
  // Ignore tasks.md read errors
757
932
  }
758
933
  }
759
- // === 合并 .correction-log 到 progress.corrections(供 Stop hook 沉淀询问使用)===
760
- try {
761
- const correctionLogPath = path.join(changeDir, '.correction-log');
762
- if (existsSync(correctionLogPath)) {
763
- const content = readFileSync(correctionLogPath, 'utf-8');
764
- const parsed = [];
765
- for (const line of content.split('\n')) {
766
- const trimmed = line.trim();
767
- if (!trimmed)
768
- continue;
769
- try {
770
- parsed.push(JSON.parse(trimmed));
771
- }
772
- catch {
773
- // skip malformed line
774
- }
775
- }
776
- if (parsed.length > 0) {
777
- progress.corrections = parsed;
778
- }
779
- }
780
- }
781
- catch {
782
- // ignore correction log read errors
783
- }
934
+ // v2.15.16 Sprint 3:.correction-log → progress.corrections 的合并逻辑已下线。
935
+ // 纠偏明细现由 user-input-hook 写入 user_inputs.json(correctionSignal / postBaseline /
936
+ // resolution 字段),review 步骤 0 与 knowledge skill 模式 B 均直接读 user_inputs.json,
937
+ // 不再依赖 progress.corrections。
784
938
  // Write progress file atomically
785
939
  await atomicWriteJson(progressPath, progress);
786
940
  // Also update tool_calls.json separately for detailed tracking
@@ -8,10 +8,11 @@
8
8
  * 4. Generate events.json entries for key events
9
9
  */
10
10
  import path from 'path';
11
+ import { promises as fsPromises } from 'fs';
11
12
  import { execSync } from 'child_process';
12
13
  import { FileSystemUtils } from '../../utils/file-system.js';
13
14
  import { executePostApply } from './post-apply.js';
14
- import { getBeijingTime, detectActiveChange } from './record-progress.js';
15
+ import { getBeijingTime, detectActiveChange, atomicWriteJson } from './record-progress.js';
15
16
  import { computeAccuracy } from '../metrics/code-accuracy.js';
16
17
  import { detectHookHost, sanitizeCodexEnvelope } from '../../utils/hook-host.js';
17
18
  export async function summarizeHook(options) {
@@ -215,6 +216,29 @@ async function runSummarize() {
215
216
  systemMessage += `- AI产出: ${accResult.aiTotalLines} 行\n`;
216
217
  systemMessage += `- 用户纠正: ${accResult.userCorrectionLines} 行\n`;
217
218
  systemMessage += `- 准确率: ${(accResult.rate * 100).toFixed(1)}%${accResult.overridden ? ' (已手动修正)' : ''}\n`;
219
+ // 分阶段准确率(Task 5):按 phase/trackedKind 展示桶级快照;历史数据无桶时降级提示
220
+ const buckets = progressData.accuracy.phaseAccuracy ?? [];
221
+ systemMessage += `\n分阶段准确率:\n`;
222
+ if (buckets.length === 0) {
223
+ systemMessage += `- —(历史数据,无分阶段指标)\n`;
224
+ }
225
+ else {
226
+ for (const b of buckets) {
227
+ const rawPct = ((b.accuracyRateRaw ?? b.accuracyRate ?? 1) * 100).toFixed(1);
228
+ const showPct = ((b.accuracyRate ?? 1) * 100).toFixed(1);
229
+ const overrideCount = b.overrideHistory?.length ?? 0;
230
+ const lastReason = overrideCount > 0
231
+ ? b.overrideHistory[overrideCount - 1].reason
232
+ : '';
233
+ const confirmedTag = b.confirmed
234
+ ? `✓已确认(${b.confirmed.by})`
235
+ : '未确认';
236
+ const overrideTag = overrideCount > 0
237
+ ? ` override=${overrideCount}次(${lastReason})`
238
+ : '';
239
+ systemMessage += `- [${b.phase}/${b.trackedKind}] AI=${b.aiTotalLines} 纠偏=${b.userCorrectionLines} raw=${rawPct}% 展示=${showPct}%${overrideTag} ${confirmedTag}\n`;
240
+ }
241
+ }
218
242
  }
219
243
  }
220
244
  if (allTasksComplete) {
@@ -260,7 +284,7 @@ async function runSummarize() {
260
284
  `2. **用户选"是"**:**必须调用 \`@skill:zhuanspec:knowledge --mode=correction --correction <change-id>\`** (或等价的 \`/zhuanspec:knowledge\` slash command 并在交互中选择模式 B)完成沉淀。`,
261
285
  ` - change-id 值为 \`${changeId}\``,
262
286
  ' - **禁止直接 Write/Edit 创建 \`zhuanspec/knowledge/\` 下的任何文件**,禁止自行判定分类或文件名格式;所有写入由 skill 统一执行。',
263
- ' - skill 将自动完成:读取 .correction-log → 分类决策树 → 去重扫描 → 套用统一 Front Matter → 写入对应分类目录 → 更新 index.md → 标记 resolve-correction。',
287
+ ' - skill 将自动完成:读取 user_inputs.json(Level 1)或 summary 关键词批量扫描(Level 2)→ 分类决策树 → 去重扫描 → 套用统一 Front Matter → 写入对应分类目录 → 更新 index.md → 标记 resolve-correction。',
264
288
  '',
265
289
  `3. **用户选"否"**:运行 \`zhuanspec progress resolve-correction ${changeId} --mark-pitfall-saved\` 标记已处理,不写入任何知识文件。`,
266
290
  '',
@@ -274,6 +298,30 @@ async function runSummarize() {
274
298
  ].join('\n');
275
299
  systemMessage += `\n⚠️ 检测到 ${n} 次用户纠偏未沉淀——请按上方说明调 askUserQuestion,用户选是后必须调用 @skill:zhuanspec:knowledge 而非自行 Write。\n`;
276
300
  }
301
+ // === Task 0 路径 1:Stop 时按核心产出齐备度自动打 phase baseline ===
302
+ // 幂等:已打标则跳过;先到先得(auto-file-complete 优于 auto-stop)
303
+ try {
304
+ if (['techDesign', 'propose', 'apply', 'review'].includes(phase)) {
305
+ const outputsReady = await isPhaseOutputsComplete(phase, changeDir, allTasksComplete);
306
+ if (outputsReady && await FileSystemUtils.fileExists(progressPath)) {
307
+ const fullProgress = JSON.parse(await FileSystemUtils.readFile(progressPath));
308
+ const now = getBeijingTime();
309
+ fullProgress.phaseBaselines = fullProgress.phaseBaselines || {};
310
+ const key = phase;
311
+ if (!fullProgress.phaseBaselines[key]?.markedAt) {
312
+ fullProgress.phaseBaselines[key] = {
313
+ markedAt: now,
314
+ markedBy: 'auto-file-complete',
315
+ firstStopAt: now,
316
+ };
317
+ await atomicWriteJson(progressPath, fullProgress);
318
+ }
319
+ }
320
+ }
321
+ }
322
+ catch {
323
+ // baseline 打标失败不阻断 summarize 主流程
324
+ }
277
325
  return {
278
326
  continue: true,
279
327
  systemMessage,
@@ -371,4 +419,44 @@ function computeWaves(tasks) {
371
419
  }
372
420
  return Array.from(wavesMap.values()).sort((a, b) => a.waveId - b.waveId);
373
421
  }
422
+ /**
423
+ * 判定指定 phase 的核心产出文件是否齐备(Task 0 路径 1 auto-file-complete 的前置条件)。
424
+ *
425
+ * 判定标准:
426
+ * - techDesign → changes/{changeId}/techDesign/ 目录存在且至少有一份 .md 文件
427
+ * - propose → proposal.md / tasks.md / design.md 全部存在
428
+ * - apply → tasks.md 全部打勾
429
+ * - review → review/review-report.md 存在
430
+ */
431
+ async function isPhaseOutputsComplete(phase, changeDir, allTasksComplete) {
432
+ if (phase === 'techDesign') {
433
+ const techDesignDir = path.join(changeDir, 'techDesign');
434
+ if (!(await FileSystemUtils.directoryExists(techDesignDir)))
435
+ return false;
436
+ try {
437
+ const files = await fsPromises.readdir(techDesignDir);
438
+ return files.some(f => f.endsWith('.md'));
439
+ }
440
+ catch {
441
+ return false;
442
+ }
443
+ }
444
+ if (phase === 'propose') {
445
+ const required = ['proposal.md', 'tasks.md', 'design.md'];
446
+ for (const f of required) {
447
+ if (!(await FileSystemUtils.fileExists(path.join(changeDir, f)))) {
448
+ return false;
449
+ }
450
+ }
451
+ return true;
452
+ }
453
+ if (phase === 'apply') {
454
+ return allTasksComplete;
455
+ }
456
+ if (phase === 'review') {
457
+ const reviewReportPath = path.join(changeDir, 'review', 'review-report.md');
458
+ return await FileSystemUtils.fileExists(reviewReportPath);
459
+ }
460
+ return false;
461
+ }
374
462
  //# sourceMappingURL=summarize.js.map
@@ -1,9 +1,42 @@
1
1
  /**
2
2
  * User Input Hook
3
3
  *
4
- * Records user input summary during ZhuanSpec workflow.
5
- * Tracks user interventions, corrections, confirmations, and additional requests.
4
+ * 统一的 UserPromptSubmit 入口(v2.15.15 起接管 deviation-check 的纠偏/需求变更注入):
5
+ * 1. 过滤 slash/bang 命令、too-short 内容、Codex Ambient Suggestions、10s 窗口重复
6
+ * 2. 将 prompt 归档到 user_inputs.json(含 fullPrompt + correctionSignal + postBaseline)
7
+ * 3. baseline 已打标时写 correctionContext(Task 0 post-baseline-prompt 语义)
8
+ * 4. phase='apply' + postBaseline + 命中 hard 关键词
9
+ * → 注入四选一(纠偏)或 AD 两选一(需求变更)additionalContext
10
+ * → 同步写 .pending-correction 供 pre-tool hook 拦截 Edit/Write
11
+ *
12
+ * 与 deviation-check 的关系:
13
+ * - 本 hook 接管 UserPromptSubmit 下原 deviation-check 的全部职责(关键词 + 四选一)
14
+ * - deviation-check 保留 pre-tool 分支(检测 .pending-correction → 拦截 Edit/Write)
15
+ * - settings.json 模板不再挂 deviation-check 到 UserPromptSubmit;旧项目请跑 zhuanspec update
16
+ *
17
+ * v2.15.16 Sprint 3:.correction-log 写入已下线,user_inputs.json 为唯一纠偏事实源。
18
+ */
19
+ /**
20
+ * 纠偏信号分级:
21
+ * - 'hard' : 命中 USER_CORRECTION_KEYWORDS / REQUIREMENT_CHANGE_KEYWORDS 中的硬关键词
22
+ * - 'soft' : 保留位,当前未使用;未来可接入语义/模式匹配
23
+ * - 'none' : 无纠偏信号
6
24
  */
25
+ export interface CorrectionSignal {
26
+ kind: 'hard' | 'soft' | 'none';
27
+ matchedKeywords: string[];
28
+ isRequirementChange?: boolean;
29
+ }
30
+ /**
31
+ * 用户对四选一的最终选择。由 `zhuanspec progress resolve-correction` 回写。
32
+ */
33
+ export interface UserInputResolution {
34
+ path?: 'A' | 'B' | 'C' | 'D';
35
+ resolvedAt?: string;
36
+ note?: string;
37
+ /** 本条 prompt 是否真的弹了四选一(hard 信号命中即触发) */
38
+ triggeredFourOption?: boolean;
39
+ }
7
40
  interface UserInputOutput {
8
41
  continue: boolean;
9
42
  systemMessage?: string;
@@ -11,6 +44,9 @@ interface UserInputOutput {
11
44
  inputRecorded: boolean;
12
45
  triggerType: string;
13
46
  summaryLength: number;
47
+ /** 四选一或 AD 两选一注入时附加 additionalContext/options */
48
+ additionalContext?: string;
49
+ options?: string[];
14
50
  };
15
51
  }
16
52
  /**