@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,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The readers an inventory can run, one per treatment.
|
|
3
|
+
*
|
|
4
|
+
* A reader runs inside the page rather than here, so each one is serialized to
|
|
5
|
+
* source and evaluated by the browser. That is why every helper a reader needs
|
|
6
|
+
* is declared inside its own body: a reference to anything at module scope
|
|
7
|
+
* survives typechecking and throws once the page tries to call it.
|
|
8
|
+
*
|
|
9
|
+
* `focus` is the first and, for now, the only subject. The four sibling
|
|
10
|
+
* instruments this shape was lifted from differ only in their element query and
|
|
11
|
+
* the property they read, which is what makes a subject a row here rather than
|
|
12
|
+
* a command of its own.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** One element as a reader saw it, before any grouping. */
|
|
16
|
+
export interface SubjectReading {
|
|
17
|
+
readonly selector: string
|
|
18
|
+
readonly treatment: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface Subject {
|
|
22
|
+
readonly name: string
|
|
23
|
+
/** What the listing means, printed above the rows so a count reads correctly. */
|
|
24
|
+
readonly summary: string
|
|
25
|
+
readonly read: (query: string) => SubjectReading[]
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Reports what each element changes about itself when it takes focus, which is
|
|
30
|
+
* the reading `governance/rules/ui/410-a11y.md` states three rules against and
|
|
31
|
+
* nothing measures.
|
|
32
|
+
*
|
|
33
|
+
* The difference between rest and focus is the treatment, rather than the
|
|
34
|
+
* focused style on its own. A card carrying a resting shadow computes a shadow
|
|
35
|
+
* either way, so reading only the focused state would report a ring on an
|
|
36
|
+
* element whose appearance never moves.
|
|
37
|
+
*
|
|
38
|
+
* An element the browser refuses to focus is named as such rather than folded
|
|
39
|
+
* into the no-treatment row, since a disabled control and a control with no
|
|
40
|
+
* ring are different findings with different remedies.
|
|
41
|
+
*/
|
|
42
|
+
function readFocusTreatments(query: string): SubjectReading[] {
|
|
43
|
+
const PROPERTIES = [
|
|
44
|
+
'outlineStyle',
|
|
45
|
+
'outlineWidth',
|
|
46
|
+
'outlineColor',
|
|
47
|
+
'outlineOffset',
|
|
48
|
+
'boxShadow',
|
|
49
|
+
'borderColor',
|
|
50
|
+
'backgroundColor',
|
|
51
|
+
'color',
|
|
52
|
+
] as const
|
|
53
|
+
|
|
54
|
+
const describe = (element: Element): string => {
|
|
55
|
+
const tag = element.tagName.toLowerCase()
|
|
56
|
+
if (element.id) return `${tag}#${element.id}`
|
|
57
|
+
const className = element.getAttribute('class')?.trim().split(/\s+/)[0]
|
|
58
|
+
return className ? `${tag}.${className}` : tag
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const snapshot = (element: Element): Record<string, string> => {
|
|
62
|
+
const computed = getComputedStyle(element)
|
|
63
|
+
const values: Record<string, string> = {}
|
|
64
|
+
for (const property of PROPERTIES) values[property] = computed[property]
|
|
65
|
+
return values
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const rows: SubjectReading[] = []
|
|
69
|
+
|
|
70
|
+
// The walk presses Tab before this runs, which leaves one element focused.
|
|
71
|
+
// Reading that element's rest state while it holds focus reports no
|
|
72
|
+
// difference and hides whatever ring it actually draws, so the page starts
|
|
73
|
+
// from nothing focused and every element is blurred again after its turn.
|
|
74
|
+
const entryFocus = document.activeElement
|
|
75
|
+
if (entryFocus instanceof HTMLElement) entryFocus.blur()
|
|
76
|
+
|
|
77
|
+
for (const element of Array.from(document.querySelectorAll(query))) {
|
|
78
|
+
if (!(element instanceof HTMLElement)) continue
|
|
79
|
+
|
|
80
|
+
const rest = snapshot(element)
|
|
81
|
+
element.focus()
|
|
82
|
+
if (document.activeElement !== element) {
|
|
83
|
+
rows.push({ selector: describe(element), treatment: 'not focusable' })
|
|
84
|
+
continue
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const focused = snapshot(element)
|
|
88
|
+
const changed = PROPERTIES.filter(
|
|
89
|
+
(property) => rest[property] !== focused[property],
|
|
90
|
+
).map((property) => `${property} ${focused[property]}`)
|
|
91
|
+
|
|
92
|
+
rows.push({
|
|
93
|
+
selector: describe(element),
|
|
94
|
+
treatment:
|
|
95
|
+
changed.length === 0 ? 'no visible change' : changed.join(', '),
|
|
96
|
+
})
|
|
97
|
+
element.blur()
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return rows
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const FOCUS: Subject = {
|
|
104
|
+
name: 'focus',
|
|
105
|
+
summary: 'what each element changes about itself when it takes focus',
|
|
106
|
+
read: readFocusTreatments,
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export const SUBJECTS: readonly Subject[] = [FOCUS]
|
|
110
|
+
|
|
111
|
+
/** Resolves a subject by name, so an unknown one is the caller's to report. */
|
|
112
|
+
export function findSubject(name: string): Subject | undefined {
|
|
113
|
+
return SUBJECTS.find((subject) => subject.name === name)
|
|
114
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { chromium } from 'playwright-core'
|
|
2
|
+
import type { Browser } from 'playwright-core'
|
|
3
|
+
import { isBrowserMissing } from '@/browser/engine'
|
|
4
|
+
import { routeUrl } from '@/inventory/config'
|
|
5
|
+
import type { Reading } from '@/inventory/group'
|
|
6
|
+
import type { Subject } from '@/inventory/subjects'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Walks a running application and reads one property off every element a
|
|
10
|
+
* subject names. Every browser reference the inventory feature adds lives here,
|
|
11
|
+
* and `src/commands/inventory.ts` reaches it through a dynamic import so no
|
|
12
|
+
* other command resolves the engine at startup.
|
|
13
|
+
*
|
|
14
|
+
* Like `@/demo/drive` and unlike `@/capture/render`, this module ships, because
|
|
15
|
+
* a command whose whole purpose is running inside someone else's project cannot
|
|
16
|
+
* stay toolkit-only.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The reader and the query arrive apart because they come from different
|
|
21
|
+
* owners. The toolkit ships the reader and the project declares which elements
|
|
22
|
+
* it runs over, which is what keeps the walk answering to the target rather
|
|
23
|
+
* than to a fixed selector nobody there chose.
|
|
24
|
+
*/
|
|
25
|
+
export interface WalkOptions {
|
|
26
|
+
readonly baseUrl: string
|
|
27
|
+
readonly routes: readonly string[]
|
|
28
|
+
readonly subject: Subject
|
|
29
|
+
readonly query: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** A route that answered, and the elements read off it. */
|
|
33
|
+
export interface RouteReading {
|
|
34
|
+
readonly route: string
|
|
35
|
+
readonly elements: number
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type WalkRefusal =
|
|
39
|
+
| 'browser-missing'
|
|
40
|
+
| 'server-unreachable'
|
|
41
|
+
| 'walk-failed'
|
|
42
|
+
|
|
43
|
+
export type WalkResult =
|
|
44
|
+
| {
|
|
45
|
+
readonly status: 'read'
|
|
46
|
+
readonly readings: readonly Reading[]
|
|
47
|
+
readonly routes: readonly RouteReading[]
|
|
48
|
+
readonly durationMs: number
|
|
49
|
+
}
|
|
50
|
+
| {
|
|
51
|
+
readonly status: 'failed'
|
|
52
|
+
readonly reason: WalkRefusal
|
|
53
|
+
readonly message: string
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function failed(reason: WalkRefusal, error: unknown): WalkResult {
|
|
57
|
+
return {
|
|
58
|
+
status: 'failed',
|
|
59
|
+
reason,
|
|
60
|
+
message: error instanceof Error ? error.message : String(error),
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Separates a server nobody started from a page that failed for its own
|
|
66
|
+
* reasons. The first is the precondition this command cannot create, and
|
|
67
|
+
* reporting it as an empty listing would say the site gives no answers when
|
|
68
|
+
* nothing was ever asked.
|
|
69
|
+
*/
|
|
70
|
+
function isServerUnreachable(error: unknown): boolean {
|
|
71
|
+
const text = error instanceof Error ? error.message : String(error)
|
|
72
|
+
return /ERR_CONNECTION_REFUSED|ERR_NAME_NOT_RESOLVED|ERR_CONNECTION_RESET|ERR_EMPTY_RESPONSE/i.test(
|
|
73
|
+
text,
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function walk(options: WalkOptions): Promise<WalkResult> {
|
|
78
|
+
const started = Date.now()
|
|
79
|
+
|
|
80
|
+
let browser: Browser
|
|
81
|
+
try {
|
|
82
|
+
browser = await chromium.launch()
|
|
83
|
+
} catch (error) {
|
|
84
|
+
return failed(
|
|
85
|
+
isBrowserMissing(error) ? 'browser-missing' : 'walk-failed',
|
|
86
|
+
error,
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const readings: Reading[] = []
|
|
91
|
+
const routes: RouteReading[] = []
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
const page = await browser.newPage()
|
|
95
|
+
|
|
96
|
+
for (const route of options.routes) {
|
|
97
|
+
await page.goto(routeUrl(options.baseUrl, route), {
|
|
98
|
+
waitUntil: 'domcontentloaded',
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
// Puts the page in keyboard modality before anything is focused, because
|
|
102
|
+
// a `:focus-visible` ring is the treatment a pointer never reveals and
|
|
103
|
+
// programmatic focus alone does not match it.
|
|
104
|
+
await page.keyboard.press('Tab')
|
|
105
|
+
|
|
106
|
+
const rows = await page.evaluate(options.subject.read, options.query)
|
|
107
|
+
for (const row of rows) readings.push({ route, ...row })
|
|
108
|
+
routes.push({ route, elements: rows.length })
|
|
109
|
+
}
|
|
110
|
+
} catch (error) {
|
|
111
|
+
return failed(
|
|
112
|
+
isServerUnreachable(error) ? 'server-unreachable' : 'walk-failed',
|
|
113
|
+
error,
|
|
114
|
+
)
|
|
115
|
+
} finally {
|
|
116
|
+
// The rejection is dropped rather than propagated, because a close that
|
|
117
|
+
// fails beside a walk that already failed would replace the refusal the
|
|
118
|
+
// caller was about to receive with a reason about teardown. Nothing the
|
|
119
|
+
// caller does depends on the browser having closed cleanly.
|
|
120
|
+
await browser.close().catch(() => undefined)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
status: 'read',
|
|
125
|
+
readings,
|
|
126
|
+
routes,
|
|
127
|
+
durationMs: Date.now() - started,
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { listChangedFiles, resolveBaseRef } from '@/git-files'
|
|
2
|
+
import { type Coverage, resolveCoverage } from '@/labels/coverage'
|
|
3
|
+
import { type MapRefusal, readLabelMap } from '@/labels/map'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Why an audit produced no reading.
|
|
7
|
+
*
|
|
8
|
+
* `no-map` travels through from the map reader and stays an answer rather than
|
|
9
|
+
* a fault. The git reasons are the opposite: a range this check asked for and
|
|
10
|
+
* could not get, which is a broken invocation rather than a project that
|
|
11
|
+
* declared nothing.
|
|
12
|
+
*
|
|
13
|
+
* A named ref that will not resolve is its own reason. Folding it into
|
|
14
|
+
* `no-base` sends the caller who already passed `--base` a message telling
|
|
15
|
+
* them to pass `--base`.
|
|
16
|
+
*/
|
|
17
|
+
export type LabelAuditRefusal =
|
|
18
|
+
| MapRefusal
|
|
19
|
+
| 'no-base'
|
|
20
|
+
| 'bad-base'
|
|
21
|
+
| 'unreadable-changes'
|
|
22
|
+
|
|
23
|
+
export type LabelAudit =
|
|
24
|
+
| {
|
|
25
|
+
readonly kind: 'measured'
|
|
26
|
+
/** Absent when the caller supplied the changed set rather than a range. */
|
|
27
|
+
readonly base?: string
|
|
28
|
+
readonly changed: readonly string[]
|
|
29
|
+
readonly coverage: Coverage
|
|
30
|
+
}
|
|
31
|
+
| { readonly kind: 'refused'; readonly reason: LabelAuditRefusal }
|
|
32
|
+
|
|
33
|
+
export interface LabelAuditOptions {
|
|
34
|
+
/** Far side of the range, defaulting to the merge base against the trunk. */
|
|
35
|
+
readonly base?: string
|
|
36
|
+
/** A changed set the caller already holds, which skips git entirely. */
|
|
37
|
+
readonly paths?: readonly string[]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Resolves a changed set against the map a project declares, and reports both
|
|
42
|
+
* what it earns and what it leaves uncovered.
|
|
43
|
+
*
|
|
44
|
+
* One verb for two readers by design. `git-pr` reads the labels at open time
|
|
45
|
+
* and the audit aggregate reads the uncovered count, and a verb shaped for the
|
|
46
|
+
* first alone returns nothing the second can retain.
|
|
47
|
+
*/
|
|
48
|
+
export async function auditLabels(
|
|
49
|
+
root: string,
|
|
50
|
+
options: LabelAuditOptions = {},
|
|
51
|
+
): Promise<LabelAudit> {
|
|
52
|
+
const map = readLabelMap(root)
|
|
53
|
+
if (map.kind === 'refused') return { kind: 'refused', reason: map.reason }
|
|
54
|
+
|
|
55
|
+
if (options.paths !== undefined) {
|
|
56
|
+
return {
|
|
57
|
+
kind: 'measured',
|
|
58
|
+
changed: [...options.paths],
|
|
59
|
+
coverage: resolveCoverage(map, options.paths),
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const base = await resolveBaseRef(root, options.base)
|
|
64
|
+
if (base === undefined) {
|
|
65
|
+
return {
|
|
66
|
+
kind: 'refused',
|
|
67
|
+
reason: options.base === undefined ? 'no-base' : 'bad-base',
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const changed = await listChangedFiles(root, base)
|
|
72
|
+
if (changed === undefined) {
|
|
73
|
+
return { kind: 'refused', reason: 'unreadable-changes' }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
kind: 'measured',
|
|
78
|
+
base,
|
|
79
|
+
changed,
|
|
80
|
+
coverage: resolveCoverage(map, changed),
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { LabelMap } from '@/labels/map'
|
|
2
|
+
|
|
3
|
+
/** A path a row leaves unlabelled on purpose, carrying the reason it gives. */
|
|
4
|
+
export interface DeclinedPath {
|
|
5
|
+
readonly path: string
|
|
6
|
+
readonly reason: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface Coverage {
|
|
10
|
+
/** Distinct labels the whole set earns, ordered as the map declares them. */
|
|
11
|
+
readonly labels: readonly string[]
|
|
12
|
+
/**
|
|
13
|
+
* Paths a `[declined]` row covers, which are a decision rather than a gap.
|
|
14
|
+
*
|
|
15
|
+
* Kept apart from `uncovered` because the response to the two differs. A
|
|
16
|
+
* surface nobody has gotten to wants a row, and one somebody decided against
|
|
17
|
+
* wants nothing, so a report folding them together is useful about neither.
|
|
18
|
+
*/
|
|
19
|
+
readonly declined: readonly DeclinedPath[]
|
|
20
|
+
/** Paths no row of either table reaches, which is the finding. */
|
|
21
|
+
readonly uncovered: readonly string[]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Prefix-anchored, matching what the map's own comment describes and what the
|
|
26
|
+
* census behind its 41 prefixes was measured against.
|
|
27
|
+
*
|
|
28
|
+
* A glob would reach every existing prefix and invalidate that measurement, so
|
|
29
|
+
* the rule stays a `startsWith` even where a glob would read more naturally.
|
|
30
|
+
*/
|
|
31
|
+
function matches(path: string, prefixes: readonly string[]): boolean {
|
|
32
|
+
return prefixes.some((prefix) => path.startsWith(prefix))
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Resolves what a changed set earns from a map, and what it leaves behind.
|
|
37
|
+
*
|
|
38
|
+
* One pass answers both readers. `git-pr` wants the labels to apply, and the
|
|
39
|
+
* audit wants the paths that earned none, and a function shaped for the first
|
|
40
|
+
* alone returns nothing the second can count.
|
|
41
|
+
*/
|
|
42
|
+
export function resolveCoverage(
|
|
43
|
+
map: Extract<LabelMap, { kind: 'map' }>,
|
|
44
|
+
paths: readonly string[],
|
|
45
|
+
): Coverage {
|
|
46
|
+
const earned = new Set<string>()
|
|
47
|
+
const declined: DeclinedPath[] = []
|
|
48
|
+
const uncovered: string[] = []
|
|
49
|
+
|
|
50
|
+
for (const path of paths) {
|
|
51
|
+
const labels = map.domains.filter((row) => matches(path, row.prefixes))
|
|
52
|
+
|
|
53
|
+
// A label wins over a decline. A path both tables claim already carries a
|
|
54
|
+
// subject, so reporting it as deliberately unlabelled would contradict the
|
|
55
|
+
// label the same run is about to apply.
|
|
56
|
+
if (labels.length > 0) {
|
|
57
|
+
for (const row of labels) earned.add(row.label)
|
|
58
|
+
continue
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const row = map.declined.find((entry) => matches(path, entry.prefixes))
|
|
62
|
+
if (row !== undefined) {
|
|
63
|
+
declined.push({ path, reason: row.reason })
|
|
64
|
+
continue
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
uncovered.push(path)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
// Read back off the map rather than out of the set, so two runs over one
|
|
72
|
+
// branch produce one order and one string.
|
|
73
|
+
labels: map.domains
|
|
74
|
+
.map((row) => row.label)
|
|
75
|
+
.filter((label) => earned.has(label)),
|
|
76
|
+
declined,
|
|
77
|
+
uncovered,
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Where a project declares its pull request label map, spelled once.
|
|
6
|
+
*
|
|
7
|
+
* The file has already moved once, and that relocation rewrote every mention
|
|
8
|
+
* across four surfaces. Nothing in code spells it anywhere else, so the next
|
|
9
|
+
* move is one edit here rather than a sweep.
|
|
10
|
+
*/
|
|
11
|
+
export const MAP_REL = join('.claude', 'canon', 'pr-labels.toml')
|
|
12
|
+
|
|
13
|
+
/** A label name and the path prefixes that earn it, in the map's own order. */
|
|
14
|
+
export interface DomainRow {
|
|
15
|
+
readonly label: string
|
|
16
|
+
readonly prefixes: readonly string[]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** A reason a path is deliberately unlabelled, and the prefixes it covers. */
|
|
20
|
+
export interface DeclinedRow {
|
|
21
|
+
readonly reason: string
|
|
22
|
+
readonly prefixes: readonly string[]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Why a map could not be read, which is never the same as a map with no rows.
|
|
27
|
+
*
|
|
28
|
+
* `no-map` is an answer rather than a fault. A project declaring no map is
|
|
29
|
+
* labelled silently by design, so a refusal read as a break would make the map
|
|
30
|
+
* mandatory for every target, which that decision declined. The other two are
|
|
31
|
+
* a file that exists and cannot be used, which is a defect in the map itself.
|
|
32
|
+
*/
|
|
33
|
+
export type MapRefusal = 'no-map' | 'unreadable-map' | 'no-domains'
|
|
34
|
+
|
|
35
|
+
export type LabelMap =
|
|
36
|
+
| {
|
|
37
|
+
readonly kind: 'map'
|
|
38
|
+
readonly domains: readonly DomainRow[]
|
|
39
|
+
readonly declined: readonly DeclinedRow[]
|
|
40
|
+
}
|
|
41
|
+
| { readonly kind: 'refused'; readonly reason: MapRefusal }
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Reads a TOML table of string arrays into rows, dropping any key whose value
|
|
45
|
+
* carries no usable prefix.
|
|
46
|
+
*
|
|
47
|
+
* A malformed row is skipped rather than refused, because both tables are
|
|
48
|
+
* authored by hand and one bad entry should not blind the check to the other
|
|
49
|
+
* forty. What it costs is that a typo reads as a row nobody wrote, which the
|
|
50
|
+
* uncovered report surfaces from the other side.
|
|
51
|
+
*/
|
|
52
|
+
function readRows(table: unknown): { key: string; prefixes: string[] }[] {
|
|
53
|
+
if (typeof table !== 'object' || table === null || Array.isArray(table)) {
|
|
54
|
+
return []
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const rows: { key: string; prefixes: string[] }[] = []
|
|
58
|
+
for (const [key, value] of Object.entries(table)) {
|
|
59
|
+
if (!Array.isArray(value)) continue
|
|
60
|
+
const prefixes = value.filter(
|
|
61
|
+
(entry): entry is string => typeof entry === 'string' && entry !== '',
|
|
62
|
+
)
|
|
63
|
+
if (prefixes.length > 0) rows.push({ key, prefixes })
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return rows
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Parses map text, so a caller holding the bytes skips the filesystem. */
|
|
70
|
+
export function parseLabelMap(source: string): LabelMap {
|
|
71
|
+
let parsed: Record<string, unknown>
|
|
72
|
+
try {
|
|
73
|
+
parsed = Bun.TOML.parse(source) as Record<string, unknown>
|
|
74
|
+
} catch {
|
|
75
|
+
return { kind: 'refused', reason: 'unreadable-map' }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const domains = readRows(parsed.domains)
|
|
79
|
+
if (domains.length === 0) return { kind: 'refused', reason: 'no-domains' }
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
kind: 'map',
|
|
83
|
+
domains: domains.map(({ key, prefixes }) => ({ label: key, prefixes })),
|
|
84
|
+
declined: readRows(parsed.declined).map(({ key, prefixes }) => ({
|
|
85
|
+
reason: key,
|
|
86
|
+
prefixes,
|
|
87
|
+
})),
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Reads the map a project declares at `root`, or says why it could not. */
|
|
92
|
+
export function readLabelMap(root: string): LabelMap {
|
|
93
|
+
let source: string
|
|
94
|
+
try {
|
|
95
|
+
source = readFileSync(join(root, MAP_REL), 'utf8')
|
|
96
|
+
} catch {
|
|
97
|
+
return { kind: 'refused', reason: 'no-map' }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return parseLabelMap(source)
|
|
101
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { linesOutsideFences, maskCodeSpans } from '@/markdown/scan'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The two version namespaces `standards/versioning.md` keeps apart, and why a
|
|
5
|
+
* pull request cannot be sorted between them by pattern alone.
|
|
6
|
+
*
|
|
7
|
+
* A phase label (`v68.5`) and a semver reference (`v3.44`) both match
|
|
8
|
+
* `v<digits>(.<digits>){1,2}`, so a shape-only scan cannot tell them apart. The
|
|
9
|
+
* first measurement of this defect tried exactly that and returned 28 of 60
|
|
10
|
+
* correct. What separates the two is not the token but the pull request: a
|
|
11
|
+
* release-please pull request legitimately carries version numbers in its
|
|
12
|
+
* generated body, and every other pull request does not.
|
|
13
|
+
*/
|
|
14
|
+
export interface PhaseScanInput {
|
|
15
|
+
readonly title: string
|
|
16
|
+
readonly body: string
|
|
17
|
+
readonly headRefName: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface PhaseScanResult {
|
|
21
|
+
/** Whether this pull request is release-please's own, per its fixed shape. */
|
|
22
|
+
readonly cutsRelease: boolean
|
|
23
|
+
readonly phaseLabels: readonly string[]
|
|
24
|
+
readonly semverTags: readonly string[]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const VERSION_TOKEN = /\bv\d+(?:\.\d+){1,2}\b/g
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The head branch release-please opens every release pull request under.
|
|
31
|
+
*
|
|
32
|
+
* `release-please-config.json` names the package `canon`, so the observed
|
|
33
|
+
* branch is `release-please--branches--main--components--canon`. The prefix
|
|
34
|
+
* stops short of the component segment, since that segment moves with the
|
|
35
|
+
* package name and the branch segment does not.
|
|
36
|
+
*/
|
|
37
|
+
const RELEASE_HEAD_PREFIX = 'release-please--branches--main'
|
|
38
|
+
|
|
39
|
+
const RELEASE_TITLE_PREFIX = 'chore(main): release '
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Two fixed signals rather than one, because either alone is a string an
|
|
43
|
+
* author's own pull request could reproduce. A title starting with the
|
|
44
|
+
* release commit type is plain conventional-commit text, and a head branch is
|
|
45
|
+
* exactly what a PR renamed for review would want to imitate to slip a real
|
|
46
|
+
* phase label past this check. Together they name release-please's own act of
|
|
47
|
+
* opening the pull request, which nothing else on the remote can perform.
|
|
48
|
+
*/
|
|
49
|
+
function isReleasePullRequest(input: PhaseScanInput): boolean {
|
|
50
|
+
return (
|
|
51
|
+
input.headRefName.startsWith(RELEASE_HEAD_PREFIX) &&
|
|
52
|
+
input.title.startsWith(RELEASE_TITLE_PREFIX)
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Drops a fenced block outright and blanks a code span inside what remains,
|
|
58
|
+
* so a token quoted rather than written is read the way a reader reads it:
|
|
59
|
+
* shown, not asserted.
|
|
60
|
+
*
|
|
61
|
+
* `canon markdown audit` excludes a fenced block and a code span from its own
|
|
62
|
+
* ban scan over the same kind of text, and this reuses that reading rather
|
|
63
|
+
* than inventing a second one. `#1208` is the corpus case that forced it: a
|
|
64
|
+
* backticked span quoting a test fixture's own version-shaped name, which the
|
|
65
|
+
* shape-only scan below cannot tell from a leak on its own.
|
|
66
|
+
*
|
|
67
|
+
* A link destination stays unmasked, unlike the ban scan's own reading. A
|
|
68
|
+
* release-please body's real semver reference sits inside the generated
|
|
69
|
+
* compare link's URL, and masking it would empty `semverTags` on the one
|
|
70
|
+
* pull request this check exists to pass, trading the corpus's one code-span
|
|
71
|
+
* leak for a hole in every release.
|
|
72
|
+
*/
|
|
73
|
+
function readable(text: string): string {
|
|
74
|
+
return linesOutsideFences(text).map(maskCodeSpans).join('\n')
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Reads a title and a body for version-shaped tokens and sorts every one
|
|
79
|
+
* found into the namespace this pull request is allowed to carry.
|
|
80
|
+
*
|
|
81
|
+
* The split runs on the pull request rather than on each token. A release
|
|
82
|
+
* pull request's tokens are read as the semver references its generated body
|
|
83
|
+
* legitimately carries, and every other pull request's tokens are read as
|
|
84
|
+
* leaked phase labels, which is what `standards/versioning.md` names the
|
|
85
|
+
* defect this exists to catch.
|
|
86
|
+
*/
|
|
87
|
+
export function scanPhaseLabels(input: PhaseScanInput): PhaseScanResult {
|
|
88
|
+
const text = readable(`${input.title}\n${input.body}`)
|
|
89
|
+
const tokens = [...new Set(text.match(VERSION_TOKEN) ?? [])]
|
|
90
|
+
const cutsRelease = isReleasePullRequest(input)
|
|
91
|
+
|
|
92
|
+
return cutsRelease
|
|
93
|
+
? { cutsRelease, phaseLabels: [], semverTags: tokens }
|
|
94
|
+
: { cutsRelease, phaseLabels: tokens, semverTags: [] }
|
|
95
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { BanSets } from '@/markdown/scan'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Single characters `markdown.md` bans under `## Punctuation`.
|
|
5
|
+
*
|
|
6
|
+
* The parenthetical-aside ban in that section is absent because it quotes a
|
|
7
|
+
* whole clause, and a literal match built from a clause reports the compliant
|
|
8
|
+
* text and reaches none of the violations.
|
|
9
|
+
*/
|
|
10
|
+
const CHARACTERS = ['—', ';'] as const
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Single lowercase words `markdown.md` bans under `## Language`.
|
|
14
|
+
*
|
|
15
|
+
* A multi-word ban is absent by the same test the character set applies. The
|
|
16
|
+
* standard bans a pattern like `It's not X, it's Y` with a placeholder standing
|
|
17
|
+
* in for the rest of the sentence, so no literal match reaches it and the
|
|
18
|
+
* phrase bans stay a reader's judgment.
|
|
19
|
+
*/
|
|
20
|
+
const WORDS = [
|
|
21
|
+
'seamless',
|
|
22
|
+
'robust',
|
|
23
|
+
'powerful',
|
|
24
|
+
'revolutionary',
|
|
25
|
+
'enhanced',
|
|
26
|
+
'allows',
|
|
27
|
+
'leverage',
|
|
28
|
+
'simply',
|
|
29
|
+
'just',
|
|
30
|
+
'easily',
|
|
31
|
+
'quickly',
|
|
32
|
+
'very',
|
|
33
|
+
'really',
|
|
34
|
+
] as const
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* British spellings of the American examples `markdown.md` lists.
|
|
38
|
+
*
|
|
39
|
+
* The set is carried whole rather than derived from a suffix rule, because a
|
|
40
|
+
* suffix pattern run over prose produced 46 of the 58 false positives measured
|
|
41
|
+
* during intake: `exercises`, `promises`, and `revised` all end in `-ise` and
|
|
42
|
+
* none is a British spelling. Matching whole words reaches none of them.
|
|
43
|
+
*
|
|
44
|
+
* `analyse` is absent because the standard's example is `analyze`, which ends
|
|
45
|
+
* in `-yze` rather than the `-ize` its rule states. The set records what the
|
|
46
|
+
* standard reaches rather than what a reader would extend it to, so adding the
|
|
47
|
+
* spelling here would widen the check past the prose it answers to.
|
|
48
|
+
*/
|
|
49
|
+
const SPELLINGS = [
|
|
50
|
+
'organise',
|
|
51
|
+
'summarise',
|
|
52
|
+
'recognise',
|
|
53
|
+
'behaviour',
|
|
54
|
+
'colour',
|
|
55
|
+
'centre',
|
|
56
|
+
] as const
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The three closed sets the audit measures, owned here rather than harvested
|
|
60
|
+
* from the standards stating them.
|
|
61
|
+
*
|
|
62
|
+
* Parsing the prose was the alternative and it put a parser contract on two
|
|
63
|
+
* documents authored for people, which the prose standard had to carry a
|
|
64
|
+
* paragraph of its own to protect. The sets are a prior an author already
|
|
65
|
+
* knows rather than a filter: measured at `60fc97bf` on 2026-08-19, these 21
|
|
66
|
+
* terms ran across 483 markdown files for a clean exit, and every one of the
|
|
67
|
+
* 70 occurrences of a banned word sat inside the ban list itself or inside an
|
|
68
|
+
* example demonstrating the ban.
|
|
69
|
+
*
|
|
70
|
+
* The set is closed rather than extensible. Enumeration cannot close the gap
|
|
71
|
+
* it aims at, and each addition costs a false-positive class, since `just`,
|
|
72
|
+
* `allows`, and `very` have honest uses no literal match separates. A project
|
|
73
|
+
* wanting its own vocabulary is asking for a different feature than this one.
|
|
74
|
+
*/
|
|
75
|
+
export const BAN_SETS: BanSets = {
|
|
76
|
+
characters: CHARACTERS,
|
|
77
|
+
words: WORDS,
|
|
78
|
+
spellings: SPELLINGS,
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Names any set that arrived empty, so a run measuring nothing says so.
|
|
83
|
+
*
|
|
84
|
+
* Empty is not the same state as finding no hit. A scan with no terms reports
|
|
85
|
+
* a clean file having looked for nothing, which is the silence the audit
|
|
86
|
+
* refused to ship back when a standard could go missing. The sets ship with
|
|
87
|
+
* the package now, so the only way one empties is an edit to this file, and
|
|
88
|
+
* the guard is what keeps that edit loud rather than quiet.
|
|
89
|
+
*/
|
|
90
|
+
export function emptyBanSets(sets: BanSets = BAN_SETS): string[] {
|
|
91
|
+
return (['characters', 'words', 'spellings'] as const).filter(
|
|
92
|
+
(name) => sets[name].length === 0,
|
|
93
|
+
)
|
|
94
|
+
}
|