@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.
- package/dist/cli/hooks.d.ts +13 -1
- package/dist/cli/hooks.js +75 -2
- package/dist/cli/index.js +94 -4
- package/dist/commands/accuracy.js +5 -1
- package/dist/commands/knowledge.d.ts +21 -0
- package/dist/commands/knowledge.js +80 -0
- package/dist/commands/progress.d.ts +66 -0
- package/dist/commands/progress.js +306 -1
- package/dist/core/archive.js +28 -0
- package/dist/core/configurators/codex.d.ts +55 -1
- package/dist/core/configurators/codex.js +234 -2
- package/dist/core/hooks/collect-knowledge.js +66 -0
- package/dist/core/hooks/deviation-check.d.ts +26 -1
- package/dist/core/hooks/deviation-check.js +32 -184
- package/dist/core/hooks/knowledge-index.d.ts +84 -0
- package/dist/core/hooks/knowledge-index.js +270 -0
- package/dist/core/hooks/record-progress.d.ts +63 -1
- package/dist/core/hooks/record-progress.js +208 -54
- package/dist/core/hooks/summarize.js +90 -2
- package/dist/core/hooks/user-input-hook.d.ts +38 -2
- package/dist/core/hooks/user-input-hook.js +346 -21
- package/dist/core/init.d.ts +11 -0
- package/dist/core/init.js +43 -4
- package/dist/core/metrics/code-accuracy.d.ts +152 -3
- package/dist/core/metrics/code-accuracy.js +323 -19
- package/dist/core/templates/agents-root-stub.d.ts +1 -1
- package/dist/core/templates/agents-root-stub.js +1 -0
- package/dist/core/templates/agents-template.d.ts +1 -1
- package/dist/core/templates/agents-template.js +21 -4
- package/dist/core/templates/codex-hooks-template.d.ts +37 -0
- package/dist/core/templates/codex-hooks-template.js +33 -2
- package/dist/core/templates/slash-command-templates.js +144 -28
- package/dist/core/templates/tasks-template.js +66 -14
- package/dist/core/update.js +17 -0
- package/dist/core/validation/strict-rules.d.ts +28 -0
- package/dist/core/validation/strict-rules.js +284 -0
- package/dist/utils/hook-merge.d.ts +6 -0
- package/dist/utils/hook-merge.js +33 -2
- package/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
|
-
*
|
|
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
|
-
|
|
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
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
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
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
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
|
-
:
|
|
684
|
-
?
|
|
685
|
-
:
|
|
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
|
-
//
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
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 将自动完成:读取 .
|
|
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
|
-
*
|
|
5
|
-
*
|
|
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
|
/**
|