sillyspec 3.32.10 → 3.33.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/.claude/skills/sillyspec-brainstorm/SKILL.md +114 -114
- package/.claude/skills/sillyspec-execute/SKILL.md +222 -222
- package/.claude/skills/sillyspec-explore/SKILL.md +107 -107
- package/.claude/skills/sillyspec-plan/SKILL.md +156 -156
- package/.github/workflows/ci.yml +58 -0
- package/.husky/pre-push +7 -6
- package/eval/eval.config.json +3 -3
- package/eval/lib/config.mjs +6 -3
- package/eval/lib/util.mjs +16 -3
- package/eval/lib/workdir.mjs +4 -1
- package/eval/results/eval.db +0 -0
- package/eval/results/logs/2026-10-15T12-37-22-986Z-004-phase-stats-armA/prompt.txt +10 -0
- package/eval/results/logs/2026-10-15T12-37-22-986Z-004-phase-stats-armA/stderr.txt +0 -0
- package/eval/results/logs/2026-10-15T12-37-22-986Z-004-phase-stats-armA/stdout.txt +1 -0
- package/eval/results/logs/2026-10-15T12-45-37-124Z-004-phase-stats-armB/prompt.txt +10 -0
- package/eval/results/logs/2026-10-15T12-45-37-124Z-004-phase-stats-armB/stderr.txt +0 -0
- package/eval/results/logs/2026-10-15T12-45-37-124Z-004-phase-stats-armB/stdout.txt +1 -0
- package/eval/results/logs/2026-10-15T12-56-19-259Z-004-phase-stats-armA/prompt.txt +10 -0
- package/eval/results/logs/2026-10-15T12-56-19-259Z-004-phase-stats-armA/stderr.txt +0 -0
- package/eval/results/logs/2026-10-15T12-56-19-259Z-004-phase-stats-armA/stdout.txt +1 -0
- package/eval/results/logs/2026-10-15T13-00-05-625Z-004-phase-stats-armB/prompt.txt +10 -0
- package/eval/results/logs/2026-10-15T13-04-51-003Z-004-phase-stats-armA/prompt.txt +10 -0
- package/eval/results/logs/2026-10-15T13-04-51-003Z-004-phase-stats-armA/stderr.txt +0 -0
- package/eval/results/logs/2026-10-15T13-04-51-003Z-004-phase-stats-armA/stdout.txt +1 -0
- package/eval/results/logs/2026-10-15T13-06-25-271Z-004-phase-stats-armB/prompt.txt +10 -0
- package/eval/results/logs/2026-10-15T13-06-25-271Z-004-phase-stats-armB/stderr.txt +0 -0
- package/eval/results/logs/2026-10-15T13-06-25-271Z-004-phase-stats-armB/stdout.txt +1 -0
- package/eval/results/logs/2026-10-15T13-24-38-308Z-004-phase-stats-armB/prompt.txt +10 -0
- package/eval/results/logs/2026-10-15T13-24-38-308Z-004-phase-stats-armB/stderr.txt +0 -0
- package/eval/results/logs/2026-10-15T13-24-38-308Z-004-phase-stats-armB/stdout.txt +1 -0
- package/eval/results/logs/2026-10-15T14-50-01-950Z-005-orders-export-armA/prompt.txt +8 -0
- package/eval/results/logs/2026-10-15T14-50-01-950Z-005-orders-export-armA/stderr.txt +0 -0
- package/eval/results/logs/2026-10-15T14-50-01-950Z-005-orders-export-armA/stdout.txt +1 -0
- package/eval/results/logs/2026-10-15T14-51-56-316Z-005-orders-export-armB/prompt.txt +8 -0
- package/eval/results/logs/2026-10-15T14-51-56-316Z-005-orders-export-armB/stderr.txt +0 -0
- package/eval/results/logs/2026-10-15T14-51-56-316Z-005-orders-export-armB/stdout.txt +1 -0
- package/eval/results/workdirs/001-slugify__armA/package.json +6 -0
- package/eval/results/workdirs/001-slugify__armA/src/slugify.mjs +26 -0
- package/eval/results/workdirs/001-slugify__armB/package.json +6 -0
- package/eval/results/workdirs/001-slugify__armB/src/slugify.mjs +26 -0
- package/eval/results/workdirs/002-each-day__armA/package.json +6 -0
- package/eval/results/workdirs/002-each-day__armA/src/dates.mjs +30 -0
- package/eval/results/workdirs/002-each-day__armB/DRY_RUN_WRONG_ANSWER.mjs +1 -0
- package/eval/results/workdirs/002-each-day__armB/package.json +6 -0
- package/eval/results/workdirs/002-each-day__armB/src/dates.mjs +4 -0
- package/eval/results/workdirs/003-word-report__armA/bin/report.mjs +36 -0
- package/eval/results/workdirs/003-word-report__armA/data/notes.txt +5 -0
- package/eval/results/workdirs/003-word-report__armA/package.json +6 -0
- package/eval/results/workdirs/003-word-report__armB/bin/report.mjs +36 -0
- package/eval/results/workdirs/003-word-report__armB/data/notes.txt +5 -0
- package/eval/results/workdirs/003-word-report__armB/package.json +6 -0
- package/eval/results/workdirs/004-phase-stats__armA/data/events.log +16 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/commands/sillyspec/flow.md +44 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/commands/sillyspec/run-archive.md +27 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/commands/sillyspec/run-brainstorm.md +34 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/commands/sillyspec/run-execute.md +30 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/commands/sillyspec/run-plan.md +28 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/commands/sillyspec/run-verify.md +30 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/commands/sillyspec/status.md +28 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-archive/SKILL.md +87 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-auto/SKILL.md +54 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-brainstorm/SKILL.md +114 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-commit/SKILL.md +67 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-continue/SKILL.md +17 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-doctor/SKILL.md +76 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-execute/SKILL.md +222 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-explore/SKILL.md +107 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-export/SKILL.md +57 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-flow/SKILL.md +62 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-init/SKILL.md +177 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-knowledge/SKILL.md +40 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-plan/SKILL.md +156 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-propose/SKILL.md +16 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-quick/SKILL.md +116 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-resume/SKILL.md +21 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-scan/SKILL.md +79 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-state/SKILL.md +25 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-status/SKILL.md +54 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-verify/SKILL.md +87 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-workspace/SKILL.md +33 -0
- package/eval/results/workdirs/004-phase-stats__armB/AGENTS.md +45 -0
- package/eval/results/workdirs/004-phase-stats__armB/CLAUDE.md +3 -0
- package/eval/results/workdirs/004-phase-stats__armB/data/events.log +16 -0
- package/eval/results/workdirs/run-001-slugify__armA/package.json +6 -0
- package/eval/results/workdirs/run-001-slugify__armA/src/slugify.mjs +26 -0
- package/eval/results/workdirs/run-001-slugify__armB/package.json +6 -0
- package/eval/results/workdirs/run-001-slugify__armB/src/slugify.mjs +26 -0
- package/eval/results/workdirs/run-002-each-day__armA/package.json +6 -0
- package/eval/results/workdirs/run-002-each-day__armA/src/dates.mjs +30 -0
- package/eval/results/workdirs/run-002-each-day__armB/DRY_RUN_WRONG_ANSWER.mjs +1 -0
- package/eval/results/workdirs/run-002-each-day__armB/package.json +6 -0
- package/eval/results/workdirs/run-002-each-day__armB/src/dates.mjs +4 -0
- package/eval/results/workdirs/run-003-word-report__armA/bin/report.mjs +36 -0
- package/eval/results/workdirs/run-003-word-report__armA/data/notes.txt +5 -0
- package/eval/results/workdirs/run-003-word-report__armA/package.json +6 -0
- package/eval/results/workdirs/run-003-word-report__armB/bin/report.mjs +36 -0
- package/eval/results/workdirs/run-003-word-report__armB/data/notes.txt +5 -0
- package/eval/results/workdirs/run-003-word-report__armB/package.json +6 -0
- package/eval/results/workdirs/run-004-phase-stats__armA/bin/phase-stats.mjs +37 -0
- package/eval/results/workdirs/run-004-phase-stats__armA/data/events.log +16 -0
- package/eval/results/workdirs/run-004-phase-stats__armB/DRY_RUN_WRONG_ANSWER.mjs +1 -0
- package/eval/results/workdirs/run-004-phase-stats__armB/data/events.log +16 -0
- package/eval/results/workdirs/run-005-orders-export__armA/bin/export-orders.mjs +57 -0
- package/eval/results/workdirs/run-005-orders-export__armA/data/.gitattributes +1 -0
- package/eval/results/workdirs/run-005-orders-export__armA/data/orders.csv +9 -0
- package/eval/results/workdirs/run-005-orders-export__armA/out/summary.json +1 -0
- package/eval/results/workdirs/run-005-orders-export__armB/bin/export-orders.mjs +57 -0
- package/eval/results/workdirs/run-005-orders-export__armB/data/.gitattributes +1 -0
- package/eval/results/workdirs/run-005-orders-export__armB/data/orders.csv +9 -0
- package/eval/results/workdirs/run-005-orders-export__armB/out/summary.json +1 -0
- package/eval/tasks/004-phase-stats/files/data/events.log +16 -0
- package/eval/tasks/004-phase-stats/solution/bin/phase-stats.mjs +37 -0
- package/eval/tasks/004-phase-stats/task.json +6 -0
- package/eval/tasks/004-phase-stats/verify.mjs +38 -0
- package/eval/tasks/005-orders-export/files/data/.gitattributes +1 -0
- package/eval/tasks/005-orders-export/files/data/orders.csv +9 -0
- package/eval/tasks/005-orders-export/solution/bin/export-orders.mjs +57 -0
- package/eval/tasks/005-orders-export/task.json +6 -0
- package/eval/tasks/005-orders-export/verify.mjs +28 -0
- package/package.json +1 -1
- package/quicklog/tmp-fix-review.mjs +21 -0
- package/src/ceremony-tier.js +2 -1
- package/src/decision-distill.js +29 -3
- package/src/decision-redomain.js +148 -0
- package/src/design-facts.js +3 -3
- package/src/docs-debt.js +3 -3
- package/src/flow-parity.js +10 -2
- package/src/flow.js +1 -1
- package/src/forensics.js +236 -0
- package/src/fr-index.js +2 -1
- package/src/index.js +34 -1
- package/src/knowledge-graph.js +3 -4
- package/src/module-impact.js +2 -5
- package/src/modules.js +31 -0
- package/src/run/gates.js +7 -14
- package/src/run/lint-ownership.js +37 -0
- package/src/run/prompt.js +40 -25
- package/src/run/verify-quality-scan.js +10 -1
- package/src/spec-sync.js +81 -2
- package/src/stage-review.js +2 -1
- package/src/stages/brainstorm.js +71 -20
- package/src/stages/plan-postcheck.js +13 -2
- package/src/stages/plan.js +19 -0
- package/src/test-bindings.js +32 -3
- package/src/ui-visual.js +119 -4
- package/src/verify-postcheck.js +117 -17
- package/src/worktree-apply.js +26 -2
package/src/design-facts.js
CHANGED
|
@@ -26,7 +26,7 @@ import { join, relative } from 'node:path'
|
|
|
26
26
|
import { parseDecisions } from './decision-distill.js'
|
|
27
27
|
import { parseFileChangeListDetailed } from './change-list.js'
|
|
28
28
|
import { parseDesignCoverageByRepo, parseRepoRegistry } from './stages/plan-postcheck.js'
|
|
29
|
-
import { parseModuleMapSimple } from './modules.js'
|
|
29
|
+
import { modulePathMatches, parseModuleMapSimple } from './modules.js'
|
|
30
30
|
|
|
31
31
|
// ---------------------------------------------------------------------------
|
|
32
32
|
// 条目解析(distill parseDecisions 的同源镜像,R-01)
|
|
@@ -173,7 +173,7 @@ export function deriveActualModules(filePaths, prefixPairs) {
|
|
|
173
173
|
const t = String(f || '').replace(/\\/g, '/').replace(/\/+$/, '')
|
|
174
174
|
if (!t) continue
|
|
175
175
|
for (const { id, path: p } of prefixPairs) {
|
|
176
|
-
if (t
|
|
176
|
+
if (modulePathMatches(t, p)) { actual.add(id); break }
|
|
177
177
|
}
|
|
178
178
|
}
|
|
179
179
|
return actual
|
|
@@ -387,7 +387,7 @@ export function generateDesignSkeleton({ changeName, decisionsText, author, now
|
|
|
387
387
|
lines.push('- [ ] frontmatter 字段齐全(author/created_at/scale)')
|
|
388
388
|
lines.push('- [ ] 引用所有当前版本 D-xxx@vN')
|
|
389
389
|
lines.push('- [ ] 涉及生命周期关键词时含「生命周期契约表」或紧邻豁免短语')
|
|
390
|
-
lines.push('- [ ] UI 原型分级核对(涉前端文件时变更目录应有 prototype-*.html
|
|
390
|
+
lines.push('- [ ] UI 原型分级核对(涉前端文件时变更目录应有 prototype-*.html——新生成(定稿=与实现同方言的真码产物,手绘仅限粗选,输出须带素材来源声明:token 源/组件来源/入口位置)或登记复用(复用须按 prototype-<名称>.html 命名,execute 才注入)或跳过原因记入风险登记;本仓形态未在 local.yaml ui_prototype 登记的,收口前补登记)')
|
|
391
391
|
lines.push('- [ ] 不确定的问题标注「⚠️ 自审存疑」')
|
|
392
392
|
lines.push('')
|
|
393
393
|
|
package/src/docs-debt.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
import { existsSync, readFileSync, readdirSync, realpathSync } from 'node:fs'
|
|
14
14
|
import { join, relative } from 'node:path'
|
|
15
15
|
import { safeGit } from './git-helper.js'
|
|
16
|
-
import { parseModuleMapSimple } from './modules.js'
|
|
16
|
+
import { modulePathMatches, parseModuleMapSimple } from './modules.js'
|
|
17
17
|
import { runDocsCheck, readDocsCheckConfig, parseDecisionEntries, anchorFilePath } from './docs-check.js'
|
|
18
18
|
import { decisionChangeQualifier } from './knowledge-match.js'
|
|
19
19
|
|
|
@@ -55,9 +55,9 @@ export function matchFilesToModules(changedFiles, moduleIndex, opts = {}) {
|
|
|
55
55
|
|
|
56
56
|
for (const f of changedFiles) {
|
|
57
57
|
let matched = null
|
|
58
|
-
// 一级:paths/core_files
|
|
58
|
+
// 一级:paths/core_files 前缀或字面量命中(含 glob 形态——modulePathMatches 统一入口)
|
|
59
59
|
for (const [id, ps] of modulePaths) {
|
|
60
|
-
if (ps.some(
|
|
60
|
+
if (ps.some((p) => modulePathMatches(f, p))) {
|
|
61
61
|
matched = id; break
|
|
62
62
|
}
|
|
63
63
|
}
|
package/src/flow-parity.js
CHANGED
|
@@ -19,12 +19,14 @@
|
|
|
19
19
|
* 命中模块变更索引机械回写(sidecar 优先、幂等)、start 注入面欠账事实行(提示前移)。
|
|
20
20
|
*/
|
|
21
21
|
import { existsSync, readFileSync, writeFileSync, readdirSync } from 'node:fs'
|
|
22
|
+
import { modulePathMatches } from './modules.js'
|
|
22
23
|
import { join, relative } from 'node:path'
|
|
23
24
|
import { createHash } from 'node:crypto'
|
|
24
25
|
import { execFileSync } from 'node:child_process'
|
|
25
26
|
import { splitOwnVsForeignDiffFiles, commitAttributionForChange, isForeignByCommit, parseChangeNamesFromSubject } from './foreign-declared.js'
|
|
26
27
|
import { collectModuleMaps, normalizeMapPath } from './module-resolve.js'
|
|
27
28
|
import { computeDocsDebt } from './docs-debt.js'
|
|
29
|
+
import { anchorBackfillCandidate } from './decision-distill.js'
|
|
28
30
|
import { writeAtomicSync } from './fs-atomic.js'
|
|
29
31
|
import { normalizePath } from './change-list.js'
|
|
30
32
|
import { readV2SectionAnswer } from './flow-review.js'
|
|
@@ -72,7 +74,7 @@ export function reconcileModuleDocs({ cwd, specBase, ownFiles, committedRaw }) {
|
|
|
72
74
|
.map((pp) => (map.prefix + normalizeMapPath(pp)).replace(/\/+$/, ''))
|
|
73
75
|
.filter(Boolean)
|
|
74
76
|
if (effs.length === 0) continue
|
|
75
|
-
const hitFiles = deliverables.filter((f) => effs.some((e) => f
|
|
77
|
+
const hitFiles = deliverables.filter((f) => effs.some((e) => modulePathMatches(f, e)))
|
|
76
78
|
if (hitFiles.length === 0) continue
|
|
77
79
|
hits++
|
|
78
80
|
const docRel = entry.doc ? `docs/${map.project}/${String(entry.doc).replace(/^\/+/, '')}` : null
|
|
@@ -93,7 +95,7 @@ export function reconcileModuleDocs({ cwd, specBase, ownFiles, committedRaw }) {
|
|
|
93
95
|
for (const pp of entry.paths || []) allPaths.push((map.prefix + normalizeMapPath(pp)).replace(/\/+$/, ''))
|
|
94
96
|
}
|
|
95
97
|
}
|
|
96
|
-
const isCovered = (f) => allPaths.some((pp) => pp && (f
|
|
98
|
+
const isCovered = (f) => allPaths.some((pp) => pp && modulePathMatches(f, pp))
|
|
97
99
|
const uncoveredMap = new Map()
|
|
98
100
|
for (const f of deliverables) {
|
|
99
101
|
if (isCovered(f)) continue
|
|
@@ -316,6 +318,11 @@ export function harvestSlot4Decision({ changeDir, change }) {
|
|
|
316
318
|
answer = readV2SectionAnswer(dText, risksSec.heading, DESIGN_QUESTIONS)
|
|
317
319
|
}
|
|
318
320
|
if (!answer || /^不适用/.test(answer)) return { harvested: false, reason: '槽4 空/不适用' }
|
|
321
|
+
// 锚点自动回填(2026-10-15-decision-anchor-backfill 收割端):风险段作答文本恰含一个
|
|
322
|
+
// 路径 token → 写入 - 锚点:行(源 decisions.md 是真相源,蒸馏自然带上、人可改);
|
|
323
|
+
// 0/多候选不写(蒸馏端渲染时还会再兜底一次同款判定)。单一源 anchorBackfillCandidate。
|
|
324
|
+
const anchorCand = anchorBackfillCandidate(answer)
|
|
325
|
+
const anchorLine = anchorCand ? [`- 锚点:${anchorCand}`] : []
|
|
319
326
|
const text = [
|
|
320
327
|
'---',
|
|
321
328
|
`author: flow-machine-draft`,
|
|
@@ -328,6 +335,7 @@ export function harvestSlot4Decision({ changeDir, change }) {
|
|
|
328
335
|
`## D-001@v1: 设计期风险与放弃方案(底稿)`,
|
|
329
336
|
`- 类型:process`,
|
|
330
337
|
`- 状态:confirmed`,
|
|
338
|
+
...anchorLine,
|
|
331
339
|
// 类型/状态字段(2026-09-26-slot4-distill-fix):蒸馏链入选要 status∈{confirmed|accepted|rejected}
|
|
332
340
|
// 且 type∈七类白名单,字段解析只认 `- 字段:值` 列表行形态——此前收割条目零字段致永不入选,
|
|
333
341
|
// 「随蒸馏链进 knowledge」断链(thin-agent-tasks 教训留档归档而 knowledge 零落地的实证)。
|
package/src/flow.js
CHANGED
|
@@ -1138,7 +1138,7 @@ export async function cmdFlowStart({ change, input, title: titleFlag = null, thi
|
|
|
1138
1138
|
` ②③断点 agent 自主跳过。`,
|
|
1139
1139
|
` 随时可查进度:sillyspec flow status --change ${change}`,
|
|
1140
1140
|
...(detectUiTouch(input)
|
|
1141
|
-
? [...buildUiGuidanceLines(), '']
|
|
1141
|
+
? [...buildUiGuidanceLines(specBase), '']
|
|
1142
1142
|
: []),
|
|
1143
1143
|
``,
|
|
1144
1144
|
`【协议调用 2/2(干完后)】sillyspec flow done --change ${change}`,
|
package/src/forensics.js
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* forensics.js — 会话取证证据包装配 + 取证任务书渲染(2026-10-15-session-forensics)
|
|
3
|
+
*
|
|
4
|
+
* 定位:`sillyspec diagnose --change <名>` 的只读装配面。对照 obra/superpowers
|
|
5
|
+
* diagnosing-superpowers 的取证形态做 SillySpec 原生版——分工沿本仓哲学:
|
|
6
|
+
* CLI 装配机械事实(证据包六面),agent 做语义判断(按任务书读 transcript、出
|
|
7
|
+
* path:line 证据报告)。全程零写入:证据源全是事后读取的静态文件,单源缺失/
|
|
8
|
+
* 损坏 fail-soft 空清单+注记,不抛错不阻断(取证要容忍现场变动)。
|
|
9
|
+
*
|
|
10
|
+
* 数据源(全部既有,零新格式):
|
|
11
|
+
* - 变更目录 / 归档目录(flow-state.yaml 身份面、review 轮次文件)
|
|
12
|
+
* - <runtimeRoot>/agent-session-log.json(run 命令登记的会话日志路径,
|
|
13
|
+
* 按 change_key 精确相等过滤——登记面唯一键,无子串误命中面)
|
|
14
|
+
* - <runtimeRoot>/friction-ledger.json(readFrictionLedger 既有 API)
|
|
15
|
+
* - <runtimeRoot>/verify-runs/<时间戳目录>/test-result.json(mtime 近序取前 N)
|
|
16
|
+
*
|
|
17
|
+
* 红线:零内容枚举——任务书六维度是结构骨架(CLI 自有任务书面集合,同
|
|
18
|
+
* review_probes scope 判例),不是内容词表;CLI 不解析 transcript 本体
|
|
19
|
+
* (三 harness 格式 × 演化泥潭,解读交给 agent)。
|
|
20
|
+
*/
|
|
21
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'
|
|
22
|
+
import { join } from 'node:path'
|
|
23
|
+
import yaml from 'js-yaml'
|
|
24
|
+
import { readFrictionLedger } from './friction-ledger.js'
|
|
25
|
+
|
|
26
|
+
/** verify-runs 证据默认截断数(近序 N 条,防大仓 runtime 灌爆输出;模块内常量——导出即死码债) */
|
|
27
|
+
const VERIFY_RUNS_LIMIT = 3
|
|
28
|
+
|
|
29
|
+
/** 读 JSON 文件,失败/不存在返回 null(fail-soft 取证立场)。 */
|
|
30
|
+
function readJsonSafe(path) {
|
|
31
|
+
try {
|
|
32
|
+
return JSON.parse(readFileSync(path, 'utf8'))
|
|
33
|
+
} catch {
|
|
34
|
+
return null
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** 读 YAML 文件,失败/不存在返回 null。 */
|
|
39
|
+
function readYamlSafe(path) {
|
|
40
|
+
try {
|
|
41
|
+
return yaml.load(readFileSync(path, 'utf8'))
|
|
42
|
+
} catch {
|
|
43
|
+
return null
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 装配证据包六面。纯读函数,零写入。
|
|
49
|
+
* @param {{ specBase: string, runtimeRoot: string, changeName: string, verifyRunsLimit?: number }} p
|
|
50
|
+
* @returns {{ identity: object, agentLogs: object[], friction: object[], verifyRuns: object[], reviews: object[], warnings: string[] }}
|
|
51
|
+
*/
|
|
52
|
+
export function assembleForensicsPack({ specBase, runtimeRoot, changeName, verifyRunsLimit = VERIFY_RUNS_LIMIT }) {
|
|
53
|
+
const warnings = []
|
|
54
|
+
const changeDir = join(specBase, 'changes', changeName)
|
|
55
|
+
const archivedDir = join(specBase, 'changes', 'archive', changeName)
|
|
56
|
+
|
|
57
|
+
// ① 变更身份(active / archived / missing + flow-state 态)
|
|
58
|
+
const identity = { changeName, status: 'missing', changeDir: null, archivedDir: null, tier: null, substeps: null }
|
|
59
|
+
if (existsSync(changeDir)) {
|
|
60
|
+
identity.status = 'active'
|
|
61
|
+
identity.changeDir = changeDir
|
|
62
|
+
} else if (existsSync(archivedDir)) {
|
|
63
|
+
identity.status = 'archived'
|
|
64
|
+
identity.changeDir = archivedDir
|
|
65
|
+
identity.archivedDir = archivedDir
|
|
66
|
+
warnings.push('变更已归档——证据包从归档目录读取,transcript 仍按登记路径取')
|
|
67
|
+
}
|
|
68
|
+
const flowStatePath = identity.changeDir ? join(identity.changeDir, 'flow-state.yaml') : null
|
|
69
|
+
if (flowStatePath && existsSync(flowStatePath)) {
|
|
70
|
+
const fs_ = readYamlSafe(flowStatePath)
|
|
71
|
+
if (fs_ && typeof fs_ === 'object') {
|
|
72
|
+
identity.tier = fs_.tier ?? null
|
|
73
|
+
identity.substeps = fs_.substeps && typeof fs_.substeps === 'object' ? fs_.substeps : null
|
|
74
|
+
} else {
|
|
75
|
+
warnings.push(`flow-state.yaml 解析失败(${flowStatePath})——身份面降级`)
|
|
76
|
+
}
|
|
77
|
+
} else {
|
|
78
|
+
warnings.push('无 flow 态(archive 件或无 flow-state.yaml)')
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ② agent 会话日志(change_key 精确相等——登记面唯一键)
|
|
82
|
+
const registryPath = join(runtimeRoot, 'agent-session-log.json')
|
|
83
|
+
const registry = readJsonSafe(registryPath)
|
|
84
|
+
let agentLogs = []
|
|
85
|
+
if (registry && Array.isArray(registry.entries)) {
|
|
86
|
+
agentLogs = registry.entries.filter((e) => e && typeof e === 'object' && e.change_key === changeName)
|
|
87
|
+
} else {
|
|
88
|
+
warnings.push(`会话日志登记缺失或不可解析(${registryPath})——本机未跑过 run 命令或登记面损坏`)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ③ 摩擦账本(readFrictionLedger 容错:缺文件/坏 JSON → [])
|
|
92
|
+
const friction = readFrictionLedger(runtimeRoot).filter((e) => e.change === changeName)
|
|
93
|
+
|
|
94
|
+
// ④ verify-runs(mtime 近序,同 mtime 路径字典序破平;截断前 N)
|
|
95
|
+
// fail-soft:runsRoot 为普通文件/权限损坏形态时 readdirSync 抛 ENOTDIR——捕获降级,不阻断取证。
|
|
96
|
+
const runsRoot = join(runtimeRoot, 'verify-runs')
|
|
97
|
+
const verifyRuns = []
|
|
98
|
+
if (existsSync(runsRoot)) {
|
|
99
|
+
let runEntries = []
|
|
100
|
+
try {
|
|
101
|
+
runEntries = readdirSync(runsRoot, { withFileTypes: true })
|
|
102
|
+
} catch {
|
|
103
|
+
warnings.push(`verify-runs 不可读(非目录或损坏形态,${runsRoot})——降级为空`)
|
|
104
|
+
}
|
|
105
|
+
const dirs = runEntries
|
|
106
|
+
.filter((d) => d.isDirectory())
|
|
107
|
+
.map((d) => join(runsRoot, d.name))
|
|
108
|
+
const withTime = dirs
|
|
109
|
+
.map((dir) => {
|
|
110
|
+
try {
|
|
111
|
+
return { dir, mtimeMs: statSync(dir).mtimeMs }
|
|
112
|
+
} catch {
|
|
113
|
+
return null
|
|
114
|
+
}
|
|
115
|
+
})
|
|
116
|
+
.filter(Boolean)
|
|
117
|
+
.sort((a, b) => (b.mtimeMs - a.mtimeMs) || (a.dir < b.dir ? -1 : a.dir > b.dir ? 1 : 0))
|
|
118
|
+
for (const { dir, mtimeMs } of withTime.slice(0, Math.max(1, verifyRunsLimit))) {
|
|
119
|
+
const resultPath = join(dir, 'test-result.json')
|
|
120
|
+
verifyRuns.push({
|
|
121
|
+
dir,
|
|
122
|
+
resultPath: existsSync(resultPath) ? resultPath : null,
|
|
123
|
+
mtimeIso: new Date(mtimeMs).toISOString(),
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
} else {
|
|
127
|
+
warnings.push(`无 verify-runs 目录(${runsRoot})——本变更未跑过收口实测或 runtime 已清理`)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ⑤ 评审轮次(review.json + superseded-*,mtime 近序;目录损坏形态同上降级)
|
|
131
|
+
const reviews = []
|
|
132
|
+
if (identity.changeDir && existsSync(identity.changeDir)) {
|
|
133
|
+
let reviewFiles = []
|
|
134
|
+
try {
|
|
135
|
+
reviewFiles = readdirSync(identity.changeDir).filter((f) => /^review\.json(\.superseded-.+)?$/.test(f))
|
|
136
|
+
} catch {
|
|
137
|
+
warnings.push(`变更目录不可读(${identity.changeDir})——评审轮次面降级为空`)
|
|
138
|
+
}
|
|
139
|
+
for (const f of reviewFiles) {
|
|
140
|
+
const p = join(identity.changeDir, f)
|
|
141
|
+
let mtimeIso = null
|
|
142
|
+
try {
|
|
143
|
+
mtimeIso = new Date(statSync(p).mtimeMs).toISOString()
|
|
144
|
+
} catch { /* 文件刚被删——列名不留 mtime */ }
|
|
145
|
+
reviews.push({ file: p, superseded: f !== 'review.json', mtimeIso })
|
|
146
|
+
}
|
|
147
|
+
reviews.sort((a, b) => (b.mtimeIso || '').localeCompare(a.mtimeIso || ''))
|
|
148
|
+
} else {
|
|
149
|
+
warnings.push('变更目录不存在——评审轮次面为空')
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return { identity, agentLogs, friction, verifyRuns, reviews, warnings }
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* 渲染取证任务书(markdown)。六维度是结构骨架不是封闭清单;四纪律钉死
|
|
157
|
+
* diagnosing-superpowers 的核心铁律(path:line 锚、唯一可写产物、先读本体、时间盒)。
|
|
158
|
+
* @param {{ identity: object, agentLogs: object[], friction: object[], verifyRuns: object[], reviews: object[], warnings: string[] }} pack
|
|
159
|
+
* @returns {string}
|
|
160
|
+
*/
|
|
161
|
+
export function renderForensicsTaskbook(pack) {
|
|
162
|
+
const { identity, agentLogs, friction, verifyRuns, reviews, warnings } = pack
|
|
163
|
+
const lines = []
|
|
164
|
+
lines.push(`# 会话取证任务书 — ${identity.changeName}`)
|
|
165
|
+
lines.push('')
|
|
166
|
+
lines.push(`【角色】你是取证分析员(干净上下文)——还原这个会话里实际发生了什么,拿证据说话,不替流程辩护也不替人辩护。报告读者是「要决定下一步怎么办的人」。`)
|
|
167
|
+
lines.push('')
|
|
168
|
+
lines.push('## 证据包(CLI 装配的机械事实——先读这些文件本体,再下结论)')
|
|
169
|
+
lines.push('')
|
|
170
|
+
lines.push(`- 变更身份:status=${identity.status}|tier=${identity.tier ?? '无'}|目录=${identity.changeDir ?? '(不存在)'}`)
|
|
171
|
+
lines.push(`- agent 会话日志(${agentLogs.length} 条,change_key=${identity.changeName} 精确过滤):`)
|
|
172
|
+
for (const e of agentLogs) {
|
|
173
|
+
lines.push(` - [${e.harness}/${e.format ?? '?'}] ${e.log_path}(session=${e.session_id ?? '?'}${e.exists === false ? ',⚠️ 文件不在场' : ''})`)
|
|
174
|
+
}
|
|
175
|
+
if (agentLogs.length === 0) lines.push(' - (零命中——本机未登记过本变更的会话日志)')
|
|
176
|
+
lines.push(`- 摩擦账本(${friction.length} 条):${friction.map((e) => JSON.stringify(e.counts ?? {})).join(' / ') || '(无)'}`)
|
|
177
|
+
lines.push(`- verify-runs(近 ${verifyRuns.length} 次):`)
|
|
178
|
+
for (const r of verifyRuns) lines.push(` - ${r.resultPath ?? r.dir + '(无 test-result.json)'} @ ${r.mtimeIso}`)
|
|
179
|
+
lines.push(`- 评审轮次(${reviews.length} 份,含 superseded):`)
|
|
180
|
+
for (const r of reviews) lines.push(` - ${r.file}${r.superseded ? '(superseded——前轮评审结论,对比两轮可还原修复轨迹)' : ''}`)
|
|
181
|
+
if (warnings.length) {
|
|
182
|
+
lines.push(`- 降级注记:${warnings.join(';')}`)
|
|
183
|
+
}
|
|
184
|
+
lines.push('')
|
|
185
|
+
lines.push('## 分析维度(结构骨架——常见失事面,不是封闭清单:证据指向维度外时照报,报告允许增设章节)')
|
|
186
|
+
lines.push('')
|
|
187
|
+
lines.push('1. **时间线重建**:这个会话按时间实际做了什么(步骤推进/等待/回滚/中断点)。')
|
|
188
|
+
lines.push('2. **计划遵从**:做的是计划内的事吗?偏离处有没有留痕(决策/账本/提交消息)?')
|
|
189
|
+
lines.push('3. **重复工作**:有没有同一件事做两遍(重派/重审/重跑)?根因是什么?')
|
|
190
|
+
lines.push('4. **停等与重试**:哪里停等了用户/子代理?重试了几次?是否有可避免的空转?')
|
|
191
|
+
lines.push('5. **成本与轮次**:轮次/token 花在哪了(读文件长尾?全量重验?)?')
|
|
192
|
+
lines.push('6. **门禁摩擦**:gate 回滚/评审打回/实测失败的完整链条与最终清偿方式。')
|
|
193
|
+
lines.push('')
|
|
194
|
+
lines.push('## 取证纪律')
|
|
195
|
+
lines.push('')
|
|
196
|
+
lines.push('1. **每条 finding 必须带 `path:line` 锚,无锚不成立**——锚指向证据包列出的文件或其内容(行号取自你实际读到的文件)。')
|
|
197
|
+
lines.push('2. **唯一可写产物是 `diagnosis-report.md`**(写在变更目录)——禁止修改任何其他文件、禁止 git 操作。固定章节:`## 问题陈述` / `## 时间线` / `## Findings`(分维度)/ `## 无法证实面`(证据不足的疑点如实列,不脑补)。')
|
|
198
|
+
lines.push('3. **先读证据包列出的文件本体再下结论,禁止全量扫读仓**——矛盾经常藏在交叉处,定向读那一处。')
|
|
199
|
+
lines.push('4. **时间盒收敛**:结论存疑时定向补证;连续扩面仍零结论 = 基于已读材料收敛,把疑点写进「无法证实面」。')
|
|
200
|
+
lines.push('')
|
|
201
|
+
lines.push('【无子代理通道时】主会话按本任务书自行分析即可,如实标注执行方式;有子代理通道时按维度分派并行分析、汇总时逐条核对锚点。')
|
|
202
|
+
lines.push('')
|
|
203
|
+
return lines.join('\n')
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* 命令入口:sillyspec diagnose --change <名> [--detect] [--json]
|
|
208
|
+
* 只读零写入;--detect 先透传 agent-session-log 现场探测(不落盘语义随既有 cmdAgentLog)。
|
|
209
|
+
* @param {string[]} args - filteredArgs.slice(1)('diagnose' 之后)
|
|
210
|
+
* @param {{ json?: boolean, cwd?: string, specDir?: string|null }} opts
|
|
211
|
+
*/
|
|
212
|
+
export async function cmdForensics(args, { json = false, cwd = process.cwd(), specDir = null } = {}) {
|
|
213
|
+
const changeIdx = args.indexOf('--change')
|
|
214
|
+
const changeName = changeIdx >= 0 && args[changeIdx + 1] && !String(args[changeIdx + 1]).startsWith('--') ? args[changeIdx + 1] : null
|
|
215
|
+
if (!changeName) {
|
|
216
|
+
console.error('用法: sillyspec diagnose --change <名> [--detect] [--json] [--spec-dir <路径>]')
|
|
217
|
+
console.error(' 只读装配会话取证证据包 + 取证任务书(path:line 铁律;唯一可写产物是变更目录的 diagnosis-report.md,由执行任务书的 agent 产出)')
|
|
218
|
+
process.exit(2)
|
|
219
|
+
}
|
|
220
|
+
const specBase = specDir || join(cwd, '.sillyspec')
|
|
221
|
+
const runtimeRoot = join(specBase, '.runtime')
|
|
222
|
+
|
|
223
|
+
if (args.includes('--detect')) {
|
|
224
|
+
const { cmdAgentLog } = await import('./agent-session-log.js')
|
|
225
|
+
await cmdAgentLog(['--detect'], { json: false, cwd, specDir })
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const pack = assembleForensicsPack({ specBase, runtimeRoot, changeName })
|
|
229
|
+
const taskbook = renderForensicsTaskbook(pack)
|
|
230
|
+
|
|
231
|
+
if (json || args.includes('--json')) {
|
|
232
|
+
console.log(JSON.stringify({ ...pack, taskbook }))
|
|
233
|
+
return
|
|
234
|
+
}
|
|
235
|
+
console.log(taskbook.trimEnd())
|
|
236
|
+
}
|
package/src/fr-index.js
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
import { readChangeTrace, upsertFrBindings, applySupersededToEntryLines, testAnchorFile, resolveTestFileRel } from './test-bindings.js'
|
|
21
|
+
import { modulePathMatches } from './modules.js'
|
|
21
22
|
import { suggestDomainFromFiles, snapDomainToDictionary } from './knowledge-digest.js'
|
|
22
23
|
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'fs';
|
|
23
24
|
import { basename, dirname, join } from 'path';
|
|
@@ -250,7 +251,7 @@ export function matchedModuleIds(moduleIndex, filePath) {
|
|
|
250
251
|
const hits = [];
|
|
251
252
|
for (const [modId, mod] of Object.entries(moduleIndex)) {
|
|
252
253
|
const paths = (mod && Array.isArray(mod.paths)) ? mod.paths : [];
|
|
253
|
-
if (paths.some((pp) => filePath
|
|
254
|
+
if (paths.some((pp) => modulePathMatches(filePath, pp))) {
|
|
254
255
|
hits.push(modId);
|
|
255
256
|
}
|
|
256
257
|
}
|
package/src/index.js
CHANGED
|
@@ -100,6 +100,8 @@ SillySpec CLI — 规范驱动开发工具包
|
|
|
100
100
|
变更合成时间线(事件流 × tasks.md × git 提交锚三源真实留痕渲染;活跃/归档变更均可)
|
|
101
101
|
sillyspec dispatch <probe | hint> SillyHub 派发能力探测 + 策略生成(agent 调用桥,仅渲染不执行 tool)
|
|
102
102
|
sillyspec agent-log [--detect] [--json] 本地 agent 会话日志查询/现场探测(run 命令自动上报平台 POST /api/agent-logs 并本地留底)
|
|
103
|
+
sillyspec diagnose --change <名> [--detect] [--json]
|
|
104
|
+
会话取证:只读装配证据包(flow 态/会话日志/摩擦账本/verify-runs/评审轮次)+ 取证任务书(path:line 铁律;报告由执行任务书的 agent 产出)
|
|
103
105
|
|
|
104
106
|
sillyspec workflow check <name> [--project <p>] [--change <c>] [--json] [--save]
|
|
105
107
|
全局 workflow 校验(--save 归档到 .sillyspec/.runtime/workflow-runs/)
|
|
@@ -3390,11 +3392,34 @@ ${generated.length} 个骨架已就绪——逐节把 <!--TODO--> 替换为语
|
|
|
3390
3392
|
// --by-change(坑 fr-domain-suggest-typo-and-no-split-migration 缺陷2):unmapped 等
|
|
3391
3393
|
// 混合池按「变更:<名>」字段分批迁移——整域迁移会错置大部分条目,来源变更才是分批键。
|
|
3392
3394
|
const byChangeD = flag('--by-change')
|
|
3393
|
-
if (!fromD || !toD) { fail('用法: sillyspec tests --redomain --from <源域> --to <目标域> [--anchor <FR-id>] [--by-change <变更名>] [--write]\n
|
|
3395
|
+
if (!fromD || !toD) { fail('用法: sillyspec tests --redomain --from <源域> --to <目标域> [--anchor <FR-id>] [--by-change <变更名>] [--decisions] [--write]\n --decisions 切 decisions/ 面(默认 fr/ 面);域名可带前缀与 .md 后缀(fr/auto-backend ≍ auto-backend.md ≍ auto-backend)'); break }
|
|
3394
3396
|
// 多 --anchor 收集
|
|
3395
3397
|
const anchorList = []
|
|
3396
3398
|
for (let i = 0; i < filteredArgs.length - 1; i++) if (filteredArgs[i] === '--anchor') anchorList.push(filteredArgs[i + 1])
|
|
3399
|
+
// --decisions(2026-10-15-decision-redomain):切 decisions/ 面的孪生迁移实现——
|
|
3400
|
+
// 同 flag 族(--from/--to/--anchor/--by-change/--write),段字节整体搬运、迁域不换号、
|
|
3401
|
+
// 跨变更同号不冲突(双键防);平台仓 unmapped 442 条存量清偿的机器通道。
|
|
3402
|
+
const useDecisions = has('--decisions')
|
|
3397
3403
|
try {
|
|
3404
|
+
if (useDecisions) {
|
|
3405
|
+
const { planDecisionRedomain, redomainDecisionEntries } = await import('./decision-redomain.js')
|
|
3406
|
+
if (!has('--write')) {
|
|
3407
|
+
const plan = planDecisionRedomain({ knowledgeRoot, from: fromD, to: toD, anchors: anchorList.length ? anchorList : null, byChange: byChangeD || null })
|
|
3408
|
+
console.log(`🔧 决策域迁移预览(--write 落盘):decisions/${fromD}.md → decisions/${toD}.md,${plan.entries.length} 条${byChangeD ? `(仅「变更:${byChangeD}」)` : ''}`)
|
|
3409
|
+
for (const e of plan.entries.slice(0, 10)) console.log(` ${e.id} ${e.title}`)
|
|
3410
|
+
if (plan.entries.length > 10) console.log(` … 共 ${plan.entries.length} 条`)
|
|
3411
|
+
console.log(` 目标域文件${plan.targetExists ? '在场' : '将新建'};源域${plan.sourceWillDelete ? '将删空壳' : '保留余条'};ID 不变(身份保持)`)
|
|
3412
|
+
break
|
|
3413
|
+
}
|
|
3414
|
+
const r = redomainDecisionEntries({ knowledgeRoot, from: fromD, to: toD, anchors: anchorList.length ? anchorList : null, byChange: byChangeD || null })
|
|
3415
|
+
console.log(`✅ 决策域迁移完成:${r.moved.length} 条 decisions/${fromD}.md → decisions/${toD}.md(ID 不变${byChangeD ? `,仅「变更:${byChangeD}」` : ''})`)
|
|
3416
|
+
for (const m of r.moved.slice(0, 10)) console.log(` ${m.id} ${m.title}`)
|
|
3417
|
+
if (r.skippedConflicts.length > 0) console.log(` ⚠️ 目标域同 id 同变更段冲突跳过 ${r.skippedConflicts.length} 条(${r.skippedConflicts.slice(0, 5).join('、')})`)
|
|
3418
|
+
if (r.sourceDeleted) console.log(` 源域空壳已删:decisions/${fromD}.md`)
|
|
3419
|
+
if (r.targetCreated) console.log(` 目标域文件新建:decisions/${toD}.md`)
|
|
3420
|
+
if (r.indexLineAdded) console.log(` INDEX 路由行已补:${toD}`)
|
|
3421
|
+
break
|
|
3422
|
+
}
|
|
3398
3423
|
if (!has('--write')) {
|
|
3399
3424
|
// 预览与落盘同口径透传 byChange(坑 redomain-plan-preview-by-change-ignored,
|
|
3400
3425
|
// 2026-10-02 实证:76338b4d 收编在途工作时此行漏透传——预览列整域、落盘只迁
|
|
@@ -4107,6 +4132,14 @@ SillySpec worktree — git worktree 隔离管理
|
|
|
4107
4132
|
await cmdAgentLog(filteredArgs.slice(1), { json, cwd: dir, specDir });
|
|
4108
4133
|
break;
|
|
4109
4134
|
}
|
|
4135
|
+
case 'diagnose': {
|
|
4136
|
+
// 会话取证(2026-10-15-session-forensics):只读装配证据包六面(flow 态/会话日志/摩擦账本/
|
|
4137
|
+
// verify-runs/评审轮次/任务书)+ 取证任务书(path:line 铁律,唯一可写产物 diagnosis-report.md
|
|
4138
|
+
// 由执行任务书的 agent 产出)。零状态写入,不进 run 状态机——诊断是随时可调用的查询动作。
|
|
4139
|
+
const { cmdForensics } = await import('./forensics.js');
|
|
4140
|
+
await cmdForensics(filteredArgs.slice(1), { json, cwd: dir, specDir });
|
|
4141
|
+
break;
|
|
4142
|
+
}
|
|
4110
4143
|
case 'patch-reanchor': {
|
|
4111
4144
|
// 收口留痕锚点回写(2026-10-12-close-trace-freeze-integrity FR-03):worktree 内收口的
|
|
4112
4145
|
// change-patch.json 记 worktree 本地 SHA,apply 重放回主仓后悬空——按提交主题归属推导
|
package/src/knowledge-graph.js
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
import { existsSync, readFileSync, readdirSync } from 'fs'
|
|
22
22
|
import { join } from 'path'
|
|
23
23
|
import { parseKnowledgeIndex, parseDecisionEntries, anchorFilePaths } from './knowledge-match.js'
|
|
24
|
-
import { parseModuleMapSimple } from './modules.js'
|
|
24
|
+
import { modulePathMatches, parseModuleMapSimple } from './modules.js'
|
|
25
25
|
|
|
26
26
|
// ── 本体常量(design.md 总体方案 §1 的代码化) ──
|
|
27
27
|
|
|
@@ -273,9 +273,8 @@ export function buildKnowledgeGraph(specRoot) {
|
|
|
273
273
|
const moduleOf = (p) => {
|
|
274
274
|
const v = posix(p)
|
|
275
275
|
for (const [mid, mp] of modulePathIndex) {
|
|
276
|
-
// 模块 paths
|
|
277
|
-
|
|
278
|
-
if (v === stem || v.startsWith(stem + '/')) return mid
|
|
276
|
+
// 模块 paths 目录/glob 语义——modulePathMatches 统一入口(**/*.tsx 等扫描卡形态可命中)
|
|
277
|
+
if (modulePathMatches(v, mp)) return mid
|
|
279
278
|
}
|
|
280
279
|
return null
|
|
281
280
|
}
|
package/src/module-impact.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* 归属分类同一套 module-map 前缀匹配,仅输入来源不同。
|
|
15
15
|
*/
|
|
16
16
|
import { existsSync, readFileSync, readdirSync, writeFileSync } from 'fs'
|
|
17
|
+
import { modulePathMatches } from './modules.js'
|
|
17
18
|
import { join } from 'path'
|
|
18
19
|
import { resolveVerifyChangedFiles } from './verify-postcheck.js'
|
|
19
20
|
import { resolveSpecDir } from './run/shared.js'
|
|
@@ -87,11 +88,7 @@ function classifyFile(posixPath, modulePaths) {
|
|
|
87
88
|
for (const [moduleId, paths] of modulePaths) {
|
|
88
89
|
for (const p of paths) {
|
|
89
90
|
// 目录条目以 / 结尾 → 前缀匹配;文件条目 → 精确或子路径(与模块上下文索引读侧口径一致)
|
|
90
|
-
if (p
|
|
91
|
-
if (posixPath.startsWith(p)) return moduleId
|
|
92
|
-
} else if (posixPath === p || posixPath.startsWith(p + '/')) {
|
|
93
|
-
return moduleId
|
|
94
|
-
}
|
|
91
|
+
if (modulePathMatches(posixPath, p)) return moduleId
|
|
95
92
|
}
|
|
96
93
|
}
|
|
97
94
|
return null
|
package/src/modules.js
CHANGED
|
@@ -2,6 +2,37 @@ import { existsSync, readdirSync, readFileSync, writeFileSync } from 'fs';
|
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { gitQuiet } from './git-helper.js';
|
|
4
4
|
import { nowWallClock } from './datetime.js';
|
|
5
|
+
import { globMatch } from './change-list.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 模块 paths 条目 × 文件路径 的统一归属判定(2026-10-15-module-path-glob-match)。
|
|
9
|
+
*
|
|
10
|
+
* 背景:sillyspec-scan 生成的 module-map(消费仓)大量使用 glob 形态 paths——
|
|
11
|
+
* `app/modules/daemon/**`、`frontend/src/components/daemon/*.tsx`、多段通配与文件通配的
|
|
12
|
+
* 组合(`**` 与 `*.tsx` 同串)、括号路径等(平台仓实测 146 条)——而历史各消费点只做朴素
|
|
13
|
+
* `file === p || startsWith(p + '/')`,glob 条目恒不命中(恰好只有精确文件路径能命中),
|
|
14
|
+
* 导致消费仓全链路域路由失效:交付文件匹配不上模块 → 三级兜底全灭 → FR 铸伪域
|
|
15
|
+
* (auto-backend 等)、决策落 unmapped。
|
|
16
|
+
*
|
|
17
|
+
* 语义(本仓无 glob map 时与旧各点逐字等价——零回归):
|
|
18
|
+
* - 尾部 `/` 的目录条目:startsWith(旧 module-impact 目录分支语义)
|
|
19
|
+
* - 其余条目:相等 或 `p/` 前缀(旧 fr-index/ceremony-tier 等语义)
|
|
20
|
+
* - 含 `*`/`?` 的 glob 条目:追加 globMatch(双星跨零或多段、单星段内、单问号单字符;
|
|
21
|
+
* change-list.js 既有完备实现单一源复用)
|
|
22
|
+
* @param {string} filePath 仓根相对文件路径(含反斜杠容忍)
|
|
23
|
+
* @param {string} rawPattern module-map 的 paths/core_files 条目
|
|
24
|
+
* @returns {boolean}
|
|
25
|
+
*/
|
|
26
|
+
export function modulePathMatches(filePath, rawPattern) {
|
|
27
|
+
const f = String(filePath || '').replace(/\\/g, '/');
|
|
28
|
+
const raw = String(rawPattern || '').replace(/\\/g, '/');
|
|
29
|
+
if (!f || !raw) return false;
|
|
30
|
+
if (raw.endsWith('/')) return f.startsWith(raw);
|
|
31
|
+
const p = raw.replace(/\/+$/, '');
|
|
32
|
+
if (!p) return false;
|
|
33
|
+
if (f === p || f.startsWith(p + '/')) return true;
|
|
34
|
+
return globMatch(f, p);
|
|
35
|
+
}
|
|
5
36
|
|
|
6
37
|
|
|
7
38
|
/**
|
package/src/run/gates.js
CHANGED
|
@@ -224,7 +224,7 @@ export function warnMissingUiPrototype(stageName, changeName, currentStepName, s
|
|
|
224
224
|
const design = readFileSync(designPath, 'utf8')
|
|
225
225
|
const feExts = [...new Set((design.match(/\.(vue|svelte|astro|tsx|jsx|css|scss|sass|less|styl|html)\b/gi) || []).map(s => s.replace('.', '').toLowerCase()))]
|
|
226
226
|
if (feExts.length === 0) return
|
|
227
|
-
console.warn(` ⚠️ design.md 命中前端文件类型(${feExts.join('/')})但变更目录无 prototype-*.html——轻微样式改动可忽略;布局/交互流程级变化应补原型(sillyspec run brainstorm --reopen 回到「分段展示设计」,或直接让 agent 补生成;已有可复跑 HTML 原型则登记复用:复制进变更目录并命名 prototype-<名称>.html,execute
|
|
227
|
+
console.warn(` ⚠️ design.md 命中前端文件类型(${feExts.join('/')})但变更目录无 prototype-*.html——轻微样式改动可忽略;布局/交互流程级变化应补原型(sillyspec run brainstorm --reopen 回到「分段展示设计」,或直接让 agent 补生成;已有可复跑 HTML 原型则登记复用:复制进变更目录并命名 prototype-<名称>.html,execute 按此命名注入;本仓形态未在 local.yaml ui_prototype 登记的,收口前补登记——form: real-code 或 single-html+豁免 ruling)`)
|
|
228
228
|
}
|
|
229
229
|
|
|
230
230
|
/**
|
|
@@ -1397,18 +1397,11 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
1397
1397
|
// 快照含基线债文件时恒败,agent 被逼范围外清偿或逃生口)。失败输出提及文件 × 本变更
|
|
1398
1398
|
// 文件集零交集 → 存量债 advisory 降档放行(债非本变更引入,硬拦只是制造范围外功);
|
|
1399
1399
|
// 有交集 / 输出无可识别路径(unattributable,纯配置错误类)→ 维持硬拦(保守)。──
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
lintOwnership = triageLintOwnership({ failureFiles: lintCheck.failureFiles || [], changeFiles: lintScopeFiles })
|
|
1406
|
-
} catch { /* 鉴定链异常维持硬拦(保守) */ }
|
|
1407
|
-
if (lintOwnership && lintOwnership.verdict === 'pre-existing') {
|
|
1408
|
-
console.warn('\n⚠️ Verify lint 实测失败,但归属鉴定为 HEAD 存量债(失败提及文件与本变更文件集零交集)——降档 advisory 不阻断 verify 完成:')
|
|
1409
|
-
console.warn(` 存量债文件:${(lintCheck.failureFiles || []).join('、') || '(见上方输出)'}`)
|
|
1410
|
-
console.warn(' 处置建议:单独 quick 机械清偿(顺手清债解锁全仓 lint 门),或本变更确认无关时设 SILLYSPEC_VERIFY_LINT_GATE=advisory(审计留痕)。债随基线存在,与本变更交付无关。')
|
|
1411
|
-
} else {
|
|
1400
|
+
// 2026-10-15-lint-triage-path-unify:判定与文案收敛到 ./lint-ownership.js(与 noAI 质量
|
|
1401
|
+
// 扫描路径同源——原两路径分叉见消费仓坑档案 verify-lint-triage-path-divergence.md)。
|
|
1402
|
+
const { lintOwnershipDowngrade } = await import('./lint-ownership.js')
|
|
1403
|
+
const lintAdvisoryDowngraded = await lintOwnershipDowngrade({ lintCheck, cwd, changeName, specBase })
|
|
1404
|
+
if (!lintAdvisoryDowngraded) {
|
|
1412
1405
|
console.error('\n❌ verify 阶段被阻断:CLI 亲自实测 lint 失败(agent 的 lint 自报告与实测不符时以实测为准)。')
|
|
1413
1406
|
if (usedSnapInfo) {
|
|
1414
1407
|
try { const { printSnapshotFailureHint } = await import('./gate-snapshot.js')
|
|
@@ -1432,7 +1425,7 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
1432
1425
|
}
|
|
1433
1426
|
console.error(' 修复后重跑 --done(进度不丢);确认要跳过实测请设 SILLYSPEC_VERIFY_LINT_GATE=advisory(审计留痕)。')
|
|
1434
1427
|
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts, { type: 'gate_rollback', detail: 'verify-lint' })
|
|
1435
|
-
} //
|
|
1428
|
+
} // 硬拦分支结束(降档放行时直接落到后续 parity/删除探针——rollback 语义不变)
|
|
1436
1429
|
}
|
|
1437
1430
|
// 契约 parity 对账:扫前端 API 调用 vs execute 提取的 provider endpoint artifact。
|
|
1438
1431
|
// 接线自 contract-matrix pipeline(verifyApiParity 的 CLI 入口)。
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lint 归属鉴定降档(共享 helper)——2026-10-15-lint-triage-path-unify。
|
|
3
|
+
*
|
|
4
|
+
* 背景(消费仓坑档案 docs/sillyspec/verify-lint-triage-path-divergence.md,2026-10-15
|
|
5
|
+
* -dsh-engine-acp verify 实证):降档判定原先只实现在 gates.js 的 verify --done 拦截面;
|
|
6
|
+
* verify step6「noAI 质量扫描」(verify-quality-scan.js)只做 shouldBlockVerifyLint 二值
|
|
7
|
+
* 判定直接 throw——同一失败两路径行为分叉(step6 throw / --done 降档)。本模块把判定与
|
|
8
|
+
* 提示文案收敛为单一来源,两路径同接线。
|
|
9
|
+
*
|
|
10
|
+
* 语义(R4-S-F 实证口径,与 gates.js 原内联实现等价):
|
|
11
|
+
* - pre-existing(失败提及文件 × 本变更文件集零交集)→ 打印降档 warning,返回 true(放行);
|
|
12
|
+
* - owned(有交集)/ unattributable(输出无可识别路径)/ 鉴定链异常 → 返回 false
|
|
13
|
+
* (维持硬拦,保守——无法排除本变更责任时宁可拦不可漏)。
|
|
14
|
+
*
|
|
15
|
+
* @param {{lintCheck?: {failureFiles?: string[]}, cwd: string, changeName: string, specBase: string, importer?: () => Promise<any>}} input
|
|
16
|
+
* importer 可注入(测试用);缺省动态加载 ../verify-postcheck.js(triageLintOwnership +
|
|
17
|
+
* resolveVerifyChangedFiles——运行时 import 避免模块加载环)。
|
|
18
|
+
* @returns {Promise<boolean>} true=降档放行(advisory),false=维持硬拦。
|
|
19
|
+
*/
|
|
20
|
+
export async function lintOwnershipDowngrade({ lintCheck, cwd, changeName, specBase, importer }) {
|
|
21
|
+
const load = importer || (async () => import('../verify-postcheck.js'))
|
|
22
|
+
let lintOwnership = null
|
|
23
|
+
try {
|
|
24
|
+
const { triageLintOwnership, resolveVerifyChangedFiles } = await load()
|
|
25
|
+
const lintScopeFiles = (resolveVerifyChangedFiles(cwd, changeName, null, { includeWorkingTree: true, specBase }) || [])
|
|
26
|
+
.map((f) => String(f).replace(/\\/g, '/')).filter((f) => !f.startsWith('.sillyspec/'))
|
|
27
|
+
lintOwnership = triageLintOwnership({ failureFiles: (lintCheck && lintCheck.failureFiles) || [], changeFiles: lintScopeFiles })
|
|
28
|
+
} catch { /* 鉴定链异常维持硬拦(保守) */ }
|
|
29
|
+
if (lintOwnership && lintOwnership.verdict === 'pre-existing') {
|
|
30
|
+
const failureFiles = (lintCheck && lintCheck.failureFiles) || []
|
|
31
|
+
console.warn('\n⚠️ Verify lint 实测失败,但归属鉴定为 HEAD 存量债(失败提及文件与本变更文件集零交集)——降档 advisory 不阻断 verify 推进:')
|
|
32
|
+
console.warn(` 存量债文件:${failureFiles.join('、') || '(见上方输出)'}`)
|
|
33
|
+
console.warn(' 处置建议:单独清偿(顺手清债解锁全仓 lint 门),或本变更确认无关时设 SILLYSPEC_VERIFY_LINT_GATE=advisory(审计留痕)。债随基线存在,与本变更交付无关。')
|
|
34
|
+
return true
|
|
35
|
+
}
|
|
36
|
+
return false
|
|
37
|
+
}
|