@zhuan-ai/zhuanspec 2.15.7 → 2.16.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/hooks.d.ts +13 -1
- package/dist/cli/hooks.js +75 -2
- package/dist/cli/index.js +111 -4
- package/dist/commands/accuracy.js +5 -1
- package/dist/commands/knowledge.d.ts +21 -0
- package/dist/commands/knowledge.js +80 -0
- package/dist/commands/progress.d.ts +78 -0
- package/dist/commands/progress.js +349 -3
- package/dist/core/archive.js +28 -0
- package/dist/core/configurators/codex.d.ts +55 -1
- package/dist/core/configurators/codex.js +234 -2
- package/dist/core/corrections/select-candidates.d.ts +58 -0
- package/dist/core/corrections/select-candidates.js +357 -0
- package/dist/core/hooks/collect-knowledge.js +80 -5
- package/dist/core/hooks/deviation-check.d.ts +26 -1
- package/dist/core/hooks/deviation-check.js +32 -184
- package/dist/core/hooks/knowledge-index.d.ts +84 -0
- package/dist/core/hooks/knowledge-index.js +270 -0
- package/dist/core/hooks/post-archive.js +25 -13
- package/dist/core/hooks/record-progress.d.ts +64 -1
- package/dist/core/hooks/record-progress.js +224 -54
- package/dist/core/hooks/summarize.js +107 -20
- package/dist/core/hooks/user-input-hook.d.ts +38 -2
- package/dist/core/hooks/user-input-hook.js +346 -21
- package/dist/core/init.d.ts +16 -0
- package/dist/core/init.js +208 -11
- package/dist/core/metrics/code-accuracy.d.ts +152 -3
- package/dist/core/metrics/code-accuracy.js +323 -19
- package/dist/core/templates/agents-root-stub.d.ts +1 -1
- package/dist/core/templates/agents-root-stub.js +1 -0
- package/dist/core/templates/agents-template.d.ts +1 -1
- package/dist/core/templates/agents-template.js +27 -25
- package/dist/core/templates/codex-hooks-template.d.ts +37 -0
- package/dist/core/templates/codex-hooks-template.js +33 -2
- package/dist/core/templates/slash-command-templates.js +208 -66
- package/dist/core/templates/tasks-template.js +66 -14
- package/dist/core/update.js +17 -0
- package/dist/core/validation/strict-rules.d.ts +28 -0
- package/dist/core/validation/strict-rules.js +284 -0
- package/dist/utils/hook-merge.d.ts +6 -0
- package/dist/utils/hook-merge.js +33 -2
- package/dist/utils/phase-utils.js +5 -0
- package/package.json +22 -20
|
@@ -68,16 +68,6 @@ const USER_CORRECTION_KEYWORDS = [
|
|
|
68
68
|
'should use', 'should follow', 'optimize this', 'needs optimization',
|
|
69
69
|
'can be optimized', 'better to',
|
|
70
70
|
];
|
|
71
|
-
// Edit intent keywords — bilingual, for Apply phase scope check
|
|
72
|
-
const EDIT_INTENT_KEYWORDS = [
|
|
73
|
-
// 中文
|
|
74
|
-
'修改', '改', '新增', '添加', '增加', '删除', '移除', '重构',
|
|
75
|
-
'优化', '更新', '替换', '重写', '实现', '完成', '做一下',
|
|
76
|
-
// English
|
|
77
|
-
'add', 'change', 'edit', 'update', 'refactor', 'remove', 'delete',
|
|
78
|
-
'replace', 'rewrite', 'implement', 'fix', 'create', 'write',
|
|
79
|
-
'modify', 'revise', 'adjust',
|
|
80
|
-
];
|
|
81
71
|
// Requirement-change keywords — 命中后升级为硬拦截(T2 需求变更场景)
|
|
82
72
|
// 与 USER_CORRECTION_KEYWORDS 互斥:这类 prompt 不是在说 AI 做错,而是在追加/变更需求
|
|
83
73
|
const REQUIREMENT_CHANGE_KEYWORDS = [
|
|
@@ -126,19 +116,20 @@ export async function deviationCheckHook(options) {
|
|
|
126
116
|
|| options.prompt
|
|
127
117
|
|| '';
|
|
128
118
|
const trigger = options.trigger || 'pre-tool';
|
|
119
|
+
// v2.15.16 Sprint 3:UserPromptSubmit 职责已由 user-input-hook 接管,
|
|
120
|
+
// deviation-check 仅服务 pre-tool(Write/Edit 拦截)与 resume(session 恢复扫描)。
|
|
121
|
+
// Codex Ambient Suggestions 警报及独立的 isCodexInternalPrompt 检测由 user-input-hook 负责。
|
|
129
122
|
const output = await runDeviationCheck(filePath, trigger, promptText, options);
|
|
130
123
|
const durationMs = Date.now() - startTime;
|
|
131
|
-
const hookEvent =
|
|
124
|
+
const hookEvent = 'PreToolUse';
|
|
132
125
|
await recordHookTrigger('deviation-check', hookEvent, output.continue, {
|
|
133
126
|
toolName,
|
|
134
127
|
filePath,
|
|
135
128
|
message: output.systemMessage,
|
|
136
129
|
durationMs,
|
|
137
130
|
});
|
|
138
|
-
//
|
|
139
|
-
|
|
140
|
-
// requested via --json. See openai/codex#14754 for protocol parity.
|
|
141
|
-
const useJson = options.json || trigger === 'post-prompt';
|
|
131
|
+
// --json toggles JSON mode explicitly; PreToolUse default emits systemMessage + exit code.
|
|
132
|
+
const useJson = options.json === true;
|
|
142
133
|
if (useJson) {
|
|
143
134
|
const jsonOutput = output.hookSpecificOutput
|
|
144
135
|
? { ...output, hookSpecificOutput: { hookEventName: hookEvent, ...output.hookSpecificOutput } }
|
|
@@ -158,6 +149,25 @@ export async function deviationCheckHook(options) {
|
|
|
158
149
|
}
|
|
159
150
|
}
|
|
160
151
|
}
|
|
152
|
+
/**
|
|
153
|
+
* 识别 Codex CLI/App 由 Ambient Suggestions 等后台特性发出的系统内部 prompt。
|
|
154
|
+
* 这类 prompt 并非用户真实输入,不应触发纠偏/需求变更/范围检查。
|
|
155
|
+
*
|
|
156
|
+
* 参考:openai/codex#18541 —— Ambient Suggestions 会调用 UserPromptSubmit 钩子
|
|
157
|
+
* 但不调用对应的 Stop 钩子,且目前无法在项目级关闭。
|
|
158
|
+
*/
|
|
159
|
+
export function isCodexInternalPrompt(prompt) {
|
|
160
|
+
if (!prompt)
|
|
161
|
+
return false;
|
|
162
|
+
const head = prompt.slice(0, 400);
|
|
163
|
+
// 已知签名(随 Codex 版本可能扩展,新发现请加在这里)
|
|
164
|
+
const signatures = [
|
|
165
|
+
/Generate\s+\d+\s+to\s+\d+\s+ambient\s+suggestions/i,
|
|
166
|
+
/recent\s+Codex\s+threads\s+from\s+this\s+(?:local\s+)?project/i,
|
|
167
|
+
/ambient\s+suggestions\s+for\s+this\s+local\s+project/i,
|
|
168
|
+
];
|
|
169
|
+
return signatures.some((re) => re.test(head));
|
|
170
|
+
}
|
|
161
171
|
async function readStdin() {
|
|
162
172
|
return new Promise((resolve) => {
|
|
163
173
|
let data = '';
|
|
@@ -184,11 +194,8 @@ async function runDeviationCheck(filePath, trigger, promptText, _options) {
|
|
|
184
194
|
phase = phase || 'idle';
|
|
185
195
|
}
|
|
186
196
|
}
|
|
187
|
-
// No change bound or idle phase → allow
|
|
197
|
+
// No change bound or idle phase → allow
|
|
188
198
|
if (!changeId || phase === 'idle') {
|
|
189
|
-
if (trigger === 'post-prompt') {
|
|
190
|
-
return { continue: true };
|
|
191
|
-
}
|
|
192
199
|
const msg = !changeId
|
|
193
200
|
? '✓ 当前无绑定变更,正常工作流'
|
|
194
201
|
: `✓ 变更 [${changeId}] 处于 idle 阶段,正常工作流`;
|
|
@@ -232,137 +239,10 @@ async function runDeviationCheck(filePath, trigger, promptText, _options) {
|
|
|
232
239
|
systemMessage: '✓ Resume deviation check passed',
|
|
233
240
|
};
|
|
234
241
|
}
|
|
235
|
-
// Post-prompt check: inject proposal context for 2-dimension consistency check in Apply phase.
|
|
236
|
-
if (phase === 'apply' && trigger === 'post-prompt') {
|
|
237
|
-
// Skip if no proposal exists
|
|
238
|
-
if (!await FileSystemUtils.fileExists(proposalPath)) {
|
|
239
|
-
return { continue: true };
|
|
240
|
-
}
|
|
241
|
-
const lowerPrompt = promptText.toLowerCase();
|
|
242
|
-
// Detect correction/pitfall keywords
|
|
243
|
-
const correctionFound = USER_CORRECTION_KEYWORDS.some(kw => lowerPrompt.includes(kw.toLowerCase()));
|
|
244
|
-
// Detect edit intent
|
|
245
|
-
const hasEditIntent = EDIT_INTENT_KEYWORDS.some(kw => lowerPrompt.includes(kw.toLowerCase()));
|
|
246
|
-
// Detect requirement-change intent(T2):独立于 hasEditIntent,命中后升级为硬拦截
|
|
247
|
-
const hasRequirementChange = REQUIREMENT_CHANGE_KEYWORDS.some(kw => lowerPrompt.includes(kw.toLowerCase()));
|
|
248
|
-
// No action needed if purely read-only and no correction / requirement-change signal
|
|
249
|
-
if (!hasEditIntent && !correctionFound && !hasRequirementChange) {
|
|
250
|
-
return { continue: true };
|
|
251
|
-
}
|
|
252
|
-
// Load proposal for context injection
|
|
253
|
-
const proposalContent = await FileSystemUtils.readFile(proposalPath);
|
|
254
|
-
const proposalSummary = extractProposalSummary(proposalContent, 1500);
|
|
255
|
-
// === 纠偏命中:硬拦截 + 写标记文件 + 记录 correction-log ===
|
|
256
|
-
if (correctionFound) {
|
|
257
|
-
try {
|
|
258
|
-
// 确保 changeDir 存在
|
|
259
|
-
if (!fs.existsSync(changeDir)) {
|
|
260
|
-
fs.mkdirSync(changeDir, { recursive: true });
|
|
261
|
-
}
|
|
262
|
-
// 写入待处理标记(含 TTL 时间戳)
|
|
263
|
-
const pendingMarker = path.join(changeDir, '.pending-correction');
|
|
264
|
-
fs.writeFileSync(pendingMarker, JSON.stringify({
|
|
265
|
-
createdAt: Date.now(),
|
|
266
|
-
prompt: promptText.slice(0, 500),
|
|
267
|
-
changeId,
|
|
268
|
-
}, null, 2), 'utf-8');
|
|
269
|
-
// 追写 .correction-log(JSONL,用于后续 Stop hook 沉淀询问)
|
|
270
|
-
const logPath = path.join(changeDir, '.correction-log');
|
|
271
|
-
const logEntry = JSON.stringify({
|
|
272
|
-
ts: new Date().toISOString(),
|
|
273
|
-
event: 'detected',
|
|
274
|
-
promptPreview: promptText.slice(0, 200),
|
|
275
|
-
});
|
|
276
|
-
fs.appendFileSync(logPath, logEntry + '\n', 'utf-8');
|
|
277
|
-
}
|
|
278
|
-
catch {
|
|
279
|
-
// 文件操作失败不阻断拦截
|
|
280
|
-
}
|
|
281
|
-
// NOTE: 返回 continue: true(而非 false),确保 additionalContext
|
|
282
|
-
// 能注入到下一轮对话中,让主 Agent 用 askUserQuestion 展示四选一。
|
|
283
|
-
// 代码编辑由 pre-tool hook(检测 .pending-correction 标记)兜底拦截。
|
|
284
|
-
return {
|
|
285
|
-
continue: true,
|
|
286
|
-
systemMessage: '⚠️ 检测到用户纠偏信号——已注入纠偏四选一上下文,请主 Agent 用 askUserQuestion 向用户展示选项。',
|
|
287
|
-
hookSpecificOutput: {
|
|
288
|
-
additionalContext: buildCorrectionOptionsContext(proposalSummary, changeId),
|
|
289
|
-
options: [
|
|
290
|
-
'A. 需求变更 — 先更新 proposal/tasks/specs,再改代码',
|
|
291
|
-
'B. Bug 修复豁免 — 直接改代码,本次不更新 Spec',
|
|
292
|
-
'C. 纯设计细节 — 不改 Spec 不改代码,仅口头澄清',
|
|
293
|
-
'D. 取消本次修改',
|
|
294
|
-
],
|
|
295
|
-
},
|
|
296
|
-
};
|
|
297
|
-
}
|
|
298
|
-
// === 需求变更(T2)硬拦截:复用 .pending-correction,但标记 reason=requirement-change ===
|
|
299
|
-
// 与纠偏互斥:仅当没命中纠偏词但命中需求变更词时进入
|
|
300
|
-
if (hasRequirementChange) {
|
|
301
|
-
try {
|
|
302
|
-
if (!fs.existsSync(changeDir)) {
|
|
303
|
-
fs.mkdirSync(changeDir, { recursive: true });
|
|
304
|
-
}
|
|
305
|
-
const pendingMarker = path.join(changeDir, '.pending-correction');
|
|
306
|
-
fs.writeFileSync(pendingMarker, JSON.stringify({
|
|
307
|
-
createdAt: Date.now(),
|
|
308
|
-
prompt: promptText.slice(0, 500),
|
|
309
|
-
changeId,
|
|
310
|
-
reason: 'requirement-change',
|
|
311
|
-
}, null, 2), 'utf-8');
|
|
312
|
-
const logPath = path.join(changeDir, '.correction-log');
|
|
313
|
-
const logEntry = JSON.stringify({
|
|
314
|
-
ts: new Date().toISOString(),
|
|
315
|
-
event: 'detected',
|
|
316
|
-
reason: 'requirement-change',
|
|
317
|
-
promptPreview: promptText.slice(0, 200),
|
|
318
|
-
});
|
|
319
|
-
fs.appendFileSync(logPath, logEntry + '\n', 'utf-8');
|
|
320
|
-
}
|
|
321
|
-
catch {
|
|
322
|
-
// 文件操作失败不阻断拦截
|
|
323
|
-
}
|
|
324
|
-
// NOTE: 同样返回 continue: true,确保 additionalContext 能注入到对话中。
|
|
325
|
-
// 代码编辑由 pre-tool hook(检测 .pending-correction 标记)兜底拦截。
|
|
326
|
-
return {
|
|
327
|
-
continue: true,
|
|
328
|
-
systemMessage: '⚠️ 检测到需求变更信号——已注入需求变更上下文,请主 Agent 用 askUserQuestion 向用户确认。',
|
|
329
|
-
hookSpecificOutput: {
|
|
330
|
-
additionalContext: buildRequirementChangeContext(proposalSummary, changeId),
|
|
331
|
-
options: [
|
|
332
|
-
'A. 确认需求变更 — 先更新 proposal/tasks/specs,再改代码',
|
|
333
|
-
'D. 取消本次需求变更',
|
|
334
|
-
],
|
|
335
|
-
},
|
|
336
|
-
};
|
|
337
|
-
}
|
|
338
|
-
// 仅有编辑意图时:保持原有两维度一致性软提示
|
|
339
|
-
const reverseSyncFiles = [
|
|
340
|
-
'`proposal.md`(更新变更原因/影响范围)',
|
|
341
|
-
'`tasks.md`(新增或调整对应任务)',
|
|
342
|
-
'`specs/*/spec.md`(更新受影响的 spec 需求条目)',
|
|
343
|
-
'`design.md`(如存在,更新技术设计决策)',
|
|
344
|
-
].join('、');
|
|
345
|
-
let additionalContext = '## ⚠️ ZhuanSpec Apply 阶段一致性检测\n\n';
|
|
346
|
-
additionalContext += `**当前变更提案摘要:**\n${proposalSummary}\n\n`;
|
|
347
|
-
additionalContext += `**处理以下用户请求前,请先做两维度一致性检测:**\n\n`;
|
|
348
|
-
additionalContext += `- **维度1(范围)**:用户请求涉及的文件/功能是否在提案中明确提及?\n`;
|
|
349
|
-
additionalContext += ` - 在提案范围内 → 正常实施\n`;
|
|
350
|
-
additionalContext += ` - **超出提案范围** → 先告知用户,执行 Reverse Sync:依次更新 ${reverseSyncFiles},经用户确认后再实施代码\n\n`;
|
|
351
|
-
additionalContext += `- **维度2(逻辑一致性)**:用户请求的逻辑调整是否与提案设计意图一致?\n`;
|
|
352
|
-
additionalContext += ` - 一致 → 正常实施\n`;
|
|
353
|
-
additionalContext += ` - **逻辑有偏差** → 说明偏差点,同样执行 Reverse Sync 更新所有相关提案文件(${reverseSyncFiles}),经用户确认后再实施\n\n`;
|
|
354
|
-
return {
|
|
355
|
-
continue: true,
|
|
356
|
-
systemMessage: '✓ ZhuanSpec: Apply 阶段一致性检测已注入上下文',
|
|
357
|
-
hookSpecificOutput: {
|
|
358
|
-
additionalContext,
|
|
359
|
-
},
|
|
360
|
-
};
|
|
361
|
-
}
|
|
362
242
|
// Propose phase → block all code modifications (Iron Law 1: No Spec No Code)
|
|
363
243
|
// But check if we can transition to Apply phase
|
|
364
244
|
if (phase === 'propose') {
|
|
365
|
-
//
|
|
245
|
+
// resume trigger 无 filePath 上下文,静默放行,仅 pre-tool 做拦截
|
|
366
246
|
if (trigger !== 'pre-tool') {
|
|
367
247
|
return { continue: true };
|
|
368
248
|
}
|
|
@@ -485,34 +365,6 @@ async function runDeviationCheck(filePath, trigger, promptText, _options) {
|
|
|
485
365
|
}
|
|
486
366
|
}
|
|
487
367
|
}
|
|
488
|
-
// === Task completion gate for Apply→Review transition ===
|
|
489
|
-
if (trigger === 'post-prompt') {
|
|
490
|
-
const lowerPrompt = promptText.toLowerCase();
|
|
491
|
-
const reviewTransitionKeywords = [
|
|
492
|
-
'review', '审查', '进入review', 'review阶段', '代码审查',
|
|
493
|
-
'zhuanspec review', '/review', '开始review',
|
|
494
|
-
];
|
|
495
|
-
const isReviewTransition = reviewTransitionKeywords.some(kw => lowerPrompt.includes(kw));
|
|
496
|
-
if (isReviewTransition && await FileSystemUtils.fileExists(tasksPath)) {
|
|
497
|
-
try {
|
|
498
|
-
const tasksContent = await FileSystemUtils.readFile(tasksPath);
|
|
499
|
-
const uncompleted = tasksContent.match(/^- \[ \] .+$/gm) || [];
|
|
500
|
-
if (uncompleted.length > 0) {
|
|
501
|
-
return {
|
|
502
|
-
continue: false,
|
|
503
|
-
stopReason: 'Task completion gate: uncompleted tasks block Review transition',
|
|
504
|
-
systemMessage: `⚠️ BLOCKED: Cannot transition to Review — ${uncompleted.length} tasks uncompleted in tasks.md.\n\nUncompleted tasks:\n${uncompleted.map(t => ` ${t.trim()}`).join('\n')}\n\nPlease complete all tasks before entering Review, or use \`zhuanspec review <id> --force\` to override.`,
|
|
505
|
-
hookSpecificOutput: {
|
|
506
|
-
additionalContext: `Task completion gate blocked Apply→Review transition. ${uncompleted.length} uncompleted tasks found.`,
|
|
507
|
-
},
|
|
508
|
-
};
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
catch {
|
|
512
|
-
// If tasks.md read fails, allow transition
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
368
|
let estimatedFiles = [];
|
|
517
369
|
let currentTaskFiles = [];
|
|
518
370
|
let estimatedSymbols = [];
|
|
@@ -581,10 +433,6 @@ ZhuanSpec Iron Law 3: Reverse Sync - When deviation is found, update proposal fi
|
|
|
581
433
|
},
|
|
582
434
|
};
|
|
583
435
|
}
|
|
584
|
-
// post-prompt in non-apply phases (propose/techDesign/review/...) → silently allow
|
|
585
|
-
if (trigger === 'post-prompt') {
|
|
586
|
-
return { continue: true };
|
|
587
|
-
}
|
|
588
436
|
// Default: allow
|
|
589
437
|
return {
|
|
590
438
|
continue: true,
|
|
@@ -664,7 +512,7 @@ async function parseEstimatedSymbols(proposalPath) {
|
|
|
664
512
|
return [];
|
|
665
513
|
}
|
|
666
514
|
}
|
|
667
|
-
function extractProposalSummary(content, maxLength) {
|
|
515
|
+
export function extractProposalSummary(content, maxLength) {
|
|
668
516
|
const patterns = [
|
|
669
517
|
/^## (?:Summary|摘要|Overview|概述)[\s\S]*?(?=\n## |\n# |$)/im,
|
|
670
518
|
/^## (?:What Changes|变更内容|Changes|修改内容)[\s\S]*?(?=\n## |\n# |$)/im,
|
|
@@ -736,13 +584,13 @@ function normalizePath(p) {
|
|
|
736
584
|
* 3. 按选择走对应路径
|
|
737
585
|
* 4. 路径完成后运行 resolve-correction 清理标记
|
|
738
586
|
*/
|
|
739
|
-
function buildCorrectionOptionsContext(proposalSummary, changeId) {
|
|
587
|
+
export function buildCorrectionOptionsContext(proposalSummary, changeId) {
|
|
740
588
|
let ctx = '## ⚠️ ZhuanSpec Apply 阶段用户纠偏——必须四选一后再继续\n\n';
|
|
741
589
|
ctx += `**当前变更提案摘要:**\n${proposalSummary}\n\n`;
|
|
742
590
|
ctx += `### 必须按以下顺序执行(严禁跳步)\n\n`;
|
|
743
591
|
ctx += `1. **立即停止**:不得调用任何 Edit/Write,不得启动新 Subagent\n`;
|
|
744
592
|
ctx += `2. **用 askUserQuestion 向用户提出四选一**(措辞与 Hook options 一致):\n`;
|
|
745
|
-
ctx += ` - A.
|
|
593
|
+
ctx += ` - A. 先修 Spec — 先更新 proposal/tasks/specs,再改代码\n`;
|
|
746
594
|
ctx += ` - B. Bug 修复豁免 — 直接改代码,本次不更新 Spec\n`;
|
|
747
595
|
ctx += ` - C. 纯设计细节 — 不改 Spec 不改代码,仅口头澄清\n`;
|
|
748
596
|
ctx += ` - D. 取消本次修改\n\n`;
|
|
@@ -761,13 +609,13 @@ function buildCorrectionOptionsContext(proposalSummary, changeId) {
|
|
|
761
609
|
* - 仅提供 A / D 两个选项(需求变更必然要先改 Spec,无 Bug 豁免路径)
|
|
762
610
|
* - 文案强调“新增/变更功能范围”而非“AI 做错”
|
|
763
611
|
*/
|
|
764
|
-
function buildRequirementChangeContext(proposalSummary, changeId) {
|
|
612
|
+
export function buildRequirementChangeContext(proposalSummary, changeId) {
|
|
765
613
|
let ctx = '## ⚠️ ZhuanSpec Apply 阶段需求变更——必须先同步 Spec 再改代码\n\n';
|
|
766
614
|
ctx += `**当前变更提案摘要:**\n${proposalSummary}\n\n`;
|
|
767
615
|
ctx += `### 必须按以下顺序执行(严禁跳步)\n\n`;
|
|
768
616
|
ctx += `1. **立即停止**:不得调用任何 Edit/Write 修改代码文件,不得启动新 Subagent\n`;
|
|
769
617
|
ctx += `2. **用 askUserQuestion 向用户确认需求变更**(措辞与 Hook options 一致):\n`;
|
|
770
|
-
ctx += ` - A.
|
|
618
|
+
ctx += ` - A. 先修 Spec — 先更新 proposal/tasks/specs,再改代码\n`;
|
|
771
619
|
ctx += ` - D. 取消本次需求变更\n\n`;
|
|
772
620
|
ctx += `3. **选 A 的处理路径**:依次 Edit \`proposal.md\` → \`tasks.md\` → \`specs/<cap>/spec.md\`;\n`;
|
|
773
621
|
ctx += ` 全部完成后执行 \`zhuanspec progress resolve-correction ${changeId} A\` 清理标记,再重新下发代码实施任务\n`;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Knowledge Index Utilities
|
|
3
|
+
*
|
|
4
|
+
* 统一 zhuanspec/knowledge/index.md 的三大分区管理:
|
|
5
|
+
* - Troubleshooting / Best Practices / Implicit Conventions 三类登记
|
|
6
|
+
* - 保留 Archive Log 既有内容
|
|
7
|
+
* - 提供"增量追加"(appendIndexEntry)与"全量重建"(rebuildIndex)
|
|
8
|
+
*
|
|
9
|
+
* 被 post-archive hook(CLI 自动提取)与 `zhuanspec knowledge reindex` 命令共用。
|
|
10
|
+
*/
|
|
11
|
+
/** 知识库三类分类目录(不得新增) */
|
|
12
|
+
export type KnowledgeCategory = 'troubleshooting' | 'best-practices' | 'implicit-conventions';
|
|
13
|
+
export declare const KNOWLEDGE_CATEGORIES: readonly KnowledgeCategory[];
|
|
14
|
+
/** 单条知识条目(来源于知识文件的 Front Matter 解析) */
|
|
15
|
+
export interface KnowledgeEntry {
|
|
16
|
+
category: KnowledgeCategory;
|
|
17
|
+
/** 相对 knowledgeDir 的文件名,形如 `20260508-foo.md` */
|
|
18
|
+
fileName: string;
|
|
19
|
+
/** 文件标题(首行 `# xxx` 或 Front Matter 的 title 字段) */
|
|
20
|
+
title: string;
|
|
21
|
+
/** 逗号分隔关键词 */
|
|
22
|
+
keywords: string;
|
|
23
|
+
/** 适用场景:一句话"何时引用本条知识" */
|
|
24
|
+
useCase: string;
|
|
25
|
+
}
|
|
26
|
+
/** Front Matter 解析结果 */
|
|
27
|
+
export interface KnowledgeFrontMatter {
|
|
28
|
+
title: string;
|
|
29
|
+
keywords: string;
|
|
30
|
+
useCase: string;
|
|
31
|
+
missingUseCase: boolean;
|
|
32
|
+
missingKeywords: boolean;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* 生成默认的空 index.md 骨架(含三分区 + Archive Log)。
|
|
36
|
+
*/
|
|
37
|
+
export declare function buildInitialIndex(): string;
|
|
38
|
+
/**
|
|
39
|
+
* 生成单条登记行。
|
|
40
|
+
* 形如:`- [标题](best-practices/xxx.md) — 关键词 · 适用场景:何时用`
|
|
41
|
+
*/
|
|
42
|
+
export declare function buildEntryLine(entry: KnowledgeEntry): string;
|
|
43
|
+
/**
|
|
44
|
+
* 确保 index.md 含完整三分区骨架;若缺失分区则补齐(不覆盖已有条目与 Archive Log)。
|
|
45
|
+
*/
|
|
46
|
+
export declare function ensureSkeleton(content: string): string;
|
|
47
|
+
/**
|
|
48
|
+
* 读取 index.md,缺失则按骨架初始化;返回当前文本。
|
|
49
|
+
*/
|
|
50
|
+
export declare function readOrInitIndex(indexPath: string): Promise<string>;
|
|
51
|
+
/**
|
|
52
|
+
* 向 index.md 对应分区追加一条登记。
|
|
53
|
+
* - 已存在同一 `(category/fileName)` 引用时跳过(幂等)。
|
|
54
|
+
* - 分区缺失时自动补齐骨架。
|
|
55
|
+
*
|
|
56
|
+
* @returns true 表示新追加;false 表示已存在被跳过
|
|
57
|
+
*/
|
|
58
|
+
export declare function appendIndexEntry(indexPath: string, entry: KnowledgeEntry): Promise<boolean>;
|
|
59
|
+
/**
|
|
60
|
+
* 从一段 markdown 文本中解析知识条目 Front Matter。
|
|
61
|
+
* 支持项目现行的 `**字段**: 值` 样式;字段可选,缺失时返回对应 missing 标记。
|
|
62
|
+
*/
|
|
63
|
+
export declare function parseFrontMatter(markdown: string): KnowledgeFrontMatter;
|
|
64
|
+
/**
|
|
65
|
+
* 扫描结果:每类目录下发现的知识条目(含缺字段告警)。
|
|
66
|
+
*/
|
|
67
|
+
export interface ScanResult {
|
|
68
|
+
entries: KnowledgeEntry[];
|
|
69
|
+
warnings: string[];
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* 扫描 knowledge/{troubleshooting,best-practices,implicit-conventions}/*.md,
|
|
73
|
+
* 解析 Front Matter 组装 KnowledgeEntry 列表,遇缺字段记录 warning(不阻断)。
|
|
74
|
+
*/
|
|
75
|
+
export declare function scanKnowledgeEntries(knowledgeDir: string): Promise<ScanResult>;
|
|
76
|
+
/**
|
|
77
|
+
* 全量重建 index.md 的三大分区(保留 Archive Log 既有内容)。
|
|
78
|
+
* 算法:
|
|
79
|
+
* 1. 读取旧 index.md,提取 `## Archive Log` 分区的全部尾部内容;
|
|
80
|
+
* 2. 用骨架 + 扫描得到的条目重新生成三分区;
|
|
81
|
+
* 3. 末尾拼回 Archive Log。
|
|
82
|
+
*/
|
|
83
|
+
export declare function rebuildIndexContent(scan: ScanResult, existingContent: string | null): string;
|
|
84
|
+
//# sourceMappingURL=knowledge-index.d.ts.map
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Knowledge Index Utilities
|
|
3
|
+
*
|
|
4
|
+
* 统一 zhuanspec/knowledge/index.md 的三大分区管理:
|
|
5
|
+
* - Troubleshooting / Best Practices / Implicit Conventions 三类登记
|
|
6
|
+
* - 保留 Archive Log 既有内容
|
|
7
|
+
* - 提供"增量追加"(appendIndexEntry)与"全量重建"(rebuildIndex)
|
|
8
|
+
*
|
|
9
|
+
* 被 post-archive hook(CLI 自动提取)与 `zhuanspec knowledge reindex` 命令共用。
|
|
10
|
+
*/
|
|
11
|
+
import path from 'path';
|
|
12
|
+
import { promises as fs } from 'fs';
|
|
13
|
+
import { FileSystemUtils } from '../../utils/file-system.js';
|
|
14
|
+
export const KNOWLEDGE_CATEGORIES = [
|
|
15
|
+
'troubleshooting',
|
|
16
|
+
'best-practices',
|
|
17
|
+
'implicit-conventions',
|
|
18
|
+
];
|
|
19
|
+
/** 分区标题(与 index.md 骨架保持一致) */
|
|
20
|
+
const SECTION_HEADINGS = {
|
|
21
|
+
'troubleshooting': '## Troubleshooting',
|
|
22
|
+
'best-practices': '## Best Practices',
|
|
23
|
+
'implicit-conventions': '## Implicit Conventions',
|
|
24
|
+
};
|
|
25
|
+
const SECTION_COMMENTS = {
|
|
26
|
+
'troubleshooting': '<!-- 踩坑 / bug / 错误做法,由 @skill:zhuanspec:knowledge 模式 B 管理 -->',
|
|
27
|
+
'best-practices': '<!-- 可复用的最佳实践 / 设计决策,由 post-archive hook 与 skill 共同管理 -->',
|
|
28
|
+
'implicit-conventions': '<!-- 隐式约定 / 默认行为 / 团队惯例,由 post-archive hook 与 skill 共同管理 -->',
|
|
29
|
+
};
|
|
30
|
+
const ARCHIVE_LOG_HEADING = '## Archive Log';
|
|
31
|
+
const INDEX_HEADER = `# Knowledge Index
|
|
32
|
+
|
|
33
|
+
> 项目知识索引。AI 在进入任何阶段前应先浏览本文件,根据每条"适用场景"判断是否需要打开对应文件。
|
|
34
|
+
> 入口:所有写入由 \`@skill:zhuanspec:knowledge\` 或 post-archive hook 统一完成,禁止直接 Write/Edit 本目录。
|
|
35
|
+
> 索引漂移时运行 \`zhuanspec knowledge reindex --dry-run\` 查看差异。`;
|
|
36
|
+
/**
|
|
37
|
+
* 生成默认的空 index.md 骨架(含三分区 + Archive Log)。
|
|
38
|
+
*/
|
|
39
|
+
export function buildInitialIndex() {
|
|
40
|
+
const sections = KNOWLEDGE_CATEGORIES.map((cat) => `${SECTION_HEADINGS[cat]}\n${SECTION_COMMENTS[cat]}\n`).join('\n');
|
|
41
|
+
return `${INDEX_HEADER}\n\n${sections}\n${ARCHIVE_LOG_HEADING}\n`;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* 生成单条登记行。
|
|
45
|
+
* 形如:`- [标题](best-practices/xxx.md) — 关键词 · 适用场景:何时用`
|
|
46
|
+
*/
|
|
47
|
+
export function buildEntryLine(entry) {
|
|
48
|
+
const keywords = entry.keywords.trim() || '(未填写关键词)';
|
|
49
|
+
const useCase = entry.useCase.trim() || '(未填写适用场景)';
|
|
50
|
+
return `- [${entry.title}](${entry.category}/${entry.fileName}) — ${keywords} · 适用场景:${useCase}`;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* 确保 index.md 含完整三分区骨架;若缺失分区则补齐(不覆盖已有条目与 Archive Log)。
|
|
54
|
+
*/
|
|
55
|
+
export function ensureSkeleton(content) {
|
|
56
|
+
let result = content;
|
|
57
|
+
// 无 `# Knowledge Index` 时视作全新文件
|
|
58
|
+
if (!/^#\s+Knowledge\s+Index/m.test(result)) {
|
|
59
|
+
return buildInitialIndex() + (result.trim() ? `\n${result.trim()}\n` : '');
|
|
60
|
+
}
|
|
61
|
+
// 逐个分区检查:缺失则插入到 `## Archive Log` 之前(保持顺序:ts / bp / ic / archive)
|
|
62
|
+
for (const cat of KNOWLEDGE_CATEGORIES) {
|
|
63
|
+
const heading = SECTION_HEADINGS[cat];
|
|
64
|
+
const headingRegex = new RegExp(`^${escapeRegex(heading)}\\s*$`, 'm');
|
|
65
|
+
if (headingRegex.test(result))
|
|
66
|
+
continue;
|
|
67
|
+
const sectionBlock = `${heading}\n${SECTION_COMMENTS[cat]}\n\n`;
|
|
68
|
+
const archiveMatch = result.match(new RegExp(`^${escapeRegex(ARCHIVE_LOG_HEADING)}`, 'm'));
|
|
69
|
+
if (archiveMatch && archiveMatch.index !== undefined) {
|
|
70
|
+
result = result.slice(0, archiveMatch.index) + sectionBlock + result.slice(archiveMatch.index);
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
// 没有 Archive Log 分区:追加到文件末尾
|
|
74
|
+
result = `${result.trimEnd()}\n\n${sectionBlock}${ARCHIVE_LOG_HEADING}\n`;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return result;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* 读取 index.md,缺失则按骨架初始化;返回当前文本。
|
|
81
|
+
*/
|
|
82
|
+
export async function readOrInitIndex(indexPath) {
|
|
83
|
+
if (await FileSystemUtils.fileExists(indexPath)) {
|
|
84
|
+
const raw = await FileSystemUtils.readFile(indexPath);
|
|
85
|
+
return ensureSkeleton(raw);
|
|
86
|
+
}
|
|
87
|
+
return buildInitialIndex();
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* 向 index.md 对应分区追加一条登记。
|
|
91
|
+
* - 已存在同一 `(category/fileName)` 引用时跳过(幂等)。
|
|
92
|
+
* - 分区缺失时自动补齐骨架。
|
|
93
|
+
*
|
|
94
|
+
* @returns true 表示新追加;false 表示已存在被跳过
|
|
95
|
+
*/
|
|
96
|
+
export async function appendIndexEntry(indexPath, entry) {
|
|
97
|
+
await FileSystemUtils.createDirectory(path.dirname(indexPath));
|
|
98
|
+
let content = await readOrInitIndex(indexPath);
|
|
99
|
+
content = ensureSkeleton(content);
|
|
100
|
+
const refPattern = `(${entry.category}/${entry.fileName})`;
|
|
101
|
+
if (content.includes(refPattern)) {
|
|
102
|
+
// 已登记 → 幂等跳过;但 content 可能因 ensureSkeleton 被补了骨架,仍需写回
|
|
103
|
+
if (content !== (await safeRead(indexPath))) {
|
|
104
|
+
await FileSystemUtils.writeFile(indexPath, content);
|
|
105
|
+
}
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
content = insertIntoSection(content, entry.category, buildEntryLine(entry));
|
|
109
|
+
await FileSystemUtils.writeFile(indexPath, content);
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
async function safeRead(p) {
|
|
113
|
+
try {
|
|
114
|
+
return await FileSystemUtils.readFile(p);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return '';
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* 在指定分区末尾(下一个 `## ` 标题之前)插入一行。
|
|
122
|
+
*/
|
|
123
|
+
function insertIntoSection(content, category, line) {
|
|
124
|
+
const heading = SECTION_HEADINGS[category];
|
|
125
|
+
const lines = content.split('\n');
|
|
126
|
+
const headingIdx = lines.findIndex((l) => l.trim() === heading);
|
|
127
|
+
if (headingIdx === -1) {
|
|
128
|
+
// ensureSkeleton 之后不应该走到这里,兜底再加一段
|
|
129
|
+
return `${content.trimEnd()}\n\n${heading}\n${line}\n`;
|
|
130
|
+
}
|
|
131
|
+
// 找到下一个 `## ` 一级分区标题位置(Archive Log 或下一个 category)
|
|
132
|
+
let nextIdx = lines.length;
|
|
133
|
+
for (let i = headingIdx + 1; i < lines.length; i++) {
|
|
134
|
+
if (/^##\s+/.test(lines[i])) {
|
|
135
|
+
nextIdx = i;
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// 从 headingIdx+1 到 nextIdx-1 是该分区内容;去掉尾部空行再插入新条目,保持紧凑
|
|
140
|
+
const before = lines.slice(0, nextIdx);
|
|
141
|
+
const after = lines.slice(nextIdx);
|
|
142
|
+
// 去掉分区内尾部的连续空行
|
|
143
|
+
while (before.length > headingIdx + 1 && before[before.length - 1].trim() === '') {
|
|
144
|
+
before.pop();
|
|
145
|
+
}
|
|
146
|
+
before.push(line, '');
|
|
147
|
+
return [...before, ...after].join('\n');
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* 从一段 markdown 文本中解析知识条目 Front Matter。
|
|
151
|
+
* 支持项目现行的 `**字段**: 值` 样式;字段可选,缺失时返回对应 missing 标记。
|
|
152
|
+
*/
|
|
153
|
+
export function parseFrontMatter(markdown) {
|
|
154
|
+
const titleMatch = markdown.match(/^#\s+(.+?)\s*$/m);
|
|
155
|
+
const title = titleMatch ? titleMatch[1].trim() : '';
|
|
156
|
+
const pick = (label) => {
|
|
157
|
+
const re = new RegExp(`\\*\\*${escapeRegex(label)}\\*\\*\\s*[::]\\s*(.+?)\\s*$`, 'm');
|
|
158
|
+
const m = markdown.match(re);
|
|
159
|
+
return m ? m[1].trim() : '';
|
|
160
|
+
};
|
|
161
|
+
const keywords = pick('关键词');
|
|
162
|
+
const useCase = pick('适用场景');
|
|
163
|
+
return {
|
|
164
|
+
title,
|
|
165
|
+
keywords,
|
|
166
|
+
useCase,
|
|
167
|
+
missingUseCase: !useCase,
|
|
168
|
+
missingKeywords: !keywords,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* 扫描 knowledge/{troubleshooting,best-practices,implicit-conventions}/*.md,
|
|
173
|
+
* 解析 Front Matter 组装 KnowledgeEntry 列表,遇缺字段记录 warning(不阻断)。
|
|
174
|
+
*/
|
|
175
|
+
export async function scanKnowledgeEntries(knowledgeDir) {
|
|
176
|
+
const entries = [];
|
|
177
|
+
const warnings = [];
|
|
178
|
+
for (const category of KNOWLEDGE_CATEGORIES) {
|
|
179
|
+
const dir = path.join(knowledgeDir, category);
|
|
180
|
+
if (!(await FileSystemUtils.directoryExists(dir)))
|
|
181
|
+
continue;
|
|
182
|
+
let files = [];
|
|
183
|
+
try {
|
|
184
|
+
files = (await fs.readdir(dir)).filter((f) => f.endsWith('.md') && !f.startsWith('.'));
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
files.sort();
|
|
190
|
+
for (const file of files) {
|
|
191
|
+
const full = path.join(dir, file);
|
|
192
|
+
let text = '';
|
|
193
|
+
try {
|
|
194
|
+
text = await FileSystemUtils.readFile(full);
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
warnings.push(`[WARN] 读取失败: ${category}/${file}`);
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
const fm = parseFrontMatter(text);
|
|
201
|
+
if (!fm.title) {
|
|
202
|
+
warnings.push(`[WARN] ${category}/${file} 缺少一级标题 (# 标题),使用文件名兜底`);
|
|
203
|
+
}
|
|
204
|
+
if (fm.missingKeywords) {
|
|
205
|
+
warnings.push(`[WARN] ${category}/${file} 缺少 **关键词** 字段`);
|
|
206
|
+
}
|
|
207
|
+
if (fm.missingUseCase) {
|
|
208
|
+
warnings.push(`[WARN] ${category}/${file} 缺少 **适用场景** 字段(请尽快补齐)`);
|
|
209
|
+
}
|
|
210
|
+
const firstKeyword = fm.keywords ? fm.keywords.split(/[,,]/)[0].trim() : '';
|
|
211
|
+
entries.push({
|
|
212
|
+
category,
|
|
213
|
+
fileName: file,
|
|
214
|
+
title: fm.title || file.replace(/\.md$/, ''),
|
|
215
|
+
keywords: fm.keywords,
|
|
216
|
+
// 兜底:适用场景缺失时用"首关键词相关任务"
|
|
217
|
+
useCase: fm.useCase || (firstKeyword ? `涉及 ${firstKeyword} 相关任务时` : '(未填写适用场景,待补齐)'),
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return { entries, warnings };
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* 全量重建 index.md 的三大分区(保留 Archive Log 既有内容)。
|
|
225
|
+
* 算法:
|
|
226
|
+
* 1. 读取旧 index.md,提取 `## Archive Log` 分区的全部尾部内容;
|
|
227
|
+
* 2. 用骨架 + 扫描得到的条目重新生成三分区;
|
|
228
|
+
* 3. 末尾拼回 Archive Log。
|
|
229
|
+
*/
|
|
230
|
+
export function rebuildIndexContent(scan, existingContent) {
|
|
231
|
+
// 1. 提取旧 Archive Log 块
|
|
232
|
+
let archiveBlock = `${ARCHIVE_LOG_HEADING}\n`;
|
|
233
|
+
if (existingContent) {
|
|
234
|
+
const archiveIdx = existingContent.indexOf(ARCHIVE_LOG_HEADING);
|
|
235
|
+
if (archiveIdx !== -1) {
|
|
236
|
+
// 从 Archive Log 标题开始到文件末尾
|
|
237
|
+
archiveBlock = existingContent.slice(archiveIdx).trimEnd() + '\n';
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
// 2. 按分类整理条目(保持扫描顺序 = 字典序)
|
|
241
|
+
const byCategory = {
|
|
242
|
+
'troubleshooting': [],
|
|
243
|
+
'best-practices': [],
|
|
244
|
+
'implicit-conventions': [],
|
|
245
|
+
};
|
|
246
|
+
for (const e of scan.entries)
|
|
247
|
+
byCategory[e.category].push(e);
|
|
248
|
+
const parts = [INDEX_HEADER, ''];
|
|
249
|
+
for (const cat of KNOWLEDGE_CATEGORIES) {
|
|
250
|
+
parts.push(SECTION_HEADINGS[cat]);
|
|
251
|
+
parts.push(SECTION_COMMENTS[cat]);
|
|
252
|
+
const list = byCategory[cat];
|
|
253
|
+
if (list.length === 0) {
|
|
254
|
+
parts.push('');
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
parts.push('');
|
|
258
|
+
for (const e of list)
|
|
259
|
+
parts.push(buildEntryLine(e));
|
|
260
|
+
parts.push('');
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
parts.push(archiveBlock.trimEnd());
|
|
264
|
+
parts.push('');
|
|
265
|
+
return parts.join('\n');
|
|
266
|
+
}
|
|
267
|
+
function escapeRegex(s) {
|
|
268
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
269
|
+
}
|
|
270
|
+
//# sourceMappingURL=knowledge-index.js.map
|