sillyspec 3.27.5 → 3.27.6
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/.worktrees/8d2d0392/.claude/skills/sillyspec-archive/SKILL.md +87 -0
- package/.worktrees/8d2d0392/.claude/skills/sillyspec-auto/SKILL.md +93 -0
- package/.worktrees/8d2d0392/.claude/skills/sillyspec-brainstorm/SKILL.md +114 -0
- package/.worktrees/8d2d0392/.claude/skills/sillyspec-commit/SKILL.md +67 -0
- package/.worktrees/8d2d0392/.claude/skills/sillyspec-continue/SKILL.md +47 -0
- package/.worktrees/8d2d0392/.claude/skills/sillyspec-doctor/SKILL.md +76 -0
- package/.worktrees/8d2d0392/.claude/skills/sillyspec-execute/SKILL.md +214 -0
- package/.worktrees/8d2d0392/.claude/skills/sillyspec-explore/SKILL.md +102 -0
- package/.worktrees/8d2d0392/.claude/skills/sillyspec-export/SKILL.md +57 -0
- package/.worktrees/8d2d0392/.claude/skills/sillyspec-init/SKILL.md +177 -0
- package/.worktrees/8d2d0392/.claude/skills/sillyspec-knowledge/SKILL.md +270 -0
- package/.worktrees/8d2d0392/.claude/skills/sillyspec-plan/SKILL.md +156 -0
- package/.worktrees/8d2d0392/.claude/skills/sillyspec-propose/SKILL.md +87 -0
- package/.worktrees/8d2d0392/.claude/skills/sillyspec-quick/SKILL.md +116 -0
- package/.worktrees/8d2d0392/.claude/skills/sillyspec-resume/SKILL.md +70 -0
- package/.worktrees/8d2d0392/.claude/skills/sillyspec-scan/SKILL.md +79 -0
- package/.worktrees/8d2d0392/.claude/skills/sillyspec-state/SKILL.md +64 -0
- package/.worktrees/8d2d0392/.claude/skills/sillyspec-status/SKILL.md +54 -0
- package/.worktrees/8d2d0392/.claude/skills/sillyspec-verify/SKILL.md +87 -0
- package/.worktrees/8d2d0392/.claude/skills/sillyspec-workspace/SKILL.md +175 -0
- package/.worktrees/8d2d0392/.claude/skills/verify-per-user/SKILL.md +6 -0
- package/.worktrees/8d2d0392/.gitattributes +2 -0
- package/.worktrees/8d2d0392/.husky/pre-push +20 -0
- package/.worktrees/8d2d0392/README.md +203 -0
- package/.worktrees/8d2d0392/SKILL.md +91 -0
- package/.worktrees/8d2d0392/architecture-analysis.md +448 -0
- package/.worktrees/8d2d0392/bin/sillyspec.js +2 -0
- package/.worktrees/8d2d0392/eval/README.md +86 -0
- package/.worktrees/8d2d0392/eval/eval.config.json +28 -0
- package/.worktrees/8d2d0392/eval/lib/adapters/cli.mjs +63 -0
- package/.worktrees/8d2d0392/eval/lib/adapters/dryrun.mjs +27 -0
- package/.worktrees/8d2d0392/eval/lib/config.mjs +82 -0
- package/.worktrees/8d2d0392/eval/lib/db.mjs +76 -0
- package/.worktrees/8d2d0392/eval/lib/report.mjs +120 -0
- package/.worktrees/8d2d0392/eval/lib/runner.mjs +157 -0
- package/.worktrees/8d2d0392/eval/lib/tasks.mjs +73 -0
- package/.worktrees/8d2d0392/eval/lib/util.mjs +107 -0
- package/.worktrees/8d2d0392/eval/lib/workdir.mjs +30 -0
- package/.worktrees/8d2d0392/eval/run.mjs +83 -0
- package/.worktrees/8d2d0392/eval/tasks/001-slugify/files/package.json +6 -0
- package/.worktrees/8d2d0392/eval/tasks/001-slugify/solution/src/slugify.mjs +26 -0
- package/.worktrees/8d2d0392/eval/tasks/001-slugify/task.json +6 -0
- package/.worktrees/8d2d0392/eval/tasks/001-slugify/verify.mjs +28 -0
- package/.worktrees/8d2d0392/eval/tasks/002-each-day/files/package.json +6 -0
- package/.worktrees/8d2d0392/eval/tasks/002-each-day/files/src/dates.mjs +4 -0
- package/.worktrees/8d2d0392/eval/tasks/002-each-day/solution/src/dates.mjs +30 -0
- package/.worktrees/8d2d0392/eval/tasks/002-each-day/task.json +6 -0
- package/.worktrees/8d2d0392/eval/tasks/002-each-day/verify.mjs +31 -0
- package/.worktrees/8d2d0392/eval/tasks/003-word-report/files/data/notes.txt +5 -0
- package/.worktrees/8d2d0392/eval/tasks/003-word-report/files/package.json +6 -0
- package/.worktrees/8d2d0392/eval/tasks/003-word-report/solution/bin/report.mjs +36 -0
- package/.worktrees/8d2d0392/eval/tasks/003-word-report/task.json +6 -0
- package/.worktrees/8d2d0392/eval/tasks/003-word-report/verify.mjs +38 -0
- package/.worktrees/8d2d0392/logo.jpg +0 -0
- package/.worktrees/8d2d0392/package-lock.json +977 -0
- package/.worktrees/8d2d0392/package.json +41 -0
- package/.worktrees/8d2d0392/packages/dashboard/dist/assets/index-Bq_Z2hne.js +7446 -0
- package/.worktrees/8d2d0392/packages/dashboard/dist/assets/index-O2W5RV4z.css +1 -0
- package/.worktrees/8d2d0392/packages/dashboard/dist/favicon.jpg +0 -0
- package/.worktrees/8d2d0392/packages/dashboard/dist/index.html +16 -0
- package/.worktrees/8d2d0392/packages/dashboard/dist/logo.jpg +0 -0
- package/.worktrees/8d2d0392/packages/dashboard/dist/prototype-dashboard.html +836 -0
- package/.worktrees/8d2d0392/packages/dashboard/dist/prototype-overview.html +256 -0
- package/.worktrees/8d2d0392/packages/dashboard/package.json +25 -0
- package/.worktrees/8d2d0392/packages/dashboard/server/executor.js +95 -0
- package/.worktrees/8d2d0392/packages/dashboard/server/index.js +679 -0
- package/.worktrees/8d2d0392/packages/dashboard/server/parser.js +526 -0
- package/.worktrees/8d2d0392/packages/dashboard/server/watcher.js +357 -0
- package/.worktrees/8d2d0392/src/agent-session-log.js +803 -0
- package/.worktrees/8d2d0392/src/change-list.js +290 -0
- package/.worktrees/8d2d0392/src/change-risk-profile.js +339 -0
- package/.worktrees/8d2d0392/src/check-primitives.js +65 -0
- package/.worktrees/8d2d0392/src/classify-change.js +109 -0
- package/.worktrees/8d2d0392/src/commit-suggest.js +202 -0
- package/.worktrees/8d2d0392/src/config-cat.js +88 -0
- package/.worktrees/8d2d0392/src/config-schema.js +328 -0
- package/.worktrees/8d2d0392/src/constants.js +97 -0
- package/.worktrees/8d2d0392/src/contract-matrix.js +533 -0
- package/.worktrees/8d2d0392/src/datetime.js +20 -0
- package/.worktrees/8d2d0392/src/db-engine.js +55 -0
- package/.worktrees/8d2d0392/src/db.js +381 -0
- package/.worktrees/8d2d0392/src/decision-distill.js +517 -0
- package/.worktrees/8d2d0392/src/dispatch/backends/local-agent.js +155 -0
- package/.worktrees/8d2d0392/src/dispatch/backends/sillyhub-mcp.js +245 -0
- package/.worktrees/8d2d0392/src/dispatch/probe.js +246 -0
- package/.worktrees/8d2d0392/src/dispatch/strategy.js +121 -0
- package/.worktrees/8d2d0392/src/docs-check.js +825 -0
- package/.worktrees/8d2d0392/src/docs-debt.js +393 -0
- package/.worktrees/8d2d0392/src/docs-gate.js +107 -0
- package/.worktrees/8d2d0392/src/doctor-diagnostics.js +1099 -0
- package/.worktrees/8d2d0392/src/endpoint-extractor.js +409 -0
- package/.worktrees/8d2d0392/src/foreign-declared.js +78 -0
- package/.worktrees/8d2d0392/src/fs-atomic.js +73 -0
- package/.worktrees/8d2d0392/src/git-helper.js +150 -0
- package/.worktrees/8d2d0392/src/hooks/claude-pre-tool-use.cjs +130 -0
- package/.worktrees/8d2d0392/src/hooks/worktree-guard.js +892 -0
- package/.worktrees/8d2d0392/src/index.js +2854 -0
- package/.worktrees/8d2d0392/src/init.js +626 -0
- package/.worktrees/8d2d0392/src/knowledge-match.js +231 -0
- package/.worktrees/8d2d0392/src/local-detect.js +128 -0
- package/.worktrees/8d2d0392/src/local-register.js +104 -0
- package/.worktrees/8d2d0392/src/machine-interface.js +543 -0
- package/.worktrees/8d2d0392/src/migrate.js +127 -0
- package/.worktrees/8d2d0392/src/module-impact.js +166 -0
- package/.worktrees/8d2d0392/src/module-resolve.js +222 -0
- package/.worktrees/8d2d0392/src/modules.js +580 -0
- package/.worktrees/8d2d0392/src/plan-adopt-waves.js +126 -0
- package/.worktrees/8d2d0392/src/progress/change-registry.js +336 -0
- package/.worktrees/8d2d0392/src/progress/consistency-doctor.js +355 -0
- package/.worktrees/8d2d0392/src/progress/shared.js +36 -0
- package/.worktrees/8d2d0392/src/progress/stage-machine.js +594 -0
- package/.worktrees/8d2d0392/src/progress/step-store.js +265 -0
- package/.worktrees/8d2d0392/src/progress.js +1304 -0
- package/.worktrees/8d2d0392/src/quick-recommend.js +115 -0
- package/.worktrees/8d2d0392/src/quicklog.js +883 -0
- package/.worktrees/8d2d0392/src/review-tier.js +77 -0
- package/.worktrees/8d2d0392/src/run/command.js +1641 -0
- package/.worktrees/8d2d0392/src/run/complete-handlers.js +1552 -0
- package/.worktrees/8d2d0392/src/run/complete.js +1259 -0
- package/.worktrees/8d2d0392/src/run/concurrent-detect.js +156 -0
- package/.worktrees/8d2d0392/src/run/gates.js +1094 -0
- package/.worktrees/8d2d0392/src/run/multi-repo-context.js +208 -0
- package/.worktrees/8d2d0392/src/run/next.js +128 -0
- package/.worktrees/8d2d0392/src/run/prompt.js +843 -0
- package/.worktrees/8d2d0392/src/run/quick-audit.js +186 -0
- package/.worktrees/8d2d0392/src/run/scan-profile.js +269 -0
- package/.worktrees/8d2d0392/src/run/shared.js +1562 -0
- package/.worktrees/8d2d0392/src/run/stage.js +587 -0
- package/.worktrees/8d2d0392/src/run.js +23 -0
- package/.worktrees/8d2d0392/src/scan-diff.js +388 -0
- package/.worktrees/8d2d0392/src/scan-postcheck.js +524 -0
- package/.worktrees/8d2d0392/src/scan-staleness.js +95 -0
- package/.worktrees/8d2d0392/src/setup.js +414 -0
- package/.worktrees/8d2d0392/src/sillyhub-mcp/client.js +597 -0
- package/.worktrees/8d2d0392/src/sillyhub-mcp/config.js +62 -0
- package/.worktrees/8d2d0392/src/spec-dir-typo.js +50 -0
- package/.worktrees/8d2d0392/src/spec-sync.js +358 -0
- package/.worktrees/8d2d0392/src/stage-contract-engine.js +171 -0
- package/.worktrees/8d2d0392/src/stage-contract-spec.js +551 -0
- package/.worktrees/8d2d0392/src/stage-contract.js +948 -0
- package/.worktrees/8d2d0392/src/stage-review.js +597 -0
- package/.worktrees/8d2d0392/src/stages/archive.js +199 -0
- package/.worktrees/8d2d0392/src/stages/brainstorm-auto.js +285 -0
- package/.worktrees/8d2d0392/src/stages/brainstorm.js +624 -0
- package/.worktrees/8d2d0392/src/stages/cmd-existence.js +145 -0
- package/.worktrees/8d2d0392/src/stages/doctor.js +540 -0
- package/.worktrees/8d2d0392/src/stages/execute.js +1277 -0
- package/.worktrees/8d2d0392/src/stages/explore.js +43 -0
- package/.worktrees/8d2d0392/src/stages/index.js +29 -0
- package/.worktrees/8d2d0392/src/stages/knowledge.js +514 -0
- package/.worktrees/8d2d0392/src/stages/plan-postcheck.js +1398 -0
- package/.worktrees/8d2d0392/src/stages/plan.js +651 -0
- package/.worktrees/8d2d0392/src/stages/quick.js +167 -0
- package/.worktrees/8d2d0392/src/stages/scan.js +608 -0
- package/.worktrees/8d2d0392/src/stages/status.js +64 -0
- package/.worktrees/8d2d0392/src/stages/verify.js +246 -0
- package/.worktrees/8d2d0392/src/sync.js +1470 -0
- package/.worktrees/8d2d0392/src/task-review.js +1498 -0
- package/.worktrees/8d2d0392/src/taskcard-placeholders.js +21 -0
- package/.worktrees/8d2d0392/src/taskcard.js +217 -0
- package/.worktrees/8d2d0392/src/verify-postcheck.js +1718 -0
- package/.worktrees/8d2d0392/src/verify-probes.js +327 -0
- package/.worktrees/8d2d0392/src/version.js +19 -0
- package/.worktrees/8d2d0392/src/workflow.js +600 -0
- package/.worktrees/8d2d0392/src/workspace.js +125 -0
- package/.worktrees/8d2d0392/src/worktree-apply.js +1611 -0
- package/.worktrees/8d2d0392/src/worktree-deps.js +576 -0
- package/.worktrees/8d2d0392/src/worktree.js +1838 -0
- package/.worktrees/8d2d0392/templates/claude-instruction.md +23 -0
- package/.worktrees/8d2d0392/templates/prompts/taskcard-rules.md +29 -0
- package/.worktrees/8d2d0392/templates/prompts/testcase-design.md +12 -0
- package/.worktrees/8d2d0392/templates/prompts/verify-probes.md +32 -0
- package/.worktrees/8d2d0392/templates/workflows/archive-impact.yaml +79 -0
- package/.worktrees/8d2d0392/templates/workflows/scan-docs.yaml +132 -0
- package/architecture-analysis.md +448 -0
- package/package.json +1 -1
- package/src/contract-matrix.js +1 -1
- package/src/endpoint-extractor.js +41 -52
- package/src/foreign-declared.js +78 -1
- package/src/index.js +9 -7
- package/src/run/command.js +2 -1
- package/src/run/gates.js +17 -0
- package/src/run/stage.js +8 -5
- package/src/stage-contract-spec.js +1 -1
- package/src/stages/brainstorm-auto.js +43 -1
- package/src/stages/brainstorm.js +2 -0
- package/src/stages/plan.js +8 -4
- package/src/stages/verify.js +1 -1
- package/src/taskcard.js +33 -0
- package/src/verify-probes.js +27 -6
- package/src/worktree-apply.js +451 -286
- package/src/worktree-deps.js +93 -1
- package/src/worktree.js +84 -19
- package/templates/prompts/taskcard-rules.md +1 -1
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scan-diff — scan 文档 vs 源码漂移清单(change: 2026-08-16-scan-diff-command, task-01)
|
|
3
|
+
*
|
|
4
|
+
* 定位:scan 文档是架构快照(基线 = 生成时源码 commit,frontmatter source_commit),源码持续演进。
|
|
5
|
+
* scan-diff 算出「基线之后源码变了什么、哪些模块的 scan 文档需要核对/刷新」,按四分类输出:
|
|
6
|
+
* A 新增文件 → scan 文档缺记(缺文档)
|
|
7
|
+
* D 已删除文件 → scan 文档多记(多文档)
|
|
8
|
+
* M / R / C 修改 / 重命名 / 复制 → scan 文档过时
|
|
9
|
+
* 其余(T 等)→ unknown(不判漂移,只展示)
|
|
10
|
+
* 扫描范围缺省 = module-map paths 覆盖集(parseModuleMapSimple 收集全部 paths 去重),
|
|
11
|
+
* 非 src/-only——范围外的源码变更与模块文档无涉,不计入漂移。
|
|
12
|
+
*
|
|
13
|
+
* 分层:
|
|
14
|
+
* computeScanDiff —— 计算层(git 只读 + frontmatter/module-map 读取;无渲染无落盘)。
|
|
15
|
+
* 硬错误返回 { ok:false, error }(无效 base / git 失败 / 无 source_commit);
|
|
16
|
+
* advisory 问题进 warnings(非祖先 base / rev-list 降级 / module-map 缺失)。
|
|
17
|
+
* runScanDiff —— IO 面(终端聚合渲染 + --report 落盘 scan-diff-report.md),返回退出码。
|
|
18
|
+
*
|
|
19
|
+
* 复用(先读源码确认签名后使用):parseSourceCommit(scan-staleness)、
|
|
20
|
+
* matchFilesToModules(docs-debt,含裸名兜底)、safeGit(git-helper)、
|
|
21
|
+
* parseModuleMapSimple(modules.js);零新依赖。
|
|
22
|
+
*/
|
|
23
|
+
import { existsSync, readFileSync, readdirSync, mkdirSync, writeFileSync } from 'node:fs'
|
|
24
|
+
import { join } from 'node:path'
|
|
25
|
+
import { safeGit } from './git-helper.js'
|
|
26
|
+
import { parseSourceCommit } from './scan-staleness.js'
|
|
27
|
+
import { matchFilesToModules } from './docs-debt.js'
|
|
28
|
+
import { parseModuleMapSimple } from './modules.js'
|
|
29
|
+
|
|
30
|
+
/** safeGit 超时(仿 docs-debt 的 GIT_TIMEOUT_MS 模式:大 diff 慢时降级不挂起) */
|
|
31
|
+
const GIT_TIMEOUT_MS = 10000
|
|
32
|
+
|
|
33
|
+
/** 每模块终端渲染默认上限(--full 展开全部) */
|
|
34
|
+
const DEFAULT_LIST_CAP = 5
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 计算层:scan 文档 vs 源码漂移事实。
|
|
38
|
+
* @param {{ projectRoot: string, specBase: string, projectName: string, base?: string|null }} opts
|
|
39
|
+
* projectRoot:git 锚(diff/rev-list 的 -C 目录)
|
|
40
|
+
* specBase / projectName:scan 文档与 module-map 定位(specBase/docs/<projectName>/...)
|
|
41
|
+
* base:基线 commit(短/长哈希、引用均可);缺省读 scan 文档 frontmatter 的 source_commit
|
|
42
|
+
* @returns {{ ok: true, base, baseSource, head, behindCommits, daysSinceScan, scope,
|
|
43
|
+
* added, deleted, modified, unknown, unmapped, outOfScope, renameMap, byModule, warnings, driftCount }
|
|
44
|
+
* | { ok: false, error: string }}
|
|
45
|
+
* ok:false = 硬错误(参数缺失 / 无 source_commit / 无效 base / git diff 失败)
|
|
46
|
+
* added/deleted/modified/unknown:范围内变更文件(POSIX 相对路径;R/C 记新路径)
|
|
47
|
+
* unmapped:范围内未匹配到模块的文件;outOfScope:范围外变更(不计入漂移)
|
|
48
|
+
* renameMap:newPath → oldPath(R 归 modified 且保留 rename 细节)
|
|
49
|
+
* byModule:{ moduleId: { doc, added, deleted, modified, files } }(A/D/M 计数 + 文件清单)
|
|
50
|
+
* driftCount = added + deleted + modified
|
|
51
|
+
*/
|
|
52
|
+
export function computeScanDiff({ projectRoot, specBase, projectName, base } = {}) {
|
|
53
|
+
if (!projectRoot || !specBase || !projectName) {
|
|
54
|
+
return { ok: false, error: 'projectRoot / specBase / projectName 必填,缺一不可' }
|
|
55
|
+
}
|
|
56
|
+
const warnings = []
|
|
57
|
+
let baseSource = '--base 显式指定'
|
|
58
|
+
|
|
59
|
+
// ── 基线:缺省读 specBase/docs/<projectName>/scan/*.md frontmatter 的 source_commit ──
|
|
60
|
+
if (!base) {
|
|
61
|
+
const sourceCommit = readSourceCommit(specBase, projectName)
|
|
62
|
+
if (!sourceCommit) {
|
|
63
|
+
return {
|
|
64
|
+
ok: false, error:
|
|
65
|
+
'scan 文档无 source_commit 字段(绿地项目 / 旧版生成 / 无 scan 文档),无法定基线——请用 --base <commit> 显式指定基线',
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
base = sourceCommit
|
|
69
|
+
baseSource = 'scan 文档 frontmatter source_commit'
|
|
70
|
+
}
|
|
71
|
+
const baseDisplay = displayBase(base)
|
|
72
|
+
|
|
73
|
+
// ── base 有效性守卫(仿 computeScanStaleness 的 isAncestor 守卫)──
|
|
74
|
+
// merge-base --is-ancestor 对「无效 commit」与「有效但非祖先」都返回非 0(safeGit 记为 error),
|
|
75
|
+
// 无法直接区分——先用 rev-parse 验有效性:无效 → 报错;有效但非祖先 → 警告(diff 按两快照对比仍可算)。
|
|
76
|
+
const revRes = safeGit(projectRoot, ['rev-parse', '--verify', `${base}^{commit}`])
|
|
77
|
+
if (revRes.error || !revRes.value) {
|
|
78
|
+
return { ok: false, error: `基线 ${baseDisplay} 不是有效 commit(${revRes.error || '无输出'}),跳过 scan-diff` }
|
|
79
|
+
}
|
|
80
|
+
const resolvedBase = revRes.value.trim()
|
|
81
|
+
const headRes = safeGit(projectRoot, ['rev-parse', 'HEAD'])
|
|
82
|
+
const headShort = headRes.error ? 'HEAD' : headRes.value.trim().slice(0, 7)
|
|
83
|
+
const isAncestor = safeGit(projectRoot, ['merge-base', '--is-ancestor', resolvedBase, 'HEAD'])
|
|
84
|
+
if (isAncestor.error) {
|
|
85
|
+
warnings.push(`基线 ${baseDisplay} 不是 HEAD(${headShort})祖先(rebase / 分支切换)——diff 按两快照对比,删除后重建可能呈假新增`)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ── 落后计数(advisory:超时/失败降级 null,不抛)──
|
|
89
|
+
const countRes = safeGit(projectRoot, ['rev-list', '--count', `${resolvedBase}..HEAD`], { timeout: GIT_TIMEOUT_MS })
|
|
90
|
+
let behindCommits = countRes.error ? null : parseInt(countRes.value, 10)
|
|
91
|
+
if (behindCommits === null || Number.isNaN(behindCommits)) {
|
|
92
|
+
behindCommits = null
|
|
93
|
+
warnings.push('rev-list 计数失败(git 降级),behindCommits=null')
|
|
94
|
+
}
|
|
95
|
+
const dateRes = safeGit(projectRoot, ['log', '-1', '--format=%cI', resolvedBase])
|
|
96
|
+
let daysSinceScan = null
|
|
97
|
+
if (!dateRes.error && dateRes.value) {
|
|
98
|
+
daysSinceScan = Math.floor((Date.now() - new Date(dateRes.value.trim()).getTime()) / 86400000)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ── 扫描范围:module-map paths 覆盖集(默认范围,非 src/-only)──
|
|
102
|
+
const cardsDir = join(specBase, 'docs', projectName, 'modules')
|
|
103
|
+
const mapPath = join(cardsDir, '_module-map.yaml')
|
|
104
|
+
let moduleIndex = null
|
|
105
|
+
if (existsSync(mapPath)) {
|
|
106
|
+
try { moduleIndex = parseModuleMapSimple(readFileSync(mapPath, 'utf8')) } catch { moduleIndex = null }
|
|
107
|
+
}
|
|
108
|
+
const scope = collectScope(moduleIndex)
|
|
109
|
+
if (scope.length === 0) {
|
|
110
|
+
warnings.push('module-map 缺失 / 解析为空(跑 sillyspec modules rebuild 生成)——扫描范围退化=全部变更文件,无法归模块')
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ── diff:只算路径(--find-renames 检出重命名;超时降级为错误返回)──
|
|
114
|
+
const diffRes = safeGit(projectRoot, ['diff', '--name-status', '--find-renames', `${resolvedBase}..HEAD`], { timeout: GIT_TIMEOUT_MS })
|
|
115
|
+
if (diffRes.error) {
|
|
116
|
+
return { ok: false, error: `git diff ${baseDisplay}..HEAD 失败(${diffRes.error}),跳过 scan-diff` }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ── 四分类 + 范围过滤 ──
|
|
120
|
+
const added = [], deleted = [], modified = [], unknown = [], outOfScope = []
|
|
121
|
+
const renameMap = {} // newPath → oldPath(R 归 modified 且保留 rename 细节)
|
|
122
|
+
const scopeFilter = scope.length > 0 ? scope : null
|
|
123
|
+
for (const item of parseNameStatus(diffRes.value)) {
|
|
124
|
+
if (scopeFilter && !inScope(item.path, scopeFilter)) { outOfScope.push(item.path); continue }
|
|
125
|
+
switch (item.status) {
|
|
126
|
+
case 'A': added.push(item.path); break
|
|
127
|
+
case 'D': deleted.push(item.path); break
|
|
128
|
+
case 'M': modified.push(item.path); break
|
|
129
|
+
case 'R': modified.push(item.path); renameMap[item.path] = item.oldPath; break
|
|
130
|
+
case 'C': modified.push(item.path); break // 复制目标(新路径)视同过时
|
|
131
|
+
default: unknown.push(item.path); break // T / B 等
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ── 归模块(matchFilesToModules,含裸名兜底;结果 = 同文件清单直接调用一致)──
|
|
136
|
+
const inScopeFiles = [...added, ...deleted, ...modified, ...unknown]
|
|
137
|
+
let byModuleMap = new Map()
|
|
138
|
+
let unmapped = inScopeFiles
|
|
139
|
+
if (inScopeFiles.length > 0) {
|
|
140
|
+
try {
|
|
141
|
+
const matched = matchFilesToModules(inScopeFiles, moduleIndex || {}, { cardsDir })
|
|
142
|
+
byModuleMap = matched.byModule
|
|
143
|
+
unmapped = matched.unmapped
|
|
144
|
+
} catch {
|
|
145
|
+
warnings.push('matchFilesToModules 归模块失败(降级为全部未归模块)')
|
|
146
|
+
}
|
|
147
|
+
} else {
|
|
148
|
+
unmapped = []
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ── byModule 聚合 A/D/M 计数 ──
|
|
152
|
+
const addedSet = new Set(added)
|
|
153
|
+
const deletedSet = new Set(deleted)
|
|
154
|
+
const modifiedSet = new Set(modified)
|
|
155
|
+
const byModule = {}
|
|
156
|
+
for (const [id, entry] of byModuleMap) {
|
|
157
|
+
let a = 0, d = 0, m = 0
|
|
158
|
+
for (const f of entry.files) {
|
|
159
|
+
if (addedSet.has(f)) a++
|
|
160
|
+
else if (deletedSet.has(f)) d++
|
|
161
|
+
else if (modifiedSet.has(f)) m++
|
|
162
|
+
}
|
|
163
|
+
byModule[id] = { doc: entry.doc || '', added: a, deleted: d, modified: m, files: entry.files }
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
ok: true,
|
|
168
|
+
base: baseDisplay,
|
|
169
|
+
baseSource,
|
|
170
|
+
head: headShort,
|
|
171
|
+
behindCommits,
|
|
172
|
+
daysSinceScan,
|
|
173
|
+
scope,
|
|
174
|
+
added, deleted, modified, unknown, unmapped, outOfScope,
|
|
175
|
+
renameMap,
|
|
176
|
+
byModule,
|
|
177
|
+
warnings,
|
|
178
|
+
driftCount: added.length + deleted.length + modified.length,
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* IO 面:终端渲染 + 可选落盘报告。
|
|
184
|
+
* @param {{ projectRoot: string, specBase: string, projectName: string, base?: string|null,
|
|
185
|
+
* full?: boolean, report?: boolean }} opts
|
|
186
|
+
* full:终端展开每模块全部文件(缺省每模块 ≤5 条);report:落盘 scan-diff-report.md
|
|
187
|
+
* @returns {number} 退出码:0 无漂移 / 1 有漂移 / 2 错误(无效 base、git 失败、无 source_commit)
|
|
188
|
+
*/
|
|
189
|
+
export function runScanDiff(opts = {}) {
|
|
190
|
+
const { projectRoot, specBase, projectName, base = null, full = false, report = false } = opts
|
|
191
|
+
const result = computeScanDiff({ projectRoot, specBase, projectName, base })
|
|
192
|
+
if (!result.ok) {
|
|
193
|
+
console.error(`scan-diff:${result.error}`)
|
|
194
|
+
return 2
|
|
195
|
+
}
|
|
196
|
+
for (const w of result.warnings) console.warn(` ⚠️ ${w}`)
|
|
197
|
+
|
|
198
|
+
console.log(`scan diff: ${result.base}..HEAD(${fmtNum(result.behindCommits)} commit / ${fmtNum(result.daysSinceScan)} 天)`)
|
|
199
|
+
if (result.driftCount === 0) {
|
|
200
|
+
console.log('scan 文档与源码一致(0 漂移)')
|
|
201
|
+
if (result.unknown.length > 0) {
|
|
202
|
+
console.log(` 无法归类(unknown)${result.unknown.length} 条:${result.unknown.join(', ')}`)
|
|
203
|
+
}
|
|
204
|
+
} else {
|
|
205
|
+
renderTerminal(result, { full })
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (report) {
|
|
209
|
+
const p = writeReport(result, { specBase, projectName })
|
|
210
|
+
if (p) console.log(`scan-diff 报告已落盘:${p}`)
|
|
211
|
+
else console.warn(' ⚠️ scan-diff-report.md 写入失败')
|
|
212
|
+
}
|
|
213
|
+
return result.driftCount > 0 ? 1 : 0
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** 终端聚合渲染:每模块计数 + 文件清单(缺省 ≤5 条,--full 展开全部) */
|
|
217
|
+
function renderTerminal(r, { full }) {
|
|
218
|
+
const cap = full ? Number.MAX_SAFE_INTEGER : DEFAULT_LIST_CAP
|
|
219
|
+
console.log(`扫描范围:module-map paths 覆盖集(${r.scope.length} 条路径)——范围外的变更不计入漂移`)
|
|
220
|
+
const moduleIds = Object.keys(r.byModule).sort()
|
|
221
|
+
if (moduleIds.length > 0) {
|
|
222
|
+
console.log('按模块聚合漂移(A=缺文档 D=多文档 M=过时,--full 展开全部):')
|
|
223
|
+
for (const id of moduleIds) {
|
|
224
|
+
const m = r.byModule[id]
|
|
225
|
+
const doc = m.doc ? `(${m.doc})` : ''
|
|
226
|
+
console.log(` [${id}] A${m.added} D${m.deleted} M${m.modified}${doc}`)
|
|
227
|
+
for (const f of m.files.slice(0, cap)) {
|
|
228
|
+
console.log(` - ${statusLetter(r, f)} ${f}${r.renameMap[f] ? `(原 ${r.renameMap[f]})` : ''}`)
|
|
229
|
+
}
|
|
230
|
+
if (m.files.length > cap) console.log(` ... 其余 ${m.files.length - cap} 条,--full 展开`)
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (r.unmapped.length > 0) {
|
|
234
|
+
console.log(`未归模块(unmapped,${r.unmapped.length} 条):`)
|
|
235
|
+
for (const f of r.unmapped.slice(0, cap)) {
|
|
236
|
+
console.log(` - ${statusLetter(r, f)} ${f}`)
|
|
237
|
+
}
|
|
238
|
+
if (r.unmapped.length > cap) console.log(` ... 其余 ${r.unmapped.length - cap} 条,--full 展开`)
|
|
239
|
+
}
|
|
240
|
+
if (r.unknown.length > 0) {
|
|
241
|
+
console.log(`无法归类(unknown,${r.unknown.length} 条):${r.unknown.slice(0, cap).join(', ')}`)
|
|
242
|
+
}
|
|
243
|
+
console.log(`漂移合计:${r.driftCount} 条(A${r.added.length} / D${r.deleted.length} / M${r.modified.length})`)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** 文件归属状态字母(渲染用):R(renameMap 命中)> A > D > M */
|
|
247
|
+
function statusLetter(r, f) {
|
|
248
|
+
if (r.renameMap[f]) return 'R'
|
|
249
|
+
if (r.added.includes(f)) return 'A'
|
|
250
|
+
if (r.deleted.includes(f)) return 'D'
|
|
251
|
+
return 'M'
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** 落盘 scan-diff-report.md(markdown 完整清单,不截断) */
|
|
255
|
+
function writeReport(r, { specBase, projectName }) {
|
|
256
|
+
const dir = join(specBase, 'docs', projectName, 'scan')
|
|
257
|
+
try { mkdirSync(dir, { recursive: true }) } catch { return null }
|
|
258
|
+
const lines = [
|
|
259
|
+
'# scan-diff 报告',
|
|
260
|
+
'',
|
|
261
|
+
`- 生成时间:${new Date().toISOString()}`,
|
|
262
|
+
`- 项目:${projectName}`,
|
|
263
|
+
`- 基线:${r.base}(${r.baseSource})`,
|
|
264
|
+
`- 对比:\`${r.base}..HEAD\``,
|
|
265
|
+
`- 落后:${fmtNum(r.behindCommits)} commit / ${fmtNum(r.daysSinceScan)} 天`,
|
|
266
|
+
`- 扫描范围:module-map paths 覆盖集(${r.scope.length} 条路径)`,
|
|
267
|
+
'',
|
|
268
|
+
'## 漂移总览',
|
|
269
|
+
'',
|
|
270
|
+
'| 分类 | 含义 | 数量 |',
|
|
271
|
+
'| --- | --- | --- |',
|
|
272
|
+
`| added | 源码新增,scan 文档缺记 | ${r.added.length} |`,
|
|
273
|
+
`| deleted | 源码已删,scan 文档多记 | ${r.deleted.length} |`,
|
|
274
|
+
`| modified | 源码已变,scan 文档过时 | ${r.modified.length} |`,
|
|
275
|
+
`| unknown | 其他状态 | ${r.unknown.length} |`,
|
|
276
|
+
`| unmapped | 范围内未匹配模块 | ${r.unmapped.length} |`,
|
|
277
|
+
'',
|
|
278
|
+
'## 按模块聚合',
|
|
279
|
+
'',
|
|
280
|
+
]
|
|
281
|
+
if (Object.keys(r.byModule).length === 0) {
|
|
282
|
+
lines.push('(无模块命中)', '')
|
|
283
|
+
} else {
|
|
284
|
+
for (const id of Object.keys(r.byModule).sort()) {
|
|
285
|
+
const m = r.byModule[id]
|
|
286
|
+
lines.push(`### ${id}(${m.doc || '无 doc'})`, '')
|
|
287
|
+
lines.push(`- added:${m.added}`, `- deleted:${m.deleted}`, `- modified:${m.modified}`, '- 文件:')
|
|
288
|
+
for (const f of m.files) {
|
|
289
|
+
const ren = r.renameMap[f] ? `,原 ${r.renameMap[f]}` : ''
|
|
290
|
+
lines.push(` - \`${f}\`(${statusLetter(r, f)}${ren})`)
|
|
291
|
+
}
|
|
292
|
+
lines.push('')
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
if (r.unmapped.length > 0) {
|
|
296
|
+
lines.push('## 未归模块(unmapped)', '')
|
|
297
|
+
for (const f of r.unmapped) lines.push(`- \`${f}\``)
|
|
298
|
+
lines.push('')
|
|
299
|
+
}
|
|
300
|
+
if (r.outOfScope.length > 0) {
|
|
301
|
+
lines.push('## 范围外变更(不计入漂移)', '')
|
|
302
|
+
for (const f of r.outOfScope) lines.push(`- \`${f}\``)
|
|
303
|
+
lines.push('')
|
|
304
|
+
}
|
|
305
|
+
if (r.warnings.length > 0) {
|
|
306
|
+
lines.push('## 提示', '')
|
|
307
|
+
for (const w of r.warnings) lines.push(`- ${w}`)
|
|
308
|
+
lines.push('')
|
|
309
|
+
}
|
|
310
|
+
lines.push('## 结论', '')
|
|
311
|
+
lines.push(`- 漂移合计:${r.driftCount} 条(A${r.added.length} / D${r.deleted.length} / M${r.modified.length})`)
|
|
312
|
+
if (r.driftCount === 0) {
|
|
313
|
+
lines.push('- scan 文档与源码一致(0 漂移)')
|
|
314
|
+
} else {
|
|
315
|
+
lines.push('- 存在漂移:请按需核对并刷新对应 scan 文档(sillyspec run scan)')
|
|
316
|
+
}
|
|
317
|
+
lines.push('')
|
|
318
|
+
lines.push('> 由 sillyspec scan-diff 自动生成;改动源码后请按需核对 scan 文档。')
|
|
319
|
+
const outPath = join(dir, 'scan-diff-report.md')
|
|
320
|
+
try {
|
|
321
|
+
writeFileSync(outPath, lines.join('\n') + '\n', 'utf8')
|
|
322
|
+
return outPath
|
|
323
|
+
} catch {
|
|
324
|
+
return null
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** 读 scan 目录任一 .md 的 source_commit(同 modules.js 写入同值,取首个命中;全失败 → null) */
|
|
329
|
+
function readSourceCommit(specBase, projectName) {
|
|
330
|
+
const scanDir = join(specBase, 'docs', projectName, 'scan')
|
|
331
|
+
if (!existsSync(scanDir)) return null
|
|
332
|
+
try {
|
|
333
|
+
for (const f of readdirSync(scanDir)) {
|
|
334
|
+
if (!f.endsWith('.md')) continue
|
|
335
|
+
const c = parseSourceCommit(readFileSync(join(scanDir, f), 'utf8'))
|
|
336
|
+
if (c) return c
|
|
337
|
+
}
|
|
338
|
+
} catch { /* 读失败 → null */ }
|
|
339
|
+
return null
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** 收集 module-map 全部 paths 去重(归一 Windows 反斜杠 + 去尾部斜杠;无 paths → 空数组) */
|
|
343
|
+
function collectScope(moduleIndex) {
|
|
344
|
+
const set = new Set()
|
|
345
|
+
for (const m of Object.values(moduleIndex || {})) {
|
|
346
|
+
if (!Array.isArray(m.paths)) continue
|
|
347
|
+
for (const raw of m.paths) {
|
|
348
|
+
if (typeof raw !== 'string' || !raw) continue
|
|
349
|
+
set.add(raw.replace(/\\/g, '/').replace(/\/+$/, ''))
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return [...set]
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** 路径是否落在范围(p 可为目录或文件:精确相等或目录前缀) */
|
|
356
|
+
function inScope(filePath, scope) {
|
|
357
|
+
return scope.some((p) => p && (filePath === p || filePath.startsWith(p + '/')))
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/** 解析 `git diff --name-status --find-renames` 输出 → [{ status, path, oldPath? }](R/C 双路径) */
|
|
361
|
+
function parseNameStatus(out) {
|
|
362
|
+
const items = []
|
|
363
|
+
if (!out) return items
|
|
364
|
+
for (const line of out.split('\n')) {
|
|
365
|
+
const l = line.replace(/\r$/, '')
|
|
366
|
+
if (!l) continue
|
|
367
|
+
const tab = l.indexOf('\t')
|
|
368
|
+
if (tab === -1) continue
|
|
369
|
+
const m = l.slice(0, tab).match(/^([A-Z])(\d*)$/)
|
|
370
|
+
if (!m) continue
|
|
371
|
+
const status = m[1]
|
|
372
|
+
const parts = l.slice(tab + 1).split('\t')
|
|
373
|
+
const item = { status, path: parts[parts.length - 1] }
|
|
374
|
+
if ((status === 'R' || status === 'C') && parts.length >= 2) item.oldPath = parts[0]
|
|
375
|
+
items.push(item)
|
|
376
|
+
}
|
|
377
|
+
return items
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/** 40 位全哈希缩成 7 位显示;引用/短哈希原样 */
|
|
381
|
+
function displayBase(b) {
|
|
382
|
+
return /^[0-9a-f]{40}$/i.test(b) ? b.slice(0, 7) : b
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/** null/NaN → '?'(降级显示) */
|
|
386
|
+
function fmtNum(n) {
|
|
387
|
+
return n === null || Number.isNaN(n) ? '?' : String(n)
|
|
388
|
+
}
|