@erclx/canon 4.0.0
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/LICENSE +21 -0
- package/README.md +129 -0
- package/claude/.claude-plugin/plugin.json +19 -0
- package/claude/skills/bash-cli-script/REQUIREMENT.md +42 -0
- package/claude/skills/bash-cli-script/SKILL.md +48 -0
- package/claude/skills/bash-cli-script/references/template.md +43 -0
- package/claude/skills/bash-script/REQUIREMENT.md +36 -0
- package/claude/skills/bash-script/SKILL.md +100 -0
- package/claude/skills/bash-script/references/patterns.md +349 -0
- package/claude/skills/canon-cli/REQUIREMENT.md +41 -0
- package/claude/skills/canon-cli/SKILL.md +103 -0
- package/claude/skills/canon-feedback-file/REQUIREMENT.md +40 -0
- package/claude/skills/canon-feedback-file/SKILL.md +80 -0
- package/claude/skills/canon-feedback-triage/REQUIREMENT.md +40 -0
- package/claude/skills/canon-feedback-triage/SKILL.md +63 -0
- package/claude/skills/canon-operator/REQUIREMENT.md +61 -0
- package/claude/skills/canon-operator/SKILL.md +108 -0
- package/claude/skills/canon-rollout/REQUIREMENT.md +59 -0
- package/claude/skills/canon-rollout/SKILL.md +147 -0
- package/claude/skills/canon-screencast/REQUIREMENT.md +39 -0
- package/claude/skills/canon-screencast/SKILL.md +167 -0
- package/claude/skills/canon-slides-draft/REQUIREMENT.md +39 -0
- package/claude/skills/canon-slides-draft/SKILL.md +62 -0
- package/claude/skills/ci-workflow/REQUIREMENT.md +40 -0
- package/claude/skills/ci-workflow/SKILL.md +65 -0
- package/claude/skills/ci-workflow/references/workflows.md +98 -0
- package/claude/skills/claude-address-review/REQUIREMENT.md +57 -0
- package/claude/skills/claude-address-review/SKILL.md +212 -0
- package/claude/skills/claude-address-review/references/rebase-conflicts.md +39 -0
- package/claude/skills/claude-autoship/REQUIREMENT.md +50 -0
- package/claude/skills/claude-autoship/SKILL.md +207 -0
- package/claude/skills/claude-design-extract/REQUIREMENT.md +42 -0
- package/claude/skills/claude-design-extract/SKILL.md +102 -0
- package/claude/skills/claude-diagram/REQUIREMENT.md +45 -0
- package/claude/skills/claude-diagram/SKILL.md +177 -0
- package/claude/skills/claude-docs/REQUIREMENT.md +60 -0
- package/claude/skills/claude-docs/SKILL.md +287 -0
- package/claude/skills/claude-docs/references/anchor-sweep.md +58 -0
- package/claude/skills/claude-docs/references/wireframe-sweep.md +45 -0
- package/claude/skills/claude-feature/REQUIREMENT.md +36 -0
- package/claude/skills/claude-feature/SKILL.md +115 -0
- package/claude/skills/claude-groundwork/REQUIREMENT.md +48 -0
- package/claude/skills/claude-groundwork/SKILL.md +142 -0
- package/claude/skills/claude-intake/REQUIREMENT.md +49 -0
- package/claude/skills/claude-intake/SKILL.md +114 -0
- package/claude/skills/claude-intake-answer/REQUIREMENT.md +48 -0
- package/claude/skills/claude-intake-answer/SKILL.md +90 -0
- package/claude/skills/claude-markdown-propose/REQUIREMENT.md +48 -0
- package/claude/skills/claude-markdown-propose/SKILL.md +118 -0
- package/claude/skills/claude-markdown-propose/references/format.md +107 -0
- package/claude/skills/claude-memory-capture/REQUIREMENT.md +50 -0
- package/claude/skills/claude-memory-capture/SKILL.md +101 -0
- package/claude/skills/claude-memory-review/REQUIREMENT.md +50 -0
- package/claude/skills/claude-memory-review/SKILL.md +210 -0
- package/claude/skills/claude-memory-review/references/receipt-format.md +48 -0
- package/claude/skills/claude-orchestrate/REQUIREMENT.md +121 -0
- package/claude/skills/claude-orchestrate/SKILL.md +241 -0
- package/claude/skills/claude-orchestrate/references/orchestrator-dispatch.md +132 -0
- package/claude/skills/claude-orchestrate/references/orchestrator-handoff.md +32 -0
- package/claude/skills/claude-orchestrate/references/orchestrator-parked.md +72 -0
- package/claude/skills/claude-orchestrate/references/orchestrator-poll.md +87 -0
- package/claude/skills/claude-orchestrate/references/orchestrator-resume.md +30 -0
- package/claude/skills/claude-orchestrate/references/orchestrator-sweep.md +21 -0
- package/claude/skills/claude-orchestrate/scripts/poll.sh +373 -0
- package/claude/skills/claude-orchestrate/scripts/watch.sh +181 -0
- package/claude/skills/claude-pr-review/REQUIREMENT.md +47 -0
- package/claude/skills/claude-pr-review/SKILL.md +295 -0
- package/claude/skills/claude-review/REQUIREMENT.md +39 -0
- package/claude/skills/claude-review/SKILL.md +142 -0
- package/claude/skills/claude-seed-sync/REQUIREMENT.md +45 -0
- package/claude/skills/claude-seed-sync/SKILL.md +156 -0
- package/claude/skills/claude-standards-audit/REQUIREMENT.md +33 -0
- package/claude/skills/claude-standards-audit/SKILL.md +99 -0
- package/claude/skills/claude-tasks/REQUIREMENT.md +43 -0
- package/claude/skills/claude-tasks/SKILL.md +161 -0
- package/claude/skills/claude-teach/REQUIREMENT.md +56 -0
- package/claude/skills/claude-teach/SKILL.md +196 -0
- package/claude/skills/claude-teach/references/lesson-craft.md +59 -0
- package/claude/skills/claude-teach/references/pedagogy.md +67 -0
- package/claude/skills/claude-teach/references/promotion.md +54 -0
- package/claude/skills/claude-ui-test/REQUIREMENT.md +40 -0
- package/claude/skills/claude-ui-test/SKILL.md +77 -0
- package/claude/skills/claude-ux-audit/REQUIREMENT.md +40 -0
- package/claude/skills/claude-ux-audit/SKILL.md +79 -0
- package/claude/skills/claude-ux-measure/REQUIREMENT.md +48 -0
- package/claude/skills/claude-ux-measure/SKILL.md +122 -0
- package/claude/skills/claude-worker/REQUIREMENT.md +54 -0
- package/claude/skills/claude-worker/SKILL.md +96 -0
- package/claude/skills/claude-worktree/REQUIREMENT.md +58 -0
- package/claude/skills/claude-worktree/SKILL.md +136 -0
- package/claude/skills/create-rule/REQUIREMENT.md +45 -0
- package/claude/skills/create-rule/SKILL.md +68 -0
- package/claude/skills/create-skill/REQUIREMENT.md +38 -0
- package/claude/skills/create-skill/SKILL.md +32 -0
- package/claude/skills/create-snippet/REQUIREMENT.md +39 -0
- package/claude/skills/create-snippet/SKILL.md +30 -0
- package/claude/skills/create-standard/REQUIREMENT.md +35 -0
- package/claude/skills/create-standard/SKILL.md +29 -0
- package/claude/skills/decision-escalate/REQUIREMENT.md +45 -0
- package/claude/skills/decision-escalate/SKILL.md +79 -0
- package/claude/skills/docs-sync/REQUIREMENT.md +41 -0
- package/claude/skills/docs-sync/SKILL.md +95 -0
- package/claude/skills/git-branch/REQUIREMENT.md +38 -0
- package/claude/skills/git-branch/SKILL.md +60 -0
- package/claude/skills/git-commit/REQUIREMENT.md +36 -0
- package/claude/skills/git-commit/SKILL.md +49 -0
- package/claude/skills/git-followup/REQUIREMENT.md +43 -0
- package/claude/skills/git-followup/SKILL.md +48 -0
- package/claude/skills/git-issue/REQUIREMENT.md +38 -0
- package/claude/skills/git-issue/SKILL.md +65 -0
- package/claude/skills/git-pr/REQUIREMENT.md +50 -0
- package/claude/skills/git-pr/SKILL.md +164 -0
- package/claude/skills/git-pr/references/labels.md +95 -0
- package/claude/skills/git-ship/REQUIREMENT.md +42 -0
- package/claude/skills/git-ship/SKILL.md +55 -0
- package/claude/skills/git-split/REQUIREMENT.md +39 -0
- package/claude/skills/git-split/SKILL.md +162 -0
- package/claude/skills/git-stage/REQUIREMENT.md +39 -0
- package/claude/skills/git-stage/SKILL.md +73 -0
- package/claude/skills/git-worktree/REQUIREMENT.md +38 -0
- package/claude/skills/git-worktree/SKILL.md +130 -0
- package/claude/skills/migration-claude-md/REQUIREMENT.md +40 -0
- package/claude/skills/migration-claude-md/SKILL.md +76 -0
- package/claude/skills/migration-context/REQUIREMENT.md +36 -0
- package/claude/skills/migration-context/SKILL.md +95 -0
- package/claude/skills/migration-standards-drop/REQUIREMENT.md +55 -0
- package/claude/skills/migration-standards-drop/SKILL.md +113 -0
- package/claude/skills/migration-superseded/REQUIREMENT.md +44 -0
- package/claude/skills/migration-superseded/SKILL.md +115 -0
- package/claude/skills/project-commands/REQUIREMENT.md +42 -0
- package/claude/skills/project-commands/SKILL.md +85 -0
- package/claude/skills/restate-plainly/REQUIREMENT.md +41 -0
- package/claude/skills/restate-plainly/SKILL.md +39 -0
- package/claude/skills/session-map/REQUIREMENT.md +57 -0
- package/claude/skills/session-map/SKILL.md +70 -0
- package/claude/skills/session-resume/REQUIREMENT.md +49 -0
- package/claude/skills/session-resume/SKILL.md +51 -0
- package/claude/skills/setup-gov/REQUIREMENT.md +37 -0
- package/claude/skills/setup-gov/SKILL.md +77 -0
- package/claude/skills/setup-indexes/REQUIREMENT.md +45 -0
- package/claude/skills/setup-indexes/SKILL.md +153 -0
- package/claude/skills/setup-init/REQUIREMENT.md +45 -0
- package/claude/skills/setup-init/SKILL.md +127 -0
- package/claude/skills/setup-plugins/REQUIREMENT.md +42 -0
- package/claude/skills/setup-plugins/SKILL.md +81 -0
- package/claude/skills/setup-plugins/references/plugin-catalog.md +53 -0
- package/claude/skills/setup-verify/REQUIREMENT.md +39 -0
- package/claude/skills/setup-verify/SKILL.md +51 -0
- package/claude/skills/systematic-debugging/REQUIREMENT.md +41 -0
- package/claude/skills/systematic-debugging/SKILL.md +70 -0
- package/claude/skills/write-human/REQUIREMENT.md +46 -0
- package/claude/skills/write-human/SKILL.md +68 -0
- package/claude/skills/write-human/references/density.md +38 -0
- package/claude/skills/write-human/references/machine-tells.md +107 -0
- package/claude/skills/write-human/references/source-material.md +37 -0
- package/claude/skills/youtube-transcripts/REQUIREMENT.md +38 -0
- package/claude/skills/youtube-transcripts/SKILL.md +34 -0
- package/docs/agents/audits.md +98 -0
- package/docs/agents/capture.md +37 -0
- package/docs/agents/census.md +23 -0
- package/docs/agents/commands.md +146 -0
- package/docs/agents/comments.md +34 -0
- package/docs/agents/context-audit-checks.md +120 -0
- package/docs/agents/context-audit.md +83 -0
- package/docs/agents/counts.md +76 -0
- package/docs/agents/demo.md +86 -0
- package/docs/agents/docs.md +17 -0
- package/docs/agents/gate.md +84 -0
- package/docs/agents/index.md +47 -0
- package/docs/agents/indexes.md +35 -0
- package/docs/agents/install-and-sync.md +385 -0
- package/docs/agents/intake.md +81 -0
- package/docs/agents/key-changes.md +103 -0
- package/docs/agents/label-coverage.md +73 -0
- package/docs/agents/markdown-audit.md +197 -0
- package/docs/agents/output-shape.md +70 -0
- package/docs/agents/overview.md +26 -0
- package/docs/agents/records.md +170 -0
- package/docs/agents/restated.md +81 -0
- package/docs/agents/review-classification.md +77 -0
- package/docs/agents/routing.md +61 -0
- package/docs/agents/rule-citations.md +98 -0
- package/docs/agents/sandbox.md +71 -0
- package/docs/agents/scripting.md +149 -0
- package/docs/agents/sessions.md +120 -0
- package/docs/agents/skills-audit.md +94 -0
- package/docs/agents/skills-reach.md +64 -0
- package/docs/agents/standards-audit.md +38 -0
- package/docs/agents/state-scoped-risk.md +105 -0
- package/docs/agents/superseded.md +85 -0
- package/docs/agents/targets.md +83 -0
- package/docs/agents/tasks.md +200 -0
- package/docs/agents/teach.md +158 -0
- package/docs/agents/test-order.md +56 -0
- package/docs/agents/worktrees.md +62 -0
- package/docs/ai-workflow.md +317 -0
- package/docs/index.md +23 -0
- package/docs/operating-model.md +223 -0
- package/docs/target-projects.md +258 -0
- package/docs/visual-design-workflow.md +151 -0
- package/docs/zshrc-aliases.md +65 -0
- package/governance/rules/ci/700-ci-workflow.md +44 -0
- package/governance/rules/claude/500-prose.md +15 -0
- package/governance/rules/claude/501-markdown.md +14 -0
- package/governance/rules/claude/510-context.md +28 -0
- package/governance/rules/claude/511-indexes.md +14 -0
- package/governance/rules/claude/520-wireframes.md +12 -0
- package/governance/rules/claude/530-requirements.md +11 -0
- package/governance/rules/claude/540-architecture.md +11 -0
- package/governance/rules/claude/550-design.md +11 -0
- package/governance/rules/claude/555-tasks.md +12 -0
- package/governance/rules/claude/556-groundwork.md +11 -0
- package/governance/rules/claude/557-intake.md +11 -0
- package/governance/rules/claude/558-plan.md +22 -0
- package/governance/rules/claude/559-memory.md +11 -0
- package/governance/rules/claude/560-diagrams.md +18 -0
- package/governance/rules/claude/561-teach.md +13 -0
- package/governance/rules/claude/562-session.md +15 -0
- package/governance/rules/claude/570-skill.md +24 -0
- package/governance/rules/claude/575-hooks.md +17 -0
- package/governance/rules/claude/576-settings.md +14 -0
- package/governance/rules/claude/580-readme.md +11 -0
- package/governance/rules/claude/590-rule-authoring.md +12 -0
- package/governance/rules/claude/591-standard-authoring.md +12 -0
- package/governance/rules/claude/592-claude-md.md +19 -0
- package/governance/rules/core/000-constitution.md +30 -0
- package/governance/rules/core/005-behavior.md +27 -0
- package/governance/rules/core/010-testing.md +35 -0
- package/governance/rules/core/015-output.md +20 -0
- package/governance/rules/core/020-concurrency.md +22 -0
- package/governance/rules/core/025-indexes.md +9 -0
- package/governance/rules/core/030-error-handling.md +31 -0
- package/governance/rules/core/035-tasks.md +13 -0
- package/governance/rules/core/040-performance.md +20 -0
- package/governance/rules/core/045-memory.md +12 -0
- package/governance/rules/core/050-logging.md +20 -0
- package/governance/rules/core/055-scratch.md +9 -0
- package/governance/rules/core/060-naming.md +19 -0
- package/governance/rules/core/065-spelling.md +19 -0
- package/governance/rules/core/070-planning.md +18 -0
- package/governance/rules/core/075-dependencies.md +25 -0
- package/governance/rules/core/080-config-comments.md +22 -0
- package/governance/rules/core/085-worktrees.md +17 -0
- package/governance/rules/core/087-git.md +11 -0
- package/governance/rules/core/090-code-comments.md +39 -0
- package/governance/rules/framework/200-react.md +51 -0
- package/governance/rules/framework/210-astro.md +41 -0
- package/governance/rules/framework/220-fastapi.md +43 -0
- package/governance/rules/framework/230-nextjs.md +48 -0
- package/governance/rules/framework/250-tailwind.md +32 -0
- package/governance/rules/framework/260-shadcn.md +34 -0
- package/governance/rules/lang/100-typescript.md +40 -0
- package/governance/rules/lang/110-python.md +42 -0
- package/governance/rules/lang/120-bash.md +19 -0
- package/governance/rules/lib/300-testing-ts.md +39 -0
- package/governance/rules/lib/305-e2e-reliability.md +34 -0
- package/governance/rules/lib/306-test-scope.md +25 -0
- package/governance/rules/lib/310-zod.md +25 -0
- package/governance/rules/lib/320-tanstack-query.md +32 -0
- package/governance/rules/lib/330-testing-py.md +44 -0
- package/governance/rules/lib/340-pydantic.md +38 -0
- package/governance/rules/lib/350-security-web.md +32 -0
- package/governance/rules/lib/360-security-server.md +39 -0
- package/governance/rules/lib/370-database.md +35 -0
- package/governance/rules/snippets/505-at-references.md +9 -0
- package/governance/rules/ui/400-ui.md +36 -0
- package/governance/rules/ui/410-a11y.md +48 -0
- package/governance/rules/ui/420-forms.md +36 -0
- package/governance/rules/ui/430-ux-completeness.md +65 -0
- package/governance/rules/ui/440-surface-capture.md +34 -0
- package/governance/rules/ui/450-link-behavior.md +19 -0
- package/governance/stacks/astro.toml +2 -0
- package/governance/stacks/base.toml +8 -0
- package/governance/stacks/node-server.toml +2 -0
- package/governance/stacks/node.toml +2 -0
- package/governance/stacks/python-fastapi.toml +2 -0
- package/governance/stacks/python.toml +2 -0
- package/governance/stacks/react.toml +2 -0
- package/package.json +69 -0
- package/scripts/config.sh +11 -0
- package/scripts/core/bootstrap.sh +81 -0
- package/scripts/core/check-color-source.sh +41 -0
- package/scripts/core/check-ignore-parity.sh +162 -0
- package/scripts/core/check-plugin-boundary.sh +45 -0
- package/scripts/core/check-seed-independence.sh +59 -0
- package/scripts/core/check-skill-paths.sh +24 -0
- package/scripts/core/clean.sh +36 -0
- package/scripts/core/install-check.sh +101 -0
- package/scripts/core/list-seed-roots.sh +18 -0
- package/scripts/core/regen-claude-copies.sh +10 -0
- package/scripts/core/regen-hero.sh +217 -0
- package/scripts/core/regen-indexes.sh +10 -0
- package/scripts/core/regen-tooling-paths.sh +61 -0
- package/scripts/core/repair-bare-flag.sh +19 -0
- package/scripts/core/snapshot.sh +134 -0
- package/scripts/core/update.sh +35 -0
- package/scripts/docs/list.sh +165 -0
- package/scripts/lib/frontmatter.sh +30 -0
- package/scripts/lib/gov.sh +14 -0
- package/scripts/lib/sandbox-fixtures.sh +191 -0
- package/scripts/lib/sandbox-git.sh +125 -0
- package/scripts/lib/sandbox-path.sh +206 -0
- package/scripts/lib/tooling.sh +35 -0
- package/scripts/lib/ui.sh +266 -0
- package/scripts/lib/worktree.sh +20 -0
- package/scripts/manage-sandbox.sh +466 -0
- package/scripts/snippets/create.sh +156 -0
- package/scripts/standards/list.sh +115 -0
- package/scripts/tooling/create.sh +109 -0
- package/scripts/tooling/verify.sh +179 -0
- package/snippets/align.md +12 -0
- package/snippets/claude/decision-memo.md +39 -0
- package/snippets/claude/feature-recap.md +19 -0
- package/snippets/claude/figma-steps.md +24 -0
- package/snippets/compact-summary.md +5 -0
- package/snippets/decision-help.md +6 -0
- package/snippets/meta-prompt.md +14 -0
- package/snippets/research-prompt.md +7 -0
- package/snippets/session-notes.md +11 -0
- package/snippets/snippets.toml +5 -0
- package/snippets/step-by-step.md +10 -0
- package/snippets/web-research.md +21 -0
- package/src/audits/baseline.ts +201 -0
- package/src/audits/catalog.ts +876 -0
- package/src/audits/run.ts +204 -0
- package/src/autoship/classify.ts +75 -0
- package/src/autoship/paths.ts +51 -0
- package/src/binary.ts +16 -0
- package/src/browser/engine.ts +40 -0
- package/src/census/count.ts +113 -0
- package/src/claude/cases/all.ts +24 -0
- package/src/claude/cases/authoring.ts +53 -0
- package/src/claude/cases/claude-workflow.ts +158 -0
- package/src/claude/cases/git.ts +44 -0
- package/src/claude/cases/misc.ts +27 -0
- package/src/claude/cases/setup.ts +94 -0
- package/src/claude/gitignore.ts +51 -0
- package/src/claude/routing.ts +283 -0
- package/src/claude/seeds-list.ts +47 -0
- package/src/claude/seeds.ts +150 -0
- package/src/claude/settings.ts +151 -0
- package/src/claude/skills-audit.ts +228 -0
- package/src/claude/skills-drift.ts +156 -0
- package/src/claude/skills-list.ts +99 -0
- package/src/claude/skills-rank.ts +320 -0
- package/src/claude/skills-reach.ts +227 -0
- package/src/cli-run.ts +43 -0
- package/src/cli.ts +200 -0
- package/src/commands/audits.ts +350 -0
- package/src/commands/autoship.ts +129 -0
- package/src/commands/capture.ts +133 -0
- package/src/commands/census.ts +105 -0
- package/src/commands/claude.ts +1286 -0
- package/src/commands/comments.ts +240 -0
- package/src/commands/context.ts +857 -0
- package/src/commands/demo.ts +389 -0
- package/src/commands/deps.ts +173 -0
- package/src/commands/design.ts +36 -0
- package/src/commands/docs.ts +60 -0
- package/src/commands/feedback-format.ts +23 -0
- package/src/commands/feedback.ts +112 -0
- package/src/commands/gate.ts +189 -0
- package/src/commands/gov.ts +1265 -0
- package/src/commands/indexes.ts +184 -0
- package/src/commands/init.ts +113 -0
- package/src/commands/intake.ts +406 -0
- package/src/commands/inventory.ts +256 -0
- package/src/commands/labels.ts +361 -0
- package/src/commands/markdown.ts +544 -0
- package/src/commands/migrate.ts +175 -0
- package/src/commands/pass-through.ts +39 -0
- package/src/commands/pr.ts +411 -0
- package/src/commands/records.ts +728 -0
- package/src/commands/sandbox.ts +468 -0
- package/src/commands/secrets.ts +132 -0
- package/src/commands/serve.ts +159 -0
- package/src/commands/sessions.ts +408 -0
- package/src/commands/slides.ts +126 -0
- package/src/commands/snippets.ts +84 -0
- package/src/commands/standards.ts +247 -0
- package/src/commands/sync.ts +428 -0
- package/src/commands/targets.ts +319 -0
- package/src/commands/tasks.ts +743 -0
- package/src/commands/teach.ts +786 -0
- package/src/commands/tooling.ts +573 -0
- package/src/commands/transcripts.ts +44 -0
- package/src/commands/upgrade.ts +231 -0
- package/src/commands/wiki.ts +100 -0
- package/src/commands/worktrees.ts +191 -0
- package/src/comments/scan.ts +338 -0
- package/src/comments/trend.ts +207 -0
- package/src/comments/vocabulary.ts +85 -0
- package/src/context/architecture.ts +364 -0
- package/src/context/audit.ts +790 -0
- package/src/context/citations.ts +196 -0
- package/src/context/folders.ts +186 -0
- package/src/context/gate.ts +57 -0
- package/src/context/index-drift.ts +64 -0
- package/src/context/narration.ts +99 -0
- package/src/copy.ts +30 -0
- package/src/counts/catalogs.ts +96 -0
- package/src/counts/numbers.ts +79 -0
- package/src/counts/scan.ts +314 -0
- package/src/demo/beats.ts +135 -0
- package/src/demo/compile.ts +326 -0
- package/src/demo/container.ts +63 -0
- package/src/demo/cursors.ts +55 -0
- package/src/demo/drive.ts +357 -0
- package/src/demo/pointer.ts +178 -0
- package/src/demo/theme.ts +112 -0
- package/src/deps/audit.ts +153 -0
- package/src/design/parse.ts +116 -0
- package/src/design/render.ts +249 -0
- package/src/docs/read.ts +77 -0
- package/src/exec.ts +16 -0
- package/src/exempt-marker.ts +43 -0
- package/src/frontmatter.ts +13 -0
- package/src/gate/measures.ts +682 -0
- package/src/gate/sequencer.ts +386 -0
- package/src/gate/stages.ts +412 -0
- package/src/git-env.ts +36 -0
- package/src/git-files.ts +105 -0
- package/src/git-ignore.ts +46 -0
- package/src/github-format.ts +13 -0
- package/src/github.ts +24 -0
- package/src/gov/adapter.ts +103 -0
- package/src/gov/citations.ts +514 -0
- package/src/gov/consumed.ts +129 -0
- package/src/gov/install.ts +132 -0
- package/src/gov/list.ts +106 -0
- package/src/gov/payload.ts +39 -0
- package/src/gov/restated.ts +814 -0
- package/src/gov/stacks.ts +205 -0
- package/src/gov/superseded.ts +415 -0
- package/src/gov/test-order.ts +407 -0
- package/src/indexes/frontmatter.ts +46 -0
- package/src/indexes/regen.ts +84 -0
- package/src/indexes/render.ts +201 -0
- package/src/indexes/walk.ts +83 -0
- package/src/init/flags.ts +60 -0
- package/src/init/plan.ts +114 -0
- package/src/init/run.ts +46 -0
- package/src/init/steps.ts +77 -0
- package/src/intake/folder.ts +320 -0
- package/src/intake/items.ts +174 -0
- package/src/inventory/config.ts +117 -0
- package/src/inventory/group.ts +76 -0
- package/src/inventory/subjects.ts +114 -0
- package/src/inventory/walk.ts +129 -0
- package/src/labels/audit.ts +82 -0
- package/src/labels/coverage.ts +79 -0
- package/src/labels/map.ts +101 -0
- package/src/labels/phase.ts +95 -0
- package/src/markdown/bans.ts +94 -0
- package/src/markdown/files.ts +102 -0
- package/src/markdown/gate.ts +28 -0
- package/src/markdown/scan.ts +295 -0
- package/src/markdown/structure.ts +730 -0
- package/src/migrate/apply.ts +115 -0
- package/src/migrate/plan.ts +103 -0
- package/src/migrate/rename.ts +183 -0
- package/src/pr/bijection.ts +145 -0
- package/src/pr/paths.ts +335 -0
- package/src/process/harness.ts +167 -0
- package/src/project-root.ts +19 -0
- package/src/records/backup.ts +455 -0
- package/src/records/migrate.ts +78 -0
- package/src/records/size.ts +260 -0
- package/src/records/validate.ts +1162 -0
- package/src/sandbox/census.ts +228 -0
- package/src/sandbox/coverage.ts +115 -0
- package/src/sandbox/expect.ts +629 -0
- package/src/sandbox/tree.ts +47 -0
- package/src/secrets/marker.ts +30 -0
- package/src/secrets/patterns.ts +142 -0
- package/src/secrets/scan.ts +123 -0
- package/src/secrets/shipped.ts +111 -0
- package/src/seed-marker.ts +74 -0
- package/src/serve/static.ts +322 -0
- package/src/sessions/claim.ts +85 -0
- package/src/sessions/live.ts +79 -0
- package/src/sessions/registry.ts +137 -0
- package/src/sessions/resolve.ts +333 -0
- package/src/slides/layouts.ts +391 -0
- package/src/slides/open.ts +18 -0
- package/src/slides/parse.ts +84 -0
- package/src/slides/render.ts +88 -0
- package/src/slides/styles.ts +44 -0
- package/src/snippets/categories.ts +66 -0
- package/src/snippets/list.ts +32 -0
- package/src/snippets/presets.ts +50 -0
- package/src/standards/audit.ts +132 -0
- package/src/standards/read.ts +100 -0
- package/src/sync/check.ts +692 -0
- package/src/sync/engine.ts +576 -0
- package/src/sync/git.ts +225 -0
- package/src/sync/history.ts +123 -0
- package/src/sync/layout.ts +140 -0
- package/src/sync/reverse.ts +268 -0
- package/src/sync/seeds-report.ts +126 -0
- package/src/sync/stamp.ts +358 -0
- package/src/sync/target.ts +70 -0
- package/src/sync/workflow.ts +200 -0
- package/src/target.ts +43 -0
- package/src/targets/pulls.ts +250 -0
- package/src/targets/registry.ts +203 -0
- package/src/targets/resolve.ts +145 -0
- package/src/targets/sweep.ts +246 -0
- package/src/tasks/archive.ts +506 -0
- package/src/tasks/record.ts +311 -0
- package/src/tasks/trunk.ts +89 -0
- package/src/tasks/validate.ts +1001 -0
- package/src/teach/lesson.ts +180 -0
- package/src/teach/workspace.ts +842 -0
- package/src/tooling/gitignore.ts +122 -0
- package/src/tooling/inject.ts +193 -0
- package/src/tooling/list.ts +39 -0
- package/src/tooling/manifest.ts +176 -0
- package/src/tooling/package.ts +166 -0
- package/src/tooling/read.ts +65 -0
- package/src/tooling/scan.ts +147 -0
- package/src/tooling/stamp.ts +44 -0
- package/src/transcripts/fetch.ts +156 -0
- package/src/transcripts/metadata.ts +54 -0
- package/src/transcripts/vtt.ts +114 -0
- package/src/ui.ts +266 -0
- package/src/version/compare.ts +53 -0
- package/src/version/installed.ts +40 -0
- package/src/version/manager.ts +67 -0
- package/src/version/skew.ts +192 -0
- package/src/wiki/init.ts +85 -0
- package/src/worktree.ts +143 -0
- package/src/worktrees/reclaim.ts +306 -0
- package/standards/architecture.md +72 -0
- package/standards/branch.md +59 -0
- package/standards/commit.md +72 -0
- package/standards/context.md +151 -0
- package/standards/design.md +93 -0
- package/standards/diagrams.md +152 -0
- package/standards/glossary.md +75 -0
- package/standards/groundwork.md +211 -0
- package/standards/index.md +36 -0
- package/standards/intake.md +192 -0
- package/standards/issue.md +94 -0
- package/standards/markdown.md +137 -0
- package/standards/memory.md +144 -0
- package/standards/plan.md +172 -0
- package/standards/pr.md +139 -0
- package/standards/publish.md +51 -0
- package/standards/readme.md +208 -0
- package/standards/requirements.md +70 -0
- package/standards/rule.md +118 -0
- package/standards/session.md +109 -0
- package/standards/skill.md +300 -0
- package/standards/slug.md +39 -0
- package/standards/snippets.md +76 -0
- package/standards/standard.md +170 -0
- package/standards/tasks.md +254 -0
- package/standards/teach.md +153 -0
- package/standards/versioning.md +71 -0
- package/standards/wireframes.md +113 -0
- package/tooling/astro/configs/astro.config.mjs +31 -0
- package/tooling/astro/configs/eslint.config.js +79 -0
- package/tooling/astro/configs/playwright.config.ts +26 -0
- package/tooling/astro/configs/tsconfig.json +12 -0
- package/tooling/astro/configs/vitest.config.ts +22 -0
- package/tooling/astro/manifest.toml +34 -0
- package/tooling/astro/reference.md +60 -0
- package/tooling/base/configs/.editorconfig +5 -0
- package/tooling/base/configs/.github/pull_request_template.md +18 -0
- package/tooling/base/configs/.github/workflows/verify.yml +35 -0
- package/tooling/base/configs/.husky/commit-msg +1 -0
- package/tooling/base/configs/.husky/post-merge +61 -0
- package/tooling/base/configs/.husky/post-rewrite +21 -0
- package/tooling/base/configs/.husky/pre-commit +1 -0
- package/tooling/base/configs/.husky/pre-push +1 -0
- package/tooling/base/configs/.prettierrc +12 -0
- package/tooling/base/configs/.shellcheckrc +1 -0
- package/tooling/base/configs/.vscode/extensions.json +9 -0
- package/tooling/base/configs/.vscode/settings.json +3 -0
- package/tooling/base/configs/commitlint.config.js +11 -0
- package/tooling/base/configs/scripts/verify.sh +64 -0
- package/tooling/base/manifest.toml +30 -0
- package/tooling/base/reference.md +98 -0
- package/tooling/base/seeds/.claude/context/ci.md +33 -0
- package/tooling/base/seeds/.claude/context/development.md +37 -0
- package/tooling/base/seeds/.claude/context/index.md +11 -0
- package/tooling/base/seeds/.cspell/project-terms.txt +0 -0
- package/tooling/base/seeds/.cspell/tech-stack.txt +19 -0
- package/tooling/base/seeds/.lintstagedrc +8 -0
- package/tooling/base/seeds/.prettierignore +0 -0
- package/tooling/base/seeds/cspell.json +20 -0
- package/tooling/claude/manifest.toml +14 -0
- package/tooling/claude/reference.md +79 -0
- package/tooling/claude/seeds/.claude/ARCHITECTURE.md +13 -0
- package/tooling/claude/seeds/.claude/DESIGN.md +62 -0
- package/tooling/claude/seeds/.claude/REQUIREMENTS.md +18 -0
- package/tooling/claude/seeds/.claude/diagrams/index.md +8 -0
- package/tooling/claude/seeds/.claude/hooks/index-reminder.sh +50 -0
- package/tooling/claude/seeds/.claude/hooks/memory-index.sh +68 -0
- package/tooling/claude/seeds/.claude/hooks/path-form.sh +57 -0
- package/tooling/claude/seeds/.claude/hooks/scratch-guard.sh +52 -0
- package/tooling/claude/seeds/.claude/hooks/standards-audit.sh +86 -0
- package/tooling/claude/seeds/.claude/hooks/tasks-index.sh +71 -0
- package/tooling/claude/seeds/.claude/memory/index.md +8 -0
- package/tooling/claude/seeds/.claude/settings.json +47 -0
- package/tooling/claude/seeds/.claude/tasks/index.md +8 -0
- package/tooling/claude/seeds/.claude/wireframes/index.md +8 -0
- package/tooling/claude/seeds/CLAUDE.md +29 -0
- package/tooling/claude/user/settings.template.json +10 -0
- package/tooling/claude/user/statusline-command.sh +53 -0
- package/tooling/python/configs/.coveragerc +14 -0
- package/tooling/python/configs/.python-version +1 -0
- package/tooling/python/configs/mypy.ini +6 -0
- package/tooling/python/configs/pytest.ini +4 -0
- package/tooling/python/configs/ruff.toml +15 -0
- package/tooling/python/configs/scripts/verify.sh +77 -0
- package/tooling/python/manifest.toml +16 -0
- package/tooling/python/reference.md +66 -0
- package/tooling/python/seeds/.cspell/tech-stack.txt +19 -0
- package/tooling/python/seeds/tests/test_smoke.py +2 -0
- package/tooling/vite-react/configs/playwright.config.ts +26 -0
- package/tooling/vite-react/configs/tsconfig.json +35 -0
- package/tooling/vite-react/configs/vite.config.ts +24 -0
- package/tooling/vite-react/configs/vitest.config.ts +26 -0
- package/tooling/vite-react/manifest.toml +23 -0
- package/tooling/vite-react/reference.md +55 -0
- package/tooling/vite-react/seeds/.cspell/project-terms.txt +1 -0
- package/tooling/vite-react/seeds/.cspell/tech-stack.txt +1 -0
- package/tooling/web/configs/.github/workflows/verify.yml +134 -0
- package/tooling/web/configs/.vscode/extensions.json +13 -0
- package/tooling/web/configs/.vscode/settings.json +10 -0
- package/tooling/web/configs/e2e/home.spec.ts +6 -0
- package/tooling/web/configs/e2e/screenshot.ts +53 -0
- package/tooling/web/configs/eslint.config.js +82 -0
- package/tooling/web/configs/scripts/screenshot.sh +28 -0
- package/tooling/web/configs/scripts/verify.sh +80 -0
- package/tooling/web/configs/scripts/worktree-port.sh +74 -0
- package/tooling/web/configs/src/test/setup.ts +8 -0
- package/tooling/web/manifest.toml +58 -0
- package/tooling/web/reference.md +115 -0
- package/tooling/web/seeds/.cspell/tech-stack.txt +18 -0
- package/tsconfig.json +14 -0
|
@@ -0,0 +1,1001 @@
|
|
|
1
|
+
import { existsSync, statSync } from 'node:fs'
|
|
2
|
+
import { readdir, readFile } from 'node:fs/promises'
|
|
3
|
+
import { join, resolve } from 'node:path'
|
|
4
|
+
import {
|
|
5
|
+
archiveDir,
|
|
6
|
+
isReservedStem,
|
|
7
|
+
readOutcomes,
|
|
8
|
+
readPullRequest,
|
|
9
|
+
tasksDir,
|
|
10
|
+
} from '@/tasks/archive'
|
|
11
|
+
import { gitTrunkReader, type TrunkReader } from '@/tasks/trunk'
|
|
12
|
+
|
|
13
|
+
const ORDERING_FILE = 'priority.md'
|
|
14
|
+
const BACKLOG_FILE = 'backlog.md'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The readiness headings `standards/tasks.md` fixes. The names are the
|
|
18
|
+
* contract rather than a suggestion, so a board grouping under names of its own
|
|
19
|
+
* reads as carrying no group at all and the run refuses instead of reporting a
|
|
20
|
+
* clean board it never parsed.
|
|
21
|
+
*/
|
|
22
|
+
export const BOARD_GROUPS = ['Run now', 'Up next', 'Needs a plan'] as const
|
|
23
|
+
|
|
24
|
+
export type BoardGroup = (typeof BOARD_GROUPS)[number]
|
|
25
|
+
|
|
26
|
+
export const VALIDATE_REFUSALS = [
|
|
27
|
+
'no-board',
|
|
28
|
+
'no-ordering',
|
|
29
|
+
'no-groups',
|
|
30
|
+
] as const
|
|
31
|
+
|
|
32
|
+
export type ValidateRefusal = (typeof VALIDATE_REFUSALS)[number]
|
|
33
|
+
|
|
34
|
+
export const FINDING_KINDS = [
|
|
35
|
+
'plan-unstated',
|
|
36
|
+
'plan-unresolved',
|
|
37
|
+
'task-unresolved',
|
|
38
|
+
'row-missing',
|
|
39
|
+
'row-duplicated',
|
|
40
|
+
'row-misshapen',
|
|
41
|
+
'row-untabled',
|
|
42
|
+
'row-misordered',
|
|
43
|
+
'touches-unstated',
|
|
44
|
+
'touches-collided',
|
|
45
|
+
'blocker-settled',
|
|
46
|
+
'blocker-unresolved',
|
|
47
|
+
] as const
|
|
48
|
+
|
|
49
|
+
export type FindingKind = (typeof FINDING_KINDS)[number]
|
|
50
|
+
|
|
51
|
+
export interface Finding {
|
|
52
|
+
readonly kind: FindingKind
|
|
53
|
+
readonly group: BoardGroup | undefined
|
|
54
|
+
readonly subject: string
|
|
55
|
+
readonly message: string
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* A parked row neither half of the blocker check reached. Three of the five
|
|
60
|
+
* blocker kinds put no fact on disk, so a run reporting findings alone would
|
|
61
|
+
* read as a board with nothing stale on it.
|
|
62
|
+
*/
|
|
63
|
+
export interface Untested {
|
|
64
|
+
readonly group: BoardGroup
|
|
65
|
+
readonly subject: string
|
|
66
|
+
readonly message: string
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* A `Touches` cell naming a bare folder, which claims every file under it and
|
|
71
|
+
* so collides with every row a later session writes there. The claim is often
|
|
72
|
+
* correct, since a row rewriting a whole directory has no other way to say so,
|
|
73
|
+
* which is why it reports beside the findings rather than inside them and moves
|
|
74
|
+
* no exit code.
|
|
75
|
+
*/
|
|
76
|
+
export interface FolderClaim {
|
|
77
|
+
readonly group: BoardGroup
|
|
78
|
+
readonly subject: string
|
|
79
|
+
readonly message: string
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* A backlog line, which carries a pointer and nothing else. The backlog is
|
|
84
|
+
* explicitly unordered, so a line has no position to read and no columns to
|
|
85
|
+
* resolve.
|
|
86
|
+
*/
|
|
87
|
+
export interface BacklogRow {
|
|
88
|
+
readonly label: string
|
|
89
|
+
readonly stem: string | undefined
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface BoardRow {
|
|
93
|
+
readonly group: BoardGroup
|
|
94
|
+
readonly label: string
|
|
95
|
+
readonly stem: string | undefined
|
|
96
|
+
readonly plan: string | undefined
|
|
97
|
+
/** Absent when the group fixes no `Touches` column, empty when it read none. */
|
|
98
|
+
readonly touches: readonly string[] | undefined
|
|
99
|
+
/** Absent when the group fixes no `Waiting on` column, which is `## Run now`. */
|
|
100
|
+
readonly waiting: string | undefined
|
|
101
|
+
/** The ordinal phrase a `Waiting on` cell states about its own position, undefined when the cell carries none. */
|
|
102
|
+
readonly ordinal: OrdinalWord | 'last' | undefined
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface ValidateReport {
|
|
106
|
+
readonly ok: true
|
|
107
|
+
readonly rows: number
|
|
108
|
+
readonly backlog: number
|
|
109
|
+
readonly tasks: number
|
|
110
|
+
readonly findings: readonly Finding[]
|
|
111
|
+
readonly untested: readonly Untested[]
|
|
112
|
+
readonly claims: readonly FolderClaim[]
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface ValidateRefused {
|
|
116
|
+
readonly ok: false
|
|
117
|
+
readonly reason: ValidateRefusal
|
|
118
|
+
readonly message: string
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export type ValidateOutcome = ValidateReport | ValidateRefused
|
|
122
|
+
|
|
123
|
+
export function orderingPath(root: string): string {
|
|
124
|
+
return join(tasksDir(root), ORDERING_FILE)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function backlogPath(root: string): string {
|
|
128
|
+
return join(tasksDir(root), BACKLOG_FILE)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Pulls the target out of a markdown link, which is how both the `Task` and the
|
|
133
|
+
* `Plan` column spell their pointer. A cell carrying prose instead of a link
|
|
134
|
+
* yields nothing, and that absence is the finding rather than a parse failure.
|
|
135
|
+
*/
|
|
136
|
+
function linkTarget(cell: string): string | undefined {
|
|
137
|
+
const match = /\[[^\]]*\]\(([^)]+)\)/.exec(cell)
|
|
138
|
+
return match ? match[1].trim() : undefined
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function linkText(cell: string): string {
|
|
142
|
+
const match = /\[([^\]]*)\]\([^)]+\)/.exec(cell)
|
|
143
|
+
return (match ? match[1] : cell).trim()
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function stemOf(target: string): string | undefined {
|
|
147
|
+
const name = target.split('/').pop()
|
|
148
|
+
if (!name || !name.endsWith('.md')) return undefined
|
|
149
|
+
return name.slice(0, -'.md'.length)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Reads the file set out of a `Touches` cell written as prose. Paths are the
|
|
154
|
+
* backticked spans, which is the only marker the column carries, and a span
|
|
155
|
+
* naming a skill or a command rather than a file is dropped by the same test.
|
|
156
|
+
*
|
|
157
|
+
* An extension opens with a letter, which is what separates `commands.md` from
|
|
158
|
+
* a task version like `v40.2`. Reading the version as a path would collide two
|
|
159
|
+
* rows that merely cite the same task.
|
|
160
|
+
*/
|
|
161
|
+
export function readPaths(cell: string): string[] {
|
|
162
|
+
const spans = cell.match(/`[^`]+`/g) ?? []
|
|
163
|
+
const paths = spans
|
|
164
|
+
.map((span) => span.slice(1, -1).trim())
|
|
165
|
+
.filter(
|
|
166
|
+
(span) => span.includes('/') || /\.[A-Za-z][A-Za-z0-9]*$/.test(span),
|
|
167
|
+
)
|
|
168
|
+
.map((span) => span.replace(/^\.\//, '').replace(/\/+$/, ''))
|
|
169
|
+
|
|
170
|
+
return [...new Set(paths)]
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Two paths touch the same work when they are equal or one contains the other.
|
|
175
|
+
* A row naming a directory and a row naming a file inside it collide, which a
|
|
176
|
+
* set intersection on the written strings alone would miss.
|
|
177
|
+
*/
|
|
178
|
+
function sharesPath(left: string, right: string): boolean {
|
|
179
|
+
return (
|
|
180
|
+
left === right ||
|
|
181
|
+
left.startsWith(`${right}/`) ||
|
|
182
|
+
right.startsWith(`${left}/`)
|
|
183
|
+
)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Whether a `Touches` path names a directory. The tree answers for a path that
|
|
188
|
+
* resolves, which is the only reading that separates a folder from a file
|
|
189
|
+
* carrying no extension, such as a hook script.
|
|
190
|
+
*
|
|
191
|
+
* A path the row has yet to create resolves to nothing, so the name decides
|
|
192
|
+
* there. `readPaths` admits a span for one of two reasons, a slash or an
|
|
193
|
+
* extension, so a span surviving without an extension is one a slash let
|
|
194
|
+
* through and reads as a folder.
|
|
195
|
+
*/
|
|
196
|
+
function isFolder(path: string, root: string): boolean {
|
|
197
|
+
const target = resolve(root, path)
|
|
198
|
+
if (existsSync(target)) return statSync(target).isDirectory()
|
|
199
|
+
|
|
200
|
+
return !/\.[A-Za-z][A-Za-z0-9]*$/.test(path)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function splitCells(line: string): string[] {
|
|
204
|
+
return line
|
|
205
|
+
.trim()
|
|
206
|
+
.replace(/^\|/, '')
|
|
207
|
+
.replace(/\|$/, '')
|
|
208
|
+
.split('|')
|
|
209
|
+
.map((cell) => cell.trim())
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function isSeparator(cells: readonly string[]): boolean {
|
|
213
|
+
return cells.every((cell) => /^:?-{3,}:?$/.test(cell))
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function columnIndex(header: readonly string[], name: string): number {
|
|
217
|
+
return header.findIndex((cell) => cell.toLowerCase() === name)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function isGroup(heading: string): heading is BoardGroup {
|
|
221
|
+
return (BOARD_GROUPS as readonly string[]).includes(heading)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const ORDINAL_WORDS = [
|
|
225
|
+
'first',
|
|
226
|
+
'second',
|
|
227
|
+
'third',
|
|
228
|
+
'fourth',
|
|
229
|
+
'fifth',
|
|
230
|
+
'sixth',
|
|
231
|
+
'seventh',
|
|
232
|
+
'eighth',
|
|
233
|
+
'ninth',
|
|
234
|
+
'tenth',
|
|
235
|
+
'eleventh',
|
|
236
|
+
'twelfth',
|
|
237
|
+
'thirteenth',
|
|
238
|
+
'fourteenth',
|
|
239
|
+
'fifteenth',
|
|
240
|
+
'sixteenth',
|
|
241
|
+
'seventeenth',
|
|
242
|
+
'eighteenth',
|
|
243
|
+
'nineteenth',
|
|
244
|
+
'twentieth',
|
|
245
|
+
] as const
|
|
246
|
+
|
|
247
|
+
type OrdinalWord = (typeof ORDINAL_WORDS)[number]
|
|
248
|
+
|
|
249
|
+
const ORDINAL_HERE = new RegExp(
|
|
250
|
+
`\\b(${ORDINAL_WORDS.join('|')})\\s+here\\b`,
|
|
251
|
+
'i',
|
|
252
|
+
)
|
|
253
|
+
const LAST_AT_END = /\blast\.?\s*$/i
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Reads the ordinal phrase a `Waiting on` cell states about its own position,
|
|
257
|
+
* searching the whole cell for `<ordinal> here` or the bare word `last` rather
|
|
258
|
+
* than anchoring to where the cell opens. The phrase sits at the end of the
|
|
259
|
+
* sentence on every row that carries one, as in `nothing, cleared 2026-08-31
|
|
260
|
+
* when it merged. Third here`. The two-word phrase is safe to find anywhere,
|
|
261
|
+
* since it needs both a closed vocabulary word and `here` beside it, but a
|
|
262
|
+
* bare `last` is one of the commonest words in English and this corpus writes
|
|
263
|
+
* it constantly, as in `the last of the two instruments this rename needs`.
|
|
264
|
+
* Anchoring it to the end of the cell is what keeps that prose out of the
|
|
265
|
+
* findings, since every row that actually declares itself last puts the word
|
|
266
|
+
* there and nowhere else. A cell carrying neither reads as unordered rather
|
|
267
|
+
* than as an error, which keeps a legitimately-phrased row out of the
|
|
268
|
+
* findings.
|
|
269
|
+
*/
|
|
270
|
+
function readOrdinal(cell: string): OrdinalWord | 'last' | undefined {
|
|
271
|
+
const here = ORDINAL_HERE.exec(cell)
|
|
272
|
+
if (here?.[1]) return here[1].toLowerCase() as OrdinalWord
|
|
273
|
+
|
|
274
|
+
return LAST_AT_END.test(cell) ? 'last' : undefined
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function isRowLine(line: string): boolean {
|
|
278
|
+
return line.trimStart().startsWith('|')
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Whether a header candidate genuinely opens a table, meaning the line right
|
|
283
|
+
* behind it is a separator carrying the same cell count. A row landing where a
|
|
284
|
+
* blank line already closed the table above it looks identical to a fresh
|
|
285
|
+
* header until this read, since both are the first pipe line the walk has
|
|
286
|
+
* seen since the reset.
|
|
287
|
+
*/
|
|
288
|
+
function opensTable(
|
|
289
|
+
header: readonly string[],
|
|
290
|
+
next: string | undefined,
|
|
291
|
+
): boolean {
|
|
292
|
+
if (next === undefined || !isRowLine(next)) return false
|
|
293
|
+
const cells = splitCells(next)
|
|
294
|
+
return isSeparator(cells) && cells.length === header.length
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Parses the ordering file into rows keyed by readiness group. Columns are read
|
|
299
|
+
* from each table's own header rather than by position, so a group whose shape
|
|
300
|
+
* differs from this repository's is reported for what it lacks instead of
|
|
301
|
+
* having its second cell read as something it never was.
|
|
302
|
+
*
|
|
303
|
+
* The walk carries one more piece of state than a header: whether the table
|
|
304
|
+
* that header opened is still open. A blank or prose line closes it, so a row
|
|
305
|
+
* stranded behind that close is read as one candidate header among many rather
|
|
306
|
+
* than as a continuation of the table above, and each surviving row's cell
|
|
307
|
+
* count is checked against the header it actually followed.
|
|
308
|
+
*/
|
|
309
|
+
export function readBoard(text: string): {
|
|
310
|
+
readonly rows: readonly BoardRow[]
|
|
311
|
+
readonly groups: readonly BoardGroup[]
|
|
312
|
+
readonly findings: readonly Finding[]
|
|
313
|
+
} {
|
|
314
|
+
const rows: BoardRow[] = []
|
|
315
|
+
const groups: BoardGroup[] = []
|
|
316
|
+
const findings: Finding[] = []
|
|
317
|
+
|
|
318
|
+
const lines = text.split('\n')
|
|
319
|
+
|
|
320
|
+
let group: BoardGroup | undefined
|
|
321
|
+
let header: string[] | undefined
|
|
322
|
+
|
|
323
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
324
|
+
const line = lines[i] ?? ''
|
|
325
|
+
const heading = /^##\s+(.+?)\s*$/.exec(line)
|
|
326
|
+
if (heading) {
|
|
327
|
+
const title = heading[1]
|
|
328
|
+
group = isGroup(title) ? title : undefined
|
|
329
|
+
header = undefined
|
|
330
|
+
if (group && !groups.includes(group)) groups.push(group)
|
|
331
|
+
continue
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (!group) continue
|
|
335
|
+
|
|
336
|
+
if (!isRowLine(line)) {
|
|
337
|
+
header = undefined
|
|
338
|
+
continue
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const cells = splitCells(line)
|
|
342
|
+
if (isSeparator(cells)) continue
|
|
343
|
+
|
|
344
|
+
if (!header) {
|
|
345
|
+
if (!opensTable(cells, lines[i + 1])) {
|
|
346
|
+
findings.push({
|
|
347
|
+
kind: 'row-untabled',
|
|
348
|
+
group,
|
|
349
|
+
subject: line.trim(),
|
|
350
|
+
message:
|
|
351
|
+
'sits outside a table. The table above it already closed at the line before it.',
|
|
352
|
+
})
|
|
353
|
+
continue
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
header = cells
|
|
357
|
+
continue
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (cells.length !== header.length) {
|
|
361
|
+
findings.push({
|
|
362
|
+
kind: 'row-misshapen',
|
|
363
|
+
group,
|
|
364
|
+
subject: line.trim(),
|
|
365
|
+
message: `carries ${cells.length} cell(s) against the ${header.length}-cell header above it.`,
|
|
366
|
+
})
|
|
367
|
+
continue
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const taskAt = columnIndex(header, 'task')
|
|
371
|
+
const planAt = columnIndex(header, 'plan')
|
|
372
|
+
const touchesAt = columnIndex(header, 'touches')
|
|
373
|
+
const waitingAt = columnIndex(header, 'waiting on')
|
|
374
|
+
|
|
375
|
+
const task = taskAt >= 0 ? (cells[taskAt] ?? '') : ''
|
|
376
|
+
const target = linkTarget(task)
|
|
377
|
+
const plan = planAt >= 0 ? linkTarget(cells[planAt] ?? '') : undefined
|
|
378
|
+
const waiting = waitingAt >= 0 ? (cells[waitingAt] ?? '') : undefined
|
|
379
|
+
|
|
380
|
+
rows.push({
|
|
381
|
+
group,
|
|
382
|
+
label: linkText(task) || task,
|
|
383
|
+
stem: target ? stemOf(target) : undefined,
|
|
384
|
+
plan,
|
|
385
|
+
touches: touchesAt >= 0 ? readPaths(cells[touchesAt] ?? '') : undefined,
|
|
386
|
+
waiting,
|
|
387
|
+
ordinal: waiting ? readOrdinal(waiting) : undefined,
|
|
388
|
+
})
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return { rows, groups, findings }
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Reports a `## Needs a plan` row whose stated ordinal disagrees with where it
|
|
396
|
+
* actually sits in that group. Reading the sequence for gaps and duplicates on
|
|
397
|
+
* its own would detect less, since row position is contiguous by
|
|
398
|
+
* construction, and a hand-renumbered sequence is exactly a case where the
|
|
399
|
+
* prose and the position have come apart.
|
|
400
|
+
*/
|
|
401
|
+
function checkOrdinals(rows: readonly BoardRow[]): Finding[] {
|
|
402
|
+
const findings: Finding[] = []
|
|
403
|
+
const parked = rows.filter((row) => row.group === 'Needs a plan')
|
|
404
|
+
|
|
405
|
+
parked.forEach((row, index) => {
|
|
406
|
+
if (!row.ordinal) return
|
|
407
|
+
|
|
408
|
+
const position = index + 1
|
|
409
|
+
const expected =
|
|
410
|
+
row.ordinal === 'last'
|
|
411
|
+
? parked.length
|
|
412
|
+
: ORDINAL_WORDS.indexOf(row.ordinal) + 1
|
|
413
|
+
|
|
414
|
+
if (expected === position) return
|
|
415
|
+
|
|
416
|
+
const phrase = row.ordinal === 'last' ? 'last' : `${row.ordinal} here`
|
|
417
|
+
findings.push({
|
|
418
|
+
kind: 'row-misordered',
|
|
419
|
+
group: row.group,
|
|
420
|
+
subject: subjectOf(row),
|
|
421
|
+
message: `declares itself "${phrase}", but sits at position ${position} of ${parked.length} in ${row.group}.`,
|
|
422
|
+
})
|
|
423
|
+
})
|
|
424
|
+
|
|
425
|
+
return findings
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Parses the backlog into one row per bullet carrying a link. The backlog is a
|
|
430
|
+
* flat list rather than a table, so there is no header to resolve and no group
|
|
431
|
+
* to sit under, and a bullet holding prose instead of a pointer is skipped.
|
|
432
|
+
*
|
|
433
|
+
* Skipping it rather than reporting it is what keeps the intro paragraph and
|
|
434
|
+
* any explanatory bullet out of the findings. The task that bullet meant to
|
|
435
|
+
* name is still accounted for, because a stem no line reaches is reported by
|
|
436
|
+
* `checkMapping` as carrying no row on either surface.
|
|
437
|
+
*/
|
|
438
|
+
export function readBacklog(text: string): readonly BacklogRow[] {
|
|
439
|
+
const rows: BacklogRow[] = []
|
|
440
|
+
|
|
441
|
+
for (const line of text.split('\n')) {
|
|
442
|
+
if (!/^\s*[-*]\s/.test(line)) continue
|
|
443
|
+
|
|
444
|
+
const target = linkTarget(line)
|
|
445
|
+
if (!target) continue
|
|
446
|
+
|
|
447
|
+
// A pointer carrying a directory names something other than a sibling task,
|
|
448
|
+
// the way `citedStem` reads the same shape on the board.
|
|
449
|
+
const path = target.split('#')[0] ?? ''
|
|
450
|
+
if (path.includes('/')) continue
|
|
451
|
+
|
|
452
|
+
const stem = stemOf(path)
|
|
453
|
+
if (stem && isReservedStem(stem)) continue
|
|
454
|
+
|
|
455
|
+
rows.push({ label: linkText(line) || line.trim(), stem })
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
return rows
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
async function listTaskStems(dir: string): Promise<string[]> {
|
|
462
|
+
const entries = await readdir(dir)
|
|
463
|
+
|
|
464
|
+
return entries
|
|
465
|
+
.filter((entry) => entry.endsWith('.md'))
|
|
466
|
+
.map((entry) => entry.slice(0, -'.md'.length))
|
|
467
|
+
.filter((stem) => !isReservedStem(stem))
|
|
468
|
+
.sort()
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Resolves a pointer against the board and against the project root both, the
|
|
473
|
+
* way `claude-docs` reads the same line. A fragment is dropped first, since an
|
|
474
|
+
* anchor is part of the link and never part of the path.
|
|
475
|
+
*/
|
|
476
|
+
function resolves(target: string, dir: string, root: string): boolean {
|
|
477
|
+
const path = target.split('#')[0]
|
|
478
|
+
if (!path) return false
|
|
479
|
+
|
|
480
|
+
return existsSync(resolve(dir, path)) || existsSync(resolve(root, path))
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Accounts every task file against both surfaces the board spans. A task sits
|
|
485
|
+
* on `priority.md` when it would plausibly be planned soon and on `backlog.md`
|
|
486
|
+
* otherwise, so a file reached by neither is the dropped one this reports and a
|
|
487
|
+
* file reached by both claims two contradictory things about itself.
|
|
488
|
+
*/
|
|
489
|
+
function checkMapping(
|
|
490
|
+
rows: readonly BoardRow[],
|
|
491
|
+
backlog: readonly BacklogRow[],
|
|
492
|
+
stems: readonly string[],
|
|
493
|
+
dir: string,
|
|
494
|
+
): Finding[] {
|
|
495
|
+
const findings: Finding[] = []
|
|
496
|
+
const seen = new Map<string, number>()
|
|
497
|
+
const listed = new Set<string>()
|
|
498
|
+
|
|
499
|
+
for (const row of backlog) {
|
|
500
|
+
if (!row.stem) {
|
|
501
|
+
findings.push({
|
|
502
|
+
kind: 'task-unresolved',
|
|
503
|
+
group: undefined,
|
|
504
|
+
subject: row.label,
|
|
505
|
+
message: 'is a backlog line naming no task file.',
|
|
506
|
+
})
|
|
507
|
+
continue
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
listed.add(row.stem)
|
|
511
|
+
|
|
512
|
+
if (!existsSync(join(dir, `${row.stem}.md`))) {
|
|
513
|
+
findings.push({
|
|
514
|
+
kind: 'task-unresolved',
|
|
515
|
+
group: undefined,
|
|
516
|
+
subject: row.stem,
|
|
517
|
+
message: 'has a backlog line and no task file.',
|
|
518
|
+
})
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
for (const row of rows) {
|
|
523
|
+
if (!row.stem) {
|
|
524
|
+
findings.push({
|
|
525
|
+
kind: 'task-unresolved',
|
|
526
|
+
group: row.group,
|
|
527
|
+
subject: row.label,
|
|
528
|
+
message: 'names no task file, so the row points at nothing.',
|
|
529
|
+
})
|
|
530
|
+
continue
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
seen.set(row.stem, (seen.get(row.stem) ?? 0) + 1)
|
|
534
|
+
|
|
535
|
+
if (!existsSync(join(dir, `${row.stem}.md`))) {
|
|
536
|
+
findings.push({
|
|
537
|
+
kind: 'task-unresolved',
|
|
538
|
+
group: row.group,
|
|
539
|
+
subject: row.stem,
|
|
540
|
+
message: 'has a row and no task file.',
|
|
541
|
+
})
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
for (const [stem, count] of seen) {
|
|
546
|
+
if (count > 1) {
|
|
547
|
+
findings.push({
|
|
548
|
+
kind: 'row-duplicated',
|
|
549
|
+
group: undefined,
|
|
550
|
+
subject: stem,
|
|
551
|
+
message: `carries ${count} rows. A task belongs to exactly one group.`,
|
|
552
|
+
})
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
if (listed.has(stem)) {
|
|
556
|
+
findings.push({
|
|
557
|
+
kind: 'row-duplicated',
|
|
558
|
+
group: undefined,
|
|
559
|
+
subject: stem,
|
|
560
|
+
message:
|
|
561
|
+
'carries a row on the board and a line on the backlog. A task sits on one surface.',
|
|
562
|
+
})
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
for (const stem of stems) {
|
|
567
|
+
if (!seen.has(stem) && !listed.has(stem)) {
|
|
568
|
+
findings.push({
|
|
569
|
+
kind: 'row-missing',
|
|
570
|
+
group: undefined,
|
|
571
|
+
subject: stem,
|
|
572
|
+
message:
|
|
573
|
+
'is a task file with no row on the board and no line on the backlog.',
|
|
574
|
+
})
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
return findings
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function checkPlans(
|
|
582
|
+
rows: readonly BoardRow[],
|
|
583
|
+
dir: string,
|
|
584
|
+
root: string,
|
|
585
|
+
): Finding[] {
|
|
586
|
+
const findings: Finding[] = []
|
|
587
|
+
|
|
588
|
+
for (const row of rows) {
|
|
589
|
+
if (row.group !== 'Run now') continue
|
|
590
|
+
|
|
591
|
+
if (!row.plan) {
|
|
592
|
+
findings.push({
|
|
593
|
+
kind: 'plan-unstated',
|
|
594
|
+
group: row.group,
|
|
595
|
+
subject: row.stem ?? row.label,
|
|
596
|
+
message:
|
|
597
|
+
'states no plan pointer, so the readiness claim cannot be checked.',
|
|
598
|
+
})
|
|
599
|
+
continue
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
if (!resolves(row.plan, dir, root)) {
|
|
603
|
+
findings.push({
|
|
604
|
+
kind: 'plan-unresolved',
|
|
605
|
+
group: row.group,
|
|
606
|
+
subject: row.stem ?? row.label,
|
|
607
|
+
message: `points at ${row.plan}, which resolves to no file.`,
|
|
608
|
+
})
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
return findings
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* The half of the `## Run now` test a person cannot check by eye. Two rows a
|
|
617
|
+
* worker may be handed at once must touch disjoint files, and the `Touches`
|
|
618
|
+
* column is the only place either set is written down.
|
|
619
|
+
*/
|
|
620
|
+
function checkCollisions(rows: readonly BoardRow[]): Finding[] {
|
|
621
|
+
const findings: Finding[] = []
|
|
622
|
+
const ready = rows.filter((row) => row.group === 'Run now')
|
|
623
|
+
|
|
624
|
+
for (const row of ready) {
|
|
625
|
+
// An absent column and an unreadable one both leave the row untested by
|
|
626
|
+
// the loop below, so reporting only the second would pass a board whose
|
|
627
|
+
// `## Run now` table declares no file set at all.
|
|
628
|
+
if (!row.touches || row.touches.length === 0) {
|
|
629
|
+
findings.push({
|
|
630
|
+
kind: 'touches-unstated',
|
|
631
|
+
group: row.group,
|
|
632
|
+
subject: row.stem ?? row.label,
|
|
633
|
+
message: 'names no file, so its collisions cannot be read.',
|
|
634
|
+
})
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
for (let i = 0; i < ready.length; i += 1) {
|
|
639
|
+
for (let j = i + 1; j < ready.length; j += 1) {
|
|
640
|
+
const left = ready[i]
|
|
641
|
+
const right = ready[j]
|
|
642
|
+
const shared = (left.touches ?? []).flatMap((path) => {
|
|
643
|
+
const other = (right.touches ?? []).find((candidate) =>
|
|
644
|
+
sharesPath(path, candidate),
|
|
645
|
+
)
|
|
646
|
+
return other === undefined
|
|
647
|
+
? []
|
|
648
|
+
: [describeShared(path, other, left, right)]
|
|
649
|
+
})
|
|
650
|
+
|
|
651
|
+
if (shared.length === 0) continue
|
|
652
|
+
|
|
653
|
+
findings.push({
|
|
654
|
+
kind: 'touches-collided',
|
|
655
|
+
group: 'Run now',
|
|
656
|
+
subject: `${subjectOf(left)} and ${subjectOf(right)}`,
|
|
657
|
+
message: `both touch ${joinShared(shared)}.`,
|
|
658
|
+
})
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
return findings
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function subjectOf(row: BoardRow): string {
|
|
666
|
+
return row.stem ?? row.label
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* Names one path two rows share, and the row that claimed it as a folder when
|
|
671
|
+
* one side named a directory holding the other's file. Which side contributed
|
|
672
|
+
* the containing path is the fact a reader acts on, and printing the shared
|
|
673
|
+
* strings alone leaves an over-broad cell and a genuine overlap identical.
|
|
674
|
+
*
|
|
675
|
+
* The shorter path is the container, because `sharesPath` holds for an unequal
|
|
676
|
+
* pair only when one is a prefix of the other up to a separator.
|
|
677
|
+
*/
|
|
678
|
+
function describeShared(
|
|
679
|
+
path: string,
|
|
680
|
+
other: string,
|
|
681
|
+
left: BoardRow,
|
|
682
|
+
right: BoardRow,
|
|
683
|
+
): string {
|
|
684
|
+
if (path === other) return path
|
|
685
|
+
|
|
686
|
+
const container = path.length < other.length ? path : other
|
|
687
|
+
const owner = container === path ? left : right
|
|
688
|
+
return `${container}, which ${subjectOf(owner)} claims as a folder`
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* A folder clause carries a comma of its own, so a comma between clauses would
|
|
693
|
+
* read as another path. The plain list keeps the comma it has always had.
|
|
694
|
+
*/
|
|
695
|
+
function joinShared(clauses: readonly string[]): string {
|
|
696
|
+
const separator = clauses.some((clause) => clause.includes(',')) ? '; ' : ', '
|
|
697
|
+
return clauses.join(separator)
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/**
|
|
701
|
+
* Reports a `Touches` cell naming a bare folder. The claim collides with every
|
|
702
|
+
* row a later session writes under that folder, and it is legitimate whenever
|
|
703
|
+
* the row does rewrite the directory, so this states the reach rather than
|
|
704
|
+
* calling it a defect.
|
|
705
|
+
*
|
|
706
|
+
* The scan takes `## Run now` alone, where `checkCollisions` takes it. A cell in
|
|
707
|
+
* another group describes work nobody has planned, so it is written as a
|
|
708
|
+
* sentence and rewritten at planning time, and a claim read off one reports on
|
|
709
|
+
* prose rather than on a file set. That is the shape that teaches a reader to
|
|
710
|
+
* skip the report. A parked folder claim surfaces when the row is promoted,
|
|
711
|
+
* which is also when its cell becomes a set anything can act on.
|
|
712
|
+
*/
|
|
713
|
+
function checkFolderClaims(
|
|
714
|
+
rows: readonly BoardRow[],
|
|
715
|
+
root: string,
|
|
716
|
+
): FolderClaim[] {
|
|
717
|
+
const claims: FolderClaim[] = []
|
|
718
|
+
|
|
719
|
+
for (const row of rows.filter((candidate) => candidate.group === 'Run now')) {
|
|
720
|
+
for (const path of row.touches ?? []) {
|
|
721
|
+
if (!isFolder(path, root)) continue
|
|
722
|
+
|
|
723
|
+
claims.push({
|
|
724
|
+
group: row.group,
|
|
725
|
+
subject: subjectOf(row),
|
|
726
|
+
message: `claims the whole ${path} folder, so it collides with every row written under it.`,
|
|
727
|
+
})
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
return claims
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* Reads the task a blocker cell cites. A task pointer is a bare sibling
|
|
736
|
+
* filename, the way every `Task` column spells one, so a target carrying a
|
|
737
|
+
* directory names something else and yields nothing. A row waiting on a plan
|
|
738
|
+
* links that plan, and reading its stem as a task would report the row settled
|
|
739
|
+
* against a folder the plan does not sit in.
|
|
740
|
+
*/
|
|
741
|
+
function citedStem(cell: string): string | undefined {
|
|
742
|
+
const target = linkTarget(cell)?.split('#')[0]
|
|
743
|
+
if (!target || target.includes('/')) return undefined
|
|
744
|
+
return stemOf(target)
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/** What one blocker citation produced, since a row can be neither settled nor open. */
|
|
748
|
+
interface CitedResult {
|
|
749
|
+
readonly findings: readonly Finding[]
|
|
750
|
+
readonly untested: readonly Untested[]
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
function nothing(): CitedResult {
|
|
754
|
+
return { findings: [], untested: [] }
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/**
|
|
758
|
+
* Reports what a cited task does to the row waiting on it. A file sitting in
|
|
759
|
+
* the archive settles the row, and a live file settles it only once the work it
|
|
760
|
+
* carries is on the trunk. A file carrying no outcome box settles nothing,
|
|
761
|
+
* since a file the check could not parse is not evidence of a finished one.
|
|
762
|
+
*
|
|
763
|
+
* A citation resolving in neither folder is a broken pointer rather than a
|
|
764
|
+
* closed task, and the two take different findings. Reading an absent file as
|
|
765
|
+
* archived states a specific fact about a file nobody ever wrote, which is what
|
|
766
|
+
* a renamed task or a typo produces.
|
|
767
|
+
*
|
|
768
|
+
* A closed outcome is not the same fact as landed work. The ship chain marks
|
|
769
|
+
* outcomes as its first step and opens the pull request several steps later, so
|
|
770
|
+
* a check reading the checkbox reports the row settled while the branch is
|
|
771
|
+
* still in review. The pull request the task names is what the trunk is asked
|
|
772
|
+
* about, and a task naming none leaves the row untested rather than settled,
|
|
773
|
+
* because the only local signal left is the checkbox that produced the defect.
|
|
774
|
+
*
|
|
775
|
+
* The outcome list comes off `readOutcomes` rather than a pattern of its own,
|
|
776
|
+
* so this check cannot disagree with the archive and outcome verbs about which
|
|
777
|
+
* checkboxes are outcomes and which sit inside a block a task displays.
|
|
778
|
+
*/
|
|
779
|
+
async function checkCitedTask(
|
|
780
|
+
group: BoardGroup,
|
|
781
|
+
subject: string,
|
|
782
|
+
cited: string,
|
|
783
|
+
root: string,
|
|
784
|
+
trunk: TrunkReader,
|
|
785
|
+
): Promise<CitedResult> {
|
|
786
|
+
const live = join(tasksDir(root), `${cited}.md`)
|
|
787
|
+
|
|
788
|
+
if (!existsSync(live)) {
|
|
789
|
+
if (existsSync(join(archiveDir(root), `${cited}.md`))) {
|
|
790
|
+
return settled(group, subject, `waits on ${cited}, which is archived.`)
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
return {
|
|
794
|
+
findings: [
|
|
795
|
+
{
|
|
796
|
+
kind: 'blocker-unresolved',
|
|
797
|
+
group,
|
|
798
|
+
subject,
|
|
799
|
+
message: `waits on ${cited}, which is neither on the board nor archived.`,
|
|
800
|
+
},
|
|
801
|
+
],
|
|
802
|
+
untested: [],
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
const text = await readFile(live, 'utf8')
|
|
807
|
+
const { open, closed } = readOutcomes(text)
|
|
808
|
+
if (open.length > 0 || closed.length === 0) return nothing()
|
|
809
|
+
|
|
810
|
+
const pullRequest = readPullRequest(text)
|
|
811
|
+
if (pullRequest === undefined) {
|
|
812
|
+
return untestedRow(
|
|
813
|
+
group,
|
|
814
|
+
subject,
|
|
815
|
+
`waits on ${cited}, which closed every outcome but names no pull request, so nothing tests whether the work reached the trunk.`,
|
|
816
|
+
)
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
const landed = await trunk(pullRequest)
|
|
820
|
+
if (landed === undefined) {
|
|
821
|
+
return untestedRow(
|
|
822
|
+
group,
|
|
823
|
+
subject,
|
|
824
|
+
`waits on ${cited}, whose pull request #${pullRequest} could not be read against the trunk.`,
|
|
825
|
+
)
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
if (!landed) return nothing()
|
|
829
|
+
|
|
830
|
+
return settled(
|
|
831
|
+
group,
|
|
832
|
+
subject,
|
|
833
|
+
`waits on ${cited}, whose pull request #${pullRequest} reached the trunk.`,
|
|
834
|
+
)
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
function settled(
|
|
838
|
+
group: BoardGroup,
|
|
839
|
+
subject: string,
|
|
840
|
+
message: string,
|
|
841
|
+
): CitedResult {
|
|
842
|
+
return {
|
|
843
|
+
findings: [{ kind: 'blocker-settled', group, subject, message }],
|
|
844
|
+
untested: [],
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
function untestedRow(
|
|
849
|
+
group: BoardGroup,
|
|
850
|
+
subject: string,
|
|
851
|
+
message: string,
|
|
852
|
+
): CitedResult {
|
|
853
|
+
return { findings: [], untested: [{ group, subject, message }] }
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
/**
|
|
857
|
+
* Re-takes the two blocker kinds a command can settle, over every row outside
|
|
858
|
+
* `## Run now`. A cited task is settled by being archived or by closing every
|
|
859
|
+
* outcome, and a cited path is settled by no `## Run now` row still holding it.
|
|
860
|
+
*
|
|
861
|
+
* Both halves gate on a citation the cell carries, never on a column beside it.
|
|
862
|
+
* The board standard gives a collision cell the file held by the running task,
|
|
863
|
+
* so a row whose cell names no file was parked by something else and its
|
|
864
|
+
* `Touches` column says nothing about what holds it. Reading that column
|
|
865
|
+
* instead reports a cleared collision on a row no collision ever parked, and
|
|
866
|
+
* counts the row as re-tested, which is the more expensive half of that error.
|
|
867
|
+
*
|
|
868
|
+
* The cell is read for citations rather than parsed into fields, since the
|
|
869
|
+
* standard fixes three forms for it and leaves it prose. A row neither half
|
|
870
|
+
* reached is returned as untested, because the other three kinds are a person's
|
|
871
|
+
* to judge and silence about them reads as a board with nothing stale on it.
|
|
872
|
+
*/
|
|
873
|
+
async function checkParked(
|
|
874
|
+
rows: readonly BoardRow[],
|
|
875
|
+
root: string,
|
|
876
|
+
trunk: TrunkReader,
|
|
877
|
+
): Promise<{ findings: Finding[]; untested: Untested[] }> {
|
|
878
|
+
const findings: Finding[] = []
|
|
879
|
+
const untested: Untested[] = []
|
|
880
|
+
const running = rows.filter((row) => row.group === 'Run now')
|
|
881
|
+
|
|
882
|
+
for (const row of rows) {
|
|
883
|
+
if (row.group === 'Run now') continue
|
|
884
|
+
|
|
885
|
+
const subject = row.stem ?? row.label
|
|
886
|
+
const cell = row.waiting ?? ''
|
|
887
|
+
const cited = citedStem(cell)
|
|
888
|
+
const contested = readPaths(cell)
|
|
889
|
+
|
|
890
|
+
if (cited) {
|
|
891
|
+
const result = await checkCitedTask(
|
|
892
|
+
row.group,
|
|
893
|
+
subject,
|
|
894
|
+
cited,
|
|
895
|
+
root,
|
|
896
|
+
trunk,
|
|
897
|
+
)
|
|
898
|
+
findings.push(...result.findings)
|
|
899
|
+
untested.push(...result.untested)
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
const held = contested.filter((path) =>
|
|
903
|
+
running.some((run) =>
|
|
904
|
+
(run.touches ?? []).some((other) => sharesPath(path, other)),
|
|
905
|
+
),
|
|
906
|
+
)
|
|
907
|
+
|
|
908
|
+
if (contested.length > 0 && held.length === 0) {
|
|
909
|
+
findings.push({
|
|
910
|
+
kind: 'blocker-settled',
|
|
911
|
+
group: row.group,
|
|
912
|
+
subject,
|
|
913
|
+
message: `waits on ${contested.join(', ')}, which nothing under Run now holds.`,
|
|
914
|
+
})
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
if (!cited && contested.length === 0) {
|
|
918
|
+
untested.push({
|
|
919
|
+
group: row.group,
|
|
920
|
+
subject,
|
|
921
|
+
message:
|
|
922
|
+
'cites no task and no file, so neither half of its blocker is mechanical.',
|
|
923
|
+
})
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
return { findings, untested }
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
function refuse(reason: ValidateRefusal, message: string): ValidateRefused {
|
|
931
|
+
return { ok: false, reason, message }
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
export interface ValidateOptions {
|
|
935
|
+
/** Overridden by tests, which supply the trunk rather than reaching for git. */
|
|
936
|
+
readonly trunk?: TrunkReader
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
/**
|
|
940
|
+
* Reports what every board row claims against what the tree holds. It writes
|
|
941
|
+
* nothing: a row is a session's claim about readiness, and a validator that
|
|
942
|
+
* repaired one would be asserting the claim it exists to test.
|
|
943
|
+
*/
|
|
944
|
+
export async function validateBoard(
|
|
945
|
+
root: string,
|
|
946
|
+
options: ValidateOptions = {},
|
|
947
|
+
): Promise<ValidateOutcome> {
|
|
948
|
+
const trunk = options.trunk ?? gitTrunkReader(root)
|
|
949
|
+
const dir = tasksDir(root)
|
|
950
|
+
if (!existsSync(dir)) {
|
|
951
|
+
return refuse('no-board', `No task board at ${dir}.`)
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
const ordering = orderingPath(root)
|
|
955
|
+
if (!existsSync(ordering)) {
|
|
956
|
+
return refuse('no-ordering', `No ordering file at ${ordering}.`)
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
const {
|
|
960
|
+
rows,
|
|
961
|
+
groups,
|
|
962
|
+
findings: shapeFindings,
|
|
963
|
+
} = readBoard(await readFile(ordering, 'utf8'))
|
|
964
|
+
|
|
965
|
+
if (groups.length === 0) {
|
|
966
|
+
return refuse(
|
|
967
|
+
'no-groups',
|
|
968
|
+
`No readiness group in ${ORDERING_FILE}. Expected one of: ${BOARD_GROUPS.join(', ')}.`,
|
|
969
|
+
)
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
// An absent backlog reads as empty rather than refusing. A project that has
|
|
973
|
+
// never needed the second surface keeps every task on the board, which is the
|
|
974
|
+
// one-to-one mapping this check ran before the backlog existed.
|
|
975
|
+
const backlogFile = backlogPath(root)
|
|
976
|
+
const backlog = existsSync(backlogFile)
|
|
977
|
+
? readBacklog(await readFile(backlogFile, 'utf8'))
|
|
978
|
+
: []
|
|
979
|
+
|
|
980
|
+
const stems = await listTaskStems(dir)
|
|
981
|
+
const parked = await checkParked(rows, root, trunk)
|
|
982
|
+
|
|
983
|
+
const findings = [
|
|
984
|
+
...shapeFindings,
|
|
985
|
+
...checkMapping(rows, backlog, stems, dir),
|
|
986
|
+
...checkPlans(rows, dir, root),
|
|
987
|
+
...checkCollisions(rows),
|
|
988
|
+
...checkOrdinals(rows),
|
|
989
|
+
...parked.findings,
|
|
990
|
+
]
|
|
991
|
+
|
|
992
|
+
return {
|
|
993
|
+
ok: true,
|
|
994
|
+
rows: rows.length,
|
|
995
|
+
backlog: backlog.length,
|
|
996
|
+
tasks: stems.length,
|
|
997
|
+
findings,
|
|
998
|
+
untested: parked.untested,
|
|
999
|
+
claims: checkFolderClaims(rows, root),
|
|
1000
|
+
}
|
|
1001
|
+
}
|