sillyspec 3.26.7 → 3.26.8
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/.claude/skills/sillyspec-doctor/SKILL.md +2 -0
- package/.husky/pre-push +9 -2
- package/bin/sillyspec.js +1 -1
- package/package.json +1 -1
- package/src/config-schema.js +305 -287
- package/src/docs-check.js +333 -0
- package/src/docs-debt.js +226 -0
- package/src/docs-gate.js +106 -0
- package/src/doctor-diagnostics.js +23 -2
- package/src/index.js +1880 -1787
- package/src/init.js +81 -14
- package/src/modules.js +490 -486
- package/src/progress.js +37 -13
- package/src/run/command.js +54 -24
- package/src/run/complete-handlers.js +1254 -1128
- package/src/run/complete.js +0 -32
- package/src/run/gates.js +18 -0
- package/src/run/prompt.js +713 -640
- package/src/run/quick-audit.js +137 -123
- package/src/run/shared.js +1109 -929
- package/src/scan-staleness.js +90 -0
- package/src/stage-contract-spec.js +15 -0
- package/src/stage-contract.js +8 -5
- package/src/stages/brainstorm-auto.js +0 -1
- package/src/stages/brainstorm.js +1 -1
- package/src/stages/execute.js +985 -967
- package/src/stages/plan-postcheck.js +93 -36
- package/src/stages/verify.js +1 -1
- package/src/sync.js +27 -17
- package/src/task-review.js +2 -1
- package/src/worktree-apply.js +6 -3
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scan-staleness — scan 文档漂移检测(债单 D-7 方案 A,change: 2026-08-15 ql-20260815-013)
|
|
3
|
+
*
|
|
4
|
+
* 原则(doc-consistency-debt 第六节用户裁决):CLI 算事实注入——git 算出 scan 文档落后程度,
|
|
5
|
+
* 注入给 agent 的是一行结论不是一段劝说指令。brainstorm「加载项目上下文」读 scan 文档前
|
|
6
|
+
* 就知道"我读的架构描述可能过期",不再盲信。
|
|
7
|
+
*
|
|
8
|
+
* 判定口径:scan 文档 frontmatter 的 source_commit(modules.js 写入、worktree-guard 覆盖
|
|
9
|
+
* 保护同源字段)vs 当前 HEAD——`git rev-list --count <source_commit>..HEAD` + 提交时间差。
|
|
10
|
+
* 纯读操作(git 只读 + frontmatter 解析),任何失败降级 null(advisory 信号不阻断流程)。
|
|
11
|
+
*/
|
|
12
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs'
|
|
13
|
+
import { join } from 'node:path'
|
|
14
|
+
import { safeGit } from './git-helper.js'
|
|
15
|
+
|
|
16
|
+
/** 漂移阈值(commit 数 / 天数,超过任一即 stale;advisory 性质允许保守默认) */
|
|
17
|
+
export const STALENESS_THRESHOLDS = { commits: 50, days: 14 }
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 纯函数:从 scan 文档内容解析 frontmatter 的 source_commit。
|
|
21
|
+
* @param {string} content 文档全文
|
|
22
|
+
* @returns {string|null} commit hex(短/长均可);无字段/无 frontmatter → null
|
|
23
|
+
*/
|
|
24
|
+
export function parseSourceCommit(content) {
|
|
25
|
+
if (!content || typeof content !== 'string') return null
|
|
26
|
+
// \r? 容错 CRLF($ 前的 \r 会致 LF 正则失配)
|
|
27
|
+
const m = content.match(/^---\r?\n[\s\S]*?^source_commit:\s*([0-9a-f]{7,40})\s*\r?$/m)
|
|
28
|
+
return m ? m[1] : null
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 计算单个 project 的 scan 文档漂移事实。
|
|
33
|
+
* @param {{ projectRoot: string, specBase: string, projectName: string, thresholds?: { commits: number, days: number } }} opts
|
|
34
|
+
* @returns {{ status: 'fresh'|'stale'|'unknown', behindCommits: number|null, daysSinceScan: number|null, sourceCommit: string|null, message: string }|null}
|
|
35
|
+
* null = 无 scan 文档目录(绿地项目等),调用方直接跳过;unknown = 有文档但算不出(无
|
|
36
|
+
* source_commit 字段 / git 失败),message 说明降级原因。
|
|
37
|
+
*/
|
|
38
|
+
export function computeScanStaleness(opts) {
|
|
39
|
+
const { projectRoot, specBase, projectName, thresholds = STALENESS_THRESHOLDS } = opts || {}
|
|
40
|
+
if (!projectRoot || !specBase || !projectName) return null
|
|
41
|
+
const scanDir = join(specBase, 'docs', projectName, 'scan')
|
|
42
|
+
if (!existsSync(scanDir)) return null
|
|
43
|
+
|
|
44
|
+
// 任一 scan 文档带 source_commit 即可代表整批(modules.js 同批写入同值)
|
|
45
|
+
let sourceCommit = null
|
|
46
|
+
let checked = 0
|
|
47
|
+
for (const f of readdirSync(scanDir)) {
|
|
48
|
+
if (!f.endsWith('.md')) continue
|
|
49
|
+
checked++
|
|
50
|
+
const c = parseSourceCommit(readFileSync(join(scanDir, f), 'utf8'))
|
|
51
|
+
if (c) { sourceCommit = c; break }
|
|
52
|
+
}
|
|
53
|
+
if (checked === 0) return null
|
|
54
|
+
if (!sourceCommit) {
|
|
55
|
+
return { status: 'unknown', behindCommits: null, daysSinceScan: null, sourceCommit: null,
|
|
56
|
+
message: 'scan 文档无 source_commit 字段(旧版生成),无法判漂移——如架构已大改建议重跑 sillyspec run scan --standard' }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const head = safeGit(projectRoot, ['rev-parse', 'HEAD'])
|
|
60
|
+
if (head.error || !head.value) {
|
|
61
|
+
return { status: 'unknown', behindCommits: null, daysSinceScan: null, sourceCommit,
|
|
62
|
+
message: `git 读取失败(${head.error || '无 HEAD'}),跳过漂移判定` }
|
|
63
|
+
}
|
|
64
|
+
const isAncestor = safeGit(projectRoot, ['merge-base', '--is-ancestor', sourceCommit, head.value.trim()])
|
|
65
|
+
if (isAncestor.error) {
|
|
66
|
+
return { status: 'unknown', behindCommits: null, daysSinceScan: null, sourceCommit,
|
|
67
|
+
message: `source_commit ${sourceCommit.slice(0, 7)} 不在当前分支历史(分支切换/rebase),跳过漂移判定` }
|
|
68
|
+
}
|
|
69
|
+
const countRes = safeGit(projectRoot, ['rev-list', '--count', `${sourceCommit}..HEAD`])
|
|
70
|
+
const behindCommits = countRes.error ? null : parseInt(countRes.value, 10)
|
|
71
|
+
if (behindCommits === null || Number.isNaN(behindCommits)) {
|
|
72
|
+
return { status: 'unknown', behindCommits: null, daysSinceScan: null, sourceCommit,
|
|
73
|
+
message: 'rev-list 计数失败,跳过漂移判定' }
|
|
74
|
+
}
|
|
75
|
+
const dateRes = safeGit(projectRoot, ['log', '-1', '--format=%cI', sourceCommit])
|
|
76
|
+
const daysSinceScan = dateRes.error ? null
|
|
77
|
+
: Math.floor((Date.now() - new Date(dateRes.value.trim()).getTime()) / 86400000)
|
|
78
|
+
|
|
79
|
+
const stale = behindCommits >= thresholds.commits || (daysSinceScan !== null && daysSinceScan >= thresholds.days)
|
|
80
|
+
if (!stale) {
|
|
81
|
+
return { status: 'fresh', behindCommits, daysSinceScan, sourceCommit,
|
|
82
|
+
message: `scan 文档新鲜(落后 ${behindCommits} commit / ${daysSinceScan ?? '?'} 天)` }
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
status: 'stale', behindCommits, daysSinceScan, sourceCommit,
|
|
86
|
+
message: `⚠️ scan 文档过期:停在 ${sourceCommit.slice(0, 7)}(${daysSinceScan ?? '?'} 天前),期间源码已推进 ${behindCommits} commit。` +
|
|
87
|
+
`本次加载的架构/结构描述可能失真——涉及结构变更的 design 不要盲信 scan 文档,以实读代码为准;` +
|
|
88
|
+
`如需刷新基线跑 sillyspec run scan --standard --force-rescan`,
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -47,6 +47,7 @@ export const CUSTOM_KINDS = new Set([
|
|
|
47
47
|
'cross-task-contract', // task 卡片 provides/expects_from 对账(plan-postcheck)
|
|
48
48
|
'design-file-coverage', // design.md 文件清单 vs allowed_paths 覆盖(plan-postcheck)
|
|
49
49
|
'task-id-continuity', // task-NN id 连续性
|
|
50
|
+
'task-plan-reconciliation', // plan.md 声明任务 ↔ tasks/ 卡片双向对账(plan-postcheck)
|
|
50
51
|
'design-readiness', // design.md 进入 plan 前的章节就绪检查(validateDesignForPlan)
|
|
51
52
|
'task-card-fields', // task-NN.md 卡片字段存在性(validateBlueprintConsistency + validatePlanFeasibility)
|
|
52
53
|
'verify-conclusion-gate', // verify-result.md 结论 PASS/FAIL 门控
|
|
@@ -321,6 +322,20 @@ const PLAN_RULES = [
|
|
|
321
322
|
spec: 'design.md「文件变更清单」中的每个源码文件,必须被至少一个 task 的 allowed_paths 覆盖(前缀/glob 容差匹配),否则 execute 子代理无权改它→必然漏改。有 task 卡片但 design 缺「文件变更清单」章节也阻断(清单是覆盖对账的基准)。',
|
|
322
323
|
failMessage: 'design.md 文件变更清单中 ${count} 个文件未被任何 task 的 allowed_paths 覆盖:\n${files}\n 这些文件在 execute 阶段将无 task 有权修改 → 必然漏改。\n 修复:为每个遗漏文件新建/补充 task 并在其 allowed_paths 声明,或在 design.md「不修改文件」章节说明不改原因。',
|
|
323
324
|
},
|
|
325
|
+
// task-plan-reconciliation(custom):plan.md 声明任务 ↔ tasks/ 卡片双向对账(债单 D-2b)。
|
|
326
|
+
// 修复场景:plan 列 17 个任务只生成 12 张卡(尾部文档同步类任务漏卡),execute 审计只看
|
|
327
|
+
// 卡片 → 漏卡任务零失败信号(sillyhub conversation-driven T-13~T-17 实证)。
|
|
328
|
+
// ${declared}/${cards} replaceAll。
|
|
329
|
+
{
|
|
330
|
+
id: 'plan.task-plan-reconciliation', stage: 'plan', source: 'validateBlueprintConsistency', severity: 'error', kind: 'task-plan-reconciliation',
|
|
331
|
+
target: { root: 'change', path: 'tasks' },
|
|
332
|
+
data: {
|
|
333
|
+
messageMissingCards: 'plan.md 声明了 ${declared} 个任务,但 tasks/ 只有部分卡片。缺卡: ${missing}\n 无卡片的任务不进 Wave、不受 execute 审计(allowed_paths/review.json 都不覆盖)→ 必然漏做(典型:尾部文档同步类任务)。\n 修复:为每个缺卡任务补 task-NN.md(含 allowed_paths/验收标准),或从 plan.md 删除该任务。',
|
|
334
|
+
messageOrphanCards: 'tasks/ 存在 plan.md 未声明的卡片: ${orphans}\n 这些任务不进任何 Wave,execute 不会执行。修复:在 plan.md 补声明,或删除孤儿卡片。',
|
|
335
|
+
},
|
|
336
|
+
spec: 'plan.md 中声明的每个 task-NN 必须有对应 tasks/task-NN.md 卡片(反向亦然),否则阻断——无卡片任务不受 execute 审计,必然漏做。',
|
|
337
|
+
failMessage: 'plan.md 任务与 tasks/ 卡片不对账',
|
|
338
|
+
},
|
|
324
339
|
// task-id-continuity(custom):task-NN id 从 task-01 起连续。算法留 validatePlanFeasibility。
|
|
325
340
|
// ${expected}/${actual} replaceAll(零填充 2 位)。
|
|
326
341
|
{
|
package/src/stage-contract.js
CHANGED
|
@@ -12,6 +12,7 @@ import { detectChangeRisk, checkIntegrationEvidence, VERIFICATION_NEEDS, RISK_LE
|
|
|
12
12
|
import { SCAN_REQUIRED_DOCS, AUXILIARY_STAGES } from './constants.js'
|
|
13
13
|
import { evaluateRules } from './stage-contract-engine.js'
|
|
14
14
|
import { getRule } from './stage-contract-spec.js'
|
|
15
|
+
import { parseAllowedPaths } from './stages/plan-postcheck.js'
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* 校验结果
|
|
@@ -381,11 +382,13 @@ function validatePlanOutputs(cwd, changeName, context = {}) {
|
|
|
381
382
|
const taskFiles = readdirSync(tasksDir).filter(f => /^task-\d+\.md$/i.test(f))
|
|
382
383
|
for (const taskFile of taskFiles) {
|
|
383
384
|
const taskContent = readFileSync(join(tasksDir, taskFile), 'utf8')
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
385
|
+
// 复用 parseAllowedPaths(坑6①:认 inline 数组/缩进块/顶格块 + 剥反引号 + CRLF 归一,
|
|
386
|
+
// 消除此处内联正则与 plan-postcheck 的解析漂移)。卡片无 frontmatter 时退全文扫描
|
|
387
|
+
// (包裹伪 frontmatter 复用同一解析器)——原内联正则是全文匹配语义,entry-point-wiring
|
|
388
|
+
// 只关心路径覆盖,卡片格式合法性归 plan-postcheck 把关,此处不收紧。
|
|
389
|
+
const paths = parseAllowedPaths(taskContent)
|
|
390
|
+
const effective = paths.length > 0 ? paths : parseAllowedPaths(`---\n${taskContent}\n---`)
|
|
391
|
+
for (const p of effective) allAllowedPaths.add(p.toLowerCase())
|
|
389
392
|
}
|
|
390
393
|
}
|
|
391
394
|
// 也从 plan.md 文件变更清单中收集
|
package/src/stages/brainstorm.js
CHANGED
|
@@ -32,6 +32,7 @@ export const definition = {
|
|
|
32
32
|
|
|
33
33
|
### 操作
|
|
34
34
|
1. 读取项目总览 \`{SPEC_ROOT}/docs/<project>/scan/PROJECT.md\` + 共享规范 + 子项目上下文
|
|
35
|
+
- 漂移事实(CLI 算):{SCAN_STALENESS}
|
|
35
36
|
2. 加载项目信息:\`cat {SPEC_ROOT}/projects/*.yaml 2>/dev/null\`
|
|
36
37
|
3. 加载本地配置:\`cat {SPEC_ROOT}/local.yaml 2>/dev/null\`
|
|
37
38
|
4. 棕地项目:读取 {SPEC_ROOT}/docs/<project>/scan/ 下的 STRUCTURE.md、CONVENTIONS.md、ARCHITECTURE.md
|
|
@@ -79,7 +80,6 @@ export const definition = {
|
|
|
79
80
|
maxWaitRounds: 8,
|
|
80
81
|
waitReason: '等待用户回答需求问题或澄清',
|
|
81
82
|
waitOptions: ['回答见--answer', '信息够了,进入方案讨论'],
|
|
82
|
-
waitFreeAnswer: true, // 开放回答型:--answer 内容为自由文本(需求澄清回答),非封闭单选,豁免 enforceWaitChoice
|
|
83
83
|
prompt: `通过对话探索需求细节、分析原型(如有)、判断拆分/批量、并对歧义点做需求澄清 Grill。本步骤合并了原有的对话探索、原型分析、需求范围评估、需求澄清 Grill 四个环节——按下方 A→D 顺序按需执行。提问遵守「一次只问一个」(见 D 节追问策略):必须问的问题逐个问,能查代码/文档解决的不问;不要把多个歧义打包成一屏清单一次抛给用户(用户记不住、答不全,反而漏掉关键约束);也不要为凑数把单个问题拖成多轮。
|
|
84
84
|
|
|
85
85
|
### A. 原型/设计图分析(如用户提供)
|