sillyspec 3.20.3 → 3.20.4
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-archive/SKILL.md +21 -17
- package/.claude/skills/sillyspec-auto/SKILL.md +83 -77
- package/.claude/skills/sillyspec-brainstorm/SKILL.md +44 -17
- package/.claude/skills/sillyspec-commit/SKILL.md +106 -105
- package/.claude/skills/sillyspec-continue/SKILL.md +45 -44
- package/.claude/skills/sillyspec-doctor/SKILL.md +31 -22
- package/.claude/skills/sillyspec-execute/SKILL.md +30 -17
- package/.claude/skills/sillyspec-explore/SKILL.md +109 -96
- package/.claude/skills/sillyspec-knowledge/SKILL.md +270 -0
- package/.claude/skills/sillyspec-plan/SKILL.md +21 -52
- package/.claude/skills/sillyspec-propose/SKILL.md +21 -17
- package/.claude/skills/sillyspec-quick/SKILL.md +21 -17
- package/.claude/skills/sillyspec-resume/SKILL.md +68 -111
- package/.claude/skills/sillyspec-scan/SKILL.md +21 -17
- package/.claude/skills/sillyspec-state/SKILL.md +54 -54
- package/.claude/skills/sillyspec-status/SKILL.md +21 -17
- package/.claude/skills/sillyspec-verify/SKILL.md +21 -17
- package/.claude/skills/sillyspec-workspace/SKILL.md +157 -149
- package/.husky/pre-push +13 -0
- package/CLAUDE.md +18 -0
- package/README.md +198 -186
- package/SKILL.md +90 -87
- package/bin/sillyspec.js +2 -2
- package/docs/brainstorm-plan-contract.md +64 -0
- package/docs/plan-execute-contract.md +123 -0
- package/docs/platform-scan-protocol.md +298 -0
- package/docs/revision-mode.md +115 -0
- package/docs/sillyspec/file-lifecycle/known-implementation-gaps.md +99 -0
- package/docs/sillyspec/file-lifecycle/platform-workflows-sync.md +218 -0
- package/docs/sillyspec/file-lifecycle/stage-artifacts.md +167 -0
- package/docs/sillyspec/file-lifecycle/storage-and-state.md +148 -0
- package/docs/sillyspec/file-lifecycle/worktree-and-guard.md +211 -0
- package/docs/sillyspec/file-lifecycle.md +125 -0
- package/docs/workflow-contract-regression.md +106 -0
- package/docs/worktree-isolation.md +252 -0
- package/package.json +40 -34
- package/packages/dashboard/dist/assets/{index-D1EVTLmc.js → index-Bq_Z2hne.js} +7446 -7446
- package/packages/dashboard/dist/assets/index-O2W5RV4z.css +1 -0
- package/packages/dashboard/dist/index.html +16 -16
- package/packages/dashboard/dist/prototype-dashboard.html +836 -0
- package/packages/dashboard/dist/prototype-overview.html +256 -0
- package/packages/dashboard/index.html +15 -15
- package/packages/dashboard/package-lock.json +2384 -2384
- package/packages/dashboard/package.json +25 -25
- package/packages/dashboard/public/prototype-dashboard.html +836 -0
- package/packages/dashboard/public/prototype-overview.html +256 -0
- package/packages/dashboard/server/executor.js +86 -86
- package/packages/dashboard/server/index.js +588 -508
- package/packages/dashboard/server/parser.js +526 -458
- package/packages/dashboard/server/watcher.js +350 -342
- package/packages/dashboard/src/App.vue +558 -325
- package/packages/dashboard/src/components/ActionBar.vue +93 -84
- package/packages/dashboard/src/components/CommandPalette.vue +96 -92
- package/packages/dashboard/src/components/DetailPanel.vue +137 -137
- package/packages/dashboard/src/components/DocPreview.vue +105 -8
- package/packages/dashboard/src/components/DocTree.vue +75 -19
- package/packages/dashboard/src/components/HResizeHandle.vue +48 -0
- package/packages/dashboard/src/components/LogStream.vue +65 -65
- package/packages/dashboard/src/components/PipelineStage.vue +95 -75
- package/packages/dashboard/src/components/PipelineView.vue +156 -129
- package/packages/dashboard/src/components/ProjectCard.vue +187 -0
- package/packages/dashboard/src/components/ProjectList.vue +210 -210
- package/packages/dashboard/src/components/ProjectOverview.vue +113 -139
- package/packages/dashboard/src/components/StageBadge.vue +67 -53
- package/packages/dashboard/src/components/StepCard.vue +94 -89
- package/packages/dashboard/src/components/VResizeHandle.vue +61 -0
- package/packages/dashboard/src/components/detail/DocsDetail.vue +48 -48
- package/packages/dashboard/src/components/detail/GitDetail.vue +61 -61
- package/packages/dashboard/src/components/detail/TechDetail.vue +43 -43
- package/packages/dashboard/src/composables/useDashboard.js +192 -164
- package/packages/dashboard/src/composables/useKeyboard.js +119 -119
- package/packages/dashboard/src/composables/useLayout.js +131 -0
- package/packages/dashboard/src/composables/useWebSocket.js +129 -129
- package/packages/dashboard/src/main.js +8 -8
- package/packages/dashboard/src/style.css +132 -132
- package/packages/dashboard/vite.config.js +18 -18
- package/src/brainstorm-postcheck.js +158 -0
- package/src/change-list.js +52 -0
- package/src/change-risk-profile.js +352 -0
- package/src/classify-change.js +73 -0
- package/src/constants.js +70 -0
- package/src/contract-matrix.js +278 -0
- package/src/db.js +201 -0
- package/src/endpoint-extractor.js +315 -0
- package/src/hooks/claude-pre-tool-use.cjs +125 -0
- package/src/hooks/worktree-guard.js +761 -0
- package/src/index.js +922 -252
- package/src/init.js +431 -375
- package/src/knowledge-match.js +130 -0
- package/src/migrate.js +117 -117
- package/src/modules.js +482 -0
- package/src/progress.js +1734 -508
- package/src/run.js +3465 -652
- package/src/scan-postcheck.js +387 -0
- package/src/setup.js +398 -460
- package/src/stage-contract.js +700 -0
- package/src/stages/archive.js +160 -54
- package/src/stages/brainstorm-auto.js +229 -0
- package/src/stages/brainstorm.js +645 -239
- package/src/stages/doctor.js +365 -312
- package/src/stages/execute.js +625 -264
- package/src/stages/explore.js +34 -0
- package/src/stages/index.js +29 -35
- package/src/stages/knowledge.js +498 -0
- package/src/stages/plan-postcheck.js +511 -0
- package/src/stages/plan.js +582 -279
- package/src/stages/propose.js +174 -115
- package/src/stages/quick.js +102 -63
- package/src/stages/scan.js +558 -141
- package/src/stages/status.js +65 -65
- package/src/stages/verify.js +322 -135
- package/src/sync.js +497 -0
- package/src/task-review.js +346 -0
- package/src/workflow.js +785 -0
- package/src/worktree-apply.js +549 -0
- package/src/worktree-deps.js +185 -0
- package/src/worktree.js +982 -0
- package/templates/workflows/archive-impact.yaml +79 -0
- package/templates/workflows/scan-docs.yaml +132 -0
- package/test/brainstorm-plan-contract.test.mjs +273 -0
- package/test/check-syntax.mjs +26 -0
- package/test/cli-top-level-aliases.test.mjs +174 -0
- package/test/contract-artifacts.test.mjs +323 -0
- package/test/decision-supersede.test.mjs +277 -0
- package/test/knowledge-match.test.mjs +231 -0
- package/test/plan-execute-contract.test.mjs +330 -0
- package/test/plan-optimization.test.mjs +572 -0
- package/test/platform-artifacts.test.mjs +190 -0
- package/test/platform-failure-samples.test.mjs +199 -0
- package/test/platform-recovery-chain.test.mjs +179 -0
- package/test/platform-recovery.test.mjs +167 -0
- package/test/platform-scan-p0.test.mjs +175 -0
- package/test/revision-v1.test.mjs +1145 -0
- package/test/run-sanitize-project-name.test.mjs +51 -0
- package/test/run-scan-postcheck-fail.test.mjs +64 -0
- package/test/run-scan-project-parse.test.mjs +200 -0
- package/test/run-tests.mjs +48 -0
- package/test/scan-docs-yaml-placeholders.test.mjs +84 -0
- package/test/scan-knowledge.test.mjs +175 -0
- package/test/scan-paths.test.mjs +68 -0
- package/test/scan-postcheck-project-priority.test.mjs +85 -0
- package/test/scan-postcheck.test.mjs +197 -0
- package/test/scan-workflow-anyfailed-block.test.mjs +52 -0
- package/test/spec-dir.test.mjs +206 -0
- package/test/stage-contract-failed-post-check.test.mjs +102 -0
- package/test/stage-contract.test.mjs +299 -0
- package/test/stage-definitions.test.mjs +39 -0
- package/test/wait-gates.test.mjs +501 -0
- package/test/workflow-spec-base.test.mjs +142 -0
- package/test/worktree-deps-provision.test.mjs +148 -0
- package/test/worktree-guard.test.mjs +136 -0
- package/test/worktree-native-overlay.test.mjs +188 -0
- package/.sillyspec/changes/archive/2026-04-08-derive-state/design.md +0 -97
- package/.sillyspec/changes/archive/2026-04-08-derive-state/plan.md +0 -51
- package/.sillyspec/changes/archive/2026-04-08-derive-state/proposal.md +0 -29
- package/.sillyspec/changes/archive/2026-04-08-derive-state/requirements.md +0 -34
- package/.sillyspec/changes/archive/2026-04-08-derive-state/tasks.md +0 -13
- package/.sillyspec/changes/archive/2026-04-08-derive-state/verify-result.md +0 -43
- package/.sillyspec/changes/auto-mode/design.md +0 -50
- package/.sillyspec/changes/auto-mode/proposal.md +0 -19
- package/.sillyspec/changes/auto-mode/requirements.md +0 -21
- package/.sillyspec/changes/auto-mode/tasks.md +0 -7
- package/.sillyspec/changes/brainstorm-archive/2026-04-05-dashboard-design.md +0 -206
- package/.sillyspec/changes/brainstorm-archive/2026-04-05-unified-docs-design.md +0 -199
- package/.sillyspec/changes/dashboard/design.md +0 -219
- package/.sillyspec/changes/dashboard/design.md.braindraft +0 -206
- package/.sillyspec/changes/run-command-design/design.md +0 -1230
- package/.sillyspec/changes/unified-docs-design/design.md +0 -199
- package/.sillyspec/docs/sillyspec/scan/.gitkeep +0 -0
- package/.sillyspec/knowledge/INDEX.md +0 -8
- package/.sillyspec/knowledge/uncategorized.md +0 -3
- package/.sillyspec/plans/2026-04-05-dashboard.md +0 -737
- package/.sillyspec/projects/sillyspec.yaml +0 -3
- package/dist/steps/brainstorm/01-load-context.md +0 -30
- package/dist/steps/brainstorm/02-reuse-check.md +0 -6
- package/dist/steps/brainstorm/03-prototype-analysis.md +0 -11
- package/dist/steps/brainstorm/04-module-split.md +0 -23
- package/dist/steps/brainstorm/05-dialog-explore.md +0 -8
- package/dist/steps/brainstorm/06-propose-approaches.md +0 -3
- package/dist/steps/brainstorm/07-present-design.md +0 -3
- package/dist/steps/brainstorm/08-write-design.md +0 -21
- package/dist/steps/brainstorm/09-self-review.md +0 -15
- package/dist/steps/brainstorm/10-user-confirm.md +0 -3
- package/dist/steps/brainstorm/11-output-spec.md +0 -7
- package/dist/steps/brainstorm/manifest.yaml +0 -26
- package/dist/steps/execute/01-load-context.md +0 -41
- package/dist/steps/execute/02-scan-conventions.md +0 -47
- package/dist/steps/execute/03-skill-mcp.md +0 -19
- package/dist/steps/execute/04-assign-task.md +0 -22
- package/dist/steps/execute/04b-prompt-template.md +0 -54
- package/dist/steps/execute/05-write-test.md +0 -7
- package/dist/steps/execute/06-write-code.md +0 -8
- package/dist/steps/execute/07-run-test.md +0 -26
- package/dist/steps/execute/08-fix-issues.md +0 -28
- package/dist/steps/execute/09-next-task.md +0 -33
- package/dist/steps/execute/manifest.yaml +0 -28
- package/dist/steps/plan/01-load-context.md +0 -22
- package/dist/steps/plan/02-anchor-confirm.md +0 -1
- package/dist/steps/plan/03-expand-tasks.md +0 -33
- package/dist/steps/plan/04-mark-order.md +0 -15
- package/dist/steps/plan/05-e2e-planning.md +0 -17
- package/dist/steps/plan/06-self-check.md +0 -16
- package/dist/steps/plan/07-save.md +0 -1
- package/dist/steps/plan/manifest.yaml +0 -18
- package/dist/steps/scan/01-env-detect.md +0 -51
- package/dist/steps/scan/02-tech-stack.md +0 -16
- package/dist/steps/scan/03-conventions.md +0 -16
- package/dist/steps/scan/04-structure.md +0 -19
- package/dist/steps/scan/05-quality.md +0 -18
- package/dist/steps/scan/06-complete.md +0 -49
- package/dist/steps/scan/manifest.yaml +0 -16
- package/dist/steps/verify/01-load-specs.md +0 -28
- package/dist/steps/verify/02-check-tasks.md +0 -1
- package/dist/steps/verify/03-check-design.md +0 -6
- package/dist/steps/verify/04-run-tests.md +0 -7
- package/dist/steps/verify/05-e2e-tests.md +0 -27
- package/dist/steps/verify/05b-e2e-fix.md +0 -33
- package/dist/steps/verify/06-code-quality.md +0 -25
- package/dist/steps/verify/07-lint-check.md +0 -27
- package/dist/steps/verify/08-output-report.md +0 -14
- package/dist/steps/verify/manifest.yaml +0 -22
- package/packages/dashboard/dist/assets/index-DGe8CqeP.css +0 -1
- package/src/derive.js +0 -147
- package/src/step.js +0 -543
package/src/constants.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SillySpec 平台状态枚举
|
|
3
|
+
*
|
|
4
|
+
* 所有平台产物(manifest、pointer、postcheck、workflow-runs)共享这些枚举。
|
|
5
|
+
* SillyHub 侧直接使用常量值,不需要猜字符串。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// ── scan 阶段状态 ──
|
|
9
|
+
export const SCAN_STATUS = Object.freeze({
|
|
10
|
+
PENDING: 'pending', // scan 未开始
|
|
11
|
+
IN_PROGRESS: 'in_progress', // scan 进行中
|
|
12
|
+
SUCCESS: 'success', // scan 成功(所有检查通过)
|
|
13
|
+
COMPLETED_WITH_WARNINGS: 'completed_with_warnings', // scan 成功但有警告
|
|
14
|
+
FAILED_POST_CHECK: 'failed_post_check', // scan 失败(post-check 不通过)
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
// ── 平台指针状态 ──
|
|
18
|
+
export const POINTER_STATUS = Object.freeze({
|
|
19
|
+
ACTIVE: 'active', // 指针活跃,任务进行中
|
|
20
|
+
SCAN_COMPLETED: 'scan_completed', // scan 已完成
|
|
21
|
+
STALE: 'stale', // 指针过时(完成超过 24h,建议清理)
|
|
22
|
+
CORRUPTED: 'corrupted', // 指针损坏(缺少必要字段)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
// ── workflow 检查状态 ──
|
|
26
|
+
export const WORKFLOW_STATUS = Object.freeze({
|
|
27
|
+
PASS: 'pass',
|
|
28
|
+
FAIL: 'fail',
|
|
29
|
+
SKIPPED: 'skipped',
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
// ── postcheck 检查严重级别 ──
|
|
33
|
+
export const CHECK_SEVERITY = Object.freeze({
|
|
34
|
+
FAILED: 'failed',
|
|
35
|
+
WARNING: 'warning',
|
|
36
|
+
PASSED: 'passed',
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
// ── stage 步骤状态 ──
|
|
40
|
+
export const STEP_STATUS = Object.freeze({
|
|
41
|
+
PENDING: 'pending',
|
|
42
|
+
IN_PROGRESS: 'in-progress',
|
|
43
|
+
COMPLETED: 'completed',
|
|
44
|
+
SKIPPED: 'skipped',
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
// ── stage 阶段状态 ──
|
|
48
|
+
export const STAGE_STATUS = Object.freeze({
|
|
49
|
+
PENDING: 'pending',
|
|
50
|
+
IN_PROGRESS: 'in_progress',
|
|
51
|
+
COMPLETED: 'completed',
|
|
52
|
+
FAILED_POST_CHECK: 'failed_post_check',
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 判断指针是否过时(完成超过 24h)
|
|
57
|
+
*/
|
|
58
|
+
export function isPointerStale(pointer) {
|
|
59
|
+
if (!pointer.completedAt) return false
|
|
60
|
+
const completed = new Date(pointer.completedAt)
|
|
61
|
+
const age = Date.now() - completed.getTime()
|
|
62
|
+
return age > 24 * 60 * 60 * 1000
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 判断指针是否损坏(缺少必要字段)
|
|
67
|
+
*/
|
|
68
|
+
export function isPointerCorrupted(pointer) {
|
|
69
|
+
return !pointer || !pointer.specRoot || !pointer.savedAt
|
|
70
|
+
}
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* contract-matrix.js — API Contract Matrix 生成与注入
|
|
3
|
+
*
|
|
4
|
+
* plan 阶段:识别 task 之间的 provider/consumer 关系,生成契约矩阵
|
|
5
|
+
* execute 阶段:
|
|
6
|
+
* - 后端 task 完成后自动提取 endpoint artifact
|
|
7
|
+
* - 前端 task 开始时注入上游契约
|
|
8
|
+
* verify 阶段:读取 artifact 做 parity check
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from 'fs'
|
|
12
|
+
import { join, resolve, basename, relative } from 'path'
|
|
13
|
+
import {
|
|
14
|
+
scanBackendEndpoints,
|
|
15
|
+
scanFrontendApiCalls,
|
|
16
|
+
normalizePath,
|
|
17
|
+
} from './endpoint-extractor.js'
|
|
18
|
+
|
|
19
|
+
// ─── 关键词检测 ─────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
const PROVIDER_KEYWORDS = /router|routes|endpoint|api|backend|controller|fastapi|flask|express|koa|spring/i
|
|
22
|
+
const CONSUMER_KEYWORDS = /frontend|client|service|apiFetch|request|fetch|axios|http/i
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 判断一个 task 文档是 provider(产出 API)还是 consumer(消费 API)
|
|
26
|
+
* @param {string} taskContent - task markdown 内容
|
|
27
|
+
* @returns {{ isProvider: boolean, isConsumer: boolean, confidence: number }}
|
|
28
|
+
*/
|
|
29
|
+
export function classifyTask(taskContent) {
|
|
30
|
+
const isProvider = PROVIDER_KEYWORDS.test(taskContent)
|
|
31
|
+
const isConsumer = CONSUMER_KEYWORDS.test(taskContent)
|
|
32
|
+
// 避免所有 task 都被标记(因为几乎所有 task 都含 "api")
|
|
33
|
+
// 加强判定:provider 要命中 router/endpoint/backend/controller + api
|
|
34
|
+
// consumer 要命中 frontend/client/apiFetch + api
|
|
35
|
+
const providerStrong = /router|endpoint|backend|controller|fastapi|flask/i.test(taskContent)
|
|
36
|
+
const consumerStrong = /frontend|apiFetch|axios|api.*client/i.test(taskContent)
|
|
37
|
+
const providerConfidence = providerStrong ? 0.8 : (isProvider ? 0.4 : 0)
|
|
38
|
+
const consumerConfidence = consumerStrong ? 0.8 : (isConsumer ? 0.4 : 0)
|
|
39
|
+
return {
|
|
40
|
+
isProvider: providerConfidence >= 0.4,
|
|
41
|
+
isConsumer: consumerConfidence >= 0.4,
|
|
42
|
+
confidence: Math.max(providerConfidence, consumerConfidence),
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 从 plan.md 解析 task 依赖关系,识别 provider → consumer 对
|
|
48
|
+
* @param {string} planContent - plan.md 内容
|
|
49
|
+
* @param {string} changeDir - changes/<name>/ 目录
|
|
50
|
+
* @returns {Array<{ provider: string, consumer: string, type: string }>}
|
|
51
|
+
*/
|
|
52
|
+
export function buildContractMatrix(planContent, changeDir) {
|
|
53
|
+
const contracts = []
|
|
54
|
+
|
|
55
|
+
// 解析 task 依赖关系
|
|
56
|
+
// 格式: - [ ] task-04: ... (depends_on: [task-01]) 或
|
|
57
|
+
// | task-04 | ... | 01 |
|
|
58
|
+
const taskDeps = parseTaskDependencies(planContent)
|
|
59
|
+
|
|
60
|
+
// 读取各 task 文档,分类 provider/consumer
|
|
61
|
+
const taskClasses = {}
|
|
62
|
+
for (const taskName of Object.keys(taskDeps)) {
|
|
63
|
+
const taskFile = join(changeDir, 'tasks', `${taskName}.md`)
|
|
64
|
+
if (existsSync(taskFile)) {
|
|
65
|
+
taskClasses[taskName] = classifyTask(readFileSync(taskFile, 'utf8'))
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 识别契约对:A depends_on B,且 A 是 consumer,B 是 provider
|
|
70
|
+
for (const [consumer, deps] of Object.entries(taskDeps)) {
|
|
71
|
+
const consumerClass = taskClasses[consumer]
|
|
72
|
+
if (!consumerClass?.isConsumer) continue
|
|
73
|
+
|
|
74
|
+
for (const provider of deps) {
|
|
75
|
+
const providerClass = taskClasses[provider]
|
|
76
|
+
if (!providerClass?.isProvider) continue
|
|
77
|
+
|
|
78
|
+
// 避免自引用和重复
|
|
79
|
+
if (consumer === provider) continue
|
|
80
|
+
const alreadyExists = contracts.some(
|
|
81
|
+
c => c.provider === provider && c.consumer === consumer
|
|
82
|
+
)
|
|
83
|
+
if (alreadyExists) continue
|
|
84
|
+
|
|
85
|
+
contracts.push({
|
|
86
|
+
provider,
|
|
87
|
+
consumer,
|
|
88
|
+
type: 'api',
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return contracts
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 从 plan.md 解析 task 依赖关系
|
|
98
|
+
* @param {string} planContent
|
|
99
|
+
* @returns {Record<string, string[]>} task → depends_on list
|
|
100
|
+
*/
|
|
101
|
+
function parseTaskDependencies(planContent) {
|
|
102
|
+
const deps = {}
|
|
103
|
+
|
|
104
|
+
// 方式 1: 表格形式 | task-04 | ... | 01,02 |
|
|
105
|
+
const tableRows = planContent.matchAll(/\|[^|]*task-(\d+)[^|]*\|[^|]*\|[^|]*(?:task-)?(\d+(?:\s*[,,]\s*\d+)*)[^|]*\|/gi)
|
|
106
|
+
for (const match of tableRows) {
|
|
107
|
+
const task = `task-${match[1]}`
|
|
108
|
+
const depList = match[2].split(/[,,\s]+/).map(d => `task-${d.trim()}`).filter(d => d.startsWith('task-'))
|
|
109
|
+
deps[task] = depList
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// 方式 2: depends_on 关键字
|
|
113
|
+
const dependsPattern = planContent.matchAll(/task-(\d+).*?depends_on.*?(\d+(?:\s*[,,]\s*\d+)*)/gi)
|
|
114
|
+
for (const match of dependsPattern) {
|
|
115
|
+
const task = `task-${match[1]}`
|
|
116
|
+
const depList = match[2].split(/[,,\s]+/).map(d => `task-${d.trim()}`).filter(d => d.startsWith('task-'))
|
|
117
|
+
if (!deps[task]) deps[task] = []
|
|
118
|
+
for (const d of depList) {
|
|
119
|
+
if (!deps[task].includes(d)) deps[task].push(d)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return deps
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ─── Execute 阶段:后端 task 完成后提取 artifact ───────────────────────
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* 后端 task 完成后,扫描变更文件提取 endpoint artifact
|
|
130
|
+
* @param {string} changeDir - changes/<name>/ 目录
|
|
131
|
+
* @param {string} worktreePath - worktree 路径(扫描源码用)
|
|
132
|
+
* @param {string} specBase - .sillyspec 目录
|
|
133
|
+
* @param {string} taskName - task-04
|
|
134
|
+
* @returns {{ ok: boolean, endpoints: Array, artifactPath: string|null }}
|
|
135
|
+
*/
|
|
136
|
+
export function extractProviderArtifact(changeDir, worktreePath, specBase, taskName) {
|
|
137
|
+
const artifactDir = join(specBase, '.runtime', 'contract-artifacts', taskName)
|
|
138
|
+
const artifactPath = join(artifactDir, 'endpoints.json')
|
|
139
|
+
|
|
140
|
+
if (!worktreePath || !existsSync(worktreePath)) {
|
|
141
|
+
return { ok: false, endpoints: [], artifactPath: null, error: 'worktree not found' }
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
const endpoints = scanBackendEndpoints(worktreePath)
|
|
146
|
+
|
|
147
|
+
if (endpoints.length > 0) {
|
|
148
|
+
mkdirSync(artifactDir, { recursive: true })
|
|
149
|
+
const artifact = {
|
|
150
|
+
task: taskName,
|
|
151
|
+
type: 'backend_endpoints',
|
|
152
|
+
extractedAt: new Date().toISOString(),
|
|
153
|
+
endpoints: endpoints.map(e => ({
|
|
154
|
+
method: e.method,
|
|
155
|
+
path: normalizePath(e.path),
|
|
156
|
+
source: relative(worktreePath, e.source),
|
|
157
|
+
line: e.line,
|
|
158
|
+
})),
|
|
159
|
+
}
|
|
160
|
+
writeFileSync(artifactPath, JSON.stringify(artifact, null, 2) + '\n')
|
|
161
|
+
return { ok: true, endpoints: artifact.endpoints, artifactPath }
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// 无端点提取到 — 不算错误(可能不是 router task)
|
|
165
|
+
return { ok: true, endpoints: [], artifactPath: null }
|
|
166
|
+
} catch (e) {
|
|
167
|
+
return { ok: false, endpoints: [], artifactPath: null, error: e.message }
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ─── Execute 阶段:前端 task 开始时注入契约 ─────────────────────────────
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* 为 consumer task 构建上游契约注入文本
|
|
175
|
+
* @param {string} changeDir - changes/<name>/ 目录
|
|
176
|
+
* @param {string} specBase - .sillyspec 目录
|
|
177
|
+
* @param {string} taskName - 当前 task(consumer)
|
|
178
|
+
* @param {Array<{ provider: string, consumer: string, type: string }>} contracts
|
|
179
|
+
* @returns {string|null} 注入到 prompt 的契约文本,无契约时返回 null
|
|
180
|
+
*/
|
|
181
|
+
export function buildConsumerInjection(changeDir, specBase, taskName, contracts) {
|
|
182
|
+
const myContracts = contracts.filter(c => c.consumer === taskName)
|
|
183
|
+
if (myContracts.length === 0) return null
|
|
184
|
+
|
|
185
|
+
const parts = []
|
|
186
|
+
for (const contract of myContracts) {
|
|
187
|
+
const artifactDir = join(specBase, '.runtime', 'contract-artifacts', contract.provider)
|
|
188
|
+
const artifactFile = join(artifactDir, 'endpoints.json')
|
|
189
|
+
|
|
190
|
+
let endpoints = []
|
|
191
|
+
if (existsSync(artifactFile)) {
|
|
192
|
+
try {
|
|
193
|
+
const artifact = JSON.parse(readFileSync(artifactFile, 'utf8'))
|
|
194
|
+
endpoints = artifact.endpoints || []
|
|
195
|
+
} catch {}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
parts.push(`### Upstream Contract: ${contract.provider}`)
|
|
199
|
+
if (endpoints.length > 0) {
|
|
200
|
+
parts.push(`\nAvailable endpoints from **${contract.provider}**:`)
|
|
201
|
+
for (const ep of endpoints) {
|
|
202
|
+
parts.push(`- **${ep.method}** \`${ep.path}\``)
|
|
203
|
+
}
|
|
204
|
+
} else {
|
|
205
|
+
parts.push(`\n⚠️ No endpoint artifact found for ${contract.provider}. This may indicate a contract gap.`)
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (parts.length === 0) return null
|
|
210
|
+
|
|
211
|
+
parts.unshift('## Upstream API Contracts')
|
|
212
|
+
parts.push('')
|
|
213
|
+
parts.push('### Rules')
|
|
214
|
+
parts.push('1. Do not invent API paths. Use only endpoints listed above.')
|
|
215
|
+
parts.push('2. If a required endpoint is missing, **stop and report the contract gap** instead of coding around it.')
|
|
216
|
+
parts.push('3. If you need to add new endpoints, you must also update the backend provider task.')
|
|
217
|
+
|
|
218
|
+
return parts.join('\n')
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ─── Verify 阶段:parity check ──────────────────────────────────────────
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* verify 阶段执行 API parity check
|
|
225
|
+
* @param {string} specBase - .sillyspec 目录
|
|
226
|
+
* @param {string} worktreePath - worktree 路径
|
|
227
|
+
* @returns {{ ok: boolean, missingBackend: Array, unusedBackend: Array, summary: string }}
|
|
228
|
+
*/
|
|
229
|
+
export function verifyApiParity(specBase, worktreePath) {
|
|
230
|
+
const { diffApiParity } = require('./endpoint-extractor.js')
|
|
231
|
+
|
|
232
|
+
// 读取所有 provider artifacts
|
|
233
|
+
const artifactBase = join(specBase, '.runtime', 'contract-artifacts')
|
|
234
|
+
const allProviderEndpoints = []
|
|
235
|
+
|
|
236
|
+
if (existsSync(artifactBase)) {
|
|
237
|
+
for (const entry of readdirSync(artifactBase, { withFileTypes: true })) {
|
|
238
|
+
if (!entry.isDirectory()) continue
|
|
239
|
+
const epFile = join(artifactBase, entry.name, 'endpoints.json')
|
|
240
|
+
if (existsSync(epFile)) {
|
|
241
|
+
try {
|
|
242
|
+
const artifact = JSON.parse(readFileSync(epFile, 'utf8'))
|
|
243
|
+
for (const ep of (artifact.endpoints || [])) {
|
|
244
|
+
allProviderEndpoints.push({
|
|
245
|
+
method: ep.method,
|
|
246
|
+
path: ep.path,
|
|
247
|
+
source: `${entry.name}/${ep.source}`,
|
|
248
|
+
})
|
|
249
|
+
}
|
|
250
|
+
} catch {}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// 扫描前端调用
|
|
256
|
+
if (!worktreePath || !existsSync(worktreePath)) {
|
|
257
|
+
return {
|
|
258
|
+
ok: true,
|
|
259
|
+
missingBackend: [],
|
|
260
|
+
unusedBackend: [],
|
|
261
|
+
summary: 'No worktree to scan for parity check',
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const frontendCalls = scanFrontendApiCalls(worktreePath)
|
|
266
|
+
const { missingBackend, unusedBackend } = diffApiParity(frontendCalls, allProviderEndpoints)
|
|
267
|
+
|
|
268
|
+
const ok = missingBackend.length === 0
|
|
269
|
+
let summary = ok
|
|
270
|
+
? `✅ API parity check passed: ${allProviderEndpoints.length} backend endpoints, ${frontendCalls.length} frontend calls`
|
|
271
|
+
: `❌ API parity check failed: ${missingBackend.length} frontend calls have no matching backend endpoint`
|
|
272
|
+
|
|
273
|
+
if (unusedBackend.length > 0) {
|
|
274
|
+
summary += ` | ${unusedBackend.length} backend endpoints unused by frontend`
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return { ok, missingBackend, unusedBackend, summary }
|
|
278
|
+
}
|
package/src/db.js
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import initSqlJs from 'sql.js';
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
3
|
+
import { dirname } from 'path';
|
|
4
|
+
|
|
5
|
+
export class DB {
|
|
6
|
+
constructor(dbPath) {
|
|
7
|
+
this.dbPath = dbPath;
|
|
8
|
+
this.db = null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async init() {
|
|
12
|
+
// 1. 确保父目录存在
|
|
13
|
+
const dir = dirname(this.dbPath);
|
|
14
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
15
|
+
|
|
16
|
+
// 2. 初始化 sql.js
|
|
17
|
+
const SQL = await initSqlJs();
|
|
18
|
+
|
|
19
|
+
// 3. 加载已有数据库或创建新库
|
|
20
|
+
if (existsSync(this.dbPath)) {
|
|
21
|
+
const buf = readFileSync(this.dbPath);
|
|
22
|
+
this.db = new SQL.Database(buf);
|
|
23
|
+
} else {
|
|
24
|
+
this.db = new SQL.Database();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// 4. 设置 PRAGMA
|
|
28
|
+
this.db.run('PRAGMA journal_mode = WAL');
|
|
29
|
+
this.db.run('PRAGMA busy_timeout = 5000');
|
|
30
|
+
this.db.run('PRAGMA foreign_keys = ON');
|
|
31
|
+
this.db.run('PRAGMA synchronous = NORMAL');
|
|
32
|
+
|
|
33
|
+
// 5. 创建表结构
|
|
34
|
+
this._createSchema();
|
|
35
|
+
|
|
36
|
+
// 6. 保存到磁盘
|
|
37
|
+
this._save();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
close() {
|
|
41
|
+
if (this.db) {
|
|
42
|
+
this._save();
|
|
43
|
+
this.db.close();
|
|
44
|
+
this.db = null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
transaction(fn) {
|
|
49
|
+
if (!this.db) throw new Error('DB not initialized');
|
|
50
|
+
this.db.run('BEGIN');
|
|
51
|
+
try {
|
|
52
|
+
const result = fn(this.db);
|
|
53
|
+
this.db.run('COMMIT');
|
|
54
|
+
this._save();
|
|
55
|
+
return result;
|
|
56
|
+
} catch (err) {
|
|
57
|
+
this.db.run('ROLLBACK');
|
|
58
|
+
throw err;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** 获取底层 db 对象(供 progress.js 直接使用) */
|
|
63
|
+
getDb() {
|
|
64
|
+
return this.db;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** 将内存中的数据库持久化到磁盘 */
|
|
68
|
+
_save() {
|
|
69
|
+
if (!this.db) return;
|
|
70
|
+
const data = this.db.export();
|
|
71
|
+
const buffer = Buffer.from(data);
|
|
72
|
+
writeFileSync(this.dbPath, buffer);
|
|
73
|
+
// sql.js 的 export() 会重置 PRAGMA 状态,需要重新设置
|
|
74
|
+
this.db.run('PRAGMA journal_mode = WAL');
|
|
75
|
+
this.db.run('PRAGMA busy_timeout = 5000');
|
|
76
|
+
this.db.run('PRAGMA foreign_keys = ON');
|
|
77
|
+
this.db.run('PRAGMA synchronous = NORMAL');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
_createSchema() {
|
|
81
|
+
// project 表
|
|
82
|
+
this.db.run(`
|
|
83
|
+
CREATE TABLE IF NOT EXISTS project (
|
|
84
|
+
id INTEGER PRIMARY KEY DEFAULT 1,
|
|
85
|
+
name TEXT NOT NULL,
|
|
86
|
+
schema_version INTEGER DEFAULT 4,
|
|
87
|
+
created_at TEXT NOT NULL,
|
|
88
|
+
updated_at TEXT NOT NULL
|
|
89
|
+
)
|
|
90
|
+
`);
|
|
91
|
+
|
|
92
|
+
// changes 表
|
|
93
|
+
this.db.run(`
|
|
94
|
+
CREATE TABLE IF NOT EXISTS changes (
|
|
95
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
96
|
+
name TEXT UNIQUE NOT NULL,
|
|
97
|
+
current_stage TEXT DEFAULT 'scan',
|
|
98
|
+
status TEXT DEFAULT 'active',
|
|
99
|
+
no_worktree INTEGER DEFAULT 0,
|
|
100
|
+
created_at TEXT NOT NULL,
|
|
101
|
+
last_active TEXT NOT NULL,
|
|
102
|
+
platform_change_id INTEGER,
|
|
103
|
+
platform_workspace_id INTEGER,
|
|
104
|
+
platform_last_sync TEXT,
|
|
105
|
+
platform_sync_enabled INTEGER DEFAULT 0
|
|
106
|
+
)
|
|
107
|
+
`);
|
|
108
|
+
|
|
109
|
+
// stages 表
|
|
110
|
+
this.db.run(`
|
|
111
|
+
CREATE TABLE IF NOT EXISTS stages (
|
|
112
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
113
|
+
change_id INTEGER NOT NULL REFERENCES changes(id) ON DELETE CASCADE,
|
|
114
|
+
stage TEXT NOT NULL,
|
|
115
|
+
status TEXT DEFAULT 'pending',
|
|
116
|
+
started_at TEXT,
|
|
117
|
+
completed_at TEXT,
|
|
118
|
+
UNIQUE(change_id, stage)
|
|
119
|
+
)
|
|
120
|
+
`);
|
|
121
|
+
|
|
122
|
+
// steps 表
|
|
123
|
+
this.db.run(`
|
|
124
|
+
CREATE TABLE IF NOT EXISTS steps (
|
|
125
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
126
|
+
stage_id INTEGER NOT NULL REFERENCES stages(id) ON DELETE CASCADE,
|
|
127
|
+
name TEXT NOT NULL,
|
|
128
|
+
status TEXT DEFAULT 'pending',
|
|
129
|
+
output TEXT,
|
|
130
|
+
completed_at TEXT,
|
|
131
|
+
ordering INTEGER NOT NULL DEFAULT 0
|
|
132
|
+
)
|
|
133
|
+
`);
|
|
134
|
+
|
|
135
|
+
// batch_progress 表
|
|
136
|
+
this.db.run(`
|
|
137
|
+
CREATE TABLE IF NOT EXISTS batch_progress (
|
|
138
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
139
|
+
change_id INTEGER NOT NULL REFERENCES changes(id) ON DELETE CASCADE,
|
|
140
|
+
total INTEGER DEFAULT 0,
|
|
141
|
+
completed INTEGER DEFAULT 0,
|
|
142
|
+
failed INTEGER DEFAULT 0,
|
|
143
|
+
skipped INTEGER DEFAULT 0,
|
|
144
|
+
UNIQUE(change_id)
|
|
145
|
+
)
|
|
146
|
+
`);
|
|
147
|
+
|
|
148
|
+
// approvals 表
|
|
149
|
+
this.db.run(`
|
|
150
|
+
CREATE TABLE IF NOT EXISTS approvals (
|
|
151
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
152
|
+
change_id INTEGER NOT NULL REFERENCES changes(id) ON DELETE CASCADE,
|
|
153
|
+
status TEXT DEFAULT 'not_required',
|
|
154
|
+
requested_at TEXT,
|
|
155
|
+
approved_by TEXT,
|
|
156
|
+
approved_at TEXT,
|
|
157
|
+
rejection_reason TEXT,
|
|
158
|
+
UNIQUE(change_id)
|
|
159
|
+
)
|
|
160
|
+
`);
|
|
161
|
+
|
|
162
|
+
// 索引
|
|
163
|
+
this.db.run('CREATE INDEX IF NOT EXISTS idx_changes_current_stage ON changes(current_stage)');
|
|
164
|
+
this.db.run('CREATE INDEX IF NOT EXISTS idx_changes_status ON changes(status)');
|
|
165
|
+
this.db.run('CREATE INDEX IF NOT EXISTS idx_stages_change ON stages(change_id)');
|
|
166
|
+
this.db.run('CREATE INDEX IF NOT EXISTS idx_steps_stage ON steps(stage_id)');
|
|
167
|
+
|
|
168
|
+
// Migration: add isolation columns to changes table (idempotent)
|
|
169
|
+
this._migrateAddColumn('changes', 'isolation_status', 'TEXT');
|
|
170
|
+
this._migrateAddColumn('changes', 'isolation_mode', 'TEXT');
|
|
171
|
+
this._migrateAddColumn('changes', 'isolation_reason', 'TEXT');
|
|
172
|
+
|
|
173
|
+
// Migration: add waiting support columns to steps table (idempotent)
|
|
174
|
+
this._migrateAddColumn('steps', 'wait_reason', 'TEXT');
|
|
175
|
+
this._migrateAddColumn('steps', 'wait_options', 'TEXT');
|
|
176
|
+
this._migrateAddColumn('steps', 'wait_answer', 'TEXT');
|
|
177
|
+
this._migrateAddColumn('steps', 'waited_at', 'TEXT');
|
|
178
|
+
// repeatableWait support
|
|
179
|
+
this._migrateAddColumn('steps', 'wait_answers', 'TEXT'); // JSON array
|
|
180
|
+
this._migrateAddColumn('steps', 'wait_round', 'INTEGER');
|
|
181
|
+
this._migrateAddColumn('steps', 'max_wait_rounds', 'INTEGER');
|
|
182
|
+
|
|
183
|
+
// Revision v1 support
|
|
184
|
+
this._migrateAddColumn('stages', 'revision', 'INTEGER DEFAULT 0');
|
|
185
|
+
this._migrateAddColumn('stages', 'reopened_from_step', 'TEXT');
|
|
186
|
+
this._migrateAddColumn('stages', 'reopened_at', 'TEXT');
|
|
187
|
+
this._migrateAddColumn('stages', 'stale_reason', 'TEXT');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* 幂等地给表添加列(列已存在则跳过)
|
|
192
|
+
* @private
|
|
193
|
+
*/
|
|
194
|
+
_migrateAddColumn(table, column, type) {
|
|
195
|
+
try {
|
|
196
|
+
this.db.run(`ALTER TABLE ${table} ADD COLUMN ${column} ${type}`);
|
|
197
|
+
} catch {
|
|
198
|
+
// 列已存在,静默跳过
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|