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,825 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* docs-check — 文档行号引用校验核心(change: 2026-08-15-docs-check-productize)
|
|
3
|
+
*
|
|
4
|
+
* 从 dogfood 私有测试 test/doc-ref-check.test.mjs 抽离产品化(design D-001~D-008):
|
|
5
|
+
* 层1 存在性(所有引用必查):文件存在 + 行号 ≥1 且 ≤ 总行数(范围引用查 end)
|
|
6
|
+
* 层2 关键词断言(keywordAssert 可配,缺省开):引用所在文档行内的反引号代码符号 token,
|
|
7
|
+
* 断言源文件 [start-2, end+5] 窗口(实现期口径,非对称——文档行号常指向块起始行
|
|
8
|
+
* 而 token 在体首几行)内含任一 token。多候选宽容:裸文件名多命中时任一候选全过即通过。
|
|
9
|
+
*
|
|
10
|
+
* 纯函数(collectDocRefs/looksLikeCodeSymbol/validateRefLines/extractExpectedTokensFromLine)
|
|
11
|
+
* 无 fs 依赖可单测;resolveCandidates/walkGlob/runDocsCheck 是 IO 面。
|
|
12
|
+
* 校验链路只读(不修改任何被校验文件);applyFixes 是唯一写回面(--fix 显式触发,design §9:
|
|
13
|
+
* 文档非多进程竞争的运行时文件,普通 writeFileSync);纯 Node 内置模块零依赖(D-008:glob 手写 walker,
|
|
14
|
+
* 仅「目录递归」「目录单层」「字面路径」三形态);Windows 路径归一化;兼容 CRLF/LF。
|
|
15
|
+
*/
|
|
16
|
+
import { readFileSync, writeFileSync, existsSync, readdirSync, statSync } from 'node:fs'
|
|
17
|
+
import { join } from 'node:path'
|
|
18
|
+
import jsYaml from 'js-yaml'
|
|
19
|
+
import { parseModuleMapSimple } from './modules.js'
|
|
20
|
+
|
|
21
|
+
/** 提取 file.js:line / file.js:start-end 引用(.js/.mjs,反引号包裹与裸文本均命中,全文扫描 D-006)。
|
|
22
|
+
* repo:// 前缀(2026-08-20):跨仓引用显式标记——`repo://<仓库名>/<路径>.js:行`。
|
|
23
|
+
* 不同设备兄弟仓库位置不同,未配映射时默认跳过本地校验(防跨设备误报);
|
|
24
|
+
* 在本机 .sillyspec/local.yaml 的 docs-check.cross_repo_roots 配
|
|
25
|
+
* `<仓库名>: <本机绝对路径>` 后,走与本地引用完全相同的层1(行号边界)+ 层2(关键词窗口)校验。
|
|
26
|
+
* 正则组:1=仓库名(可选),2=文件,3=start,4=end。 */
|
|
27
|
+
const REF_RE = /(?:repo:\/\/([A-Za-z0-9_.\-]+)\/)?([A-Za-z0-9_.\-\/]+\.(?:js|mjs)):(\d+)(?:-(\d+))?/g
|
|
28
|
+
|
|
29
|
+
/** 缺省扫描范围:docs/ + .sillyspec/docs/(scan/modules 产物同是文档,失效即该暴露;2026-08-16 用户裁决改缺省,见 doc-consistency-debt.md §八) */
|
|
30
|
+
const DEFAULT_DOC_PATHS = ['docs/**/*.md', '.sillyspec/docs/**/*.md']
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 读 local.yaml 的 docs-check 段(best-effort,绝不抛;缺文件/无段 → 全缺省)。
|
|
34
|
+
* @param {string} projectRoot 源码仓根(local.yaml 在 <root>/.sillyspec/local.yaml)
|
|
35
|
+
* @returns {{ paths?: string[], skip: string[], keywordAssert: boolean, crossRepoRoots: Record<string, string> }}
|
|
36
|
+
*/
|
|
37
|
+
export function readDocsCheckConfig(projectRoot) {
|
|
38
|
+
const fallback = { paths: null, skip: [], keywordAssert: true, crossRepoRoots: {} }
|
|
39
|
+
try {
|
|
40
|
+
const p = join(projectRoot, '.sillyspec', 'local.yaml')
|
|
41
|
+
if (!existsSync(p)) return fallback
|
|
42
|
+
const doc = jsYaml.load(readFileSync(p, 'utf8'))
|
|
43
|
+
if (!doc || typeof doc !== 'object') return fallback
|
|
44
|
+
const dc = doc['docs-check']
|
|
45
|
+
if (!dc || typeof dc !== 'object') return fallback
|
|
46
|
+
const crr = dc['cross_repo_roots']
|
|
47
|
+
return {
|
|
48
|
+
paths: Array.isArray(dc.paths) ? dc.paths.filter(s => typeof s === 'string') : null,
|
|
49
|
+
skip: Array.isArray(dc.skip) ? dc.skip.filter(s => typeof s === 'string') : [],
|
|
50
|
+
keywordAssert: typeof dc.keywordAssert === 'boolean' ? dc.keywordAssert : true,
|
|
51
|
+
// repo://<name> → 本机仓库根(每台设备各自配;local.yaml 是 gitignored,绝对路径不入库)
|
|
52
|
+
crossRepoRoots: crr && typeof crr === 'object' && !Array.isArray(crr)
|
|
53
|
+
? Object.fromEntries(
|
|
54
|
+
Object.entries(crr).filter(([, v]) => typeof v === 'string' && v.trim() !== ''),
|
|
55
|
+
)
|
|
56
|
+
: {},
|
|
57
|
+
}
|
|
58
|
+
} catch {
|
|
59
|
+
return fallback
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** 配置错误(glob 形态不支持等)→ CLI exit 2 */
|
|
64
|
+
export class DocsCheckConfigError extends Error {}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 纯函数:从 markdown 全文提取全部 file:line 引用。
|
|
68
|
+
* @param {string} md 文档全文
|
|
69
|
+
* @returns {Array<{ ref, file, start, end, docLine }>} docLine 为 1-based 文档行号
|
|
70
|
+
*/
|
|
71
|
+
export function collectDocRefs(md) {
|
|
72
|
+
if (!md || typeof md !== 'string') return []
|
|
73
|
+
const refs = []
|
|
74
|
+
const re = new RegExp(REF_RE.source, 'g')
|
|
75
|
+
// 行号单遍计算:regex match 天然按 index 升序,游标只前进——原逐引用
|
|
76
|
+
// slice(0,m.index).split() 是 O(引用数 × 文档长度),长文档×多引用退化明显
|
|
77
|
+
let line = 1
|
|
78
|
+
let cursor = 0
|
|
79
|
+
let m
|
|
80
|
+
while ((m = re.exec(md)) !== null) {
|
|
81
|
+
for (let i = cursor; i < m.index; i++) {
|
|
82
|
+
if (md.charCodeAt(i) === 10) line++ // '\n'(\r\n 也以 \n 计数,1-based 行号不受影响)
|
|
83
|
+
}
|
|
84
|
+
cursor = m.index
|
|
85
|
+
refs.push({
|
|
86
|
+
ref: m[0],
|
|
87
|
+
repo: m[1] !== undefined ? m[1] : null,
|
|
88
|
+
file: m[2],
|
|
89
|
+
start: parseInt(m[3], 10),
|
|
90
|
+
end: m[4] !== undefined ? parseInt(m[4], 10) : parseInt(m[3], 10),
|
|
91
|
+
docLine: line,
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
return refs
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* 纯函数:判定 token 是否「像代码符号」:首字符字母/_/$,且含大写字母/下划线/点/$ 之一。
|
|
99
|
+
* 纯小写英文单词(local/platform/abort)→ false(自然语言,跳过断言防误报)。
|
|
100
|
+
*/
|
|
101
|
+
export function looksLikeCodeSymbol(token) {
|
|
102
|
+
if (!/^[A-Za-z_$][A-Za-z0-9_$.]*$/.test(token)) return false
|
|
103
|
+
return /[A-Z_$.]/.test(token)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* 纯函数:层1 行号边界校验。
|
|
108
|
+
* @param {number} totalLines 源文件总行数
|
|
109
|
+
* @param {number} start 1-based 起始行
|
|
110
|
+
* @param {number} end 1-based 结束行(单行引用 = start)
|
|
111
|
+
* @returns {{ ok: boolean, reasons: string[] }}
|
|
112
|
+
*/
|
|
113
|
+
export function validateRefLines(totalLines, start, end) {
|
|
114
|
+
const reasons = []
|
|
115
|
+
if (start < 1 || start > totalLines) reasons.push(`行号超界(start=${start} > 总行数 ${totalLines})`)
|
|
116
|
+
if (end < start || end > totalLines) reasons.push(`范围 end=${end} 超界(总行数 ${totalLines})`)
|
|
117
|
+
return { ok: reasons.length === 0, reasons }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* 纯函数:层2 token 提取——引用所在的文档行内所有反引号 token 中的代码符号。
|
|
122
|
+
* token 归一:①剥函数括号(getDispatchMode());②点分名拆段(syncMod.checkApproval → 两段)。
|
|
123
|
+
* 空数组 = 纯位置引用,跳过层2。
|
|
124
|
+
* @param {string} line 引用所在的文档行全文
|
|
125
|
+
* @returns {string[]} token 列表
|
|
126
|
+
*/
|
|
127
|
+
export function extractExpectedTokensFromLine(line) {
|
|
128
|
+
if (!line || typeof line !== 'string') return []
|
|
129
|
+
const tokens = new Set()
|
|
130
|
+
for (const mm of line.matchAll(/`([^`\n]{1,60})`/g)) {
|
|
131
|
+
const t = mm[1].split('(')[0].trim()
|
|
132
|
+
if (!looksLikeCodeSymbol(t)) continue
|
|
133
|
+
tokens.add(t)
|
|
134
|
+
if (t.includes('.')) {
|
|
135
|
+
for (const seg of t.split('.')) {
|
|
136
|
+
if (looksLikeCodeSymbol(seg)) tokens.add(seg)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return [...tokens]
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** 递归收集 dir 下与 baseName 同名的文件(相对 dir 的 POSIX 路径数组;排除 node_modules/.git)。
|
|
144
|
+
* treeCache 给定时按 baseName 复用全树扫描结果(N 条裸名引用同一文件免 N 次 walk) */
|
|
145
|
+
function findInTree(dir, baseName, rel = '', treeCache = null) {
|
|
146
|
+
if (treeCache && rel === '') {
|
|
147
|
+
const hit = treeCache.get(baseName)
|
|
148
|
+
if (hit) return hit
|
|
149
|
+
const result = findInTree(dir, baseName, '', null)
|
|
150
|
+
treeCache.set(baseName, result)
|
|
151
|
+
return result
|
|
152
|
+
}
|
|
153
|
+
const out = []
|
|
154
|
+
let entries
|
|
155
|
+
try { entries = readdirSync(dir, { withFileTypes: true }) } catch { return out }
|
|
156
|
+
for (const e of entries) {
|
|
157
|
+
if (e.name === 'node_modules' || e.name === '.git') continue
|
|
158
|
+
const relPath = rel ? `${rel}/${e.name}` : e.name
|
|
159
|
+
if (e.isDirectory()) out.push(...findInTree(join(dir, e.name), baseName, relPath))
|
|
160
|
+
else if (e.name === baseName) out.push(relPath)
|
|
161
|
+
}
|
|
162
|
+
return out
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* 解析引用文件 → 候选绝对路径数组。文档写法三种混用(首跑实证):
|
|
167
|
+
* ①仓库根相对(src/sync.js / docs/...)→ 直拼;②src/ 内部相对(dispatch/probe.js)→
|
|
168
|
+
* src/ 前缀重试;③裸中缀(backends/sillyhub-mcp.js)→ src/ 树内按路径后缀匹配。
|
|
169
|
+
* 空数组 = 不存在。treeCache(Map basename→findInTree 结果)给定时透传复用全树扫描。
|
|
170
|
+
*/
|
|
171
|
+
export function resolveCandidates(projectRoot, refFile, treeCache = null) {
|
|
172
|
+
if (refFile.includes('/')) {
|
|
173
|
+
const direct = join(projectRoot, refFile)
|
|
174
|
+
if (existsSync(direct)) return [direct]
|
|
175
|
+
const inSrc = join(projectRoot, 'src', refFile)
|
|
176
|
+
if (existsSync(inSrc)) return [inSrc]
|
|
177
|
+
const slash = refFile.lastIndexOf('/')
|
|
178
|
+
const baseName = slash === -1 ? refFile : refFile.slice(slash + 1)
|
|
179
|
+
return findInTree(join(projectRoot, 'src'), baseName, '', treeCache)
|
|
180
|
+
.filter((rel) => ('src/' + rel).endsWith('/' + refFile) || 'src/' + rel === 'src/' + refFile)
|
|
181
|
+
.map((rel) => join(projectRoot, 'src', rel))
|
|
182
|
+
}
|
|
183
|
+
return findInTree(join(projectRoot, 'src'), refFile, '', treeCache).map((rel) => join(projectRoot, 'src', rel))
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** 读文件行数组(CRLF/LF 归一:split(/\r?\n/),层2 只做子串查找不污染原文)。
|
|
187
|
+
* linesCache 给定时按绝对路径复用(同文件被 N 条引用消费免 N 次读盘+split) */
|
|
188
|
+
function readLines(absPath, linesCache = null) {
|
|
189
|
+
if (linesCache) {
|
|
190
|
+
if (linesCache.has(absPath)) return linesCache.get(absPath)
|
|
191
|
+
const lines = readLines(absPath)
|
|
192
|
+
linesCache.set(absPath, lines)
|
|
193
|
+
return lines
|
|
194
|
+
}
|
|
195
|
+
try { return readFileSync(absPath, 'utf8').split(/\r?\n/) } catch { return null }
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** 简单通配匹配(skip 排除用,仅 * 单段) */
|
|
199
|
+
function matchSimple(text, pattern) {
|
|
200
|
+
const parts = pattern.split('*')
|
|
201
|
+
let idx = 0
|
|
202
|
+
for (let i = 0; i < parts.length; i++) {
|
|
203
|
+
if (parts[i] === '') continue
|
|
204
|
+
const found = text.indexOf(parts[i], idx)
|
|
205
|
+
if (found === -1) return false
|
|
206
|
+
if (i === 0 && found !== 0) return false
|
|
207
|
+
idx = found + parts[i].length
|
|
208
|
+
}
|
|
209
|
+
return true
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* glob walker(D-008:手写不引依赖)。仅支持三形态:
|
|
214
|
+
* - `dir/「递归」*.ext` 递归收集(maxDepth 12 兜底防符号链接环)
|
|
215
|
+
* - `dir/*.ext` 单层
|
|
216
|
+
* - 字面路径直传(存在才返回)
|
|
217
|
+
* 其他形态(?、{a,b}、多层 * 混合等)抛 DocsCheckConfigError → CLI exit 2。
|
|
218
|
+
* @param {string} projectRoot 锚点(平台模式也锚源码仓根,design §3.3)
|
|
219
|
+
* @param {string} pattern glob 模式
|
|
220
|
+
* @param {string[]} skip 排除 glob(前缀或单 * 简单匹配)
|
|
221
|
+
* @returns {string[]} 命中文件的 POSIX 相对路径
|
|
222
|
+
*/
|
|
223
|
+
export function walkGlob(projectRoot, pattern, skip = []) {
|
|
224
|
+
const norm = (p) => p.replace(/\\/g, '/')
|
|
225
|
+
const isSkipped = (rel) => skip.some((s) => {
|
|
226
|
+
const sn = norm(s).replace(/\/$/, '')
|
|
227
|
+
return rel === sn || rel.startsWith(sn + '/') ||
|
|
228
|
+
(sn.includes('*') && matchSimple(rel, sn))
|
|
229
|
+
})
|
|
230
|
+
const matchExt = (name, ext) => name.endsWith(ext)
|
|
231
|
+
// 形态 0:**/*.ext(根级递归,无 base 前缀——B11b:原被形态 2 误解析为字面目录 `**` 静默 0 命中)
|
|
232
|
+
let m0 = norm(pattern).match(/^\*\*\/\*(\.[A-Za-z0-9]+)$/)
|
|
233
|
+
if (m0) {
|
|
234
|
+
const [, ext] = m0
|
|
235
|
+
const out = []
|
|
236
|
+
const rec = (dir, rel, depth) => {
|
|
237
|
+
if (depth > 12) return
|
|
238
|
+
let entries
|
|
239
|
+
try { entries = readdirSync(dir, { withFileTypes: true }) } catch { return }
|
|
240
|
+
for (const e of entries) {
|
|
241
|
+
if (e.name === 'node_modules' || e.name === '.git') continue
|
|
242
|
+
const relPath = rel ? `${rel}/${e.name}` : e.name
|
|
243
|
+
if (isSkipped(relPath)) continue
|
|
244
|
+
if (e.isDirectory()) rec(join(dir, e.name), relPath, depth + 1)
|
|
245
|
+
else if (matchExt(e.name, ext)) out.push(relPath)
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
rec(projectRoot, '', 0)
|
|
249
|
+
return out
|
|
250
|
+
}
|
|
251
|
+
// 形态 1:**/*.ext
|
|
252
|
+
let m = norm(pattern).match(/^(.+?)\/\*\*\/\*(\.[A-Za-z0-9]+)$/)
|
|
253
|
+
if (m) {
|
|
254
|
+
const [, baseDir, ext] = m
|
|
255
|
+
const out = []
|
|
256
|
+
const rec = (dir, rel, depth) => {
|
|
257
|
+
if (depth > 12) return
|
|
258
|
+
let entries
|
|
259
|
+
try { entries = readdirSync(dir, { withFileTypes: true }) } catch { return }
|
|
260
|
+
for (const e of entries) {
|
|
261
|
+
if (e.name === 'node_modules' || e.name === '.git') continue
|
|
262
|
+
const relPath = rel ? `${rel}/${e.name}` : e.name
|
|
263
|
+
if (isSkipped(relPath)) continue
|
|
264
|
+
if (e.isDirectory()) rec(join(dir, e.name), relPath, depth + 1)
|
|
265
|
+
else if (matchExt(e.name, ext)) out.push(relPath)
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
rec(join(projectRoot, baseDir), baseDir, 0)
|
|
269
|
+
return out
|
|
270
|
+
}
|
|
271
|
+
// 形态 2:dir/*.ext(单层)
|
|
272
|
+
m = norm(pattern).match(/^(.+?)\/\*(\.[A-Za-z0-9]+)$/)
|
|
273
|
+
if (m) {
|
|
274
|
+
const [, baseDir, ext] = m
|
|
275
|
+
const out = []
|
|
276
|
+
let entries
|
|
277
|
+
try { entries = readdirSync(join(projectRoot, baseDir), { withFileTypes: true }) } catch { return out }
|
|
278
|
+
for (const e of entries) {
|
|
279
|
+
if (!e.isFile()) continue
|
|
280
|
+
const relPath = `${baseDir}/${e.name}`
|
|
281
|
+
if (isSkipped(relPath)) continue
|
|
282
|
+
if (matchExt(e.name, ext)) out.push(relPath)
|
|
283
|
+
}
|
|
284
|
+
return out
|
|
285
|
+
}
|
|
286
|
+
// 形态 3:字面路径(不含任何通配元字符才直传;含 ?、{ }、[ ] 等不支持形态落 error)
|
|
287
|
+
if (!/[*?{}\[\]]/.test(pattern)) {
|
|
288
|
+
const abs = join(projectRoot, pattern)
|
|
289
|
+
if (!existsSync(abs)) return []
|
|
290
|
+
// B11b:目录字面量抛配置错误(原直传 readFileSync 撞 EISDIR 裸崩 exit 1;契约应 exit 2)
|
|
291
|
+
if (statSync(abs).isDirectory()) {
|
|
292
|
+
throw new DocsCheckConfigError(`路径是目录不是文档:${pattern}(文档路径应为 .md 文件或 glob 形态 **/*.ext / dir/**/*.ext / dir/*.ext)`)
|
|
293
|
+
}
|
|
294
|
+
return [norm(pattern)]
|
|
295
|
+
}
|
|
296
|
+
throw new DocsCheckConfigError(`不支持的 glob 形态:${pattern}(当前仅支持 dir/**/*.ext、dir/*.ext、字面路径)`)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** 候选路径 → 相对 projectRoot 显示(失败信息用;Windows 反斜杠归一) */
|
|
300
|
+
function relDisplay(absPath, projectRoot) {
|
|
301
|
+
return absPath
|
|
302
|
+
.replace(/\\/g, '/')
|
|
303
|
+
.replace(projectRoot.replace(/\\/g, '/') + '/', '')
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* IO 入口:跑一次完整校验。
|
|
308
|
+
* @param {{ projectRoot: string, docs?: string[], paths?: string[], skip?: string[], keywordAssert?: boolean, crossRepoRoots?: Record<string, string> }} opts
|
|
309
|
+
* projectRoot 源码仓根(候选解析与 glob 的锚点,平台模式也传源码仓根)
|
|
310
|
+
* docs 显式文档相对路径列表(优先于 paths glob 展开结果)
|
|
311
|
+
* crossRepoRoots repo://<仓库名> → 本机仓库根映射(未映射的跨仓引用跳过不计失效)
|
|
312
|
+
* @returns {{ ok: boolean, total: number, invalid: Array<{doc, docLine, ref, reason, suggest?: number[], fix: {fixable: boolean, newLine?: number, reason: string}}>, warnings: string[], kwChecked: number, crossRepoSkipped: number }}
|
|
313
|
+
* @throws {DocsCheckConfigError} glob 形态不支持
|
|
314
|
+
*/
|
|
315
|
+
export function runDocsCheck(opts) {
|
|
316
|
+
// paths/docs 显式传 null(readDocsCheckConfig 无 local.yaml 段时的回退值)须落回缺省 glob,
|
|
317
|
+
// 解构默认值只挡 undefined 不挡 null——docs-check 无配置裸跑曾因此 null.flatMap 崩溃。
|
|
318
|
+
const {
|
|
319
|
+
projectRoot, docs = null, paths: rawPaths = DEFAULT_DOC_PATHS, skip = [],
|
|
320
|
+
keywordAssert = true, crossRepoRoots = {},
|
|
321
|
+
} = opts || {}
|
|
322
|
+
const paths = Array.isArray(rawPaths) && rawPaths.length > 0 ? rawPaths : DEFAULT_DOC_PATHS
|
|
323
|
+
const warnings = []
|
|
324
|
+
if (keywordAssert === false) warnings.push('关键词断言已关闭(keywordAssert=false),仅做存在性校验')
|
|
325
|
+
const docFiles = docs && docs.length > 0
|
|
326
|
+
? docs
|
|
327
|
+
: [...new Set(paths.flatMap((p) => walkGlob(projectRoot, p, skip)))].sort()
|
|
328
|
+
|
|
329
|
+
const invalid = []
|
|
330
|
+
let total = 0
|
|
331
|
+
let kwChecked = 0
|
|
332
|
+
// repo:// 跨仓引用:未配映射被跳过的数量(不计入 total/invalid,跨设备零误报)
|
|
333
|
+
let crossRepoSkipped = 0
|
|
334
|
+
// per-call 缓存:裸名引用的 src/ 全树扫描结果 + 候选文件行数组——100 文档 × 10 裸名引用
|
|
335
|
+
// 此前 = 1000 次全树 walk + 同文件 N 次重读(2026-08-21 性能审查 PERF-5)。仅本次调用
|
|
336
|
+
// 生命周期内有效,不跨调用(防测试流程中途改树后读到陈旧结果)
|
|
337
|
+
const treeCache = new Map()
|
|
338
|
+
const linesCache = new Map()
|
|
339
|
+
|
|
340
|
+
for (const docRel of docFiles) {
|
|
341
|
+
const docAbs = join(projectRoot, docRel)
|
|
342
|
+
if (!existsSync(docAbs)) {
|
|
343
|
+
invalid.push({
|
|
344
|
+
doc: docRel, docLine: 0, ref: '', reason: '文档不存在',
|
|
345
|
+
fix: { fixable: false, reason: '文档不存在,无法自动定位' },
|
|
346
|
+
})
|
|
347
|
+
continue
|
|
348
|
+
}
|
|
349
|
+
const md = readFileSync(docAbs, 'utf8')
|
|
350
|
+
const refs = collectDocRefs(md)
|
|
351
|
+
const mdLines = md.split(/\r?\n/)
|
|
352
|
+
|
|
353
|
+
for (const r of refs) {
|
|
354
|
+
// repo:// 跨仓引用:未配映射 → 跳过(不同设备仓库位置不同,不按本仓解析误报);
|
|
355
|
+
// 配了映射 → 以映射根为唯一候选,走与本地引用相同的层1+层2 校验。
|
|
356
|
+
let candidates
|
|
357
|
+
if (r.repo) {
|
|
358
|
+
const repoRoot = crossRepoRoots && typeof crossRepoRoots[r.repo] === 'string'
|
|
359
|
+
? crossRepoRoots[r.repo]
|
|
360
|
+
: null
|
|
361
|
+
if (!repoRoot) {
|
|
362
|
+
crossRepoSkipped++
|
|
363
|
+
continue
|
|
364
|
+
}
|
|
365
|
+
const abs = join(repoRoot, r.file)
|
|
366
|
+
if (!existsSync(abs)) {
|
|
367
|
+
total++
|
|
368
|
+
invalid.push({
|
|
369
|
+
doc: docRel, docLine: r.docLine, ref: r.ref,
|
|
370
|
+
reason: `跨仓 repo://${r.repo} → 文件不存在(cross_repo_roots 映射根:${repoRoot})`,
|
|
371
|
+
fix: { fixable: false, reason: '跨仓引用无法自动定位' },
|
|
372
|
+
})
|
|
373
|
+
continue
|
|
374
|
+
}
|
|
375
|
+
candidates = [abs]
|
|
376
|
+
} else {
|
|
377
|
+
candidates = resolveCandidates(projectRoot, r.file, treeCache)
|
|
378
|
+
}
|
|
379
|
+
total++
|
|
380
|
+
if (candidates.length === 0) {
|
|
381
|
+
invalid.push({
|
|
382
|
+
doc: docRel, docLine: r.docLine, ref: r.ref,
|
|
383
|
+
reason: '文件不存在(含 / 按仓库根解析;裸文件名在 src/ 递归)',
|
|
384
|
+
fix: { fixable: false, reason: '文件不存在,无法自动定位' },
|
|
385
|
+
})
|
|
386
|
+
continue
|
|
387
|
+
}
|
|
388
|
+
// 多候选宽容:逐候选跑层1+层2,任一全过即通过
|
|
389
|
+
const tokens = keywordAssert
|
|
390
|
+
? extractExpectedTokensFromLine(mdLines[r.docLine - 1] || '')
|
|
391
|
+
: []
|
|
392
|
+
const candidateFails = []
|
|
393
|
+
let passedAny = false
|
|
394
|
+
for (const candAbs of candidates) {
|
|
395
|
+
const lines = readLines(candAbs, linesCache)
|
|
396
|
+
if (lines === null) { candidateFails.push(`${relDisplay(candAbs, projectRoot)}: 读取失败`); continue }
|
|
397
|
+
const v = validateRefLines(lines.length, r.start, r.end)
|
|
398
|
+
// 层2:token 任一在 [start-2, end+5] 窗口命中即过
|
|
399
|
+
let kwOk = true
|
|
400
|
+
if (tokens.length > 0) {
|
|
401
|
+
const from = Math.max(0, r.start - 2)
|
|
402
|
+
const to = Math.min(lines.length, r.end + 5)
|
|
403
|
+
const window = lines.slice(from, to).join('\n')
|
|
404
|
+
kwOk = tokens.some((t) => window.includes(t))
|
|
405
|
+
}
|
|
406
|
+
if (v.ok && kwOk) { passedAny = true; break }
|
|
407
|
+
const reasons = [...v.reasons]
|
|
408
|
+
if (!kwOk) reasons.push(`关键词缺失:期望任一「${tokens.join(' / ')}」在 [start-2, end+5] 窗口内`)
|
|
409
|
+
candidateFails.push(`${relDisplay(candAbs, projectRoot)}: ${reasons.join(';')}`)
|
|
410
|
+
}
|
|
411
|
+
if (tokens.length > 0) kwChecked++
|
|
412
|
+
if (!passedAny) {
|
|
413
|
+
// 跨仓校验失败的 reason 加 repo:// 前缀,用户一眼区分「改外部仓引用」还是「改本仓引用」
|
|
414
|
+
const repoPrefix = r.repo ? `跨仓 repo://${r.repo} → ` : ''
|
|
415
|
+
invalid.push({
|
|
416
|
+
doc: docRel, docLine: r.docLine, ref: r.ref,
|
|
417
|
+
reason: repoPrefix + (candidateFails.length > 1 ? `多候选全失败 → ${candidateFails.join(' | ')}` : candidateFails[0]),
|
|
418
|
+
// 建议行号(--suggest):token 在首个候选文件的全量命中行,供人工确认改锚——不自动改文件
|
|
419
|
+
suggest: suggestLines(candidates, tokens, linesCache),
|
|
420
|
+
// 修复分类(--fix 判定依据):全量候选 token 命中打分——唯一命中或选优严格领先可自动重锚
|
|
421
|
+
fix: classifyFix(candidates, tokens, r.start, linesCache),
|
|
422
|
+
})
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
return { ok: invalid.length === 0, total, invalid, warnings, kwChecked, crossRepoSkipped }
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* 建议行号计算(--suggest):对失效引用,在候选文件里找 token 全量命中行。
|
|
432
|
+
* 无 token(纯行号断言)时返回空数组——没有符号线索无法定位,只能人工看。
|
|
433
|
+
*/
|
|
434
|
+
function suggestLines(candidates, tokens, linesCache = null) {
|
|
435
|
+
if (tokens.length === 0) return []
|
|
436
|
+
try {
|
|
437
|
+
const lines = readLines(candidates[0], linesCache)
|
|
438
|
+
if (lines === null) return []
|
|
439
|
+
const out = []
|
|
440
|
+
lines.forEach((l, i) => { if (tokens.some((t) => l.includes(t))) out.push(i + 1) })
|
|
441
|
+
return out.slice(0, 8)
|
|
442
|
+
} catch { return [] }
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* 修复分类(--fix 判定依据):对 resolveCandidates 全量候选跑 token 命中统计,跨候选合并行号后——
|
|
447
|
+
* 唯一命中 → fixable=true + newLine(自动重锚唯一解);
|
|
448
|
+
* 多命中 → 打分选优(2026-08-21 docs-ref-auto-pick,替代 D-006 一律人工):top1 严格优于
|
|
449
|
+
* top2(分数差 > 0)自动重锚,同分仍人工。实证:一次大改 34 处漂移只有 2 处唯一命中,
|
|
450
|
+
* 32 处人工逐个挑——多命中不代表歧义,行号漂移多为单次编辑的单调位移。
|
|
451
|
+
* 打分信号(全部来自现有数据,不新增 IO):
|
|
452
|
+
* - 距离:|命中行 - 文档旧行号|(权重封顶 400)——最近的命中大概率是原锚点漂移后的位置
|
|
453
|
+
* - 定义行模式 +50:function/const/let/class/def/export 关键词**紧跟** token(引用锚通常指
|
|
454
|
+
* 定义起始行而非调用点;「keyword 后 60 字符内出现」的宽松口径会把调用点误判为定义)
|
|
455
|
+
* - 注释行 -30(// * # -- 开头)
|
|
456
|
+
* - 含最长 token(点分整串)+10,优于只含拆段
|
|
457
|
+
* 与 suggestLines 的差异:suggest 只查首个候选(candidates[0]),本函数查全量候选(design §12 自审存疑 +
|
|
458
|
+
* R-01——多候选同名文件场景下防止定位到另一文件的同名符号)。合并口径按「去重后的行号集合」:
|
|
459
|
+
* 重锚写回的目标是行号,多个候选命中同一行号仍是唯一改写目标。
|
|
460
|
+
* 纯增量分类,不参与层1/层2 判定(D-004 兼容红线)。
|
|
461
|
+
* @param {string[]} candidates resolveCandidates 返回的全量候选绝对路径
|
|
462
|
+
* @param {string[]} tokens 层2 提取的期望 token(keywordAssert=false 时为空数组)
|
|
463
|
+
* @param {number|null} [refStart] 文档引用的旧行号(打分用距离信号;范围引用取 start)
|
|
464
|
+
* @returns {{ fixable: boolean, newLine?: number, reason: string }}
|
|
465
|
+
*/
|
|
466
|
+
function escapeReLocal(s) {
|
|
467
|
+
return String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function classifyFix(candidates, tokens, refStart = null, linesCache = null) {
|
|
471
|
+
if (tokens.length === 0) {
|
|
472
|
+
return { fixable: false, reason: '无 token 符号线索(纯位置引用或关键词断言关闭),无法自动定位' }
|
|
473
|
+
}
|
|
474
|
+
const longest = tokens.reduce((a, b) => (b.length > a.length ? b : a), '')
|
|
475
|
+
const defToken = longest.includes('.') ? longest.split('.').pop() : longest
|
|
476
|
+
const defRe = new RegExp(`(?:export\\s+)?(?:async\\s+)?(?:function|const|let|class|def)\\s+${escapeReLocal(defToken)}\\b`)
|
|
477
|
+
const byLine = new Map()
|
|
478
|
+
for (const candAbs of candidates) {
|
|
479
|
+
const lines = readLines(candAbs, linesCache)
|
|
480
|
+
if (lines === null) continue
|
|
481
|
+
lines.forEach((l, i) => {
|
|
482
|
+
if (!tokens.some((t) => l.includes(t))) return
|
|
483
|
+
const lineNo = i + 1
|
|
484
|
+
let score = 0
|
|
485
|
+
if (/^(\/\/|\*|#|--)/.test(l.trim())) score -= 30
|
|
486
|
+
if (defRe.test(l)) score += 50
|
|
487
|
+
if (l.includes(longest)) score += 10
|
|
488
|
+
if (refStart != null && Number.isFinite(refStart)) score -= Math.min(Math.abs(lineNo - refStart), 400)
|
|
489
|
+
// 同一行跨候选取最高分(合并口径不变:重锚目标是行号)
|
|
490
|
+
if (score > (byLine.get(lineNo) ?? -Infinity)) byLine.set(lineNo, score)
|
|
491
|
+
})
|
|
492
|
+
}
|
|
493
|
+
if (byLine.size === 0) {
|
|
494
|
+
return { fixable: false, reason: 'token 在全量候选文件零命中,无法自动定位' }
|
|
495
|
+
}
|
|
496
|
+
if (byLine.size === 1) {
|
|
497
|
+
return { fixable: true, newLine: [...byLine.keys()][0], reason: 'token 在全量候选唯一命中,可自动重锚' }
|
|
498
|
+
}
|
|
499
|
+
const ranked = [...byLine.entries()].sort((a, b) => (b[1] - a[1]) || (a[0] - b[0]))
|
|
500
|
+
const [topLine, topScore] = ranked[0]
|
|
501
|
+
const [runLine, runScore] = ranked[1]
|
|
502
|
+
if (topScore > runScore) {
|
|
503
|
+
return {
|
|
504
|
+
fixable: true,
|
|
505
|
+
newLine: topLine,
|
|
506
|
+
reason: `多命中自动选优:picked=${topLine}(${topScore} 分)runnerUp=${runLine}(${runScore} 分,严格落后才自动改)`,
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
const shown = ranked.slice(0, 8).map(([l]) => l).join('、') + (ranked.length > 8 ? '…' : '')
|
|
510
|
+
return { fixable: false, reason: `token 多处命中且最优两行同分(候选行号:${shown}),歧义需人工确认(D-006 保守默认)` }
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* 将可确定性修复的失效引用写回文档(--fix 消费面,design §7 接口逐字)。
|
|
515
|
+
* 纯机械定点替换:按 docLine + ref 串在该行内的字符偏移替换为 newRef,不解析行号语义、
|
|
516
|
+
* 不改引用文件名与 token(D-003:ref/newRef 一致性由调用方保证);候选定位全部依赖调用方
|
|
517
|
+
* 传入(不读 local.yaml 不解析配置)。范围引用(src/a.js:3-8)newRef 同样由调用方给全串。
|
|
518
|
+
* 同一行多个 fix 按行内偏移从后往前(降序)替换,防前序替换挤偏后序偏移(R-04);
|
|
519
|
+
* 其余字节不动:行内容只动命中片段,行结束符按原文(\r\n / \n 整文件统一口径)原样 join(R-05)。
|
|
520
|
+
* @param {string} projectRoot 源码仓根(doc 相对路径的锚点)
|
|
521
|
+
* @param {Array<{doc: string, docLine: number, ref: string, newRef: string}>} fixes
|
|
522
|
+
* doc 为相对 projectRoot 的文档路径;ref 旧引用串(如 src/a.js:6),newRef 新引用串(如 src/a.js:1)
|
|
523
|
+
* @param {{dryRun?: boolean}} opts dryRun 为真时不写盘仅返回将应用列表
|
|
524
|
+
* @returns {{applied: number, skipped: Array<{ref: string, reason: string}>}}
|
|
525
|
+
* dryRun 时 applied 为「将应用」计数;skipped 条目含 ref 与可读 reason(文档不存在/docLine 越界/
|
|
526
|
+
* 行内找不到 ref 串),跳过不中断批量
|
|
527
|
+
*/
|
|
528
|
+
export function applyFixes(projectRoot, fixes, opts = {}) {
|
|
529
|
+
const dryRun = opts.dryRun === true
|
|
530
|
+
let applied = 0
|
|
531
|
+
const skipped = []
|
|
532
|
+
if (!Array.isArray(fixes) || fixes.length === 0) return { applied, skipped }
|
|
533
|
+
|
|
534
|
+
// 按 doc 分组,每文档一次读改写(design §9:普通 writeFileSync,文档非运行时竞争文件)
|
|
535
|
+
const byDoc = new Map()
|
|
536
|
+
for (const f of fixes) {
|
|
537
|
+
if (!f || typeof f !== 'object' || typeof f.doc !== 'string' || typeof f.docLine !== 'number' ||
|
|
538
|
+
typeof f.ref !== 'string' || typeof f.newRef !== 'string') {
|
|
539
|
+
skipped.push({ ref: f && typeof f.ref === 'string' ? f.ref : '(条目缺失)', reason: 'fix 条目字段不完整(doc/docLine/ref/newRef)' })
|
|
540
|
+
continue
|
|
541
|
+
}
|
|
542
|
+
if (!byDoc.has(f.doc)) byDoc.set(f.doc, [])
|
|
543
|
+
byDoc.get(f.doc).push(f)
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
for (const [docRel, docFixes] of byDoc) {
|
|
547
|
+
const docAbs = join(projectRoot, docRel)
|
|
548
|
+
if (!existsSync(docAbs)) {
|
|
549
|
+
for (const f of docFixes) skipped.push({ ref: f.ref, reason: `文档不存在:${docRel}` })
|
|
550
|
+
continue
|
|
551
|
+
}
|
|
552
|
+
const raw = readFileSync(docAbs, 'utf8')
|
|
553
|
+
// 行结束符检测整文件统一口径(R-05):原文含 \r\n 则 CRLF join,否则 LF——与 readLines 归一逻辑一致
|
|
554
|
+
const eol = raw.includes('\r\n') ? '\r\n' : '\n'
|
|
555
|
+
const lines = raw.split(/\r?\n/)
|
|
556
|
+
let dirty = false
|
|
557
|
+
|
|
558
|
+
// 同一 docLine 的多 fix 按行内偏移从后往前替换(R-04):先按 docLine 分组,
|
|
559
|
+
// 组内按 ref 在该行内的命中偏移降序逐个定点替换
|
|
560
|
+
const byLine = new Map()
|
|
561
|
+
for (const f of docFixes) {
|
|
562
|
+
if (!byLine.has(f.docLine)) byLine.set(f.docLine, [])
|
|
563
|
+
byLine.get(f.docLine).push(f)
|
|
564
|
+
}
|
|
565
|
+
for (const [docLine, lineFixes] of byLine) {
|
|
566
|
+
if (!Number.isInteger(docLine) || docLine < 1 || docLine > lines.length) {
|
|
567
|
+
for (const f of lineFixes) skipped.push({ ref: f.ref, reason: `docLine=${docLine} 越界(文档共 ${lines.length} 行)` })
|
|
568
|
+
continue
|
|
569
|
+
}
|
|
570
|
+
const li = docLine - 1
|
|
571
|
+
const located = []
|
|
572
|
+
let cursor = 0
|
|
573
|
+
for (const f of lineFixes) {
|
|
574
|
+
// 找不到 ref 串、或两次命中同一偏移(重复 fix),该条目跳过不中断批量
|
|
575
|
+
const at = lines[li].indexOf(f.ref, cursor)
|
|
576
|
+
if (at === -1) {
|
|
577
|
+
skipped.push({ ref: f.ref, reason: `docLine=${docLine} 行内未找到引用串` })
|
|
578
|
+
} else {
|
|
579
|
+
located.push({ at, f })
|
|
580
|
+
cursor = at + f.ref.length
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
located.sort((a, b) => b.at - a.at) // 降序:后位先替换,前序替换不挤偏后序偏移
|
|
584
|
+
let line = lines[li]
|
|
585
|
+
for (const { at, f } of located) {
|
|
586
|
+
line = line.slice(0, at) + f.newRef + line.slice(at + f.ref.length)
|
|
587
|
+
applied++
|
|
588
|
+
}
|
|
589
|
+
if (located.length > 0) {
|
|
590
|
+
lines[li] = line
|
|
591
|
+
dirty = true
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
if (dirty && !dryRun) writeFileSync(docAbs, lines.join(eol))
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
return { applied, skipped }
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// ---------------------------------------------------------------------------
|
|
602
|
+
// 决策规则族(advisory,change: 2026-08-23-adopt-harness-practices task-05,FR-06 / D-003@v1)
|
|
603
|
+
//
|
|
604
|
+
// 校验对象:knowledge/decisions/<模块域>.md 的 D-xxx@vN 条目(producer = decision-distill
|
|
605
|
+
// 归档提炼,条目字段行 状态/锚点/最近确认 是本处的机械解析契约)。两项规则:
|
|
606
|
+
// 1) 锚点校验:implemented 条目「锚点:src/…」路径存在性(锚点:未记录 → 补录提示,非失效);
|
|
607
|
+
// 2) behind 复核:锚定模块源码在「最近确认」commit 后的前进数超阈值 → 决策待复核提示
|
|
608
|
+
// (computeModuleBehind 复用 docs-debt 既有 git 口径,C-10)。
|
|
609
|
+
// 铁律(docs-consistency 模块卡):advisory 只 warn 不进 runDocsCheck 的 invalid 阻断链——
|
|
610
|
+
// ok/invalid 判定与 docs gate 阻断行为不受影响;校验链路只读零写盘;无 decisions 库 /
|
|
611
|
+
// 无超阈 → findings 空(无信号零输出)。豁免走 known_failures 新键 decisions.* 命名空间
|
|
612
|
+
// (C-17:条目级语义键匹配,不复用 docs-gate 的 baseline ratchet)。
|
|
613
|
+
// ---------------------------------------------------------------------------
|
|
614
|
+
|
|
615
|
+
/** behind 复核缺省阈值(local.yaml decisions.behind_threshold 可调;schema 注册归 task-11,
|
|
616
|
+
* 本处读键按「存在则读、不存在用缺省」容错,不依赖 schema 注册)。dogfood 校准前取 10。 */
|
|
617
|
+
export const DECISIONS_DEFAULT_BEHIND_THRESHOLD = 10
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* known_failures 键提取——读法逐字对齐 verify-postcheck.extractKnownFailures(块式 + 流式,
|
|
621
|
+
* 剥引号/行内注释,CRLF 归一;注释互指防两处口径漂移)。此处独立复刻不 import
|
|
622
|
+
* verify-postcheck(重模块静态依赖;本模块仅消费 decisions.* 前缀键,见 isDecisionExempt)。
|
|
623
|
+
* @param {string} yamlText local.yaml 全文
|
|
624
|
+
* @returns {string[]} 键列表;无声明返回 []
|
|
625
|
+
*/
|
|
626
|
+
function extractKnownFailureKeys(yamlText) {
|
|
627
|
+
if (!yamlText) return []
|
|
628
|
+
const yaml = String(yamlText).replace(/\r\n?/g, '\n')
|
|
629
|
+
const inline = yaml.match(/^known_failures:\s*\[([^\]]*)\]\s*(?:#.*)?$/m)
|
|
630
|
+
if (inline) {
|
|
631
|
+
return inline[1].split(',').map(s => s.trim().replace(/^['"]|['"]$/g, '')).filter(Boolean)
|
|
632
|
+
}
|
|
633
|
+
const block = yaml.match(/^known_failures:\s*\n((?:[ \t]+-[ \t].+\n?)+)/m)
|
|
634
|
+
if (block) {
|
|
635
|
+
return (block[1].match(/^[ \t]+-[ \t]+(.+)/gm) || [])
|
|
636
|
+
.map(s => s.replace(/^[ \t]+-[ \t]+/, '').trim().replace(/^['"]|['"]$/g, '').replace(/#.*$/, '').trim())
|
|
637
|
+
.filter(Boolean)
|
|
638
|
+
}
|
|
639
|
+
return []
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* 读 local.yaml 的决策规则配置(best-effort 绝不抛;读取模式与 readDocsCheckConfig 一致:
|
|
644
|
+
* 缺文件/无段 → 全缺省)。decisions.behind_threshold 存在则读(数值校验),不存在用缺省 10
|
|
645
|
+
* ——schema 注册(config-schema.js)归 task-11,本处不依赖。
|
|
646
|
+
* @param {string} projectRoot 源码仓根(local.yaml 在 <root>/.sillyspec/local.yaml)
|
|
647
|
+
* @returns {{ behindThreshold: number, knownFailures: string[] }}
|
|
648
|
+
*/
|
|
649
|
+
export function readDecisionRulesConfig(projectRoot) {
|
|
650
|
+
const fallback = { behindThreshold: DECISIONS_DEFAULT_BEHIND_THRESHOLD, knownFailures: [] }
|
|
651
|
+
try {
|
|
652
|
+
const p = join(projectRoot, '.sillyspec', 'local.yaml')
|
|
653
|
+
if (!existsSync(p)) return fallback
|
|
654
|
+
const raw = readFileSync(p, 'utf8')
|
|
655
|
+
const knownFailures = extractKnownFailureKeys(raw)
|
|
656
|
+
const doc = jsYaml.load(raw)
|
|
657
|
+
if (!doc || typeof doc !== 'object') return { ...fallback, knownFailures }
|
|
658
|
+
const d = doc['decisions']
|
|
659
|
+
if (!d || typeof d !== 'object' || Array.isArray(d)) return { ...fallback, knownFailures }
|
|
660
|
+
const t = d['behind_threshold']
|
|
661
|
+
return {
|
|
662
|
+
behindThreshold: typeof t === 'number' && Number.isFinite(t) && t > 0 ? t : fallback.behindThreshold,
|
|
663
|
+
knownFailures,
|
|
664
|
+
}
|
|
665
|
+
} catch {
|
|
666
|
+
return fallback
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/** 豁免键匹配(本变更范围先支持键匹配,非子串):规则级 `decisions.<id>.<kind>` 精确命中,
|
|
671
|
+
* 或条目级 `decisions.<id>` 伞形命中(一条豁免该决策全部规则)。无 decisions. 前缀的键不参与。 */
|
|
672
|
+
function isDecisionExempt(knownFailures, entryId, kind) {
|
|
673
|
+
const ruleKey = `decisions.${entryId}.${kind}`
|
|
674
|
+
const entryKey = `decisions.${entryId}`
|
|
675
|
+
return knownFailures.some(k => k === ruleKey || k === entryKey)
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* 纯函数:解析 knowledge/decisions/<域>.md 的 D-xxx@vN 条目(与 decision-distill.renderBlockLines
|
|
680
|
+
* 的写入契约对偶:producer=decision-distill → consumer=本规则族;字段行非列表项,顶层
|
|
681
|
+
* 「状态:/锚点:/最近确认:」标签精确匹配,防误吞正文)。旧格式字段缺失容错为 null。
|
|
682
|
+
* @param {string} md 域文件全文
|
|
683
|
+
* @returns {Array<{ id: string, title: string, status: string|null, anchor: string|null, lastConfirmed: string|null }>}
|
|
684
|
+
*/
|
|
685
|
+
export function parseDecisionEntries(md) {
|
|
686
|
+
const entries = []
|
|
687
|
+
let cur = null
|
|
688
|
+
const flush = () => { if (cur) entries.push(cur) }
|
|
689
|
+
for (const line of String(md || '').replace(/\r\n?/g, '\n').split('\n')) {
|
|
690
|
+
const h = line.match(/^##\s+(D-\d+)(?:@v(\d+))?\s*(.*)$/)
|
|
691
|
+
if (h) {
|
|
692
|
+
flush()
|
|
693
|
+
const version = h[2] ? parseInt(h[2], 10) : 1
|
|
694
|
+
cur = { id: `${h[1]}@v${version}`, title: h[3].trim(), status: null, anchor: null, lastConfirmed: null }
|
|
695
|
+
continue
|
|
696
|
+
}
|
|
697
|
+
if (!cur) continue
|
|
698
|
+
const f = line.match(/^(状态|锚点|最近确认)\s*[::]\s*(.*)$/)
|
|
699
|
+
if (!f) continue
|
|
700
|
+
if (f[1] === '状态') cur.status = f[2].trim().split(/[\s((]/)[0] || null
|
|
701
|
+
else if (f[1] === '锚点') cur.anchor = f[2].trim()
|
|
702
|
+
else cur.lastConfirmed = f[2].trim()
|
|
703
|
+
}
|
|
704
|
+
flush()
|
|
705
|
+
return entries
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/** 锚点值 → 源码路径:剥「:行号 / :行号-行号 / :符号」后缀(锚点契约 `<src 路径>:<行号或符号>`;
|
|
709
|
+
* 旧条目可能只写路径)。「未记录」/空 → null(补录提示路径,非失效)。
|
|
710
|
+
* export(2026-08-24-decision-touch-cli-drift task-01):docs-debt.computeDecisionTouches
|
|
711
|
+
* 复用同一剥离口径(勿复刻正则防两处漂移);仅加关键字,行为不变。 */
|
|
712
|
+
export function anchorFilePath(anchor) {
|
|
713
|
+
const a = String(anchor || '').trim()
|
|
714
|
+
if (!a || a === '未记录') return null
|
|
715
|
+
return a.replace(/:(?:\d+(?:-\d+)?|[A-Za-z_$][A-Za-z0-9_$]*)$/, '').replace(/\\/g, '/') || null
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
/** specBase/docs 下任一项目的 modules/_module-map.yaml 尽力发现(对齐
|
|
719
|
+
* decision-distill.discoverModuleIndex 口径:首个命中;失败 → null,域模块源码集退化为锚点文件兜底)。 */
|
|
720
|
+
function discoverModuleMapIndex(specBase) {
|
|
721
|
+
try {
|
|
722
|
+
const docsDir = join(specBase, 'docs')
|
|
723
|
+
if (!existsSync(docsDir)) return null
|
|
724
|
+
for (const d of readdirSync(docsDir, { withFileTypes: true })) {
|
|
725
|
+
if (!d.isDirectory()) continue
|
|
726
|
+
const mapPath = join(docsDir, d.name, 'modules', '_module-map.yaml')
|
|
727
|
+
if (existsSync(mapPath)) return parseModuleMapSimple(readFileSync(mapPath, 'utf8'))
|
|
728
|
+
}
|
|
729
|
+
} catch { /* 尽力而为,失败归 null */ }
|
|
730
|
+
return null
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* IO 入口(advisory):跑一次决策规则族。只产出 findings(warn 语义),不进 runDocsCheck 的
|
|
735
|
+
* invalid 阻断链、不写任何文件——无 decisions 库 / 无信号 → findings 空(零输出)。
|
|
736
|
+
* computeModuleBehind 经动态 import 取自 docs-debt(本模块被 docs-debt 静态 import,静态互引
|
|
737
|
+
* 成 ESM 环——对齐 run/shared.js 对 docs-debt 的动态 import 先例)。
|
|
738
|
+
* @param {{ projectRoot: string, specBase?: string, behindThreshold?: number, knownFailures?: string[], moduleIndex?: object|null }} opts
|
|
739
|
+
* projectRoot 源码仓根(锚点解析与 git 锚);specBase 缺省 = <projectRoot>/.sillyspec;
|
|
740
|
+
* behindThreshold / knownFailures 缺省读 local.yaml(decisions.behind_threshold / known_failures);
|
|
741
|
+
* moduleIndex 缺省按 specBase/docs 下任一项目的 modules/_module-map.yaml 尽力发现
|
|
742
|
+
* @returns {Promise<{ empty: boolean, domains: number, entries: number, implemented: number,
|
|
743
|
+
* threshold: number, findings: Array<{ id: string, domain: string, file: string,
|
|
744
|
+
* kind: 'anchor'|'behind', behind?: number|null, threshold?: number, anchor?: string|null,
|
|
745
|
+
* message: string }>, exempted: Array<{ key: string, id: string, kind: string }> }>}
|
|
746
|
+
* empty=true = decisions 库不存在(冷启动,R-02 由 doctor 层提示);exempted 为经
|
|
747
|
+
* known_failures decisions.* 键豁免的条目(披露不隐藏)
|
|
748
|
+
*/
|
|
749
|
+
export async function runDecisionRules(opts) {
|
|
750
|
+
const { projectRoot, specBase, behindThreshold = null, knownFailures = null, moduleIndex = null } = opts || {}
|
|
751
|
+
const emptyResult = (threshold) => ({ empty: true, domains: 0, entries: 0, implemented: 0, threshold, findings: [], exempted: [] })
|
|
752
|
+
if (!projectRoot) return emptyResult(DECISIONS_DEFAULT_BEHIND_THRESHOLD)
|
|
753
|
+
|
|
754
|
+
const cfg = readDecisionRulesConfig(projectRoot)
|
|
755
|
+
const threshold = typeof behindThreshold === 'number' && Number.isFinite(behindThreshold) && behindThreshold > 0
|
|
756
|
+
? behindThreshold
|
|
757
|
+
: cfg.behindThreshold
|
|
758
|
+
const failures = Array.isArray(knownFailures) ? knownFailures : cfg.knownFailures
|
|
759
|
+
|
|
760
|
+
const kb = specBase || join(projectRoot, '.sillyspec')
|
|
761
|
+
const decisionsDir = join(kb, 'knowledge', 'decisions')
|
|
762
|
+
let domainFiles = []
|
|
763
|
+
try {
|
|
764
|
+
domainFiles = readdirSync(decisionsDir).filter(f => f.endsWith('.md')).sort()
|
|
765
|
+
} catch {
|
|
766
|
+
return emptyResult(threshold) // 库不存在 → 零信号(doctor 层另行提示空库)
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// 模块域 → 源码路径集(module-map paths∪core_files;未命中 map 的域退化为锚点文件兜底)
|
|
770
|
+
const mapIndex = moduleIndex || discoverModuleMapIndex(kb)
|
|
771
|
+
const flatIndex = mapIndex && mapIndex.modules ? mapIndex.modules : mapIndex
|
|
772
|
+
const { computeModuleBehind } = await import('./docs-debt.js')
|
|
773
|
+
|
|
774
|
+
const findings = []
|
|
775
|
+
const exempted = []
|
|
776
|
+
let entriesTotal = 0
|
|
777
|
+
let implementedTotal = 0
|
|
778
|
+
const treeCache = new Map() // resolveCandidates 全树扫描复用(同 docs check 主链路口径)
|
|
779
|
+
|
|
780
|
+
const emit = (id, domain, file, kind, extra, message) => {
|
|
781
|
+
if (isDecisionExempt(failures, id, kind)) {
|
|
782
|
+
exempted.push({ key: `decisions.${id}.${kind}`, id, kind })
|
|
783
|
+
return
|
|
784
|
+
}
|
|
785
|
+
findings.push({ id, domain, file, kind, ...extra, message })
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
for (const f of domainFiles) {
|
|
789
|
+
const domain = f.replace(/\.md$/, '')
|
|
790
|
+
const rel = `knowledge/decisions/${f}`
|
|
791
|
+
let entries
|
|
792
|
+
try { entries = parseDecisionEntries(readFileSync(join(decisionsDir, f), 'utf8')) } catch { continue }
|
|
793
|
+
for (const e of entries) {
|
|
794
|
+
entriesTotal++
|
|
795
|
+
if ((e.status || '').toLowerCase() !== 'implemented') continue // rejected/未知状态不做锚点与 behind 校验
|
|
796
|
+
implementedTotal++
|
|
797
|
+
const anchorPath = anchorFilePath(e.anchor)
|
|
798
|
+
// 规则 1:锚点存在性(未记录 → 补录提示;路径不存在 → 失效提示)
|
|
799
|
+
if (!anchorPath) {
|
|
800
|
+
emit(e.id, domain, rel, 'anchor', { anchor: e.anchor || null },
|
|
801
|
+
`「${e.id}」锚点未记录(${rel})——旧格式条目,补「锚点:src/…」后可机械校验(advisory)`)
|
|
802
|
+
} else if (resolveCandidates(projectRoot, anchorPath, treeCache).length === 0) {
|
|
803
|
+
emit(e.id, domain, rel, 'anchor', { anchor: e.anchor },
|
|
804
|
+
`「${e.id}」锚点文件不存在:${anchorPath}(${rel})——决策落点已漂移,人工复核(advisory)`)
|
|
805
|
+
}
|
|
806
|
+
// 规则 2:behind 复核(「最近确认」后锚定模块源码前进数超阈值 → 决策待复核;
|
|
807
|
+
// 「最近确认」未记录/域无源码集(无 map 无锚点)→ 无法计算,静默跳过——advisory 不产噪声)
|
|
808
|
+
if (!e.lastConfirmed || !/^[0-9a-f]{4,40}$/i.test(e.lastConfirmed)) continue
|
|
809
|
+
let srcPaths = null
|
|
810
|
+
const m = flatIndex && typeof flatIndex === 'object' ? flatIndex[domain] : null
|
|
811
|
+
if (m) srcPaths = [...(Array.isArray(m.paths) ? m.paths : []), ...(Array.isArray(m.core_files) ? m.core_files : [])]
|
|
812
|
+
if ((!srcPaths || srcPaths.length === 0) && anchorPath) srcPaths = [anchorPath]
|
|
813
|
+
if (!srcPaths || srcPaths.length === 0) continue
|
|
814
|
+
const { behind } = computeModuleBehind(domain, e.lastConfirmed, { projectRoot, srcPaths })
|
|
815
|
+
if (behind !== null && behind > threshold) {
|
|
816
|
+
emit(e.id, domain, rel, 'behind', { behind, threshold },
|
|
817
|
+
`「${e.id}」决策待复核:锚定模块 ${domain} 在最近确认 ${e.lastConfirmed} 后源码已前进 ${behind} commit,超阈值 ${threshold}(${rel})——复核后更新「最近确认」`)
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
findings.sort((a, b) => a.file.localeCompare(b.file) || a.id.localeCompare(b.id))
|
|
823
|
+
exempted.sort((a, b) => a.key.localeCompare(b.key))
|
|
824
|
+
return { empty: false, domains: domainFiles.length, entries: entriesTotal, implemented: implementedTotal, threshold, findings, exempted }
|
|
825
|
+
}
|