sillyspec 3.27.1 → 3.27.5
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-execute/SKILL.md +8 -0
- package/.claude/skills/sillyspec-quick/SKILL.md +2 -1
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-archive/SKILL.md +87 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-auto/SKILL.md +93 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-brainstorm/SKILL.md +114 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-commit/SKILL.md +67 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-continue/SKILL.md +47 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-doctor/SKILL.md +76 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-execute/SKILL.md +214 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-explore/SKILL.md +102 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-export/SKILL.md +57 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-init/SKILL.md +177 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-knowledge/SKILL.md +270 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-plan/SKILL.md +156 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-quick/SKILL.md +116 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-resume/SKILL.md +70 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-scan/SKILL.md +79 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-state/SKILL.md +64 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-status/SKILL.md +54 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-verify/SKILL.md +87 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-workspace/SKILL.md +175 -0
- package/.claude/worktrees/sillyspec-data-model-research/.gitattributes +2 -0
- package/.claude/worktrees/sillyspec-data-model-research/.husky/pre-push +20 -0
- package/.claude/worktrees/sillyspec-data-model-research/README.md +203 -0
- package/.claude/worktrees/sillyspec-data-model-research/SKILL.md +91 -0
- package/.claude/worktrees/sillyspec-data-model-research/bin/sillyspec.js +2 -0
- package/.claude/worktrees/sillyspec-data-model-research/logo.jpg +0 -0
- package/.claude/worktrees/sillyspec-data-model-research/package-lock.json +977 -0
- package/.claude/worktrees/sillyspec-data-model-research/package.json +39 -0
- package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/assets/index-Bq_Z2hne.js +7446 -0
- package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/assets/index-O2W5RV4z.css +1 -0
- package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/favicon.jpg +0 -0
- package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/index.html +16 -0
- package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/logo.jpg +0 -0
- package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/prototype-dashboard.html +836 -0
- package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/prototype-overview.html +256 -0
- package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/package.json +25 -0
- package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/server/executor.js +95 -0
- package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/server/index.js +679 -0
- package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/server/parser.js +526 -0
- package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/server/watcher.js +357 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/agent-session-log.js +803 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/change-list.js +290 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/change-risk-profile.js +270 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/check-primitives.js +65 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/classify-change.js +109 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/commit-suggest.js +202 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/config-cat.js +88 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/config-schema.js +328 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/constants.js +97 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/contract-matrix.js +480 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/datetime.js +20 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/db-engine.js +55 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/db.js +381 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/decision-distill.js +517 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/dispatch/backends/local-agent.js +155 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/dispatch/backends/sillyhub-mcp.js +245 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/dispatch/probe.js +246 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/dispatch/strategy.js +121 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/docs-check.js +825 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/docs-debt.js +393 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/docs-gate.js +107 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/doctor-diagnostics.js +1099 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/endpoint-extractor.js +420 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/foreign-declared.js +78 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/fs-atomic.js +73 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/git-helper.js +150 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/hooks/claude-pre-tool-use.cjs +130 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/hooks/worktree-guard.js +892 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/index.js +2798 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/init.js +626 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/knowledge-match.js +231 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/local-detect.js +128 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/local-register.js +104 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/machine-interface.js +543 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/migrate.js +127 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/module-impact.js +166 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/module-resolve.js +222 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/modules.js +580 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/progress/change-registry.js +336 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/progress/consistency-doctor.js +355 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/progress/shared.js +36 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/progress/stage-machine.js +594 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/progress/step-store.js +265 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/progress.js +1304 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/quick-recommend.js +115 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/quicklog.js +852 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/review-tier.js +77 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run/command.js +1595 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run/complete-handlers.js +1552 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run/complete.js +1259 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run/concurrent-detect.js +156 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run/gates.js +1084 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run/multi-repo-context.js +208 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run/next.js +128 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run/prompt.js +843 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run/quick-audit.js +186 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run/scan-profile.js +269 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run/shared.js +1562 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run/stage.js +584 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run.js +23 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/scan-diff.js +388 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/scan-postcheck.js +512 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/scan-staleness.js +95 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/setup.js +414 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/sillyhub-mcp/client.js +597 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/sillyhub-mcp/config.js +62 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/spec-dir-typo.js +50 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/spec-sync.js +358 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stage-contract-engine.js +171 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stage-contract-spec.js +550 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stage-contract.js +889 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stage-review.js +597 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/archive.js +199 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/brainstorm-auto.js +236 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/brainstorm.js +617 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/cmd-existence.js +145 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/doctor.js +540 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/execute.js +1275 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/explore.js +43 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/index.js +29 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/knowledge.js +514 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/plan-postcheck.js +1344 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/plan.js +650 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/quick.js +167 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/scan.js +608 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/status.js +64 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/verify.js +246 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/sync.js +1470 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/task-review.js +1498 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/taskcard.js +210 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/verify-postcheck.js +1718 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/verify-probes.js +277 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/version.js +19 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/workflow.js +600 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/workspace.js +125 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/worktree-apply.js +1446 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/worktree-deps.js +576 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/worktree.js +1715 -0
- package/.claude/worktrees/sillyspec-data-model-research/templates/claude-instruction.md +23 -0
- package/.claude/worktrees/sillyspec-data-model-research/templates/prompts/taskcard-rules.md +28 -0
- package/.claude/worktrees/sillyspec-data-model-research/templates/prompts/testcase-design.md +12 -0
- package/.claude/worktrees/sillyspec-data-model-research/templates/prompts/verify-probes.md +32 -0
- package/.claude/worktrees/sillyspec-data-model-research/templates/workflows/archive-impact.yaml +79 -0
- package/.claude/worktrees/sillyspec-data-model-research/templates/workflows/scan-docs.yaml +132 -0
- package/README.md +1 -1
- package/eval/README.md +86 -0
- package/eval/eval.config.json +28 -0
- package/eval/lib/adapters/cli.mjs +63 -0
- package/eval/lib/adapters/dryrun.mjs +27 -0
- package/eval/lib/config.mjs +82 -0
- package/eval/lib/db.mjs +76 -0
- package/eval/lib/report.mjs +120 -0
- package/eval/lib/runner.mjs +157 -0
- package/eval/lib/tasks.mjs +73 -0
- package/eval/lib/util.mjs +107 -0
- package/eval/lib/workdir.mjs +30 -0
- package/eval/run.mjs +83 -0
- package/eval/tasks/001-slugify/files/package.json +6 -0
- package/eval/tasks/001-slugify/solution/src/slugify.mjs +26 -0
- package/eval/tasks/001-slugify/task.json +6 -0
- package/eval/tasks/001-slugify/verify.mjs +28 -0
- package/eval/tasks/002-each-day/files/package.json +6 -0
- package/eval/tasks/002-each-day/files/src/dates.mjs +4 -0
- package/eval/tasks/002-each-day/solution/src/dates.mjs +30 -0
- package/eval/tasks/002-each-day/task.json +6 -0
- package/eval/tasks/002-each-day/verify.mjs +31 -0
- package/eval/tasks/003-word-report/files/data/notes.txt +5 -0
- package/eval/tasks/003-word-report/files/package.json +6 -0
- package/eval/tasks/003-word-report/solution/bin/report.mjs +36 -0
- package/eval/tasks/003-word-report/task.json +6 -0
- package/eval/tasks/003-word-report/verify.mjs +38 -0
- package/package.json +4 -2
- package/src/agent-session-log.js +803 -0
- package/src/change-list.js +22 -2
- package/src/change-risk-profile.js +83 -14
- package/src/config-schema.js +18 -3
- package/src/contract-matrix.js +153 -8
- package/src/datetime.js +20 -0
- package/src/decision-distill.js +517 -0
- package/src/dispatch/backends/local-agent.js +4 -4
- package/src/docs-check.js +227 -0
- package/src/docs-debt.js +187 -24
- package/src/doctor-diagnostics.js +102 -2
- package/src/foreign-declared.js +78 -0
- package/src/index.js +99 -10
- package/src/init.js +9 -7
- package/src/knowledge-match.js +107 -6
- package/src/local-register.js +7 -1
- package/src/module-resolve.js +12 -1
- package/src/modules.js +2 -1
- package/src/plan-adopt-waves.js +126 -0
- package/src/quicklog.js +61 -7
- package/src/run/command.js +139 -20
- package/src/run/complete-handlers.js +48 -19
- package/src/run/complete.js +43 -3
- package/src/run/gates.js +87 -22
- package/src/run/prompt.js +84 -42
- package/src/run/quick-audit.js +28 -0
- package/src/run/shared.js +176 -11
- package/src/run/stage.js +23 -0
- package/src/run.js +2 -2
- package/src/scan-postcheck.js +30 -17
- package/src/stage-contract-spec.js +2 -1
- package/src/stage-contract.js +68 -4
- package/src/stages/archive.js +53 -12
- package/src/stages/brainstorm-auto.js +7 -0
- package/src/stages/brainstorm.js +11 -0
- package/src/stages/doctor.js +157 -1
- package/src/stages/execute.js +33 -6
- package/src/stages/plan-postcheck.js +79 -18
- package/src/stages/plan.js +4 -0
- package/src/stages/quick.js +6 -1
- package/src/stages/verify.js +31 -14
- package/src/sync.js +94 -2
- package/src/task-review.js +2 -2
- package/src/taskcard-placeholders.js +21 -0
- package/src/taskcard.js +10 -1
- package/src/verify-postcheck.js +315 -13
- package/src/verify-probes.js +46 -4
- package/src/worktree-apply.js +184 -7
- package/src/worktree-deps.js +69 -0
- package/src/worktree.js +201 -58
- package/templates/prompts/taskcard-rules.md +1 -0
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* endpoint-extractor.js — 从代码中提取 HTTP 端点定义和调用
|
|
3
|
+
*
|
|
4
|
+
* provider 端:扫描 router 文件,提取注册的 API 路径
|
|
5
|
+
* consumer 端:扫描前端文件,提取 apiFetch/request 调用路径
|
|
6
|
+
*
|
|
7
|
+
* 契约对账时:provider 产出 ≠ consumer 消费 → gap
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'fs'
|
|
11
|
+
import { join, extname, basename, dirname, resolve } from 'path'
|
|
12
|
+
import { execSync } from 'child_process'
|
|
13
|
+
|
|
14
|
+
// ─── Provider: 扫描后端 router 注册的端点 ───────────────────────────────
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 从单个文件提取 FastAPI router 端点
|
|
18
|
+
* 支持 APIRouter(prefix=...) 和 @router.get/post/put/delete/patch("/path")
|
|
19
|
+
*
|
|
20
|
+
* @param {string} filePath - 文件绝对路径
|
|
21
|
+
* @returns {Array<{ method: string, path: string, source: string, line: number }>}
|
|
22
|
+
*/
|
|
23
|
+
export function extractFastApiEndpoints(filePath) {
|
|
24
|
+
const content = readFileSync(filePath, 'utf8')
|
|
25
|
+
const lines = content.split('\n')
|
|
26
|
+
const endpoints = []
|
|
27
|
+
|
|
28
|
+
// 1. 提取 router prefix
|
|
29
|
+
let routerPrefix = ''
|
|
30
|
+
for (const line of lines) {
|
|
31
|
+
const prefixMatch = line.match(/(?:APIRouter|router)\s*\(\s*(?:prefix\s*=\s*)?["'`]([^"'`]+)["'`]/)
|
|
32
|
+
|| line.match(/\.include_router\s*\([^)]*prefix\s*=\s*["'`]([^"'`]+)["'`]/)
|
|
33
|
+
if (prefixMatch) {
|
|
34
|
+
routerPrefix = prefixMatch[1]
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// 2. 提取 @router.method("/path") 或分散式定义
|
|
39
|
+
// FastAPI 支持两种写法:
|
|
40
|
+
// a) @router.get("/path", ...) 下一行 def func():
|
|
41
|
+
// b) @router.get 下一行 ("/path", ...)
|
|
42
|
+
for (let i = 0; i < lines.length; i++) {
|
|
43
|
+
const line = lines[i]
|
|
44
|
+
const decoratorMatch = line.match(
|
|
45
|
+
/@(?:router|api_router)\.(get|post|put|delete|patch)\s*\(\s*["'`]([^"'`]+)["'`]/
|
|
46
|
+
)
|
|
47
|
+
if (decoratorMatch) {
|
|
48
|
+
const method = decoratorMatch[1].toUpperCase()
|
|
49
|
+
const rawPath = decoratorMatch[2]
|
|
50
|
+
endpoints.push({
|
|
51
|
+
method,
|
|
52
|
+
path: routerPrefix + rawPath,
|
|
53
|
+
source: filePath,
|
|
54
|
+
line: i + 1,
|
|
55
|
+
})
|
|
56
|
+
continue
|
|
57
|
+
}
|
|
58
|
+
// 分散式: @router.get\n ("/path",
|
|
59
|
+
const splitMatch = line.match(/@(?:router|api_router)\.(get|post|put|delete|patch)\s*$/)
|
|
60
|
+
if (splitMatch && i + 1 < lines.length) {
|
|
61
|
+
const nextLine = lines[i + 1]
|
|
62
|
+
const pathMatch = nextLine.match(/\(\s*["'`]([^"'`]+)["'`]/)
|
|
63
|
+
if (pathMatch) {
|
|
64
|
+
endpoints.push({
|
|
65
|
+
method: splitMatch[1].toUpperCase(),
|
|
66
|
+
path: routerPrefix + pathMatch[1],
|
|
67
|
+
source: filePath,
|
|
68
|
+
line: i + 1,
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return endpoints
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 从单个文件提取 Express(Node)路由端点。
|
|
79
|
+
* 支持 router/app.get|post|put|delete|patch("path") 与链式 router.route("path").<method>()。
|
|
80
|
+
* app.use("prefix", router) 的前缀只做同文件近似(跨文件追踪不在首版范围)。
|
|
81
|
+
*
|
|
82
|
+
* @param {string} filePath - 文件绝对路径
|
|
83
|
+
* @returns {Array<{ method: string, path: string, source: string, line: number }>}
|
|
84
|
+
*/
|
|
85
|
+
export function extractExpressEndpoints(filePath) {
|
|
86
|
+
const content = readFileSync(filePath, 'utf8')
|
|
87
|
+
const lines = content.split('\n')
|
|
88
|
+
const endpoints = []
|
|
89
|
+
|
|
90
|
+
// 同文件内最后一个 app.use("prefix", ...) 作为前缀近似
|
|
91
|
+
let routerPrefix = ''
|
|
92
|
+
for (const line of lines) {
|
|
93
|
+
const useMatch = line.match(/\bapp\.use\s*\(\s*["'`]([^"'`]+)["'`]/)
|
|
94
|
+
if (useMatch) routerPrefix = useMatch[1]
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
for (let i = 0; i < lines.length; i++) {
|
|
98
|
+
const line = lines[i]
|
|
99
|
+
const m = line.match(/\b(?:app|router)\.(get|post|put|delete|patch)\s*\(\s*["'`]([^"'`]+)["'`]/i)
|
|
100
|
+
if (m) {
|
|
101
|
+
endpoints.push({ method: m[1].toUpperCase(), path: routerPrefix + m[2], source: filePath, line: i + 1 })
|
|
102
|
+
continue
|
|
103
|
+
}
|
|
104
|
+
const routeChain = line.match(/\.route\s*\(\s*["'`]([^"'`]+)["'`]/)
|
|
105
|
+
if (routeChain) {
|
|
106
|
+
// 链式 .route(path).get(h).put(h) —— 同行所有 method 配上 path
|
|
107
|
+
const chainPath = routerPrefix + routeChain[1]
|
|
108
|
+
for (const mm of line.matchAll(/\.(get|post|put|delete|patch)\s*\(/gi)) {
|
|
109
|
+
endpoints.push({ method: mm[1].toUpperCase(), path: chainPath, source: filePath, line: i + 1 })
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return endpoints
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* 从单个文件提取 Spring(Java)端点。
|
|
119
|
+
* 支持 @GetMapping/@PostMapping/.../@PatchMapping 与旧式 @RequestMapping(value="path", method=RequestMethod.X);
|
|
120
|
+
* 类级 @RequestMapping("api") 前缀合并(取文件内第一个作类前缀,近似)。
|
|
121
|
+
*
|
|
122
|
+
* @param {string} filePath - 文件绝对路径
|
|
123
|
+
* @returns {Array<{ method: string, path: string, source: string, line: number }>}
|
|
124
|
+
*/
|
|
125
|
+
export function extractSpringEndpoints(filePath) {
|
|
126
|
+
const content = readFileSync(filePath, 'utf8')
|
|
127
|
+
const lines = content.split('\n')
|
|
128
|
+
const endpoints = []
|
|
129
|
+
|
|
130
|
+
let classPrefix = ''
|
|
131
|
+
for (const line of lines) {
|
|
132
|
+
const cm = line.match(/@RequestMapping\s*\(\s*(?:value\s*=\s*)?["'`]([^"'`]+)["'`]/)
|
|
133
|
+
if (cm) { classPrefix = cm[1]; break }
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const methodMap = { get: 'GET', post: 'POST', put: 'PUT', delete: 'DELETE', patch: 'PATCH' }
|
|
137
|
+
|
|
138
|
+
for (let i = 0; i < lines.length; i++) {
|
|
139
|
+
const line = lines[i]
|
|
140
|
+
const shortMatch = line.match(/@(Get|Post|Put|Delete|Patch)Mapping\s*\(\s*(?:value\s*=\s*)?["'`]([^"'`]+)["'`]/i)
|
|
141
|
+
if (shortMatch) {
|
|
142
|
+
endpoints.push({ method: methodMap[shortMatch[1].toLowerCase()], path: classPrefix + shortMatch[2], source: filePath, line: i + 1 })
|
|
143
|
+
continue
|
|
144
|
+
}
|
|
145
|
+
const oldMatch = line.match(/@RequestMapping\s*\(\s*(?:value\s*=\s*)?["'`]([^"'`]+)["'`][^)]*?method\s*=\s*RequestMethod\.(\w+)/i)
|
|
146
|
+
if (oldMatch) {
|
|
147
|
+
endpoints.push({ method: oldMatch[2].toUpperCase(), path: classPrefix + oldMatch[1], source: filePath, line: i + 1 })
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return endpoints
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* 从目录递归扫描后端端点(多框架)。
|
|
156
|
+
* 按扩展名分派:.py→FastAPI / .js,.ts→Express / .java→Spring。
|
|
157
|
+
* 保留 excludePatterns(向后兼容 opts);filePattern 在多框架分派下不再适用。
|
|
158
|
+
* @param {string} dir
|
|
159
|
+
* @param {{ excludePatterns?: RegExp[] }} opts
|
|
160
|
+
* @returns {Array<{ method: string, path: string, source: string, line: number }>}
|
|
161
|
+
*/
|
|
162
|
+
export function scanBackendEndpoints(dir, opts = {}) {
|
|
163
|
+
// 排除清单对齐 scanFrontendApiCalls(坑 parity-scan-stale-dirs:.claude/worktrees 陈旧检出
|
|
164
|
+
// 与各类 build 产物混入端点扫描全是噪音)——backend 侧另含 __pycache__/.gradle/maven target
|
|
165
|
+
const excludePatterns = opts.excludePatterns || [
|
|
166
|
+
/^__pycache__$/, /^node_modules$/, /^\.venv$/, /^venv$/, /^\.git$/, /^\.gradle$/,
|
|
167
|
+
/^\.claude$/, /^\.vscode$/, /^\.idea$/, /^\.cursor$/, /^\.next$/, /^\.nuxt$/,
|
|
168
|
+
/^\.output$/, /^\.turbo$/, /^\.parcel-cache$/, /^coverage$/,
|
|
169
|
+
/^\.sillyspec$/, /^\.worktrees$/, /^\.d\.ts$/,
|
|
170
|
+
/^(dist|build|target|out)$/i,
|
|
171
|
+
/test/i,
|
|
172
|
+
]
|
|
173
|
+
|
|
174
|
+
const results = []
|
|
175
|
+
if (!existsSync(dir)) return results
|
|
176
|
+
|
|
177
|
+
function walk(d) {
|
|
178
|
+
for (const entry of readdirSync(d, { withFileTypes: true })) {
|
|
179
|
+
const full = join(d, entry.name)
|
|
180
|
+
if (entry.isDirectory()) {
|
|
181
|
+
if (excludePatterns.some(p => p.test(entry.name))) continue
|
|
182
|
+
walk(full)
|
|
183
|
+
} else if (entry.isFile()) {
|
|
184
|
+
const ext = extname(entry.name).toLowerCase()
|
|
185
|
+
try {
|
|
186
|
+
if (ext === '.py') results.push(...extractFastApiEndpoints(full))
|
|
187
|
+
else if (ext === '.js' || ext === '.ts') results.push(...extractExpressEndpoints(full))
|
|
188
|
+
else if (ext === '.java') results.push(...extractSpringEndpoints(full))
|
|
189
|
+
} catch {}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
walk(dir)
|
|
194
|
+
return results
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ─── Consumer: 扫描前端 API 调用路径 ─────────────────────────────────────
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* 从前端文件提取 API 调用路径
|
|
201
|
+
* 支持:
|
|
202
|
+
* apiFetch("/api/xxx")
|
|
203
|
+
* request("/api/xxx")
|
|
204
|
+
* axios.get("/api/xxx")
|
|
205
|
+
* axios.post("/api/xxx")
|
|
206
|
+
* fetch("/api/xxx")
|
|
207
|
+
*
|
|
208
|
+
* @param {string} filePath
|
|
209
|
+
* @returns {Array<{ method: string, path: string, source: string, line: number, raw: string }>}
|
|
210
|
+
*/
|
|
211
|
+
export function extractFrontendApiCalls(filePath) {
|
|
212
|
+
const content = readFileSync(filePath, 'utf8')
|
|
213
|
+
const lines = content.split('\n')
|
|
214
|
+
const results = []
|
|
215
|
+
|
|
216
|
+
for (let i = 0; i < lines.length; i++) {
|
|
217
|
+
const line = lines[i]
|
|
218
|
+
|
|
219
|
+
// Pattern 1: apiFetch<T>("/path", { method: "POST" }) — default GET
|
|
220
|
+
const apiFetchMatch = line.match(/apiFetch\s*(?:<[^>]*>)?\s*\(\s*["'`]([^"'`]+)["'`]/)
|
|
221
|
+
if (apiFetchMatch) {
|
|
222
|
+
// 检查是否有 method 字段(在后续行或同一行)
|
|
223
|
+
let method = 'GET'
|
|
224
|
+
const snippet = lines.slice(i, Math.min(i + 3, lines.length)).join(' ')
|
|
225
|
+
const methodMatch = snippet.match(/method\s*:\s*["'`](GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)["'`]/i)
|
|
226
|
+
if (methodMatch) method = methodMatch[1].toUpperCase()
|
|
227
|
+
|
|
228
|
+
results.push({
|
|
229
|
+
method,
|
|
230
|
+
path: normalizePath(apiFetchMatch[1]),
|
|
231
|
+
source: filePath,
|
|
232
|
+
line: i + 1,
|
|
233
|
+
raw: apiFetchMatch[1],
|
|
234
|
+
})
|
|
235
|
+
continue
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Pattern 2: axios.get/post/put/delete("/path")
|
|
239
|
+
const axiosMatch = line.match(/axios\.(get|post|put|delete|patch)\s*\(\s*["'`]([^"'`]+)["'`]/i)
|
|
240
|
+
if (axiosMatch) {
|
|
241
|
+
results.push({
|
|
242
|
+
method: axiosMatch[1].toUpperCase(),
|
|
243
|
+
path: normalizePath(axiosMatch[2]),
|
|
244
|
+
source: filePath,
|
|
245
|
+
line: i + 1,
|
|
246
|
+
raw: axiosMatch[2],
|
|
247
|
+
})
|
|
248
|
+
continue
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Pattern 3: fetch("/api/xxx", { method: "POST" })
|
|
252
|
+
const fetchMatch = line.match(/(?:api_?)?fetch\s*\(\s*["'`]([^"'`]+)["'`]/)
|
|
253
|
+
if (fetchMatch && !apiFetchMatch) {
|
|
254
|
+
let method = 'GET'
|
|
255
|
+
const snippet = lines.slice(i, Math.min(i + 3, lines.length)).join(' ')
|
|
256
|
+
const methodMatch = snippet.match(/method\s*:\s*["'`](GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)["'`]/i)
|
|
257
|
+
if (methodMatch) method = methodMatch[1].toUpperCase()
|
|
258
|
+
|
|
259
|
+
results.push({
|
|
260
|
+
method,
|
|
261
|
+
path: normalizePath(fetchMatch[1]),
|
|
262
|
+
source: filePath,
|
|
263
|
+
line: i + 1,
|
|
264
|
+
raw: fetchMatch[1],
|
|
265
|
+
})
|
|
266
|
+
continue
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return results
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* 递归扫描前端目录的 API 调用
|
|
275
|
+
* @param {string} dir
|
|
276
|
+
* @param {{ filePattern?: RegExp, excludePatterns?: RegExp[] }} opts
|
|
277
|
+
* @returns {Array<{ method: string, path: string, source: string, line: number, raw: string }>}
|
|
278
|
+
*/
|
|
279
|
+
export function scanFrontendApiCalls(dir, opts = {}) {
|
|
280
|
+
const filePattern = opts.filePattern || /\.(ts|tsx|js|jsx)$/
|
|
281
|
+
// 排除清单(坑 parity-scan-stale-dirs,2026-08-21 实证:扫进 .claude/worktrees/agent-* 陈旧
|
|
282
|
+
// 检出与 build 产物,872 条调用全是噪音)——目录名匹配,覆盖:
|
|
283
|
+
// 版本控制/编辑器:.git .claude(含 worktrees/agent-* 陈旧检出).vscode .idea .cursor
|
|
284
|
+
// 构建产物:dist build out .next .nuxt .output .turbo .parcel-cache coverage
|
|
285
|
+
// 依赖/运行时:node_modules .venv venv target(rust/java)__pycache__
|
|
286
|
+
// 本工具自身:.sillyspec .sillyspec-platform.json 所在 .worktrees
|
|
287
|
+
// 文件级:.d.ts 声明、*.min.js / *.bundle.js 压缩产物(正则命中即跳)
|
|
288
|
+
const excludePatterns = opts.excludePatterns || [
|
|
289
|
+
/^node_modules$/, /^\.git$/, /^\.claude$/, /^\.vscode$/, /^\.idea$/, /^\.cursor$/,
|
|
290
|
+
/^dist$/, /^build$/, /^out$/, /^\.next$/, /^\.nuxt$/, /^\.output$/, /^\.turbo$/,
|
|
291
|
+
/^\.parcel-cache$/, /^coverage$/, /^\.venv$/, /^venv$/, /^target$/, /^__pycache__$/,
|
|
292
|
+
/^\.sillyspec$/, /^\.worktrees$/, /^__tests__$/, /^\.d\.ts$/,
|
|
293
|
+
]
|
|
294
|
+
|
|
295
|
+
const results = []
|
|
296
|
+
if (!existsSync(dir)) return results
|
|
297
|
+
|
|
298
|
+
function walk(d) {
|
|
299
|
+
for (const entry of readdirSync(d, { withFileTypes: true })) {
|
|
300
|
+
const full = join(d, entry.name)
|
|
301
|
+
if (entry.isDirectory()) {
|
|
302
|
+
if (excludePatterns.some(p => p.test(entry.name))) continue
|
|
303
|
+
walk(full)
|
|
304
|
+
} else if (entry.isFile() && filePattern.test(entry.name)) {
|
|
305
|
+
if (/\.d\.ts$/.test(entry.name)) continue
|
|
306
|
+
if (/\.min\.(js|ts)$/.test(entry.name) || /\.bundle\.(js|ts)$/.test(entry.name)) continue
|
|
307
|
+
try {
|
|
308
|
+
results.push(...extractFrontendApiCalls(full))
|
|
309
|
+
} catch {}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
walk(dir)
|
|
314
|
+
return results
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// ─── 路径归一化 ─────────────────────────────────────────────────────────
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* 将动态路径归一化为参数占位符
|
|
321
|
+
* /api/ppm/project-plan/${id}/plan-nodes → /api/ppm/project-plan/{param}/plan-nodes
|
|
322
|
+
* /api/ppm/project-plan/:planId/plan-nodes → /api/ppm/project-plan/{param}/plan-nodes
|
|
323
|
+
* @param {string} rawPath
|
|
324
|
+
* @returns {string}
|
|
325
|
+
*/
|
|
326
|
+
export function normalizePath(rawPath) {
|
|
327
|
+
return rawPath
|
|
328
|
+
.replace(/\$\{[^}]+\}/g, '{param}')
|
|
329
|
+
.replace(/:\w+/g, '{param}')
|
|
330
|
+
.replace(/\$\w+/g, '{param}')
|
|
331
|
+
.replace(/\{[^}]+\}/g, '{param}') // Spring {planId} / FastAPI {plan_id} → {param}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// ─── 对账 ───────────────────────────────────────────────────────────────
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* 比较前端调用的路径和后端注册的端点,返回差异
|
|
338
|
+
*
|
|
339
|
+
* @param {Array<{ path: string, method: string, source: string }>} frontendCalls
|
|
340
|
+
* @param {Array<{ path: string, method: string, source: string }>} backendEndpoints
|
|
341
|
+
* @returns {{
|
|
342
|
+
* missingBackend: Array<{ path: string, method: string, consumerFile: string, consumerLine: number }>,
|
|
343
|
+
* unusedBackend: Array<{ path: string, method: string, providerFile: string }>
|
|
344
|
+
* }}
|
|
345
|
+
*/
|
|
346
|
+
export function diffApiParity(frontendCalls, backendEndpoints) {
|
|
347
|
+
// 构建 backend 注册表:归一化 path + method → endpoint
|
|
348
|
+
const backendMap = new Map()
|
|
349
|
+
for (const ep of backendEndpoints) {
|
|
350
|
+
const key = `${ep.method}:${normalizePath(ep.path)}`
|
|
351
|
+
if (!backendMap.has(key)) backendMap.set(key, ep)
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const missingBackend = []
|
|
355
|
+
for (const call of frontendCalls) {
|
|
356
|
+
const key = `${call.method}:${normalizePath(call.path)}`
|
|
357
|
+
if (!backendMap.has(key)) {
|
|
358
|
+
missingBackend.push({
|
|
359
|
+
path: normalizePath(call.path),
|
|
360
|
+
method: call.method,
|
|
361
|
+
consumerFile: call.source,
|
|
362
|
+
consumerLine: call.line,
|
|
363
|
+
})
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// 构建 frontend 调用表
|
|
368
|
+
const frontendSet = new Set(
|
|
369
|
+
frontendCalls.map(c => `${c.method}:${normalizePath(c.path)}`)
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
const unusedBackend = []
|
|
373
|
+
for (const ep of backendEndpoints) {
|
|
374
|
+
const key = `${ep.method}:${normalizePath(ep.path)}`
|
|
375
|
+
if (!frontendSet.has(key)) {
|
|
376
|
+
unusedBackend.push({
|
|
377
|
+
path: normalizePath(ep.path),
|
|
378
|
+
method: ep.method,
|
|
379
|
+
providerFile: ep.source,
|
|
380
|
+
})
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return { missingBackend, unusedBackend, ok: missingBackend.length === 0 }
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// ─── CLI 入口 ────────────────────────────────────────────────────────────
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* CLI 子命令入口:sillyspec contract scan [--backend dir] [--frontend dir]
|
|
391
|
+
* 输出 JSON 格式的端点清单和对账结果
|
|
392
|
+
*/
|
|
393
|
+
export async function contractScan(args, cwd) {
|
|
394
|
+
const backendIdx = args.indexOf('--backend')
|
|
395
|
+
const frontendIdx = args.indexOf('--frontend')
|
|
396
|
+
const backendDir = backendIdx !== -1 && args[backendIdx + 1]
|
|
397
|
+
? resolve(cwd, args[backendIdx + 1])
|
|
398
|
+
: resolve(cwd, 'backend')
|
|
399
|
+
const frontendDir = frontendIdx !== -1 && args[frontendIdx + 1]
|
|
400
|
+
? resolve(cwd, args[frontendIdx + 1])
|
|
401
|
+
: resolve(cwd, 'frontend')
|
|
402
|
+
|
|
403
|
+
const backendEndpoints = scanBackendEndpoints(backendDir)
|
|
404
|
+
const frontendCalls = scanFrontendApiCalls(frontendDir)
|
|
405
|
+
const { missingBackend, unusedBackend } = diffApiParity(frontendCalls, backendEndpoints)
|
|
406
|
+
|
|
407
|
+
return {
|
|
408
|
+
backend: backendEndpoints.map(e => ({ method: e.method, path: normalizePath(e.path), file: e.source })),
|
|
409
|
+
frontend: frontendCalls.map(c => ({ method: c.method, path: normalizePath(c.path), file: c.source })),
|
|
410
|
+
missingBackend,
|
|
411
|
+
unusedBackend,
|
|
412
|
+
summary: {
|
|
413
|
+
backendEndpointCount: backendEndpoints.length,
|
|
414
|
+
frontendCallCount: frontendCalls.length,
|
|
415
|
+
missingBackendCount: missingBackend.length,
|
|
416
|
+
unusedBackendCount: unusedBackend.length,
|
|
417
|
+
ok: missingBackend.length === 0,
|
|
418
|
+
},
|
|
419
|
+
}
|
|
420
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 他者会话显式声明文件集(坑 verify-reconcile-foreign-wip,2026-08-23 实证:verify 实测
|
|
3
|
+
* 对账在主仓共享工作区取 git diff,无 meta 回退时把并行会话在途 WIP 全量算进本变更——误
|
|
4
|
+
* 命中他者模块跑他者测试 / 他者删除进本变更删除对账误报)。
|
|
5
|
+
*
|
|
6
|
+
* 归属口径与坑 35(quick 审计豁免,run/shared 的 collectOtherQuickSessionDeclarations)
|
|
7
|
+
* 同款:只认**显式声明**——其他 quick 会话 guard.allowedFiles + 其他变更 design §6 清单;
|
|
8
|
+
* 无主文件保留参与判定(fail-closed,不放松「未声明删除/越界」的抓取底线)。
|
|
9
|
+
*
|
|
10
|
+
* 独立零环模块:被 verify-postcheck / verify-probes / contract-matrix 共用(verify-
|
|
11
|
+
* postcheck import contract-matrix,parity 侧不能反向 import verify-postcheck)。
|
|
12
|
+
*/
|
|
13
|
+
import { existsSync, readFileSync, readdirSync } from 'fs'
|
|
14
|
+
import { join } from 'path'
|
|
15
|
+
import { parseFileChangeListDetailed } from './change-list.js'
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param {string} cwd 项目根
|
|
19
|
+
* @param {string|null} currentChangeName 本变更名(quick 会话名或 change 名,排除自身)
|
|
20
|
+
* @returns {Map<string, string[]>} file → 声明者列表(quick-<hex> 或变更名)
|
|
21
|
+
*/
|
|
22
|
+
export function collectForeignDeclaredFiles(cwd, currentChangeName) {
|
|
23
|
+
const foreign = new Map()
|
|
24
|
+
const add = (file, owner) => {
|
|
25
|
+
const f = String(file || '').replace(/\\/g, '/')
|
|
26
|
+
if (!f) return
|
|
27
|
+
if (!foreign.has(f)) foreign.set(f, [])
|
|
28
|
+
if (!foreign.get(f).includes(owner)) foreign.get(f).push(owner)
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
const specBase = join(cwd, '.sillyspec')
|
|
32
|
+
// ① 其他 quick 会话的 --files 显式声明(guard.json)
|
|
33
|
+
const sessionsDir = join(specBase, '.runtime', 'quick-sessions')
|
|
34
|
+
let sessionDirs = []
|
|
35
|
+
try { sessionDirs = readdirSync(sessionsDir, { withFileTypes: true }).filter(e => e.isDirectory()).map(e => e.name) } catch {}
|
|
36
|
+
for (const sid of sessionDirs) {
|
|
37
|
+
if (sid === currentChangeName) continue
|
|
38
|
+
try {
|
|
39
|
+
const guard = JSON.parse(readFileSync(join(sessionsDir, sid, 'guard.json'), 'utf8'))
|
|
40
|
+
for (const f of Array.isArray(guard.allowedFiles) ? guard.allowedFiles : []) add(f, sid)
|
|
41
|
+
} catch { /* 损坏/缺失跳过 */ }
|
|
42
|
+
}
|
|
43
|
+
// ② 其他变更的 design §6 清单(目录在即视为在途变更;archive/ 下不算)
|
|
44
|
+
const changesDir = join(specBase, 'changes')
|
|
45
|
+
let changeDirs = []
|
|
46
|
+
try { changeDirs = readdirSync(changesDir, { withFileTypes: true }).filter(e => e.isDirectory()).map(e => e.name) } catch {}
|
|
47
|
+
for (const cn of changeDirs) {
|
|
48
|
+
if (cn === currentChangeName || cn === 'archive') continue
|
|
49
|
+
const designPath = join(changesDir, cn, 'design.md')
|
|
50
|
+
if (!existsSync(designPath)) continue
|
|
51
|
+
try {
|
|
52
|
+
for (const e of parseFileChangeListDetailed(designPath)) add(e.path, cn)
|
|
53
|
+
} catch { /* 清单解析失败跳过该变更 */ }
|
|
54
|
+
}
|
|
55
|
+
} catch { /* 整体异常 → 空集(等价不过滤,回到原行为) */ }
|
|
56
|
+
return foreign
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 按他者声明集切分 diff 文件(collectForeignDeclaredFiles 的消费端快捷封装)。
|
|
61
|
+
* @param {string} cwd 项目根
|
|
62
|
+
* @param {string|null} currentChangeName 本变更名
|
|
63
|
+
* @param {string[]|null} diffFiles 待切分文件列表(null 透传,保持调用方 null 语义)
|
|
64
|
+
* @returns {{ own: string[]|null, foreign: Array<{file: string, owners: string[]}> }}
|
|
65
|
+
*/
|
|
66
|
+
export function splitOwnVsForeignDiffFiles(cwd, currentChangeName, diffFiles) {
|
|
67
|
+
if (!Array.isArray(diffFiles)) return { own: diffFiles, foreign: [] }
|
|
68
|
+
const foreignMap = collectForeignDeclaredFiles(cwd, currentChangeName)
|
|
69
|
+
if (foreignMap.size === 0) return { own: diffFiles, foreign: [] }
|
|
70
|
+
const own = []
|
|
71
|
+
const foreign = []
|
|
72
|
+
for (const f of diffFiles) {
|
|
73
|
+
const owners = foreignMap.get(String(f).replace(/\\/g, '/'))
|
|
74
|
+
if (owners) foreign.push({ file: f, owners })
|
|
75
|
+
else own.push(f)
|
|
76
|
+
}
|
|
77
|
+
return { own, foreign }
|
|
78
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fs-atomic.js — 原子文件写 + Windows 友好的 rename 重试
|
|
3
|
+
*
|
|
4
|
+
* 用于 .runtime/*.json、pointer、guard.json 等「会被其他进程 / hook 读取」的配置文件。
|
|
5
|
+
* 保证读者要么看到旧完整内容、要么看到新完整内容,不会读到半截 JSON。
|
|
6
|
+
*
|
|
7
|
+
* 注意:DB 持久化由 node:sqlite 引擎承担(提交即落盘 sillyspec.db + WAL 侧车),
|
|
8
|
+
* 不经此处的 writeAtomicSync;sillyspec.db 不走原子写改名。
|
|
9
|
+
*/
|
|
10
|
+
import { writeFileSync, renameSync, unlinkSync } from 'fs';
|
|
11
|
+
import { randomBytes } from 'node:crypto';
|
|
12
|
+
import { dirname, basename, join } from 'path';
|
|
13
|
+
|
|
14
|
+
// Windows 上杀毒 / 索引 / IDE 占用文件时,rename 偶发这些错误,短退避重试即可
|
|
15
|
+
const RENAME_RETRY_CODES = ['EPERM', 'EBUSY', 'EACCES', 'ENOTEMPTY'];
|
|
16
|
+
|
|
17
|
+
/** 同步退避:优先 Atomics.wait,不可用时退化忙等(仅在 rename 冲突的极端情况触发)。 */
|
|
18
|
+
function sleepSync(ms) {
|
|
19
|
+
try {
|
|
20
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
21
|
+
} catch {
|
|
22
|
+
const end = Date.now() + ms;
|
|
23
|
+
while (Date.now() < end) { /* busy wait */ }
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* rename 带退避重试,覆盖 Windows 偶发的 EPERM/EBUSY/EACCES/ENOTEMPTY。
|
|
29
|
+
* 目录 rename 同样适用(归档移动目录时可复用)。
|
|
30
|
+
* @param {string} from
|
|
31
|
+
* @param {string} to
|
|
32
|
+
* @param {number} [retries=5]
|
|
33
|
+
*/
|
|
34
|
+
export function renameSyncRetry(from, to, retries = 5) {
|
|
35
|
+
let lastErr;
|
|
36
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
37
|
+
try {
|
|
38
|
+
renameSync(from, to);
|
|
39
|
+
return;
|
|
40
|
+
} catch (err) {
|
|
41
|
+
lastErr = err;
|
|
42
|
+
const code = err && err.code;
|
|
43
|
+
// 非可重试错误码立即抛出,不浪费时间
|
|
44
|
+
if (!code || !RENAME_RETRY_CODES.includes(code)) throw err;
|
|
45
|
+
if (attempt < retries) sleepSync(15 * (attempt + 1));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
throw lastErr;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 原子写文本文件:同目录 tmp 写入 → rename 覆盖目标(带 Windows 退避重试)。
|
|
53
|
+
* - tmp 名含 pid,避免多进程并发写同一目标时 tmp 名碰撞(如多会话写 local.yaml)。
|
|
54
|
+
* - rename 失败时清理 tmp,避免留下孤儿 .tmp 文件。
|
|
55
|
+
* - Node 的 rename 在 Windows 上用 MoveFileEx(REPLACE_EXISTING),可覆盖已存在目标;
|
|
56
|
+
* EPERM 仅在文件被其他进程锁定时出现,由 renameSyncRetry 退避重试。
|
|
57
|
+
* @param {string} filePath - 目标文件绝对路径
|
|
58
|
+
* @param {string} content - 文件内容
|
|
59
|
+
*/
|
|
60
|
+
export function writeAtomicSync(filePath, content) {
|
|
61
|
+
const dir = dirname(filePath);
|
|
62
|
+
// pid + 随机段双因子:Windows PID 重用激进,两进程可能撞同 pid,tmp 名碰撞 rename 互相覆盖。
|
|
63
|
+
// crypto 随机源(Math.random 可预测,tmp 名被预猜中有 symlink 预放置理论面)
|
|
64
|
+
const rnd = randomBytes(6).toString('hex');
|
|
65
|
+
const tmpPath = join(dir, `.${basename(filePath)}.${process.pid}.${rnd}.tmp`);
|
|
66
|
+
writeFileSync(tmpPath, content);
|
|
67
|
+
try {
|
|
68
|
+
renameSyncRetry(tmpPath, filePath);
|
|
69
|
+
} catch (err) {
|
|
70
|
+
try { unlinkSync(tmpPath); } catch { /* tmp 可能已被 rename 走,忽略 */ }
|
|
71
|
+
throw err;
|
|
72
|
+
}
|
|
73
|
+
}
|