sillyspec 3.27.2 → 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/.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/.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/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 +45 -5
- package/src/change-risk-profile.js +83 -14
- package/src/config-schema.js +18 -3
- package/src/contract-matrix.js +90 -29
- 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/endpoint-extractor.js +41 -52
- package/src/foreign-declared.js +155 -0
- package/src/index.js +95 -13
- 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 +50 -4
- package/src/run/command.js +124 -44
- package/src/run/complete-handlers.js +48 -19
- package/src/run/complete.js +5 -2
- package/src/run/gates.js +104 -22
- package/src/run/prompt.js +84 -42
- package/src/run/quick-audit.js +11 -0
- package/src/run/shared.js +152 -10
- package/src/run/stage.js +8 -5
- package/src/run.js +2 -2
- package/src/scan-postcheck.js +30 -17
- package/src/stage-contract-spec.js +3 -2
- package/src/stage-contract.js +68 -4
- package/src/stages/archive.js +53 -12
- package/src/stages/brainstorm-auto.js +50 -1
- package/src/stages/brainstorm.js +13 -0
- package/src/stages/doctor.js +157 -1
- package/src/stages/execute.js +32 -6
- package/src/stages/plan-postcheck.js +76 -17
- package/src/stages/plan.js +9 -4
- package/src/stages/quick.js +6 -1
- package/src/stages/verify.js +32 -15
- package/src/sync.js +41 -3
- package/src/task-review.js +2 -2
- package/src/taskcard-placeholders.js +21 -0
- package/src/taskcard.js +43 -1
- package/src/verify-postcheck.js +315 -13
- package/src/verify-probes.js +73 -10
- package/src/worktree-apply.js +569 -227
- package/src/worktree-deps.js +150 -1
- package/src/worktree.js +285 -77
- package/templates/prompts/taskcard-rules.md +2 -1
|
@@ -0,0 +1,889 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StageContract — 阶段协议
|
|
3
|
+
*
|
|
4
|
+
* 每个阶段声明:允许的前置阶段、必须的产出、校验器、后续阶段。
|
|
5
|
+
* CLI 不再相信 prompt 完成,completeStep 后必须过 validator。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync, readdirSync, readFileSync } from 'fs'
|
|
9
|
+
import { join, basename } from 'path'
|
|
10
|
+
import { safeGit } from './git-helper.js'
|
|
11
|
+
import { detectChangeRisk, checkIntegrationEvidence, VERIFICATION_NEEDS, RISK_LEVEL_CAUSES } from './change-risk-profile.js'
|
|
12
|
+
import { SCAN_REQUIRED_DOCS, AUXILIARY_STAGES } from './constants.js'
|
|
13
|
+
import { evaluateRules } from './stage-contract-engine.js'
|
|
14
|
+
import { getRule } from './stage-contract-spec.js'
|
|
15
|
+
import { parseAllowedPaths } from './stages/plan-postcheck.js'
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 校验结果
|
|
19
|
+
* @typedef {{ ok: boolean, errors: string[], warnings: string[] }} ValidationResult
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 阶段合约
|
|
24
|
+
* @typedef {{
|
|
25
|
+
* stage: string,
|
|
26
|
+
* description: string,
|
|
27
|
+
* allowedFrom: string[],
|
|
28
|
+
* allowedTo: string[],
|
|
29
|
+
* validators: Function[],
|
|
30
|
+
* }} StageContract
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
// ============ Validators ============
|
|
34
|
+
|
|
35
|
+
function resolveChangeDir(cwd, changeName, specRoot = null) {
|
|
36
|
+
const changesRoot = specRoot ? join(specRoot, 'changes') : join(cwd, '.sillyspec', 'changes')
|
|
37
|
+
return join(changesRoot, changeName)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* --change 变更名存在性校验(治 cwd 漂移误匹配,缺陷 execute-in-place-windows-pitfalls 坑5)。
|
|
42
|
+
*
|
|
43
|
+
* 多项目 monorepo 下,cwd 在子项目目录会让 resolveSpecDir 命中子项目 spec,
|
|
44
|
+
* 此时 --change 传根项目变更名,旧逻辑(resolveChangeDir 纯拼路径)不报错而静默
|
|
45
|
+
* fallback 误启动子项目流程。本校验对「操作已有变更」的阶段强制 changes/<changeName>
|
|
46
|
+
* 存在,不存在则 fail-fast,把 cwd 漂移暴露给用户。
|
|
47
|
+
*
|
|
48
|
+
* 豁免:
|
|
49
|
+
* - 非 plan/execute/verify/archive 阶段(scan/brainstorm/quick/explore 不校验:
|
|
50
|
+
* brainstorm 可新建变更、quick 用 sessionId、scan/explore 无 change 语义)
|
|
51
|
+
* - quick-<8hex> sessionId(quick 会话 changeName 是 sessionId,不在 changes/)
|
|
52
|
+
*
|
|
53
|
+
* @param {string} specBase - 规范根目录(.sillyspec 或平台 specRoot)
|
|
54
|
+
* @param {string} stageName - 阶段名
|
|
55
|
+
* @param {string} changeName - 变更名(--change 或 auto)
|
|
56
|
+
* @returns {{ changeName: string, specBase: string, message: string } | null} null=通过,对象=失败
|
|
57
|
+
*/
|
|
58
|
+
export function validateChangeExists(specBase, stageName, changeName) {
|
|
59
|
+
const STAGES_REQUIRE_EXISTING_CHANGE = new Set(['plan', 'execute', 'verify', 'archive'])
|
|
60
|
+
if (!changeName || !STAGES_REQUIRE_EXISTING_CHANGE.has(stageName)) return null
|
|
61
|
+
if (/^quick-[0-9a-f]{8}$/.test(changeName)) return null
|
|
62
|
+
if (existsSync(join(specBase, 'changes', changeName))) return null
|
|
63
|
+
|
|
64
|
+
// archive 特例:step4 --confirm 已把变更从 changes/<name>/ 移到 changes/archive/<name>/。
|
|
65
|
+
// step5(更新路线图/提交)--change <name> 不应因 changes/<name>/ 已移走而被前置校验误拦。
|
|
66
|
+
if (stageName === 'archive') {
|
|
67
|
+
const archiveRoot = join(specBase, 'changes', 'archive')
|
|
68
|
+
if (existsSync(archiveRoot) && existsSync(join(archiveRoot, changeName))) {
|
|
69
|
+
return null
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
changeName,
|
|
74
|
+
specBase,
|
|
75
|
+
message: `变更 "${changeName}" 在当前 spec 下不存在:${join(specBase, 'changes', changeName)}\n` +
|
|
76
|
+
` 排查方向(三选一):① 变更名是否打错(sillyspec status 或 progress show 查看活跃变更名);` +
|
|
77
|
+
`② cwd 是否漂到了子项目目录(回到项目根再跑,或加 --spec-dir <根>/.sillyspec);` +
|
|
78
|
+
`③ 是否该先跑 sillyspec run brainstorm 新建该变更。`,
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function collectIdsFromLine(line, re, ids) {
|
|
83
|
+
for (const match of line.matchAll(re)) {
|
|
84
|
+
ids.add(match[0].toUpperCase())
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function extractIds(content, prefix) {
|
|
89
|
+
if (!content) return []
|
|
90
|
+
const ids = new Set()
|
|
91
|
+
const idRe = new RegExp(`\\b${prefix}-\\d+(?:@v\\d+)?\\b`, 'gi')
|
|
92
|
+
const headingLineRe = /^\s{0,3}#{1,6}\s+/i
|
|
93
|
+
const fieldLineRe = /^\s*(?:[-*]\s*)?(?:id|decision[-_ ]?ids?|requirement[-_ ]?ids?|covers?|coverage|references?|impacts?|覆盖(?:来源|决策|需求)?)\s*[::]/i
|
|
94
|
+
const tableLineRe = /^\s*\|/
|
|
95
|
+
const listStartsWithIdRe = new RegExp(`^\\s*(?:[-*]|\\d+\\.)\\s*(?:\\[[ xX]\\]\\s*)?${prefix}-\\d+(?:@v\\d+)?\\b`, 'i')
|
|
96
|
+
|
|
97
|
+
for (const line of content.split(/\r?\n/)) {
|
|
98
|
+
if (!headingLineRe.test(line) && !fieldLineRe.test(line) && !tableLineRe.test(line) && !listStartsWithIdRe.test(line)) continue
|
|
99
|
+
collectIdsFromLine(line, idRe, ids)
|
|
100
|
+
}
|
|
101
|
+
return [...ids].sort()
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function readDecisionField(body, fieldPattern, fallback = '') {
|
|
105
|
+
const re = new RegExp(`^\\s*(?:[-*]\\s*)?(?:${fieldPattern})\\s*[::]\\s*([^\\n]+)`, 'im')
|
|
106
|
+
return (body.match(re)?.[1] || fallback).trim()
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function buildDecisionRecord(id, body) {
|
|
110
|
+
const status = readDecisionField(body, 'status', 'accepted').toLowerCase()
|
|
111
|
+
const blockerValue = readDecisionField(body, 'blocker', 'false').toLowerCase()
|
|
112
|
+
const blocker = ['true', 'yes', '1'].includes(blockerValue)
|
|
113
|
+
const priorityValue = readDecisionField(body, 'priority|level|severity')
|
|
114
|
+
const priorityMissing = priorityValue.length === 0
|
|
115
|
+
const fallbackPriority = (['unresolved', 'blocking'].includes(status) || blocker) ? 'P1' : 'P2'
|
|
116
|
+
const priority = (priorityValue.match(/P[0-2]/i)?.[0] || fallbackPriority).toUpperCase()
|
|
117
|
+
// 解析 supersedes 字段:记录本条决策取代了哪个旧版本
|
|
118
|
+
const supersedesRaw = readDecisionField(body, 'supersedes', '')
|
|
119
|
+
const supersedes = supersedesRaw
|
|
120
|
+
? supersedesRaw.split(',').map(s => s.trim().toUpperCase().replace(/['"]/g, '')).filter(Boolean)
|
|
121
|
+
: []
|
|
122
|
+
return { id: id.toUpperCase(), body, status, priority, blocker, priorityMissing, supersedes }
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function findNextDecisionBoundary(content, startIndex) {
|
|
126
|
+
const boundaryRe = /^(\s{0,3}#{2,6}\s+D-\d+(?:@v\d+)?\b|\s*(?:[-*]\s*)?(?:id|decision[-_ ]?id|decision)\s*[::]\s*D-\d+(?:@v\d+)?\b)/gmi
|
|
127
|
+
boundaryRe.lastIndex = startIndex
|
|
128
|
+
const next = boundaryRe.exec(content)
|
|
129
|
+
return next ? next.index : content.length
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function isInsideRange(index, ranges) {
|
|
133
|
+
return ranges.some(range => index >= range.start && index < range.end)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function parseDecisionRecords(content) {
|
|
137
|
+
if (!content) return []
|
|
138
|
+
const records = []
|
|
139
|
+
const ranges = []
|
|
140
|
+
const headingRe = /^\s{0,3}#{2,6}\s+(D-\d+(?:@v\d+)?)(?:\b|:)[^\n]*$/gmi
|
|
141
|
+
const headings = []
|
|
142
|
+
let match
|
|
143
|
+
while ((match = headingRe.exec(content)) !== null) {
|
|
144
|
+
headings.push({ id: match[1].toUpperCase(), index: match.index, end: headingRe.lastIndex })
|
|
145
|
+
}
|
|
146
|
+
for (let i = 0; i < headings.length; i++) {
|
|
147
|
+
const current = headings[i]
|
|
148
|
+
const next = headings[i + 1]
|
|
149
|
+
const body = content.slice(current.end, next ? next.index : content.length)
|
|
150
|
+
const end = next ? next.index : content.length
|
|
151
|
+
ranges.push({ start: current.index, end })
|
|
152
|
+
records.push(buildDecisionRecord(current.id, body))
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const idLineRe = /^\s*(?:[-*]\s*)?(?:id|decision[-_ ]?id|decision)\s*[::]\s*(D-\d+(?:@v\d+)?)(?:\b|$)/gmi
|
|
156
|
+
while ((match = idLineRe.exec(content)) !== null) {
|
|
157
|
+
if (isInsideRange(match.index, ranges)) continue
|
|
158
|
+
const bodyEnd = findNextDecisionBoundary(content, idLineRe.lastIndex)
|
|
159
|
+
const body = content.slice(match.index, bodyEnd)
|
|
160
|
+
records.push(buildDecisionRecord(match[1], body))
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return records
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function extractCurrentDecisionIds(content) {
|
|
167
|
+
const records = parseDecisionRecords(content)
|
|
168
|
+
if (records.length === 0) return extractIds(content, 'D')
|
|
169
|
+
// 收集所有被 supersedes 声明取代的旧版本 ID
|
|
170
|
+
const supersededIds = new Set()
|
|
171
|
+
for (const r of records) {
|
|
172
|
+
for (const oldId of r.supersedes) {
|
|
173
|
+
supersededIds.add(oldId)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return records
|
|
177
|
+
.filter(r => !['superseded', 'rejected'].includes(r.status))
|
|
178
|
+
.filter(r => !supersededIds.has(r.id)) // 被新版本显式取代的旧版本不再校验
|
|
179
|
+
.map(r => r.id)
|
|
180
|
+
.sort()
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function findBlockingDecisionIssues(content) {
|
|
184
|
+
return parseDecisionRecords(content)
|
|
185
|
+
.filter(r => (r.blocker || ['unresolved', 'blocking'].includes(r.status)) && ['P0', 'P1'].includes(r.priority))
|
|
186
|
+
.map(r => `${r.id} (${r.priority}${r.priorityMissing ? ', priority=missing->P1' : ''}, status=${r.status})`)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function readIfExists(file) {
|
|
190
|
+
return existsSync(file) ? readFileSync(file, 'utf8') : ''
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function warnMissingIds(warnings, ids, targetContent, targetName, sourceName) {
|
|
194
|
+
// 剥版本后缀(@vN)按基号词边界匹配:target 里写裸号 D-001 即视为引用了 D-001@V1
|
|
195
|
+
// 的当前版本(prompt 常裸号引用,旧版字面 includes("D-001@V1") 会批量误报)。
|
|
196
|
+
// 大小写不敏感:target 整体大写后比对。
|
|
197
|
+
const targetUpper = targetContent.toUpperCase()
|
|
198
|
+
for (const id of ids) {
|
|
199
|
+
const base = id.replace(/@V\d+$/, '')
|
|
200
|
+
const re = new RegExp(`\\b${base}\\b`)
|
|
201
|
+
if (!re.test(targetUpper)) {
|
|
202
|
+
warnings.push(getRule('shared.id-traceability').failMessage.replaceAll('${target}', targetName).replaceAll('${source}', sourceName).replaceAll('${id}', id))
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* scan 完成校验:检查 7 份 scan 文档 + manifest
|
|
209
|
+
*/
|
|
210
|
+
function validateScanOutputs(cwd, changeName, context = {}) {
|
|
211
|
+
const { projectName, specRoot } = context
|
|
212
|
+
// 平台模式:specRoot 直接是规范目录(含 docs/)
|
|
213
|
+
// 本地模式:规范目录是 cwd/.sillyspec
|
|
214
|
+
// 不用 isSpecDir 启发式猜测——很多项目根目录有自己的 docs/,会误判
|
|
215
|
+
const specBase = specRoot || join(cwd, '.sillyspec')
|
|
216
|
+
const docsRoot = projectName
|
|
217
|
+
? join(specBase, 'docs', projectName, 'scan')
|
|
218
|
+
: join(specBase, 'docs', 'scan')
|
|
219
|
+
|
|
220
|
+
// 7 份 scan 文档存在性由引擎消费 manifest(与 SCAN_REQUIRED_DOCS 单源,文案/事前契约同源)
|
|
221
|
+
const engineResult = evaluateRules('scan', { docsRoot })
|
|
222
|
+
const errors = [...engineResult.errors]
|
|
223
|
+
const warnings = [...engineResult.warnings]
|
|
224
|
+
|
|
225
|
+
// 检查 modules 目录
|
|
226
|
+
const modulesRoot = projectName
|
|
227
|
+
? join(specBase, 'docs', projectName, 'modules')
|
|
228
|
+
: join(specBase, 'docs', 'modules')
|
|
229
|
+
if (!existsSync(modulesRoot)) {
|
|
230
|
+
warnings.push('modules 目录不存在')
|
|
231
|
+
} else {
|
|
232
|
+
const modules = readdirSync(modulesRoot).filter(f => f.endsWith('.md'))
|
|
233
|
+
if (modules.length === 0) {
|
|
234
|
+
warnings.push('modules 目录为空')
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return { ok: errors.length === 0, errors, warnings }
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* brainstorm 完成校验:检查四件套规范文件是否生成
|
|
243
|
+
*/
|
|
244
|
+
function validateBrainstormOutputs(cwd, changeName, context = {}) {
|
|
245
|
+
const { specRoot } = context
|
|
246
|
+
const changesRoot = specRoot ? join(specRoot, 'changes') : join(cwd, '.sillyspec', 'changes')
|
|
247
|
+
if (specRoot && !existsSync(changesRoot)) {
|
|
248
|
+
return { ok: false, errors: [`平台模式 specRoot 缺少 changes 目录: ${changesRoot}`], warnings: [] }
|
|
249
|
+
}
|
|
250
|
+
const changeDir = resolveChangeDir(cwd, changeName, specRoot)
|
|
251
|
+
// 读 design.md frontmatter scale(若有)传入引擎 ctx,供 BRAINSTORM_RULES 的 condition 判定:
|
|
252
|
+
// scale=small → proposal/requirements/tasks 三规则跳过(小变更只产 design.md,与末步 prompt 一致,
|
|
253
|
+
// 避免"照 Step8 small 指引只写 design.md 后 --done 必撞四件套 error墙")。
|
|
254
|
+
// fail-safe:design.md 不存在或无 scale → null → condition(ne 'small')成立 → 四件套全要求(保守走重流程)。
|
|
255
|
+
const designPath = join(changeDir, 'design.md')
|
|
256
|
+
let scale = null
|
|
257
|
+
let designContent = null
|
|
258
|
+
if (existsSync(designPath)) {
|
|
259
|
+
designContent = readFileSync(designPath, 'utf8')
|
|
260
|
+
const fm = designContent.match(/^---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*\r?\n/)
|
|
261
|
+
const sm = fm && fm[1].match(/^scale:[ \t]*['"]?(\w+)/m)
|
|
262
|
+
if (sm) scale = sm[1]
|
|
263
|
+
}
|
|
264
|
+
// 纯 kind 规则(四件套存在性 + proposal/requirements/design 章节 + tasks 列表)由引擎消费
|
|
265
|
+
// stage-contract-spec.js manifest,与报错文案 + prompt 事前契约严格同源(事前给的 == 事后查的)。
|
|
266
|
+
// custom kind(lifecycle / decisions)保留下方旧逻辑,数据/文案在 Batch 3 迁入 manifest。
|
|
267
|
+
const engineResult = evaluateRules('brainstorm', { changeDir, scale })
|
|
268
|
+
const errors = [...engineResult.errors]
|
|
269
|
+
const warnings = [...engineResult.warnings]
|
|
270
|
+
|
|
271
|
+
if (designContent) {
|
|
272
|
+
const content = designContent
|
|
273
|
+
|
|
274
|
+
// lifecycle-exemption(custom kind):判定算法保留(trigger/exemption/table 三段短路),
|
|
275
|
+
// data + failMessage/exemptionPassedMessage 从 stage-contract-spec.js manifest 同源。
|
|
276
|
+
const lcRule = getRule('brainstorm.design.lifecycle-table')
|
|
277
|
+
const { trigger: lcTrigger, exemptions: lcExempts, table: lcTable } = lcRule.data
|
|
278
|
+
if (new RegExp(lcTrigger.pattern, lcTrigger.flags).test(content)) {
|
|
279
|
+
const declaresNotApplicable = lcExempts.some(e => new RegExp(e.pattern, e.flags).test(content))
|
|
280
|
+
if (declaresNotApplicable) {
|
|
281
|
+
warnings.push(lcRule.exemptionPassedMessage)
|
|
282
|
+
} else {
|
|
283
|
+
const hasLifecycleTable = lcTable.some(t => new RegExp(t.pattern, t.flags).test(content))
|
|
284
|
+
if (!hasLifecycleTable) {
|
|
285
|
+
errors.push(lcRule.failMessage)
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const decisionsFile = join(changeDir, 'decisions.md')
|
|
292
|
+
if (existsSync(decisionsFile)) {
|
|
293
|
+
const decisions = readFileSync(decisionsFile, 'utf8')
|
|
294
|
+
const blockers = findBlockingDecisionIssues(decisions)
|
|
295
|
+
for (const issue of blockers) {
|
|
296
|
+
errors.push(getRule('shared.decision-blocker').failMessage.replace('${issue}', issue))
|
|
297
|
+
}
|
|
298
|
+
const decisionIds = extractCurrentDecisionIds(decisions)
|
|
299
|
+
if (decisionIds.length === 0) {
|
|
300
|
+
warnings.push('decisions.md 存在但没有当前版本 D-xxx@vN 决策 ID')
|
|
301
|
+
} else {
|
|
302
|
+
const design = readIfExists(join(changeDir, 'design.md'))
|
|
303
|
+
const requirements = readIfExists(join(changeDir, 'requirements.md'))
|
|
304
|
+
const tasks = readIfExists(join(changeDir, 'tasks.md'))
|
|
305
|
+
// decision 的天然引用落点是 design.md;requirements(需求按 FR 组织)与
|
|
306
|
+
// tasks(骨架,待 plan 展开)不强求逐条引用每个架构决策,否则批量误报。
|
|
307
|
+
warnMissingIds(warnings, decisionIds, design, 'design.md', 'decisions.md')
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return { ok: errors.length === 0, errors, warnings }
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* plan 完成校验:检查 plan.md 生成
|
|
316
|
+
*/
|
|
317
|
+
function validatePlanOutputs(cwd, changeName, context = {}) {
|
|
318
|
+
const { specRoot } = context
|
|
319
|
+
const changeDir = resolveChangeDir(cwd, changeName, specRoot)
|
|
320
|
+
const planFile = join(changeDir, 'plan.md')
|
|
321
|
+
|
|
322
|
+
// 读 design.md frontmatter scale(若有)传入引擎 ctx,供 plan.module-impact.exists 的 condition
|
|
323
|
+
// (scale≠small)判定——large 要求 module-impact 首版,small 豁免(走 quick)。fail-safe:无 scale→null→
|
|
324
|
+
// condition(ne 'small')成立→保守要求 module-impact(同 brainstorm scale 读取模式,line 264-272)。
|
|
325
|
+
const designPathForScale = join(changeDir, 'design.md')
|
|
326
|
+
let scale = null
|
|
327
|
+
if (existsSync(designPathForScale)) {
|
|
328
|
+
const fm = readFileSync(designPathForScale, 'utf8').match(/^---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*\r?\n/)
|
|
329
|
+
const sm = fm && fm[1].match(/^scale:[ \t]*['"]?(\w+)/m)
|
|
330
|
+
if (sm) scale = sm[1]
|
|
331
|
+
}
|
|
332
|
+
// plan.md 存在性由引擎消费 manifest。entryPoint/id-trace/decisions 为 custom kind(下方保留)。
|
|
333
|
+
const engineResult = evaluateRules('plan', { changeDir, scale })
|
|
334
|
+
const errors = [...engineResult.errors]
|
|
335
|
+
const warnings = [...engineResult.warnings]
|
|
336
|
+
|
|
337
|
+
if (existsSync(planFile)) {
|
|
338
|
+
const plan = readFileSync(planFile, 'utf8')
|
|
339
|
+
const requirements = readIfExists(join(changeDir, 'requirements.md'))
|
|
340
|
+
const requirementIds = extractIds(requirements, 'FR')
|
|
341
|
+
warnMissingIds(warnings, requirementIds, plan, 'plan.md', 'requirements.md')
|
|
342
|
+
|
|
343
|
+
const decisions = readIfExists(join(changeDir, 'decisions.md'))
|
|
344
|
+
const blockers = findBlockingDecisionIssues(decisions)
|
|
345
|
+
for (const issue of blockers) {
|
|
346
|
+
errors.push(getRule('shared.decision-blocker').failMessage.replace('${issue}', issue))
|
|
347
|
+
}
|
|
348
|
+
const decisionIds = extractCurrentDecisionIds(decisions)
|
|
349
|
+
warnMissingIds(warnings, decisionIds, plan, 'plan.md', 'decisions.md')
|
|
350
|
+
}
|
|
351
|
+
// ── P0: 生产接线路径检查:design 提到入口但 task 的 allowed_paths 不含入口文件 ──
|
|
352
|
+
// entry-point-wiring(custom):trigger/file 抽取/exemption/failMessage 从 manifest 同源
|
|
353
|
+
// (stage-contract-spec.js),保留多源 allowed_paths 收集 + 逐文件对账 + 豁免算法。
|
|
354
|
+
const designContent = readIfExists(join(changeDir, 'design.md'))
|
|
355
|
+
if (designContent) {
|
|
356
|
+
const epRule = getRule('plan.entry-point-wiring')
|
|
357
|
+
const { entryPointPatterns, fileExtractionPattern, exemptionPattern } = epRule.data
|
|
358
|
+
const fileExtractRe = new RegExp(fileExtractionPattern.pattern, fileExtractionPattern.flags)
|
|
359
|
+
const mentionedFiles = new Set()
|
|
360
|
+
for (const ep of entryPointPatterns) {
|
|
361
|
+
const pattern = new RegExp(ep.pattern, ep.flags)
|
|
362
|
+
pattern.lastIndex = 0
|
|
363
|
+
for (const match of designContent.matchAll(pattern)) {
|
|
364
|
+
const fileMatch = match[0].match(fileExtractRe)
|
|
365
|
+
if (fileMatch) mentionedFiles.add(fileMatch[1].toLowerCase())
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
if (mentionedFiles.size > 0) {
|
|
369
|
+
const tasksDir = join(changeDir, 'tasks')
|
|
370
|
+
const allAllowedPaths = new Set()
|
|
371
|
+
if (existsSync(tasksDir)) {
|
|
372
|
+
const taskFiles = readdirSync(tasksDir).filter(f => /^task-\d+\.md$/i.test(f))
|
|
373
|
+
for (const taskFile of taskFiles) {
|
|
374
|
+
const taskContent = readFileSync(join(tasksDir, taskFile), 'utf8')
|
|
375
|
+
// 复用 parseAllowedPaths(坑6①:认 inline 数组/缩进块/顶格块 + 剥反引号 + CRLF 归一,
|
|
376
|
+
// 消除此处内联正则与 plan-postcheck 的解析漂移)。卡片无 frontmatter 时退全文扫描
|
|
377
|
+
// (包裹伪 frontmatter 复用同一解析器)——原内联正则是全文匹配语义,entry-point-wiring
|
|
378
|
+
// 只关心路径覆盖,卡片格式合法性归 plan-postcheck 把关,此处不收紧。
|
|
379
|
+
const paths = parseAllowedPaths(taskContent)
|
|
380
|
+
const effective = paths.length > 0 ? paths : parseAllowedPaths(`---\n${taskContent}\n---`)
|
|
381
|
+
for (const p of effective) allAllowedPaths.add(p.toLowerCase())
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
// 也从 plan.md 文件变更清单中收集
|
|
385
|
+
if (existsSync(planFile)) {
|
|
386
|
+
const planContent = readFileSync(planFile, 'utf8')
|
|
387
|
+
const planFileChanges = planContent.match(/\|\s*(?:新增|修改|new|modify|update)\s*\|\s*`?([^`|]+)`?\s*\|/gi) || []
|
|
388
|
+
for (const line of planFileChanges) {
|
|
389
|
+
const file = line.match(/\|\s*(?:新增|修改|new|modify|update)\s*\|\s*`?([^`|]+)`?\s*\|/i)
|
|
390
|
+
if (file) allAllowedPaths.add(file[1].trim().toLowerCase())
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
for (const mentionedFile of mentionedFiles) {
|
|
394
|
+
const found = [...allAllowedPaths].some(p => p.includes(mentionedFile))
|
|
395
|
+
if (!found) {
|
|
396
|
+
const noChangePattern = new RegExp(exemptionPattern.replaceAll('${file}', mentionedFile), 'i')
|
|
397
|
+
if (!noChangePattern.test(designContent)) {
|
|
398
|
+
errors.push(epRule.failMessage.replaceAll('${file}', mentionedFile))
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return { ok: errors.length === 0, errors, warnings }
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* 从 verify-result.md 提取结论关键词(PASS / PASS WITH NOTES / FAIL)。
|
|
409
|
+
* 标题放宽:含「结论/Conclusion/Result/结果」的二级标题均可(B3c),
|
|
410
|
+
* PASS/FAIL 可在标题行本身(如「## 验收结论:✅ PASS」)或紧邻标题的正文里。
|
|
411
|
+
* 历史教训:原正则锚定确切「## 结论」,用户写「## 验收结论:✅ PASS」不被识别。
|
|
412
|
+
*/
|
|
413
|
+
function extractVerifyConclusion(verify) {
|
|
414
|
+
const headingRe = /^##\s[^\n]*(?:结论|conclusion|result|结果)/im
|
|
415
|
+
const headingMatch = verify.match(headingRe)
|
|
416
|
+
if (!headingMatch) return ''
|
|
417
|
+
const start = headingMatch.index
|
|
418
|
+
const slice = verify.slice(start, start + 400)
|
|
419
|
+
const kw = slice.match(/\b(PASS(?:\s+WITH\s+NOTES)?|FAIL)\b/i)
|
|
420
|
+
return kw ? kw[1].toUpperCase().replace(/\s+/g, ' ') : ''
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function validateVerifyOutputs(cwd, changeName, context = {}) {
|
|
424
|
+
const { specRoot } = context
|
|
425
|
+
const changeDir = resolveChangeDir(cwd, changeName, specRoot)
|
|
426
|
+
|
|
427
|
+
if (!existsSync(changeDir)) {
|
|
428
|
+
return { ok: false, errors: [`变更目录缺失: ${changeDir}`], warnings: [] }
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// 核心文档存在性(verify-result.md / design.md / plan.md)由引擎消费 manifest。
|
|
432
|
+
// verify-result.md 不存在则不能完成——历史教训:AI 可能跳过报告直接 --done 导致"假完成"。
|
|
433
|
+
const engineResult = evaluateRules('verify', { changeDir })
|
|
434
|
+
const errors = [...engineResult.errors]
|
|
435
|
+
const warnings = [...engineResult.warnings]
|
|
436
|
+
|
|
437
|
+
const verifyResult = join(changeDir, 'verify-result.md')
|
|
438
|
+
if (existsSync(verifyResult)) {
|
|
439
|
+
const verify = readFileSync(verifyResult, 'utf8')
|
|
440
|
+
const decisions = readIfExists(join(changeDir, 'decisions.md'))
|
|
441
|
+
const blockers = findBlockingDecisionIssues(decisions)
|
|
442
|
+
for (const issue of blockers) {
|
|
443
|
+
errors.push(getRule('shared.decision-blocker').failMessage.replace('${issue}', issue))
|
|
444
|
+
}
|
|
445
|
+
const decisionIds = extractCurrentDecisionIds(decisions)
|
|
446
|
+
warnMissingIds(warnings, decisionIds, verify, 'verify-result.md', 'decisions.md')
|
|
447
|
+
|
|
448
|
+
// ── FAIL 结论门控(适用于所有变更,不限风险等级)──
|
|
449
|
+
// verify-result.md 结论为 FAIL 时,verify 阶段不能 completed。
|
|
450
|
+
// 历史教训:CLI 曾不校验结论,AI 写 FAIL 后 verify 仍被标记完成并提示"验证通过可以归档"。
|
|
451
|
+
const conclusionStr = extractVerifyConclusion(verify)
|
|
452
|
+
if (conclusionStr === 'FAIL') {
|
|
453
|
+
errors.push(getRule('verify.conclusion.fail-gate').failMessage)
|
|
454
|
+
} else if (!conclusionStr) {
|
|
455
|
+
warnings.push(getRule('verify.conclusion.fail-gate').noConclusionWarning)
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// ── P0: Change Risk Gate — 核心功能缺少真实集成验证时 FAIL ──
|
|
459
|
+
const changeRiskProfile = detectChangeRisk({
|
|
460
|
+
designContent: readIfExists(join(changeDir, 'design.md')),
|
|
461
|
+
planContent: readIfExists(join(changeDir, 'plan.md')),
|
|
462
|
+
})
|
|
463
|
+
const conclusion = extractVerifyConclusion(verify)
|
|
464
|
+
if (['integration-critical', 'deployment-critical'].includes(changeRiskProfile.level)) {
|
|
465
|
+
// 关键词误伤早期引导(坑2,FR-02):命中高危关键词且无 frontmatter risk_level 时,
|
|
466
|
+
// 无条件透出 frontmatter 覆盖指引(不依赖 conclusion / evidence),让 agent 早期即可
|
|
467
|
+
// 判断是否属误判并显式覆盖,而非撞到 evidence gate 末尾才发现出路③。遵 6417a27:不做
|
|
468
|
+
// body 否定语境扫描,只走 frontmatter 显式覆盖通道。
|
|
469
|
+
if (!changeRiskProfile.explicit) {
|
|
470
|
+
warnings.push(
|
|
471
|
+
`[${changeRiskProfile.level}] 本次变更被关键词判级(命中:${changeRiskProfile.triggers.join(', ')})。` +
|
|
472
|
+
`若属关键词误伤(实际未触碰 daemon/session/启动入口/跨进程),可在 design.md frontmatter 加 risk_level: <真实等级>(如 unit-sufficient)显式覆盖后重跑。`
|
|
473
|
+
)
|
|
474
|
+
}
|
|
475
|
+
// 显式 risk_level 声明(design frontmatter,非关键词误判)下,PASS WITH NOTES 视为对残留项的
|
|
476
|
+
// 诚实声明,不强求全量集成证据——豁免本就来自 design 的明确判断。自动关键词判级维持严格:
|
|
477
|
+
// PASS WITH NOTES 仍要求证据齐全,防 agent 用 PASS WITH NOTES 绕证据门控。
|
|
478
|
+
const requiresEvidence = conclusion === 'PASS' || (conclusion === 'PASS WITH NOTES' && !changeRiskProfile.explicit)
|
|
479
|
+
if (conclusion === 'PASS WITH NOTES' && changeRiskProfile.explicit) {
|
|
480
|
+
warnings.push(`[${changeRiskProfile.level}] 结论 PASS WITH NOTES:design frontmatter 显式声明 risk_level=${changeRiskProfile.level},残留项须为真实集成证据缺口,并在 verify-result.md 如实说明`)
|
|
481
|
+
}
|
|
482
|
+
if (requiresEvidence) {
|
|
483
|
+
// CLI 回执注入(坑 verify-literal-evidence-mismatch,2026-08-22 实证:证据第一轮就齐
|
|
484
|
+
// 但自然措辞不含字面词被误拦三轮):verify 服务回收器落的回执 = 真实启动+PID 登记
|
|
485
|
+
// 的结构化证据,作为附加匹配文本——有回执不依赖 agent 措辞;无回执维持原字面匹配
|
|
486
|
+
let receiptText = ''
|
|
487
|
+
try {
|
|
488
|
+
// 回执按变更分片(坑 verify-pids-cross-session-kill:原单份被并行会话后写覆盖),
|
|
489
|
+
// 优先读本变更分片,兼容旧单份名(升级过渡期 receipt.change 过滤仍生效)
|
|
490
|
+
const runtimeDir = join(cwd, '.sillyspec', '.runtime')
|
|
491
|
+
const receiptPath = existsSync(join(runtimeDir, `verify-services-${changeName}.receipt.json`))
|
|
492
|
+
? join(runtimeDir, `verify-services-${changeName}.receipt.json`)
|
|
493
|
+
: join(runtimeDir, 'verify-services.receipt.json')
|
|
494
|
+
if (existsSync(receiptPath)) {
|
|
495
|
+
const receipt = JSON.parse(readFileSync(receiptPath, 'utf8'))
|
|
496
|
+
if (receipt && (receipt.reapedPidCount || 0) > 0 && (!receipt.change || receipt.change === changeName)) {
|
|
497
|
+
receiptText = `CLI 回执:verify 服务进程已回收 ${receipt.reapedPidCount} 个(PID 已登记,真实启动,运行时证据 reapedAt=${receipt.reapedAt})`
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
} catch { /* 回执损坏按无回执处理(fail 回原字面匹配) */ }
|
|
501
|
+
const evidenceCheck = checkIntegrationEvidence(verify, changeRiskProfile.requiredVerification, { extraEvidenceText: receiptText })
|
|
502
|
+
if (!evidenceCheck.ok) {
|
|
503
|
+
// A: 报错说人话 —— 把「缺哪一项、要写/做什么、判级原因」逐条列出,
|
|
504
|
+
// 让 agent 不必靠改结论文案撞墙。detail 指明真实启动须是本变更实际改动的
|
|
505
|
+
// 部署/启动入口(非无关进程),以及每项的字面期望。
|
|
506
|
+
const needs = changeRiskProfile.requiredVerification
|
|
507
|
+
.filter(k => VERIFICATION_NEEDS[k] && VERIFICATION_NEEDS[k].desc)
|
|
508
|
+
.map(k => {
|
|
509
|
+
const vn = VERIFICATION_NEEDS[k]
|
|
510
|
+
const lit = vn.literals && vn.literals.length ? '字面命中其一:' + vn.literals.join(' / ') : ''
|
|
511
|
+
return `\n 〔${k}〕${vn.desc}${lit}`
|
|
512
|
+
})
|
|
513
|
+
.join('')
|
|
514
|
+
const cause = changeRiskProfile.explicit
|
|
515
|
+
? RISK_LEVEL_CAUSES.explicit
|
|
516
|
+
: (RISK_LEVEL_CAUSES[changeRiskProfile.level] || '')
|
|
517
|
+
errors.push(
|
|
518
|
+
`[${changeRiskProfile.level}] 验证结论为 ${conclusion},但缺少真实集成证据。\n` +
|
|
519
|
+
` 缺失项(需在 verify-result.md 如实提供并满足):${evidenceCheck.errors.join('; ')}\n` +
|
|
520
|
+
` 每项要提供什么:${needs}\n` +
|
|
521
|
+
` 风险判级原因:${cause}\n` +
|
|
522
|
+
` 命中触发词:${changeRiskProfile.triggers.join(', ')}\n` +
|
|
523
|
+
` 出路:① 补全上述缺失的真实集成证据(真实启动 daemon/backend、集成测试、运行日志)后保持 PASS;` +
|
|
524
|
+
`或 ② 如实改结论 FAIL(承认端到端未验,留待部署后补);` +
|
|
525
|
+
`或 ③ 若判级是关键词误伤(实际并未触碰 daemon/session/启动入口),在 design.md frontmatter 加 risk_level: <真实等级>(如 unit-sufficient)显式覆盖后重跑。` +
|
|
526
|
+
`仅改结论文案/措辞蹭字面关键词会被对账。`
|
|
527
|
+
)
|
|
528
|
+
}
|
|
529
|
+
warnings.push(...evidenceCheck.warnings)
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
return { ok: errors.length === 0, errors, warnings }
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* 计算归档目标目录名:保持原变更名不变,直接移入 archive/。
|
|
539
|
+
*/
|
|
540
|
+
export function archiveDestDirName(date, changeName) {
|
|
541
|
+
return String(changeName)
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* archive 完成校验:检查归档目录完整性
|
|
546
|
+
*/
|
|
547
|
+
function validateArchiveOutputs(cwd, changeName) {
|
|
548
|
+
const archiveDir = join(cwd, '.sillyspec', 'changes', 'archive')
|
|
549
|
+
const destDir = join(archiveDir, changeName)
|
|
550
|
+
|
|
551
|
+
// 归档目录不存在 early return(引擎在存在时才跑)
|
|
552
|
+
if (!existsSync(destDir)) {
|
|
553
|
+
return { ok: false, errors: [`归档目录缺失: ${destDir}`], warnings: [] }
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// plan.md(必备)/design.md/module-impact.md(推荐)存在性由引擎消费 manifest
|
|
557
|
+
const engineResult = evaluateRules('archive', { archiveDir: destDir }, undefined, { source: 'validateArchiveOutputs' })
|
|
558
|
+
return { ok: engineResult.errors.length === 0, errors: engineResult.errors, warnings: engineResult.warnings }
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* archive 前置校验:所有主流程阶段完成
|
|
563
|
+
*/
|
|
564
|
+
function validateChangeClosed(cwd, changeName) {
|
|
565
|
+
const errors = []
|
|
566
|
+
const warnings = []
|
|
567
|
+
|
|
568
|
+
// 检查前置阶段状态
|
|
569
|
+
const progressDir = join(cwd, '.sillyspec', '.runtime')
|
|
570
|
+
// 这里只做文件层面的检查,DB 检查在 run.js 里做
|
|
571
|
+
const changeDir = join(cwd, '.sillyspec', 'changes', changeName)
|
|
572
|
+
if (!existsSync(changeDir)) {
|
|
573
|
+
errors.push(`变更目录不存在: ${changeDir}`)
|
|
574
|
+
return { ok: false, errors, warnings }
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// plan.md 存在性由引擎消费 manifest(source 区分 validateChangeClosed vs validateArchiveOutputs)
|
|
578
|
+
const engineResult = evaluateRules('archive', { changeDir }, undefined, { source: 'validateChangeClosed' })
|
|
579
|
+
return { ok: engineResult.errors.length === 0, errors: engineResult.errors, warnings: engineResult.warnings }
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// ============ Execute 代码变更客观核验 ============
|
|
583
|
+
|
|
584
|
+
// QUAL-01 收口:本地 execFileSync 裸调(无 safe.directory)→ git-helper safeGit 统一入口,
|
|
585
|
+
// {ok,out,error} 适配语义与 timeout 15000 不变
|
|
586
|
+
function gitTry(dir, args) {
|
|
587
|
+
const r = safeGit(dir, args, { timeout: 15000 })
|
|
588
|
+
if (r.value !== null) return { ok: true, out: r.value }
|
|
589
|
+
return { ok: false, out: '', error: r.error || 'git 执行失败' }
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* 客观核验 execute 阶段是否产生了真实代码变更。
|
|
594
|
+
*
|
|
595
|
+
* 历史漏洞:execute 无 stage-level validator,agent 勾选 plan.md 全部 checkbox
|
|
596
|
+
* 即可让 execute 被标 completed,代码完成度与真实变更完全脱钩。
|
|
597
|
+
*
|
|
598
|
+
* 判定顺序(fail-open on uncertainty,避免环境差异误杀):
|
|
599
|
+
* 1. worktree meta 存在且有 baseHash → 在 worktree 内查 baseHash..HEAD diff + 未提交改动
|
|
600
|
+
* 2. sillyspec/<change> 分支存在 → 查 merge-base..branch diff
|
|
601
|
+
* 3. 主工作区存在未提交改动(apply 后未 commit 的常见形态)→ changed
|
|
602
|
+
* 4. 均无法判定 → unknown(由调用方降级为 warning)
|
|
603
|
+
*
|
|
604
|
+
* @returns {{ status: 'changed'|'unchanged'|'unknown', detail: string }}
|
|
605
|
+
*/
|
|
606
|
+
export function checkExecuteCodeEvidence(cwd, changeName) {
|
|
607
|
+
const metaPath = join(cwd, '.sillyspec', '.runtime', 'worktrees', changeName, 'meta.json')
|
|
608
|
+
let meta = null
|
|
609
|
+
if (existsSync(metaPath)) {
|
|
610
|
+
try { meta = JSON.parse(readFileSync(metaPath, 'utf8')) } catch {}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// 1. worktree(或 in-place)meta 有 baseHash:最权威的对账基准
|
|
614
|
+
if (meta?.baseHash) {
|
|
615
|
+
const gitDir = (meta.worktreePath && meta.mode !== 'in-place-fallback' && existsSync(meta.worktreePath))
|
|
616
|
+
? meta.worktreePath
|
|
617
|
+
: cwd
|
|
618
|
+
const diff = gitTry(gitDir, ['diff', '--name-only', `${meta.baseHash}..HEAD`])
|
|
619
|
+
const status = gitTry(gitDir, ['status', '--porcelain'])
|
|
620
|
+
if (diff.ok && status.ok) {
|
|
621
|
+
const committedFiles = diff.out ? diff.out.split('\n').filter(Boolean).length : 0
|
|
622
|
+
const hasUncommitted = status.out.trim().length > 0
|
|
623
|
+
if (committedFiles > 0 || hasUncommitted) {
|
|
624
|
+
return { status: 'changed', detail: `${committedFiles} 个已提交变更文件${hasUncommitted ? ' + 未提交改动' : ''}(base ${meta.baseHash.slice(0, 8)})` }
|
|
625
|
+
}
|
|
626
|
+
return { status: 'unchanged', detail: `${gitDir} 相对 base ${meta.baseHash.slice(0, 8)} 无任何提交或未提交改动` }
|
|
627
|
+
}
|
|
628
|
+
return { status: 'unknown', detail: `git 核验失败: ${diff.error || status.error}` }
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// 2. worktree 已清理但分支残留:diff merge-base..branch
|
|
632
|
+
const branch = `sillyspec/${changeName}`
|
|
633
|
+
const branchHash = gitTry(cwd, ['rev-parse', '--verify', '--quiet', branch])
|
|
634
|
+
if (branchHash.ok && branchHash.out) {
|
|
635
|
+
const mergeBase = gitTry(cwd, ['merge-base', 'HEAD', branch])
|
|
636
|
+
if (mergeBase.ok && mergeBase.out) {
|
|
637
|
+
const diff = gitTry(cwd, ['diff', '--name-only', `${mergeBase.out}..${branch}`])
|
|
638
|
+
if (diff.ok && diff.out.split('\n').filter(Boolean).length > 0) {
|
|
639
|
+
return { status: 'changed', detail: `分支 ${branch} 相对 merge-base 有变更` }
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// 3. 主工作区未提交改动(worktree apply 后的常见形态)
|
|
645
|
+
const status = gitTry(cwd, ['status', '--porcelain'])
|
|
646
|
+
if (status.ok && status.out.trim()) {
|
|
647
|
+
return { status: 'changed', detail: '主工作区存在未提交改动' }
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
return { status: 'unknown', detail: '无 worktree meta 且无可对账的分支/未提交改动(变更可能已 apply 并提交),无法客观判定' }
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* execute 完成校验:plan.md 声明了任务时,必须存在真实代码变更。
|
|
655
|
+
* 防止"勾选 checkbox = 完成 execute"的谎报路径。
|
|
656
|
+
*/
|
|
657
|
+
function validateExecuteOutputs(cwd, changeName, context = {}) {
|
|
658
|
+
const { specRoot, evidence: passedEvidence } = context
|
|
659
|
+
const errors = []
|
|
660
|
+
const warnings = []
|
|
661
|
+
|
|
662
|
+
const changeDir = resolveChangeDir(cwd, changeName, specRoot)
|
|
663
|
+
const planPath = join(changeDir, 'plan.md')
|
|
664
|
+
// 无 plan.md 的 execute(旧流程/quick 混用)不在此核验范围
|
|
665
|
+
if (!existsSync(planPath)) return { ok: true, errors, warnings }
|
|
666
|
+
|
|
667
|
+
const planContent = readFileSync(planPath, 'utf8')
|
|
668
|
+
const hasTasks = /^\s*[-*]\s*\[[ xX]\]\s*task-\d+/m.test(planContent)
|
|
669
|
+
if (!hasTasks) return { ok: true, errors, warnings }
|
|
670
|
+
|
|
671
|
+
// W4-G (D-008):evidence 可由调用方(runGate)预先算好传入,避免与 execute-evidence check
|
|
672
|
+
// 重复 spawn git(gate execute 一次省 2 个 git 进程 ≈ 60-200ms on Windows)。未传则内部算(向后兼容)。
|
|
673
|
+
const evidence = passedEvidence || checkExecuteCodeEvidence(cwd, changeName)
|
|
674
|
+
if (evidence.status === 'unchanged') {
|
|
675
|
+
errors.push(`execute 代码变更核验失败:plan.md 声明了任务,但 ${evidence.detail} — 勾选 checkbox 不等于完成实现`)
|
|
676
|
+
} else if (evidence.status === 'unknown') {
|
|
677
|
+
warnings.push(`execute 代码变更无法客观核验:${evidence.detail}`)
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
return { ok: errors.length === 0, errors, warnings }
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// ============ Contract Registry ============
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* 主流程阶段(有严格转换顺序)
|
|
687
|
+
*/
|
|
688
|
+
const mainFlowStages = ['brainstorm', 'plan', 'execute', 'verify']
|
|
689
|
+
|
|
690
|
+
// 辅助阶段(可独立运行,无严格转换顺序)定义在 constants.js 的 AUXILIARY_STAGES,
|
|
691
|
+
// 与 stages/index.js 共用单一真相源,避免两处逐字重复分叉。
|
|
692
|
+
|
|
693
|
+
/**
|
|
694
|
+
* @type {Object<string, StageContract>}
|
|
695
|
+
*/
|
|
696
|
+
const contracts = {
|
|
697
|
+
// === 主流程 ===
|
|
698
|
+
brainstorm: {
|
|
699
|
+
stage: 'brainstorm',
|
|
700
|
+
description: '需求分析与设计',
|
|
701
|
+
allowedFrom: [], // 任何变更的起始阶段
|
|
702
|
+
allowedTo: ['plan'],
|
|
703
|
+
validators: [validateBrainstormOutputs],
|
|
704
|
+
},
|
|
705
|
+
plan: {
|
|
706
|
+
stage: 'plan',
|
|
707
|
+
description: '任务拆解与规划',
|
|
708
|
+
allowedFrom: ['brainstorm'],
|
|
709
|
+
allowedTo: ['execute'],
|
|
710
|
+
validators: [validatePlanOutputs],
|
|
711
|
+
},
|
|
712
|
+
execute: {
|
|
713
|
+
stage: 'execute',
|
|
714
|
+
description: '代码实现',
|
|
715
|
+
allowedFrom: ['plan'],
|
|
716
|
+
allowedTo: ['verify'],
|
|
717
|
+
validators: [validateExecuteOutputs],
|
|
718
|
+
},
|
|
719
|
+
verify: {
|
|
720
|
+
stage: 'verify',
|
|
721
|
+
description: '验证与测试',
|
|
722
|
+
allowedFrom: ['execute'],
|
|
723
|
+
allowedTo: ['archive'],
|
|
724
|
+
validators: [validateVerifyOutputs],
|
|
725
|
+
},
|
|
726
|
+
archive: {
|
|
727
|
+
stage: 'archive',
|
|
728
|
+
description: '归档与收口',
|
|
729
|
+
allowedFrom: ['verify'],
|
|
730
|
+
allowedTo: [],
|
|
731
|
+
// 阶段级 validator 全部移除,改为 run.js 中 step 4 完成后的硬编码校验。
|
|
732
|
+
// 理由:两个 validator 的生效窗口互斥 ——
|
|
733
|
+
// validateChangeClosed 要求变更目录存在(step 4 --confirm 后已被移到 archive 目录)
|
|
734
|
+
// validateArchiveOutputs 要求 archive 目录存在(step 4 前还不存在)
|
|
735
|
+
// 注册为阶段级 validator 会导致每步都误报错误。
|
|
736
|
+
// run.js:893-909 已在正确的时机(step 4 完成后)执行相同检查。
|
|
737
|
+
validators: [],
|
|
738
|
+
},
|
|
739
|
+
|
|
740
|
+
// === 辅助阶段 ===
|
|
741
|
+
scan: {
|
|
742
|
+
stage: 'scan',
|
|
743
|
+
description: '项目扫描',
|
|
744
|
+
allowedFrom: [], // 无前置要求
|
|
745
|
+
allowedTo: [], // 不进入主流程
|
|
746
|
+
validators: [validateScanOutputs],
|
|
747
|
+
},
|
|
748
|
+
quick: {
|
|
749
|
+
stage: 'quick',
|
|
750
|
+
description: '快速任务',
|
|
751
|
+
allowedFrom: [], // 无前置要求
|
|
752
|
+
allowedTo: [], // 不进入主流程
|
|
753
|
+
validators: [],
|
|
754
|
+
},
|
|
755
|
+
explore: {
|
|
756
|
+
stage: 'explore',
|
|
757
|
+
description: '代码探索',
|
|
758
|
+
allowedFrom: [],
|
|
759
|
+
allowedTo: [],
|
|
760
|
+
validators: [],
|
|
761
|
+
},
|
|
762
|
+
status: {
|
|
763
|
+
stage: 'status',
|
|
764
|
+
description: '项目快照',
|
|
765
|
+
allowedFrom: [],
|
|
766
|
+
allowedTo: [],
|
|
767
|
+
validators: [],
|
|
768
|
+
},
|
|
769
|
+
doctor: {
|
|
770
|
+
stage: 'doctor',
|
|
771
|
+
description: '环境诊断',
|
|
772
|
+
allowedFrom: [],
|
|
773
|
+
allowedTo: [],
|
|
774
|
+
validators: [],
|
|
775
|
+
},
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// ============ Public API ============
|
|
779
|
+
|
|
780
|
+
/**
|
|
781
|
+
* 获取阶段合约
|
|
782
|
+
*/
|
|
783
|
+
export function getContract(stageName) {
|
|
784
|
+
return contracts[stageName] || null
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
/**
|
|
788
|
+
* 校验状态转换是否允许
|
|
789
|
+
* @param {string} fromStage - 当前阶段(空字符串表示变更起始)
|
|
790
|
+
* @param {string} toStage - 目标阶段
|
|
791
|
+
* @param {{ fromStageData?: { status?: string } | undefined }} [options] - 可选,从 progress.stages[prevStage] 提取
|
|
792
|
+
* @returns {{ allowed: boolean, reason?: string }}
|
|
793
|
+
*/
|
|
794
|
+
export function checkTransition(fromStage, toStage, options = {}) {
|
|
795
|
+
const { fromStageData } = options // { status?: string } | undefined
|
|
796
|
+
const contract = contracts[toStage]
|
|
797
|
+
if (!contract) {
|
|
798
|
+
return { allowed: false, reason: `未知阶段: ${toStage}` }
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// 辅助阶段随时可执行(archive 除外:从主流程进入 archive 需要校验)
|
|
802
|
+
if (AUXILIARY_STAGES.includes(toStage) && toStage !== 'archive') {
|
|
803
|
+
return { allowed: true }
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// 同阶段内重复运行:允许(继续执行当前阶段的下一步、或修订模式继续)
|
|
807
|
+
if (fromStage === toStage) {
|
|
808
|
+
return { allowed: true }
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
// task-07: failed_post_check 门控
|
|
812
|
+
// scan post-check 未通过时,禁止进入主流程的下游阶段(brainstorm/plan/execute/verify/archive)
|
|
813
|
+
// 必须先重跑 scan 修复。toStage === 'scan' 的重跑路径已被上方 fromStage === toStage 放行。
|
|
814
|
+
// fromStageData.status 缺失(旧数据)时门控不触发(向后兼容)。
|
|
815
|
+
if (fromStage === 'scan' && fromStageData?.status === 'failed_post_check' && toStage !== 'scan') {
|
|
816
|
+
return {
|
|
817
|
+
allowed: false,
|
|
818
|
+
reason: 'scan post-check 未通过(failed_post_check),需修复后重跑 scan 再进入 ' + toStage,
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
// archive 特殊处理:从 verify 来的允许,从其他主流程阶段来的需要校验
|
|
823
|
+
if (toStage === 'archive') {
|
|
824
|
+
if (fromStage === 'verify') {
|
|
825
|
+
return { allowed: true }
|
|
826
|
+
}
|
|
827
|
+
// 独立运行 archive(无前置)也允许
|
|
828
|
+
if (!fromStage || AUXILIARY_STAGES.includes(fromStage)) {
|
|
829
|
+
return { allowed: true }
|
|
830
|
+
}
|
|
831
|
+
return { allowed: false, reason: 'archive 的前置阶段是 verify,不能从 ' + fromStage + ' 跳转' }
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// 从辅助阶段进入主流程:允许
|
|
835
|
+
if (AUXILIARY_STAGES.includes(fromStage)) {
|
|
836
|
+
return { allowed: true }
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// 无前置阶段(变更起始):只能开始 brainstorm 或辅助阶段
|
|
840
|
+
if (!fromStage) {
|
|
841
|
+
// 主流程必须从 brainstorm 开始
|
|
842
|
+
if (contract.allowedFrom.length === 0) {
|
|
843
|
+
return { allowed: true }
|
|
844
|
+
}
|
|
845
|
+
return { allowed: false, reason: `${toStage} 需要先完成 ${contract.allowedFrom.join(' 或 ')}` }
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// 主流程内部跳转:检查目标阶段的 allowedFrom 是否包含 fromStage
|
|
849
|
+
if (contract.allowedFrom.includes(fromStage)) {
|
|
850
|
+
return { allowed: true }
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
return { allowed: false, reason: `${toStage} 的前置阶段是 ${contract.allowedFrom.join(' 或 ')},不能从 ${fromStage} 跳转` }
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
/**
|
|
857
|
+
* 执行阶段完成校验
|
|
858
|
+
* @param {string} stageName
|
|
859
|
+
* @param {string} cwd
|
|
860
|
+
* @param {string} changeName
|
|
861
|
+
* @param {object} context - 额外上下文(如 projectName)
|
|
862
|
+
* @returns {ValidationResult}
|
|
863
|
+
*/
|
|
864
|
+
export function runValidators(stageName, cwd, changeName, context = {}) {
|
|
865
|
+
const contract = contracts[stageName]
|
|
866
|
+
if (!contract || contract.validators.length === 0) {
|
|
867
|
+
return { ok: true, errors: [], warnings: [] }
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
const allErrors = []
|
|
871
|
+
const allWarnings = []
|
|
872
|
+
|
|
873
|
+
for (const validator of contract.validators) {
|
|
874
|
+
try {
|
|
875
|
+
const result = validator(cwd, changeName, context)
|
|
876
|
+
allErrors.push(...(result.errors || []))
|
|
877
|
+
allWarnings.push(...(result.warnings || []))
|
|
878
|
+
} catch (e) {
|
|
879
|
+
allErrors.push(`校验器 ${validator.name || 'unknown'} 异常: ${e.message}`)
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
return { ok: allErrors.length === 0, errors: allErrors, warnings: allWarnings }
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
/**
|
|
887
|
+
* 获取所有主流程阶段
|
|
888
|
+
*/
|
|
889
|
+
export { mainFlowStages, AUXILIARY_STAGES as auxiliaryStages }
|