@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,790 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises'
|
|
2
|
+
import { basename, relative } from 'node:path'
|
|
3
|
+
import { BARE_NAME, IGNORE_MARKER } from '@/context/citations'
|
|
4
|
+
import type { AuditedFolder } from '@/context/folders'
|
|
5
|
+
import type { NarrationTerms } from '@/context/narration'
|
|
6
|
+
import { type BodyLine, bodyLines, maskDisplayed } from '@/markdown/scan'
|
|
7
|
+
import { renderedHeight } from '@/markdown/structure'
|
|
8
|
+
import { isStubSeed } from '@/seed-marker'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Checkpoint quoted from the standard stating it. It is not a cap.
|
|
12
|
+
*
|
|
13
|
+
* Entry length rests on one entry per domain, which is a domain fact, so
|
|
14
|
+
* `standards/context.md` keeps it. Depth and bullet weight read the same over
|
|
15
|
+
* any markdown file, so they are stated at the attribute tier and measured by
|
|
16
|
+
* `canon markdown audit` rather than here.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Every reason `canon context audit` refuses for.
|
|
20
|
+
*
|
|
21
|
+
* `no-folders` is the one ordinary absence: a project that never adopted
|
|
22
|
+
* `.claude/context/`, `.claude/diagrams/`, or `.claude/wireframes/` names no
|
|
23
|
+
* corpus this audit can measure, the same state `no-skills` reads for the
|
|
24
|
+
* skill corpora. The other four are a malformed invocation or a checkout git
|
|
25
|
+
* cannot read, which stay a break rather than an absence.
|
|
26
|
+
*/
|
|
27
|
+
export type ContextAuditRefusal =
|
|
28
|
+
| 'conflicting-options'
|
|
29
|
+
| 'bad-folder-list'
|
|
30
|
+
| 'no-folders'
|
|
31
|
+
| 'no-citation-scope'
|
|
32
|
+
| 'no-git'
|
|
33
|
+
|
|
34
|
+
export const LENGTH_CHECKPOINT = 150
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* A table this size or larger whose first column mostly names artifacts reads
|
|
38
|
+
* as a catalog that grows a row per shipped thing, which is the shape the
|
|
39
|
+
* standard routes to a bullet list. Below it, a table is small enough that a
|
|
40
|
+
* reflow rewrites little.
|
|
41
|
+
*/
|
|
42
|
+
export const CATALOG_ROW_CHECKPOINT = 6
|
|
43
|
+
|
|
44
|
+
/** Share of first cells that must name an artifact for a table to qualify. */
|
|
45
|
+
const CATALOG_NAMED_RATIO = 0.6
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Sections `standards/context.md` marks required, in the order it states them.
|
|
49
|
+
*
|
|
50
|
+
* The list is held here rather than read out of the standard, the way the four
|
|
51
|
+
* checkpoints above quote their numbers. A parser over the standard's prose
|
|
52
|
+
* would decide which sections are required from the wording around them, so it
|
|
53
|
+
* fails on a rewrite of that wording rather than on a defect in an entry.
|
|
54
|
+
*
|
|
55
|
+
* These names do not generalize the way a length threshold does, which is why
|
|
56
|
+
* the measure is scoped to the folder `governsContent` names. A diagram entry
|
|
57
|
+
* declares a heading per kind and a wireframe entry per screen, and neither
|
|
58
|
+
* sibling standard states a required section at all.
|
|
59
|
+
*/
|
|
60
|
+
export const REQUIRED_SECTIONS: readonly string[] = ['Overview', 'Layout']
|
|
61
|
+
|
|
62
|
+
const HEADING_TEXT = /^#{1,6}\s+(.+?)\s*$/
|
|
63
|
+
const TABLE_ROW = /^\s*\|/
|
|
64
|
+
const TABLE_SEPARATOR = /^\s*\|[\s:|-]+\|\s*$/
|
|
65
|
+
const NAMED_CELL = /`[^`]+`|\[[^\]]+\]\([^)]+\)/
|
|
66
|
+
|
|
67
|
+
const TOP_BULLET = /^-\s+(.+)$/
|
|
68
|
+
/** Any indented line, which is a nested bullet or a wrapped continuation. */
|
|
69
|
+
const INSIDE_LIST = /^\s+\S/
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Spellings of how the domain reached its shape rather than what it is now.
|
|
73
|
+
*
|
|
74
|
+
* The standard admits a rejected alternative and its reasoning while refusing
|
|
75
|
+
* the provenance attached to it, and these three are what a session reaches for
|
|
76
|
+
* when it records the second: when a change landed, which change carried it,
|
|
77
|
+
* and which release labelled it. A marker is a judgment rather than a defect,
|
|
78
|
+
* so this is measured and reported and never gates.
|
|
79
|
+
*
|
|
80
|
+
* The release pattern accepts three segments without a leading `v`, since the
|
|
81
|
+
* standard cuts a release label rather than a spelling of one and `a CLI at
|
|
82
|
+
* 0.83.0` names a release exactly as `v0.83.0` does. Two segments still require
|
|
83
|
+
* the `v`, because an unprefixed pair is a dollar cost or a duration far more
|
|
84
|
+
* often than a release in a corpus that records what its own runs cost. What
|
|
85
|
+
* the widening reaches by accident is another tool's version, which the
|
|
86
|
+
* standard asks nobody to cut, and those report rather than being excluded:
|
|
87
|
+
* an exclusion keyed on a nearby tool name goes stale with nothing saying so.
|
|
88
|
+
*/
|
|
89
|
+
const PROVENANCE: readonly { kind: ProvenanceKind; pattern: RegExp }[] = [
|
|
90
|
+
{ kind: 'date', pattern: /\b\d{4}-\d{2}-\d{2}\b/g },
|
|
91
|
+
{ kind: 'change', pattern: /#\d{3,}\b/g },
|
|
92
|
+
{ kind: 'release', pattern: /\b(?:v\d+\.\d+(?:\.\d+)?|\d+\.\d+\.\d+)\b/g },
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Verbs that read the date behind them as stamping a measurement.
|
|
97
|
+
*
|
|
98
|
+
* `standards/context.md` cuts a date attached to a change and permits one
|
|
99
|
+
* stamping a measurement, so a pattern matching every ISO date reports against
|
|
100
|
+
* a rule that admits half of what it finds. Separating the two needs the clause
|
|
101
|
+
* rather than the token, since `measured on 2026-08-19` and `overturned on
|
|
102
|
+
* 2026-08-19` differ only in the verb.
|
|
103
|
+
*
|
|
104
|
+
* These five are unambiguous wherever they appear in the clause, which is what
|
|
105
|
+
* lets them match at any distance from the date. `Measured across the corpus on
|
|
106
|
+
* 2026-08-14` puts four words between the two, and no line in the corpus uses
|
|
107
|
+
* any of them as a noun a change date could attach to.
|
|
108
|
+
*
|
|
109
|
+
* The set is closed and drawn from what the corpus already writes, so a
|
|
110
|
+
* phrasing nobody enumerated goes unmatched. That direction is the safe one: an
|
|
111
|
+
* unmatched date falls back to the change marker it is today, which reports one
|
|
112
|
+
* date too many rather than clearing one the standard cuts. Widening the set is
|
|
113
|
+
* how a missed phrasing is fixed, and a date the clause cannot place is never a
|
|
114
|
+
* third state, because the length finding already spends `unanswered` on the
|
|
115
|
+
* two questions nothing measures at all.
|
|
116
|
+
*/
|
|
117
|
+
const STAMPING = /\b(?:measured|verified|driven|passed|fired)\b/i
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* The noun form, which has to sit against the date to count.
|
|
121
|
+
*
|
|
122
|
+
* `A run on 2026-08-14` stamps a measurement and `Runs on #632 and #634 landed
|
|
123
|
+
* 2026-08-02` dates a change, and the two differ only in what follows the noun.
|
|
124
|
+
* Requiring the connector and then the date immediately is what separates them,
|
|
125
|
+
* so this is anchored to the end of the clause where `STAMPING` is not.
|
|
126
|
+
*/
|
|
127
|
+
const STAMPING_RUN = /\b(?:an?\s+)?runs?\s+(?:on|at|in)\s+$/i
|
|
128
|
+
|
|
129
|
+
/** Sentence boundary, so a clause is read rather than the whole line. */
|
|
130
|
+
const SENTENCE_END = /[.!?:;]\s(?=[^.!?:;]*$)/
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* The folder whose standard carries the exclusion above.
|
|
134
|
+
*
|
|
135
|
+
* Three measures narrow here rather than one: provenance, the required
|
|
136
|
+
* sections, and the superseded-decision narration. The name is the first of
|
|
137
|
+
* them because it was the first, and it stays because renaming a constant the
|
|
138
|
+
* JSON record publishes as `checkpoints.provenanceFolder` breaks every consumer
|
|
139
|
+
* reading that field for a gain of one word.
|
|
140
|
+
*
|
|
141
|
+
* `standards/context.md` opens its scope by handing diagrams and wireframes to
|
|
142
|
+
* `diagrams.md` and `wireframes.md`, so a marker reported in either would cite
|
|
143
|
+
* a rule that entry's own standard routes elsewhere. The length and table
|
|
144
|
+
* checkpoints are quoted from the same standard and keep reaching every audited
|
|
145
|
+
* folder, because a threshold on how far a reader travels generalizes across
|
|
146
|
+
* entry types while a rule about what an entry may say does not.
|
|
147
|
+
*
|
|
148
|
+
* What gates here is a rule only this standard states. Bullet weight does not,
|
|
149
|
+
* since `standards/markdown.md` owns that checkpoint across document types and
|
|
150
|
+
* its remedy sends the overflow to prose, which any entry type can act on.
|
|
151
|
+
* `standards/context.md` specializes that remedy for an entry carrying
|
|
152
|
+
* decisions, and specializing a rule narrows the advice rather than the measure.
|
|
153
|
+
*
|
|
154
|
+
* Restating the exclusion in the sibling standards was the alternative for what
|
|
155
|
+
* gates here. It duplicates one knowledge item across three surfaces, which the
|
|
156
|
+
* root instruction file forbids, and pointing is not available because the
|
|
157
|
+
* surface they would point at is the one disclaiming them.
|
|
158
|
+
*/
|
|
159
|
+
export const PROVENANCE_FOLDER = 'context'
|
|
160
|
+
|
|
161
|
+
export type ProvenanceKind = 'date' | 'change' | 'release'
|
|
162
|
+
|
|
163
|
+
export interface TableFinding {
|
|
164
|
+
readonly line: number
|
|
165
|
+
readonly rows: number
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export interface ProvenanceFinding {
|
|
169
|
+
readonly line: number
|
|
170
|
+
readonly kind: ProvenanceKind
|
|
171
|
+
/** The marker as written, so a report names what to go and look at. */
|
|
172
|
+
readonly text: string
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export interface BareReferenceFinding {
|
|
176
|
+
readonly line: number
|
|
177
|
+
/** The name as written, so a report says which reference to respell. */
|
|
178
|
+
readonly name: string
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export interface NarrationFinding {
|
|
182
|
+
readonly line: number
|
|
183
|
+
/** The opening that points back at the bullet above. */
|
|
184
|
+
readonly pronoun: string
|
|
185
|
+
/** The past-tense verb that turns the back-reference into a narration. */
|
|
186
|
+
readonly verb: string
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export interface EntryReport {
|
|
190
|
+
readonly rel: string
|
|
191
|
+
/**
|
|
192
|
+
* Rendered lines across the whole file, counting frontmatter and fenced
|
|
193
|
+
* blocks. It shares `renderedHeight` with the depth checkpoint in
|
|
194
|
+
* `src/markdown/structure.ts`, since the two sit in one section of the
|
|
195
|
+
* standard and a reader compares them.
|
|
196
|
+
*
|
|
197
|
+
* Fences are counted here and excluded there. The depth measure skips one so
|
|
198
|
+
* an example cannot break the run around it, and a file measure has no run to
|
|
199
|
+
* protect. Excluding them here would change which entries report by one and
|
|
200
|
+
* would not reach the case that motivates it: the most fenced entry in the
|
|
201
|
+
* corpus runs 20 percent fenced and sits past the checkpoint either way.
|
|
202
|
+
*/
|
|
203
|
+
readonly lines: number
|
|
204
|
+
readonly catalogTables: readonly TableFinding[]
|
|
205
|
+
/** Empty for an entry no standard bans a change narrative in. */
|
|
206
|
+
readonly provenance: readonly ProvenanceFinding[]
|
|
207
|
+
/**
|
|
208
|
+
* Empty outside the governed folder, and empty on a run whose caller loaded
|
|
209
|
+
* no term sets. The report distinguishes the two from the vocabulary itself,
|
|
210
|
+
* since an empty list here is silent about which one produced it.
|
|
211
|
+
*/
|
|
212
|
+
readonly narration: readonly NarrationFinding[]
|
|
213
|
+
/**
|
|
214
|
+
* References naming a sibling entry by bare filename, and empty for a caller
|
|
215
|
+
* that passed no sibling names. Which entries have siblings worth matching is
|
|
216
|
+
* the caller's judgment, stated where it builds the list.
|
|
217
|
+
*/
|
|
218
|
+
readonly bareReferences: readonly BareReferenceFinding[]
|
|
219
|
+
/**
|
|
220
|
+
* Required sections this entry declares, in the standard's order, and empty
|
|
221
|
+
* outside the folder whose standard names them. What the folder is short of
|
|
222
|
+
* is `missingSections`, since one entry answers for its siblings.
|
|
223
|
+
*/
|
|
224
|
+
readonly sections: readonly string[]
|
|
225
|
+
/**
|
|
226
|
+
* Whether the file declares itself a skeleton, which excludes it from the
|
|
227
|
+
* section check alone. Every other measure still reads it, since a stub is
|
|
228
|
+
* exempt from owing sections rather than from being well formed.
|
|
229
|
+
*/
|
|
230
|
+
readonly stub: boolean
|
|
231
|
+
/**
|
|
232
|
+
* Whether a content rule reached this entry, which is what parts an empty
|
|
233
|
+
* `provenance` list that was measured from one that was never scanned. The
|
|
234
|
+
* length finding reads it, since reporting a clean history for an entry
|
|
235
|
+
* outside the governed folder answers a question nothing asked.
|
|
236
|
+
*/
|
|
237
|
+
readonly governed: boolean
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* The three questions `standards/context.md` asks of an entry past the
|
|
242
|
+
* checkpoint, in the order it states them.
|
|
243
|
+
*
|
|
244
|
+
* The standard calls no entry over the checkpoint wrong. It asks whether the
|
|
245
|
+
* entry still covers a single domain, whether it has filled with content `ls`
|
|
246
|
+
* or `--help` reproduces, and whether it has accumulated the history of its own
|
|
247
|
+
* changes, then directs a fix at whichever is true. A report naming the count
|
|
248
|
+
* alone leaves all three unasked, which is why the finding carries them.
|
|
249
|
+
*/
|
|
250
|
+
export type LengthQuestion = 'domain' | 'reproduced' | 'history'
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* What the audit can say about one question for one entry.
|
|
254
|
+
*
|
|
255
|
+
* `unanswered` is a state rather than an omission. Two of the three questions
|
|
256
|
+
* are judgments no measure settles, and dropping them would read as an entry
|
|
257
|
+
* nothing found rather than one nothing checked.
|
|
258
|
+
*/
|
|
259
|
+
export type QuestionState = 'yes' | 'no' | 'unanswered'
|
|
260
|
+
|
|
261
|
+
export interface LengthCause {
|
|
262
|
+
readonly question: LengthQuestion
|
|
263
|
+
readonly state: QuestionState
|
|
264
|
+
/**
|
|
265
|
+
* Change markers behind a `yes`, and absent wherever nothing was counted. It
|
|
266
|
+
* cites the provenance finding rather than restating it, so the two sections
|
|
267
|
+
* of the report describe the same measurement once. A date stamping a
|
|
268
|
+
* measurement never reaches that list, so an entry whose only dates are
|
|
269
|
+
* measurement anchors answers `no` here.
|
|
270
|
+
*/
|
|
271
|
+
readonly markers?: number
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export interface LengthFinding {
|
|
275
|
+
readonly rel: string
|
|
276
|
+
readonly lines: number
|
|
277
|
+
/** One entry per question in `LengthQuestion` order, never empty. */
|
|
278
|
+
readonly causes: readonly LengthCause[]
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export interface SectionFinding {
|
|
282
|
+
/**
|
|
283
|
+
* Repo-relative path of whatever owes the sections: the entry itself in the
|
|
284
|
+
* folder named under `.claude/`, and the folder in a domain split across
|
|
285
|
+
* one, since the split folder's entries answer for each other.
|
|
286
|
+
*/
|
|
287
|
+
readonly rel: string
|
|
288
|
+
/** Required sections the path above does not declare, never empty. */
|
|
289
|
+
readonly missing: readonly string[]
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function firstCell(row: string): string {
|
|
293
|
+
return row.split('|').slice(1)[0] ?? ''
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Finds the tables whose rows name shipped artifacts.
|
|
298
|
+
*
|
|
299
|
+
* A bare table count reports mostly fixed comparison tables, where a reflow
|
|
300
|
+
* costs nothing because no row is ever added. The reflow problem belongs to a
|
|
301
|
+
* catalog that gains a row per artifact, and a first column carrying a path,
|
|
302
|
+
* command, or link is what separates the two without reading the prose.
|
|
303
|
+
*/
|
|
304
|
+
function catalogTables(entry: readonly BodyLine[]): TableFinding[] {
|
|
305
|
+
const findings: TableFinding[] = []
|
|
306
|
+
const lines = entry.filter((line) => !line.fenced)
|
|
307
|
+
let index = 0
|
|
308
|
+
|
|
309
|
+
while (index < lines.length) {
|
|
310
|
+
const line = lines[index]
|
|
311
|
+
|
|
312
|
+
if (!TABLE_ROW.test(line.text)) {
|
|
313
|
+
index++
|
|
314
|
+
continue
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const separator = lines[index + 1]
|
|
318
|
+
if (!separator || !TABLE_SEPARATOR.test(separator.text)) {
|
|
319
|
+
index++
|
|
320
|
+
continue
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const body: BodyLine[] = []
|
|
324
|
+
let cursor = index + 2
|
|
325
|
+
while (cursor < lines.length && TABLE_ROW.test(lines[cursor].text)) {
|
|
326
|
+
body.push(lines[cursor])
|
|
327
|
+
cursor++
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const named = body.filter((row) => NAMED_CELL.test(firstCell(row.text)))
|
|
331
|
+
if (
|
|
332
|
+
body.length >= CATALOG_ROW_CHECKPOINT &&
|
|
333
|
+
named.length / body.length >= CATALOG_NAMED_RATIO
|
|
334
|
+
) {
|
|
335
|
+
findings.push({ line: line.number, rows: body.length })
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
index = cursor
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return findings
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Finds the markers narrating a change rather than describing the domain.
|
|
346
|
+
*
|
|
347
|
+
* Fenced blocks are skipped for the same reason the table scan skips them: a
|
|
348
|
+
* sample command or a fixture inside an example is content the entry displays
|
|
349
|
+
* rather than a claim it makes, and a version pinned in an install line is the
|
|
350
|
+
* ordinary shape of one.
|
|
351
|
+
*
|
|
352
|
+
* A date stamping a measurement is dropped rather than reported under a kind of
|
|
353
|
+
* its own. One list with one meaning is what lets every consumer read it
|
|
354
|
+
* without filtering: the report names what the standard cuts, and the length
|
|
355
|
+
* finding counts the same thing. A separate kind would put the split in three
|
|
356
|
+
* places and leave each free to read it differently.
|
|
357
|
+
*/
|
|
358
|
+
function provenance(lines: readonly BodyLine[]): ProvenanceFinding[] {
|
|
359
|
+
// Scanning one pattern at a time emits a line's markers grouped by kind, so
|
|
360
|
+
// the column is carried out of the match and sorted on. Without it a line
|
|
361
|
+
// holding a date and two change numbers reports them in an order the reader
|
|
362
|
+
// cannot find by scanning left to right.
|
|
363
|
+
const found: { finding: ProvenanceFinding; column: number }[] = []
|
|
364
|
+
|
|
365
|
+
for (const line of lines) {
|
|
366
|
+
if (line.fenced) continue
|
|
367
|
+
|
|
368
|
+
for (const { kind, pattern } of PROVENANCE) {
|
|
369
|
+
for (const match of line.text.matchAll(pattern)) {
|
|
370
|
+
if (kind === 'date' && stampsMeasurement(line.text, match.index)) {
|
|
371
|
+
continue
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
found.push({
|
|
375
|
+
finding: { line: line.number, kind, text: match[0] },
|
|
376
|
+
column: match.index,
|
|
377
|
+
})
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return found
|
|
383
|
+
.sort((a, b) => a.finding.line - b.finding.line || a.column - b.column)
|
|
384
|
+
.map((each) => each.finding)
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Reads the clause in front of a date for a verb that stamps a measurement.
|
|
389
|
+
*
|
|
390
|
+
* The clause rather than the line, because a sentence recording a measurement
|
|
391
|
+
* and a later one dating a change sit side by side often enough that a
|
|
392
|
+
* line-wide read would clear the second from the first. A date opening its own
|
|
393
|
+
* line has no clause in front of it and stays a change marker, which is the
|
|
394
|
+
* fallback rather than a separate answer.
|
|
395
|
+
*/
|
|
396
|
+
function stampsMeasurement(text: string, index: number): boolean {
|
|
397
|
+
const before = text.slice(0, index)
|
|
398
|
+
const boundary = before.search(SENTENCE_END)
|
|
399
|
+
const clause = boundary === -1 ? before : before.slice(boundary + 1)
|
|
400
|
+
|
|
401
|
+
return STAMPING.test(clause) || STAMPING_RUN.test(clause)
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function escape(term: string): string {
|
|
405
|
+
return term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Copulas that turn the verb behind them into a present-tense passive.
|
|
410
|
+
*
|
|
411
|
+
* `used to` is the term this exists for, since `is used to resolve the folder`
|
|
412
|
+
* is the passive of `use` rather than the past habitual the set means. This
|
|
413
|
+
* list is English grammar rather than corpus vocabulary, so it stays in code
|
|
414
|
+
* while the two tunable sets stay in the rule, and a rule publishing two of
|
|
415
|
+
* three headings would be a fourth absent state to carry for no tuning anyone
|
|
416
|
+
* wants.
|
|
417
|
+
*
|
|
418
|
+
* What it costs is the perfect passive. `has been superseded` narrates a
|
|
419
|
+
* supersession and is rejected with the rest, which is a recall gap taken
|
|
420
|
+
* knowingly on the trade this check already makes everywhere else.
|
|
421
|
+
*/
|
|
422
|
+
const COPULA = ['is', 'are', 'be', 'been', 'being', 'was', 'were']
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Finds the bullets narrating a decision the bullet above them replaced.
|
|
426
|
+
*
|
|
427
|
+
* The signal is structural rather than lexical, and the corpus is what decides
|
|
428
|
+
* that. The terms carrying clean signal for a supersession are too rare to
|
|
429
|
+
* catch anything, and the one term that would have caught the known case is
|
|
430
|
+
* `now`, which appears 57 times across 24 entries in correct present-tense
|
|
431
|
+
* prose. What separates the shape instead is a bullet pointing back at its
|
|
432
|
+
* sibling and putting the sibling's design in the past: an opening pronoun with
|
|
433
|
+
* no antecedent of its own, plus a past-tense verb, plus a bullet above it to
|
|
434
|
+
* refer to. Precision is the whole value, so recall is the accepted exposure
|
|
435
|
+
* and a narration written as a single bullet is not reached.
|
|
436
|
+
*
|
|
437
|
+
* The pronoun is matched cased and anchored, since a mid-sentence `this` is a
|
|
438
|
+
* determiner rather than a back-reference. The verb is matched uncased anywhere
|
|
439
|
+
* in the bullet, since the tense is what carries the signal wherever it sits,
|
|
440
|
+
* and rejected behind a `COPULA` for the reason stated there. Displayed spans
|
|
441
|
+
* are masked so a term quoted in backticks is not read as prose the entry
|
|
442
|
+
* writes, and fenced blocks are skipped for the reason the scans above skip
|
|
443
|
+
* them.
|
|
444
|
+
*
|
|
445
|
+
* A blank line does not end the run. Markdown reads two bullets around one as a
|
|
446
|
+
* single loose list, so breaking there would leave the shape reachable by
|
|
447
|
+
* anyone who spaced their bullets out. What ends a run is content that is
|
|
448
|
+
* neither a bullet nor indented under one, which the fenced branch below has to
|
|
449
|
+
* answer for itself because a fenced line is skipped before that test.
|
|
450
|
+
*/
|
|
451
|
+
function narration(
|
|
452
|
+
lines: readonly BodyLine[],
|
|
453
|
+
terms: NarrationTerms,
|
|
454
|
+
): NarrationFinding[] {
|
|
455
|
+
if (terms.pronouns.length === 0 || terms.verbs.length === 0) return []
|
|
456
|
+
|
|
457
|
+
const findings: NarrationFinding[] = []
|
|
458
|
+
let following = false
|
|
459
|
+
let fenceBlock: number | undefined
|
|
460
|
+
let fenceInsideList = false
|
|
461
|
+
|
|
462
|
+
for (const line of lines) {
|
|
463
|
+
// A fenced line is never a bullet, but an unindented block still ends the
|
|
464
|
+
// run. CommonMark reads a fence at column zero as interrupting the list, so
|
|
465
|
+
// the bullets around it are two lists and the second has no antecedent
|
|
466
|
+
// above it. A block indented under its bullet stays inside the item.
|
|
467
|
+
//
|
|
468
|
+
// The opening delimiter decides for every line of its own block. Reading
|
|
469
|
+
// each line instead ends the run on a blank line inside an indented fence,
|
|
470
|
+
// which has no indentation to read, and on a content line at column zero,
|
|
471
|
+
// which CommonMark permits since only the fence's own indent is stripped.
|
|
472
|
+
//
|
|
473
|
+
// The block index is what re-opens that decision, so two blocks written
|
|
474
|
+
// with nothing between them are answered separately rather than the second
|
|
475
|
+
// inheriting the first. Indentation stays the test here because what
|
|
476
|
+
// interrupts a list is this check's own judgment, and the walker only says
|
|
477
|
+
// which lines share a block.
|
|
478
|
+
if (line.fenced) {
|
|
479
|
+
if (line.fenceBlock !== fenceBlock) {
|
|
480
|
+
fenceBlock = line.fenceBlock
|
|
481
|
+
fenceInsideList = INSIDE_LIST.test(line.text)
|
|
482
|
+
}
|
|
483
|
+
if (!fenceInsideList) following = false
|
|
484
|
+
continue
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
if (line.text.trim() === '') continue
|
|
488
|
+
|
|
489
|
+
const bullet = line.text.match(TOP_BULLET)
|
|
490
|
+
if (!bullet) {
|
|
491
|
+
// A nested bullet and a wrapped continuation both sit inside the list, so
|
|
492
|
+
// neither ends the run. Anything else does, which is what keeps a bullet
|
|
493
|
+
// opening the list under a heading from reading as a reply to the last
|
|
494
|
+
// bullet of the list before it.
|
|
495
|
+
if (!INSIDE_LIST.test(line.text)) following = false
|
|
496
|
+
continue
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const text = maskDisplayed(bullet[1])
|
|
500
|
+
|
|
501
|
+
if (following) {
|
|
502
|
+
const pronoun = terms.pronouns.find((term) =>
|
|
503
|
+
new RegExp(`^${escape(term)}\\b`).test(text),
|
|
504
|
+
)
|
|
505
|
+
const verb = pronoun
|
|
506
|
+
? terms.verbs.find((term) =>
|
|
507
|
+
new RegExp(
|
|
508
|
+
`(?<!\\b(?:${COPULA.join('|')})\\s+)\\b${escape(term)}\\b`,
|
|
509
|
+
'i',
|
|
510
|
+
).test(text),
|
|
511
|
+
)
|
|
512
|
+
: undefined
|
|
513
|
+
|
|
514
|
+
if (pronoun && verb) findings.push({ line: line.number, pronoun, verb })
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
following = true
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
return findings
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Finds which required sections the entry declares.
|
|
525
|
+
*
|
|
526
|
+
* A heading at any level counts rather than the `##` the standard writes its
|
|
527
|
+
* examples at. A domain that split into a folder puts its overview in a sibling
|
|
528
|
+
* named for it, where the section is the `#` title and an `##` beneath it would
|
|
529
|
+
* repeat the file's own name. All three split folders in this repository are
|
|
530
|
+
* that shape, so matching `##` alone would report every one of them. Nothing is
|
|
531
|
+
* titled for a required section without being about it, so the looser match
|
|
532
|
+
* costs no precision.
|
|
533
|
+
*
|
|
534
|
+
* Fenced blocks are skipped for the reason the scans above skip them. A
|
|
535
|
+
* standard quoted inside an example declares nothing about the entry quoting it.
|
|
536
|
+
*/
|
|
537
|
+
function declaredSections(lines: readonly BodyLine[]): string[] {
|
|
538
|
+
const found = new Set<string>()
|
|
539
|
+
|
|
540
|
+
for (const line of lines) {
|
|
541
|
+
if (line.fenced) continue
|
|
542
|
+
|
|
543
|
+
const match = line.text.match(HEADING_TEXT)
|
|
544
|
+
if (match && REQUIRED_SECTIONS.includes(match[1])) found.add(match[1])
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
return REQUIRED_SECTIONS.filter((section) => found.has(section))
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Finds the references naming a sibling entry by bare filename.
|
|
552
|
+
*
|
|
553
|
+
* The standard asks for the path because a bare name resolves against whichever
|
|
554
|
+
* folder the reader is in, so a folder split strands every inbound reference
|
|
555
|
+
* and nothing reads the break. A path is checkable and a bare name is not,
|
|
556
|
+
* which is what makes this the one form rule worth measuring.
|
|
557
|
+
*
|
|
558
|
+
* Matching stops at the sibling set, which reaches less than the rule does. The
|
|
559
|
+
* standard governs a reference to any other entry, so a split entry naming one
|
|
560
|
+
* that sits in a different folder is a violation this never sees. What the set
|
|
561
|
+
* buys is that a name resolving inside the folder is a reference by
|
|
562
|
+
* construction, where a bare filename matched anywhere would report every
|
|
563
|
+
* sentence that happens to name a file. The entry's own name is out of the set
|
|
564
|
+
* on separate grounds, since naming itself points at nothing a split can strand.
|
|
565
|
+
*
|
|
566
|
+
* Fenced blocks are skipped for the reason the scans above skip them, and a line
|
|
567
|
+
* carrying the citation ignore marker is skipped because that marker already
|
|
568
|
+
* means the line displays a name rather than pointing at one.
|
|
569
|
+
*/
|
|
570
|
+
function bareReferences(
|
|
571
|
+
lines: readonly BodyLine[],
|
|
572
|
+
siblings: readonly string[],
|
|
573
|
+
): BareReferenceFinding[] {
|
|
574
|
+
if (siblings.length === 0) return []
|
|
575
|
+
|
|
576
|
+
const named = new Set(siblings)
|
|
577
|
+
const findings: BareReferenceFinding[] = []
|
|
578
|
+
|
|
579
|
+
for (const line of lines) {
|
|
580
|
+
if (line.fenced || line.text.includes(IGNORE_MARKER)) continue
|
|
581
|
+
|
|
582
|
+
for (const match of line.text.matchAll(BARE_NAME)) {
|
|
583
|
+
if (named.has(match[1])) {
|
|
584
|
+
findings.push({ line: line.number, name: match[1] })
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
return findings
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Measures one entry, scanning for provenance only when a standard claims it.
|
|
594
|
+
*
|
|
595
|
+
* The caller passes jurisdiction rather than deriving it from `rel`, because a
|
|
596
|
+
* path prefix hardcodes what `--folder` exists to override and misses a domain
|
|
597
|
+
* split into `context/<sub-area>/`. Sibling names arrive the same way and for
|
|
598
|
+
* the same reason, since the folder an entry sits in is what holds them.
|
|
599
|
+
*/
|
|
600
|
+
export function measureEntry(
|
|
601
|
+
rel: string,
|
|
602
|
+
source: string,
|
|
603
|
+
governsContent = true,
|
|
604
|
+
terms?: NarrationTerms,
|
|
605
|
+
siblings: readonly string[] = [],
|
|
606
|
+
): EntryReport {
|
|
607
|
+
const lines = bodyLines(source)
|
|
608
|
+
|
|
609
|
+
return {
|
|
610
|
+
rel,
|
|
611
|
+
lines: source
|
|
612
|
+
.replace(/\n$/, '')
|
|
613
|
+
.split('\n')
|
|
614
|
+
.reduce((sum, text) => sum + renderedHeight(text), 0),
|
|
615
|
+
catalogTables: catalogTables(lines),
|
|
616
|
+
provenance: governsContent ? provenance(lines) : [],
|
|
617
|
+
narration: governsContent && terms ? narration(lines, terms) : [],
|
|
618
|
+
bareReferences: bareReferences(lines, siblings),
|
|
619
|
+
sections: governsContent ? declaredSections(lines) : [],
|
|
620
|
+
stub: isStubSeed(source),
|
|
621
|
+
governed: governsContent,
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* Names each entry past the checkpoint with the standard's three questions
|
|
627
|
+
* answered as far as anything measures them.
|
|
628
|
+
*
|
|
629
|
+
* Only accumulated history is mechanical, and it is already measured by the
|
|
630
|
+
* provenance check, so this joins that count rather than counting again. What
|
|
631
|
+
* it joins is a count of change markers, since a date stamping a measurement is
|
|
632
|
+
* not one and an entry recording what its runs cost would otherwise report
|
|
633
|
+
* accumulated history on the anchors dating those runs. The other two are read
|
|
634
|
+
* by a person: whether an entry still covers one domain is a judgment about its
|
|
635
|
+
* subject, and recognizing content `ls` or `--help` reproduces needs a reader
|
|
636
|
+
* who knows what those emit.
|
|
637
|
+
*
|
|
638
|
+
* An entry outside the governed folder has no measured question at all, since
|
|
639
|
+
* provenance is scoped to the standard stating it, and reporting `no` there
|
|
640
|
+
* would answer from a scan that never ran.
|
|
641
|
+
*
|
|
642
|
+
* Sorted longest first, which is the order the report already printed and the
|
|
643
|
+
* order the questions are worth asking in.
|
|
644
|
+
*/
|
|
645
|
+
export function lengthFindings(
|
|
646
|
+
entries: readonly EntryReport[],
|
|
647
|
+
): LengthFinding[] {
|
|
648
|
+
return entries
|
|
649
|
+
.filter((entry) => entry.lines > LENGTH_CHECKPOINT)
|
|
650
|
+
.sort((a, b) => b.lines - a.lines)
|
|
651
|
+
.map((entry) => ({
|
|
652
|
+
rel: entry.rel,
|
|
653
|
+
lines: entry.lines,
|
|
654
|
+
causes: [
|
|
655
|
+
{ question: 'domain', state: 'unanswered' },
|
|
656
|
+
{ question: 'reproduced', state: 'unanswered' },
|
|
657
|
+
historyCause(entry),
|
|
658
|
+
] satisfies LengthCause[],
|
|
659
|
+
}))
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function historyCause(entry: EntryReport): LengthCause {
|
|
663
|
+
if (!entry.governed) return { question: 'history', state: 'unanswered' }
|
|
664
|
+
|
|
665
|
+
return entry.provenance.length > 0
|
|
666
|
+
? { question: 'history', state: 'yes', markers: entry.provenance.length }
|
|
667
|
+
: { question: 'history', state: 'no' }
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* Measures every entry in the audited folders. A generated `index.md` is not
|
|
672
|
+
* among them, since its body is rewritten on every regen and no checkpoint
|
|
673
|
+
* describes a catalog.
|
|
674
|
+
*
|
|
675
|
+
* Jurisdiction is applied here rather than at the report, so the JSON record
|
|
676
|
+
* and the printed run agree on which entries a content rule reached.
|
|
677
|
+
*/
|
|
678
|
+
export async function measureFolders(
|
|
679
|
+
root: string,
|
|
680
|
+
folders: readonly AuditedFolder[],
|
|
681
|
+
terms?: NarrationTerms,
|
|
682
|
+
): Promise<EntryReport[]> {
|
|
683
|
+
const reports: EntryReport[] = []
|
|
684
|
+
|
|
685
|
+
for (const folder of folders) {
|
|
686
|
+
const names = matchesSiblings(folder)
|
|
687
|
+
? folder.entries.map((path) => basename(path))
|
|
688
|
+
: []
|
|
689
|
+
|
|
690
|
+
for (const path of folder.entries) {
|
|
691
|
+
const self = basename(path)
|
|
692
|
+
|
|
693
|
+
reports.push(
|
|
694
|
+
measureEntry(
|
|
695
|
+
relative(root, path),
|
|
696
|
+
await readFile(path, 'utf8'),
|
|
697
|
+
governsContent(folder),
|
|
698
|
+
terms,
|
|
699
|
+
names.filter((name) => name !== self),
|
|
700
|
+
),
|
|
701
|
+
)
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
return reports
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/** Reports whether the folder's standard is the one carrying the exclusion. */
|
|
709
|
+
export function governsContent(folder: AuditedFolder): boolean {
|
|
710
|
+
return folder.name === PROVENANCE_FOLDER
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
/**
|
|
714
|
+
* Reports whether a bare sibling name here is a reference by construction.
|
|
715
|
+
*
|
|
716
|
+
* A split folder's entries are named for sub-areas of one domain, so a bare
|
|
717
|
+
* name matching one of them points at it and nothing else. The folder named
|
|
718
|
+
* under `.claude/` is where that stops holding, since its entries are named for
|
|
719
|
+
* whole domains and a domain name is a common noun that a seed, a script, or
|
|
720
|
+
* another tree spells the same way. Both false positives this measure was tuned
|
|
721
|
+
* against sat there, naming a seed that shares a filename with the entry beside
|
|
722
|
+
* them, and no signal in the name separates the two.
|
|
723
|
+
*
|
|
724
|
+
* What the exemption costs is the references a future split of the named folder
|
|
725
|
+
* would strand, which are the ones this measure would most like to hold. It is
|
|
726
|
+
* taken because a report firing on correct prose is what teaches a reader to
|
|
727
|
+
* stop reading the section.
|
|
728
|
+
*/
|
|
729
|
+
export function matchesSiblings(folder: AuditedFolder): boolean {
|
|
730
|
+
return governsContent(folder) && folder.nested
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* Names what does not declare the sections the standard requires.
|
|
735
|
+
*
|
|
736
|
+
* Which unit answers depends on what the folder is. A split folder's entries
|
|
737
|
+
* describe one domain between them and carry the overview and the layout in a
|
|
738
|
+
* sibling named for them, so any one of them answers and a per-file rule there
|
|
739
|
+
* would report every other child of all three shipped splits. The entries of
|
|
740
|
+
* the folder named under `.claude/` are one domain each, so each answers for
|
|
741
|
+
* itself. Rolling those up too was the first shape of this check, and it let a
|
|
742
|
+
* single sibling stand in for thirteen domains it says nothing about.
|
|
743
|
+
*
|
|
744
|
+
* The judgment sits in the caller because the split case needs the folder's
|
|
745
|
+
* other entries, which `measureFolders` holds and `measureEntry` does not. A
|
|
746
|
+
* folder with no entries of its own is a split parent holding an index and
|
|
747
|
+
* subfolders, and it has nothing to require a section of.
|
|
748
|
+
*
|
|
749
|
+
* The standard sanctions omitting `## Layout` from a domain owning no paths,
|
|
750
|
+
* which no measure can tell from an entry that forgot it. An entry of that
|
|
751
|
+
* shape therefore reports, which is a reason this is printed and never gated on.
|
|
752
|
+
*/
|
|
753
|
+
export function missingSections(
|
|
754
|
+
root: string,
|
|
755
|
+
folders: readonly AuditedFolder[],
|
|
756
|
+
entries: readonly EntryReport[],
|
|
757
|
+
): SectionFinding[] {
|
|
758
|
+
const byRel = new Map(entries.map((entry) => [entry.rel, entry]))
|
|
759
|
+
const findings: SectionFinding[] = []
|
|
760
|
+
|
|
761
|
+
const shortOf = (declared: readonly string[]): string[] =>
|
|
762
|
+
REQUIRED_SECTIONS.filter((name) => !declared.includes(name))
|
|
763
|
+
|
|
764
|
+
for (const folder of folders) {
|
|
765
|
+
if (!governsContent(folder) || folder.entries.length === 0) continue
|
|
766
|
+
|
|
767
|
+
// A stub owes no sections, so it is dropped before either branch rather
|
|
768
|
+
// than inside them. Leaving one in the split-folder aggregate would let a
|
|
769
|
+
// skeleton answer for the siblings that do owe the sections.
|
|
770
|
+
const reports = folder.entries
|
|
771
|
+
.map((path) => byRel.get(relative(root, path)))
|
|
772
|
+
.filter((entry) => entry !== undefined)
|
|
773
|
+
.filter((entry) => !entry.stub)
|
|
774
|
+
|
|
775
|
+
if (reports.length === 0) continue
|
|
776
|
+
|
|
777
|
+
if (folder.nested) {
|
|
778
|
+
const missing = shortOf(reports.flatMap((entry) => entry.sections))
|
|
779
|
+
if (missing.length > 0) findings.push({ rel: folder.rel, missing })
|
|
780
|
+
continue
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
for (const entry of reports) {
|
|
784
|
+
const missing = shortOf(entry.sections)
|
|
785
|
+
if (missing.length > 0) findings.push({ rel: entry.rel, missing })
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
return findings
|
|
790
|
+
}
|