@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,320 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { readdir, readFile, writeFile } from 'node:fs/promises'
|
|
3
|
+
import { basename, join, relative } from 'node:path'
|
|
4
|
+
import {
|
|
5
|
+
INDEX_FILE,
|
|
6
|
+
type IntakeItem,
|
|
7
|
+
isMalformed,
|
|
8
|
+
isUnread,
|
|
9
|
+
readItems,
|
|
10
|
+
writeAnswerLine,
|
|
11
|
+
} from '@/intake/items'
|
|
12
|
+
|
|
13
|
+
export const INTAKE_REFUSALS = [
|
|
14
|
+
'no-intake',
|
|
15
|
+
'no-folder',
|
|
16
|
+
'ambiguous-slug',
|
|
17
|
+
'no-cluster',
|
|
18
|
+
'no-item',
|
|
19
|
+
'answered',
|
|
20
|
+
'bad-input',
|
|
21
|
+
] as const
|
|
22
|
+
|
|
23
|
+
export type IntakeRefusal = (typeof INTAKE_REFUSALS)[number]
|
|
24
|
+
|
|
25
|
+
export interface IntakeRefused {
|
|
26
|
+
readonly ok: false
|
|
27
|
+
readonly reason: IntakeRefusal
|
|
28
|
+
readonly message: string
|
|
29
|
+
readonly detail: readonly string[]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface FolderSummary {
|
|
33
|
+
readonly slug: string
|
|
34
|
+
readonly items: number
|
|
35
|
+
readonly open: number
|
|
36
|
+
readonly unread: number
|
|
37
|
+
/** Items carrying no answer slot, which no route here can answer. */
|
|
38
|
+
readonly malformed: number
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface ClusterItems {
|
|
42
|
+
readonly cluster: string
|
|
43
|
+
readonly items: readonly IntakeItem[]
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface FolderListed {
|
|
47
|
+
readonly ok: true
|
|
48
|
+
readonly folders: readonly FolderSummary[]
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface FolderRead {
|
|
52
|
+
readonly ok: true
|
|
53
|
+
readonly slug: string
|
|
54
|
+
readonly clusters: readonly ClusterItems[]
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface AnswerWritten {
|
|
58
|
+
readonly ok: true
|
|
59
|
+
readonly slug: string
|
|
60
|
+
readonly cluster: string
|
|
61
|
+
readonly path: string
|
|
62
|
+
readonly answered: readonly Selection[]
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export type ListOutcome = FolderListed | IntakeRefused
|
|
66
|
+
export type ReadOutcome = FolderRead | IntakeRefused
|
|
67
|
+
export type AnswerOutcome = AnswerWritten | IntakeRefused
|
|
68
|
+
|
|
69
|
+
export interface Selection {
|
|
70
|
+
readonly label: string
|
|
71
|
+
readonly answer: string
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function refuse(
|
|
75
|
+
reason: IntakeRefusal,
|
|
76
|
+
message: string,
|
|
77
|
+
detail: readonly string[] = [],
|
|
78
|
+
): IntakeRefused {
|
|
79
|
+
return { ok: false, reason, message, detail }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function intakeDir(root: string): string {
|
|
83
|
+
return join(root, '.claude', 'intake')
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Cluster files in read order, which is the numbering the folder carries. The
|
|
88
|
+
* index is dropped because it answers nothing, so every remaining file is one a
|
|
89
|
+
* selection can land in.
|
|
90
|
+
*/
|
|
91
|
+
export async function listClusters(dir: string): Promise<string[]> {
|
|
92
|
+
const entries = await readdir(dir, { withFileTypes: true })
|
|
93
|
+
|
|
94
|
+
return entries
|
|
95
|
+
.filter(
|
|
96
|
+
(entry) =>
|
|
97
|
+
entry.isFile() &&
|
|
98
|
+
entry.name.endsWith('.md') &&
|
|
99
|
+
entry.name !== INDEX_FILE,
|
|
100
|
+
)
|
|
101
|
+
.map((entry) => entry.name)
|
|
102
|
+
.sort()
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function listSlugs(dir: string): Promise<string[]> {
|
|
106
|
+
const entries = await readdir(dir, { withFileTypes: true })
|
|
107
|
+
|
|
108
|
+
return entries
|
|
109
|
+
.filter((entry) => entry.isDirectory())
|
|
110
|
+
.map((entry) => entry.name)
|
|
111
|
+
.sort()
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
type SlugMatch =
|
|
115
|
+
| { readonly kind: 'matched'; readonly name: string }
|
|
116
|
+
| { readonly kind: 'ambiguous'; readonly names: readonly string[] }
|
|
117
|
+
| { readonly kind: 'none' }
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* A folder carries a `<nn>-<slug>` name, but a caller names the topic alone.
|
|
121
|
+
* An exact match wins first, since it is what a name with no ordinal, or one
|
|
122
|
+
* already copied in full from a listing, resolves against. Otherwise the one
|
|
123
|
+
* entry whose name is an ordinal ahead of the given slug wins, which is what
|
|
124
|
+
* lets a topic keep working as its folder's identity gains a prefix. Two or
|
|
125
|
+
* more such entries is a collision the caller needs told apart from a typo,
|
|
126
|
+
* not a folder silently picked or silently missing.
|
|
127
|
+
*/
|
|
128
|
+
function matchSlug(names: readonly string[], slug: string): SlugMatch {
|
|
129
|
+
if (names.includes(slug)) return { kind: 'matched', name: slug }
|
|
130
|
+
|
|
131
|
+
const suffixed = names.filter(
|
|
132
|
+
(name) => name === `${extractOrdinal(name)}-${slug}`,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
if (suffixed.length === 1) return { kind: 'matched', name: suffixed[0] }
|
|
136
|
+
if (suffixed.length > 1) return { kind: 'ambiguous', names: suffixed }
|
|
137
|
+
return { kind: 'none' }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function extractOrdinal(name: string): string {
|
|
141
|
+
return /^\d{2,}-/.exec(name)?.[0].slice(0, -1) ?? ''
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function openFolder(
|
|
145
|
+
root: string,
|
|
146
|
+
slug: string,
|
|
147
|
+
): Promise<string | IntakeRefused> {
|
|
148
|
+
const dir = intakeDir(root)
|
|
149
|
+
|
|
150
|
+
if (!existsSync(dir)) {
|
|
151
|
+
return refuse('no-intake', `No intake at ${relative(root, dir)}.`)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const names = await listSlugs(dir)
|
|
155
|
+
const match = matchSlug(names, slug)
|
|
156
|
+
|
|
157
|
+
if (match.kind === 'none') {
|
|
158
|
+
return refuse('no-folder', `No intake folder named ${slug}.`, names)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (match.kind === 'ambiguous') {
|
|
162
|
+
return refuse(
|
|
163
|
+
'ambiguous-slug',
|
|
164
|
+
`More than one intake folder matches ${slug}.`,
|
|
165
|
+
match.names,
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return join(dir, match.name)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Counts per folder, which is what a session picks a folder to work from. */
|
|
173
|
+
export async function listFolders(root: string): Promise<ListOutcome> {
|
|
174
|
+
const dir = intakeDir(root)
|
|
175
|
+
|
|
176
|
+
if (!existsSync(dir)) {
|
|
177
|
+
return refuse('no-intake', `No intake at ${relative(root, dir)}.`)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const slugs = await listSlugs(dir)
|
|
181
|
+
|
|
182
|
+
const folders = await Promise.all(
|
|
183
|
+
slugs.map(async (slug) => {
|
|
184
|
+
const clusters = await readClusters(join(dir, slug))
|
|
185
|
+
const items = clusters.flatMap((cluster) => cluster.items)
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
slug,
|
|
189
|
+
items: items.length,
|
|
190
|
+
open: items.filter((item) => item.open !== undefined).length,
|
|
191
|
+
unread: items.filter(isUnread).length,
|
|
192
|
+
malformed: items.filter(isMalformed).length,
|
|
193
|
+
}
|
|
194
|
+
}),
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
return { ok: true, folders }
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function readClusters(folder: string): Promise<ClusterItems[]> {
|
|
201
|
+
const names = await listClusters(folder)
|
|
202
|
+
|
|
203
|
+
return Promise.all(
|
|
204
|
+
names.map(async (cluster) => ({
|
|
205
|
+
cluster,
|
|
206
|
+
items: readItems(await readFile(join(folder, cluster), 'utf8')),
|
|
207
|
+
})),
|
|
208
|
+
)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Every item in a folder, grouped by the cluster file that holds it. */
|
|
212
|
+
export async function readFolder(
|
|
213
|
+
root: string,
|
|
214
|
+
slug: string,
|
|
215
|
+
): Promise<ReadOutcome> {
|
|
216
|
+
const opened = await openFolder(root, slug)
|
|
217
|
+
if (typeof opened !== 'string') return opened
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
ok: true,
|
|
221
|
+
slug: basename(opened),
|
|
222
|
+
clusters: await readClusters(opened),
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Lands a batch of selections in one cluster file.
|
|
228
|
+
*
|
|
229
|
+
* The batch is scoped to a cluster and applied in one read-modify-write because
|
|
230
|
+
* the alternative is a call per selection, and four of those against the same
|
|
231
|
+
* file race on the read and drop every answer but the last.
|
|
232
|
+
*/
|
|
233
|
+
export async function answerItems(
|
|
234
|
+
root: string,
|
|
235
|
+
slug: string,
|
|
236
|
+
cluster: string,
|
|
237
|
+
selections: readonly Selection[],
|
|
238
|
+
): Promise<AnswerOutcome> {
|
|
239
|
+
const opened = await openFolder(root, slug)
|
|
240
|
+
if (typeof opened !== 'string') return opened
|
|
241
|
+
|
|
242
|
+
const name = cluster.endsWith('.md') ? cluster : `${cluster}.md`
|
|
243
|
+
|
|
244
|
+
if (name === INDEX_FILE) {
|
|
245
|
+
return refuse(
|
|
246
|
+
'no-cluster',
|
|
247
|
+
`${INDEX_FILE} is the index and carries no answer slot.`,
|
|
248
|
+
)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const path = join(opened, name)
|
|
252
|
+
|
|
253
|
+
if (!existsSync(path)) {
|
|
254
|
+
return refuse(
|
|
255
|
+
'no-cluster',
|
|
256
|
+
`No cluster named ${name} in ${slug}.`,
|
|
257
|
+
await listClusters(opened),
|
|
258
|
+
)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const broken = selections.filter((selection) =>
|
|
262
|
+
/[\r\n]/.test(selection.answer),
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
if (broken.length > 0) {
|
|
266
|
+
return refuse(
|
|
267
|
+
'bad-input',
|
|
268
|
+
`An answer is one line, so item ${broken.map((entry) => entry.label).join(', ')} cannot carry a line break.`,
|
|
269
|
+
broken.map((entry) => entry.label),
|
|
270
|
+
)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
let text = await readFile(path, 'utf8')
|
|
274
|
+
const items = readItems(text)
|
|
275
|
+
|
|
276
|
+
const find = (label: string) =>
|
|
277
|
+
items.find((item) => item.label === label.toLowerCase())
|
|
278
|
+
|
|
279
|
+
const missing = selections.filter(
|
|
280
|
+
(selection) => find(selection.label)?.answerLine === undefined,
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
if (missing.length > 0) {
|
|
284
|
+
return refuse(
|
|
285
|
+
'no-item',
|
|
286
|
+
`${name} carries no answer slot for item ${missing.map((entry) => entry.label).join(', ')}.`,
|
|
287
|
+
items.map((item) => `${item.label}. ${item.title}`),
|
|
288
|
+
)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const filled = selections.filter(
|
|
292
|
+
(selection) => find(selection.label)?.answer !== undefined,
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
if (filled.length > 0) {
|
|
296
|
+
return refuse(
|
|
297
|
+
'answered',
|
|
298
|
+
`${name} item ${filled.map((entry) => entry.label).join(', ')} already carries an answer.`,
|
|
299
|
+
filled.map(
|
|
300
|
+
(entry) => `${entry.label}. ${find(entry.label)?.answer ?? ''}`,
|
|
301
|
+
),
|
|
302
|
+
)
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
for (const selection of selections) {
|
|
306
|
+
const item = find(selection.label)
|
|
307
|
+
if (item?.answerLine === undefined) continue
|
|
308
|
+
text = writeAnswerLine(text, item.answerLine, selection.answer)
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
await writeFile(path, text)
|
|
312
|
+
|
|
313
|
+
return {
|
|
314
|
+
ok: true,
|
|
315
|
+
slug: basename(opened),
|
|
316
|
+
cluster: name,
|
|
317
|
+
path,
|
|
318
|
+
answered: selections,
|
|
319
|
+
}
|
|
320
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { bodyLines } from '@/markdown/scan'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The index points at items and answers nothing itself, so it carries no
|
|
5
|
+
* answer slot for this verb to reach. It also displays the item format inside a
|
|
6
|
+
* fence, which the fence walk already masks, so skipping it by name is about
|
|
7
|
+
* what the file is rather than about the block it holds.
|
|
8
|
+
*/
|
|
9
|
+
export const INDEX_FILE = '00-overview.md'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* An item heading, labeled per cluster file rather than per folder.
|
|
13
|
+
*
|
|
14
|
+
* The label carries an optional letter suffix because a pass that splits one
|
|
15
|
+
* finding after the fact numbers the halves `3a` and `3b` rather than
|
|
16
|
+
* renumbering every item below them. A pattern accepting digits alone parses
|
|
17
|
+
* such a file without complaint and drops those items, leaving them
|
|
18
|
+
* unanswerable through this verb with nothing reporting the gap.
|
|
19
|
+
*/
|
|
20
|
+
const HEADING = /^###\s+(\d+[a-z]*)\.\s*(.*)$/i
|
|
21
|
+
|
|
22
|
+
/** A bolded field bullet, which is every line an item carries. */
|
|
23
|
+
const FIELD = /^-\s+\*\*([^*]+):\*\*\s*(.*)$/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Any heading, which ends the item above it.
|
|
27
|
+
*
|
|
28
|
+
* A numbered `###` is tested first and opens the next item, so what reaches
|
|
29
|
+
* here is a heading that is not an item and therefore closes the one open. The
|
|
30
|
+
* narrower test for `##` alone let a `### Notes` block stay inside the item
|
|
31
|
+
* above it, and the stray slot such a block carries then displaced the real
|
|
32
|
+
* one, leaving the item reading as answered while its own slot sat empty and
|
|
33
|
+
* sending a write into the wrong section.
|
|
34
|
+
*/
|
|
35
|
+
const SECTION = /^#{1,6}\s/
|
|
36
|
+
|
|
37
|
+
export interface IntakeItem {
|
|
38
|
+
/** The label as the heading spells it, such as `3` or `3a`. */
|
|
39
|
+
readonly label: string
|
|
40
|
+
readonly title: string
|
|
41
|
+
/** Line the heading sits on, 1-based against the whole file. */
|
|
42
|
+
readonly line: number
|
|
43
|
+
/** Line the answer slot sits on, absent when the item carries no slot. */
|
|
44
|
+
readonly answerLine: number | undefined
|
|
45
|
+
/** Text in the slot, absent when the slot is empty and the item is unread. */
|
|
46
|
+
readonly answer: string | undefined
|
|
47
|
+
readonly open: string | undefined
|
|
48
|
+
readonly suggested: string | undefined
|
|
49
|
+
readonly worth: string | undefined
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface Draft {
|
|
53
|
+
label: string
|
|
54
|
+
title: string
|
|
55
|
+
line: number
|
|
56
|
+
answerLine: number | undefined
|
|
57
|
+
answer: string | undefined
|
|
58
|
+
open: string | undefined
|
|
59
|
+
suggested: string | undefined
|
|
60
|
+
worth: string | undefined
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function seal(draft: Draft): IntakeItem {
|
|
64
|
+
return { ...draft }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Reads every item a cluster file holds, in file order.
|
|
69
|
+
*
|
|
70
|
+
* The walk runs over `bodyLines` rather than a raw split so the item format
|
|
71
|
+
* block a folder copies into its own files parses as the sample it is. A
|
|
72
|
+
* heading counted out of a fence shifts nothing on its own, but it offers an
|
|
73
|
+
* answer slot no reader owns and the write-back would land inside the sample.
|
|
74
|
+
*/
|
|
75
|
+
export function readItems(text: string): IntakeItem[] {
|
|
76
|
+
const items: IntakeItem[] = []
|
|
77
|
+
let draft: Draft | undefined
|
|
78
|
+
|
|
79
|
+
for (const line of bodyLines(text)) {
|
|
80
|
+
if (line.fenced) continue
|
|
81
|
+
|
|
82
|
+
const heading = HEADING.exec(line.text)
|
|
83
|
+
|
|
84
|
+
if (heading) {
|
|
85
|
+
if (draft) items.push(seal(draft))
|
|
86
|
+
draft = {
|
|
87
|
+
label: heading[1].toLowerCase(),
|
|
88
|
+
title: heading[2].trim(),
|
|
89
|
+
line: line.number,
|
|
90
|
+
answerLine: undefined,
|
|
91
|
+
answer: undefined,
|
|
92
|
+
open: undefined,
|
|
93
|
+
suggested: undefined,
|
|
94
|
+
worth: undefined,
|
|
95
|
+
}
|
|
96
|
+
continue
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!draft) continue
|
|
100
|
+
|
|
101
|
+
if (SECTION.test(line.text)) {
|
|
102
|
+
items.push(seal(draft))
|
|
103
|
+
draft = undefined
|
|
104
|
+
continue
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const field = FIELD.exec(line.text)
|
|
108
|
+
if (!field) continue
|
|
109
|
+
|
|
110
|
+
const value = field[2].trim()
|
|
111
|
+
|
|
112
|
+
switch (field[1].trim().toLowerCase()) {
|
|
113
|
+
case 'you':
|
|
114
|
+
draft.answerLine = line.number
|
|
115
|
+
draft.answer = value === '' ? undefined : value
|
|
116
|
+
break
|
|
117
|
+
case 'open':
|
|
118
|
+
draft.open = value
|
|
119
|
+
break
|
|
120
|
+
case 'suggested':
|
|
121
|
+
draft.suggested = value
|
|
122
|
+
break
|
|
123
|
+
case 'worth it':
|
|
124
|
+
draft.worth = value
|
|
125
|
+
break
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (draft) items.push(seal(draft))
|
|
130
|
+
|
|
131
|
+
return items
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Puts a selection in one item's slot, leaving every other line as it was.
|
|
136
|
+
*
|
|
137
|
+
* The rewrite replaces the whole line rather than patching inside it, which is
|
|
138
|
+
* the reason this is a verb at all. A stream editor expands an unescaped
|
|
139
|
+
* ampersand in the replacement to the whole match and exits zero on a
|
|
140
|
+
* non-match, so an answer carrying one would rewrite the line it anchored to
|
|
141
|
+
* and a missed slot would report success with the answer lost.
|
|
142
|
+
*
|
|
143
|
+
* The answer occupies one line, and the caller owes that guarantee. A line
|
|
144
|
+
* break splices a bare continuation into the item matching none of the patterns
|
|
145
|
+
* the reader tests, so the slot reads back as the text before the break while
|
|
146
|
+
* the item counts as answered, which puts correcting it behind the refusal on
|
|
147
|
+
* an item that already carries one.
|
|
148
|
+
*/
|
|
149
|
+
export function writeAnswerLine(
|
|
150
|
+
text: string,
|
|
151
|
+
answerLine: number,
|
|
152
|
+
answer: string,
|
|
153
|
+
): string {
|
|
154
|
+
const lines = text.split('\n')
|
|
155
|
+
lines[answerLine - 1] = `- **You:** ${answer}`
|
|
156
|
+
return lines.join('\n')
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** An item nobody has reached, which is an empty slot rather than a missing one. */
|
|
160
|
+
export function isUnread(item: IntakeItem): boolean {
|
|
161
|
+
return item.answerLine !== undefined && item.answer === undefined
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* An item carrying no slot at all, which the format says ships on every one.
|
|
166
|
+
*
|
|
167
|
+
* Such an item is neither unread nor answered, and counting it as either hides
|
|
168
|
+
* it: as answered it drops out of the work a reader is told remains, and as
|
|
169
|
+
* unread it joins a list whose every entry the answer verb then refuses. It is
|
|
170
|
+
* reported on its own so the file gets fixed.
|
|
171
|
+
*/
|
|
172
|
+
export function isMalformed(item: IntakeItem): boolean {
|
|
173
|
+
return item.answerLine === undefined
|
|
174
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Where a project declares what an inventory walks, spelled once.
|
|
6
|
+
*
|
|
7
|
+
* It sits at the project root rather than under `.claude/`, because the routes
|
|
8
|
+
* and the element queries describe the application rather than the agent
|
|
9
|
+
* surface, and a target reads them the way it reads any other build input.
|
|
10
|
+
*/
|
|
11
|
+
export const CONFIG_REL = 'inventory.toml'
|
|
12
|
+
|
|
13
|
+
/** One measurable treatment and the elements that carry it, as the project spells them. */
|
|
14
|
+
export interface InventorySubject {
|
|
15
|
+
readonly name: string
|
|
16
|
+
readonly query: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface InventoryConfig {
|
|
20
|
+
readonly baseUrl: string
|
|
21
|
+
readonly routes: readonly string[]
|
|
22
|
+
readonly subjects: readonly InventorySubject[]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Why a config could not be used, which is never the same as a walk that read
|
|
27
|
+
* nothing.
|
|
28
|
+
*
|
|
29
|
+
* Every value here is a refusal rather than an empty listing, because a listing
|
|
30
|
+
* with no rows reads as a site giving one consistent answer, which is the
|
|
31
|
+
* report an unconfigured project would get for free and the exact wrong reading.
|
|
32
|
+
*/
|
|
33
|
+
export type ConfigRefusal =
|
|
34
|
+
| 'no-config'
|
|
35
|
+
| 'unreadable-config'
|
|
36
|
+
| 'no-base-url'
|
|
37
|
+
| 'no-routes'
|
|
38
|
+
| 'no-subjects'
|
|
39
|
+
|
|
40
|
+
export type ConfigRead =
|
|
41
|
+
| { readonly kind: 'config'; readonly config: InventoryConfig }
|
|
42
|
+
| { readonly kind: 'refused'; readonly reason: ConfigRefusal }
|
|
43
|
+
|
|
44
|
+
function readSubjects(table: unknown): InventorySubject[] {
|
|
45
|
+
if (typeof table !== 'object' || table === null || Array.isArray(table)) {
|
|
46
|
+
return []
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const subjects: InventorySubject[] = []
|
|
50
|
+
for (const [name, value] of Object.entries(table)) {
|
|
51
|
+
if (typeof value !== 'object' || value === null) continue
|
|
52
|
+
const query = (value as Record<string, unknown>).query
|
|
53
|
+
if (typeof query !== 'string' || query === '') continue
|
|
54
|
+
subjects.push({ name, query })
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return subjects
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Parses config text, so a caller holding the bytes skips the filesystem.
|
|
62
|
+
*
|
|
63
|
+
* A malformed route or subject is dropped rather than refused, matching the row
|
|
64
|
+
* handling in `@/labels/map`: one bad entry should not blind the walk to the
|
|
65
|
+
* other twenty. What that costs is a typo reading as an entry nobody wrote,
|
|
66
|
+
* which the listing surfaces from the other side as a route with no elements.
|
|
67
|
+
*/
|
|
68
|
+
export function parseInventoryConfig(source: string): ConfigRead {
|
|
69
|
+
let parsed: Record<string, unknown>
|
|
70
|
+
try {
|
|
71
|
+
parsed = Bun.TOML.parse(source) as Record<string, unknown>
|
|
72
|
+
} catch {
|
|
73
|
+
return { kind: 'refused', reason: 'unreadable-config' }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const baseUrl = parsed['base-url']
|
|
77
|
+
if (typeof baseUrl !== 'string' || baseUrl === '') {
|
|
78
|
+
return { kind: 'refused', reason: 'no-base-url' }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const routes = Array.isArray(parsed.routes)
|
|
82
|
+
? parsed.routes.filter(
|
|
83
|
+
(route): route is string => typeof route === 'string' && route !== '',
|
|
84
|
+
)
|
|
85
|
+
: []
|
|
86
|
+
if (routes.length === 0) return { kind: 'refused', reason: 'no-routes' }
|
|
87
|
+
|
|
88
|
+
const subjects = readSubjects(parsed.subjects)
|
|
89
|
+
if (subjects.length === 0) return { kind: 'refused', reason: 'no-subjects' }
|
|
90
|
+
|
|
91
|
+
return { kind: 'config', config: { baseUrl, routes, subjects } }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Reads the config a project declares at `root`, or says why it could not. */
|
|
95
|
+
export function readInventoryConfig(root: string): ConfigRead {
|
|
96
|
+
let source: string
|
|
97
|
+
try {
|
|
98
|
+
source = readFileSync(join(root, CONFIG_REL), 'utf8')
|
|
99
|
+
} catch {
|
|
100
|
+
return { kind: 'refused', reason: 'no-config' }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return parseInventoryConfig(source)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Joins a base and a route into the address a walk opens.
|
|
108
|
+
*
|
|
109
|
+
* Concatenation rather than `new URL(route, base)`, because the resolver reads
|
|
110
|
+
* a leading slash as absolute and drops any path the base already carries, so a
|
|
111
|
+
* project served under a subdirectory would have every route walk the origin.
|
|
112
|
+
*/
|
|
113
|
+
export function routeUrl(baseUrl: string, route: string): string {
|
|
114
|
+
const base = baseUrl.replace(/\/+$/, '')
|
|
115
|
+
const path = route.startsWith('/') ? route : `/${route}`
|
|
116
|
+
return `${base}${path}`
|
|
117
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How many elements a row names before it stops naming them. A row exists to
|
|
3
|
+
* say how many answers a site gives, and the elements under it are there to
|
|
4
|
+
* make one findable rather than to enumerate the set.
|
|
5
|
+
*/
|
|
6
|
+
export const SAMPLE_LIMIT = 3
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Names a bounded head of a list and says how many it left out, so a treatment
|
|
10
|
+
* carried by fifty routes reads as fifty rather than as the three named.
|
|
11
|
+
*
|
|
12
|
+
* `total` arrives separately because the two lists reach here differently. A
|
|
13
|
+
* row's samples are already capped when they are collected, so their own length
|
|
14
|
+
* cannot say how many elements the row covers, while its routes arrive whole.
|
|
15
|
+
*/
|
|
16
|
+
export function abbreviate(entries: readonly string[], total: number): string {
|
|
17
|
+
const head = entries.slice(0, SAMPLE_LIMIT)
|
|
18
|
+
const elided = total - head.length
|
|
19
|
+
return elided > 0 ? `${head.join(', ')} and ${elided} more` : head.join(', ')
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** One element as the walk read it, on the route it was read from. */
|
|
23
|
+
export interface Reading {
|
|
24
|
+
readonly route: string
|
|
25
|
+
readonly selector: string
|
|
26
|
+
readonly treatment: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** One answer the site gives, and what carries it. */
|
|
30
|
+
export interface TreatmentGroup {
|
|
31
|
+
readonly treatment: string
|
|
32
|
+
readonly count: number
|
|
33
|
+
readonly routes: readonly string[]
|
|
34
|
+
readonly samples: readonly string[]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Turns a per-element walk into a per-answer listing, which is the whole point
|
|
39
|
+
* of the command.
|
|
40
|
+
*
|
|
41
|
+
* Grouping by the component instead was the obvious shape and it reports what a
|
|
42
|
+
* reader already knows, that a site has buttons and links. Grouping by the
|
|
43
|
+
* computed answer reports the thing nobody can see while building, which is
|
|
44
|
+
* that five components resolved to five different rings.
|
|
45
|
+
*
|
|
46
|
+
* Order is heaviest first, then by treatment, so the dominant answer leads and
|
|
47
|
+
* two runs over one site report one order rather than whatever the walk hit.
|
|
48
|
+
*/
|
|
49
|
+
export function groupByTreatment(
|
|
50
|
+
readings: readonly Reading[],
|
|
51
|
+
): readonly TreatmentGroup[] {
|
|
52
|
+
const byTreatment = new Map<
|
|
53
|
+
string,
|
|
54
|
+
{ count: number; routes: string[]; samples: string[] }
|
|
55
|
+
>()
|
|
56
|
+
|
|
57
|
+
for (const { route, selector, treatment } of readings) {
|
|
58
|
+
const row = byTreatment.get(treatment) ?? {
|
|
59
|
+
count: 0,
|
|
60
|
+
routes: [],
|
|
61
|
+
samples: [],
|
|
62
|
+
}
|
|
63
|
+
row.count += 1
|
|
64
|
+
if (!row.routes.includes(route)) row.routes.push(route)
|
|
65
|
+
if (row.samples.length < SAMPLE_LIMIT) row.samples.push(selector)
|
|
66
|
+
byTreatment.set(treatment, row)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return [...byTreatment.entries()]
|
|
70
|
+
.map(([treatment, row]) => ({ treatment, ...row }))
|
|
71
|
+
.sort(
|
|
72
|
+
(left, right) =>
|
|
73
|
+
right.count - left.count ||
|
|
74
|
+
left.treatment.localeCompare(right.treatment),
|
|
75
|
+
)
|
|
76
|
+
}
|