@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,730 @@
|
|
|
1
|
+
import { type BodyLine, visibleText } from '@/markdown/scan'
|
|
2
|
+
|
|
3
|
+
const HEADING = /^#{1,6}\s/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A section marker holding its line alone, which is the signpost a document
|
|
7
|
+
* uses where its own template asks for bold rather than a heading.
|
|
8
|
+
*
|
|
9
|
+
* `standards/plan.md` gives `## Summary` a heading and marks the four sections
|
|
10
|
+
* below it this way, so a conforming plan read as one run from its first line
|
|
11
|
+
* to its last and all seven live plans reported past the depth checkpoint, at
|
|
12
|
+
* 106 to 166 rendered lines. A measure firing on a whole corpus says nothing
|
|
13
|
+
* about it, and it costs more than silence, since a reader who learns to skip
|
|
14
|
+
* the depth section skips it on the file where a deep run genuinely sits.
|
|
15
|
+
*
|
|
16
|
+
* Breaking on the marker clears two of those seven and moves the other five
|
|
17
|
+
* onto the seams inside them, at 42 to 77 lines, which is the measure telling
|
|
18
|
+
* a long section from a long file for the first time. Moving the template to
|
|
19
|
+
* headings was the alternative and it clears no plan already written, leaving
|
|
20
|
+
* each flagged until someone rewrites it.
|
|
21
|
+
*
|
|
22
|
+
* The marker starts at column zero and the whole line is the marker or none of
|
|
23
|
+
* it is. A bold phrase opening a sentence is emphasis rather than a seam, and
|
|
24
|
+
* an indented one is a label inside a list item, so both stay prose. This ships
|
|
25
|
+
* as package data every project reads, where a missed break costs one unbroken
|
|
26
|
+
* run and a false one shortens every run around it until the measure stops
|
|
27
|
+
* reporting, which is the dearer of the two.
|
|
28
|
+
*
|
|
29
|
+
* A colon ends most markers and not all of them. `claude-pr-review` writes
|
|
30
|
+
* three colon-less ones into every body it posts and a bold path heading for
|
|
31
|
+
* each file it reviews, so requiring the colon held a real seam out. Two
|
|
32
|
+
* signals stand in where the colon is absent, because the shape a marker has to
|
|
33
|
+
* be told from is a sentence set in bold and no one test separates both kinds
|
|
34
|
+
* of marker from it.
|
|
35
|
+
*
|
|
36
|
+
* A label that is one whole code span breaks at any width. A path runs long and
|
|
37
|
+
* a sentence set in bold is never a single span, so shape settles this half
|
|
38
|
+
* where width cannot. Across 81 colon-less markers in the review bodies posted
|
|
39
|
+
* on this repository, the 30 path headings run from 20 to 70 characters, and no
|
|
40
|
+
* colon-less line in the records tree is a whole span at all, so the rule adds
|
|
41
|
+
* reach without adding a false break.
|
|
42
|
+
*
|
|
43
|
+
* `MARKER_WIDTH` covers the rest and is read off the corpus rather than picked.
|
|
44
|
+
* Of the remaining 51 markers in those review bodies, 50 sit at or under 20
|
|
45
|
+
* visible characters, and so do 48 of the 94 colon-less lines in the records
|
|
46
|
+
* tree at `9960a4d7`, every one of them a label. The 21 to 30 band above it
|
|
47
|
+
* holds 8 lines nobody can classify on sight, where `One change across four
|
|
48
|
+
* files.` reads as a sentence and `Rule plus a mechanical half` reads as a
|
|
49
|
+
* seam, so the ceiling sits under that band rather than over it. That is where
|
|
50
|
+
* the asymmetry above points, a false break being the dearer error. Terminal
|
|
51
|
+
* punctuation separates nothing, since 45 of the 48 shortest colon-less lines
|
|
52
|
+
* end in one.
|
|
53
|
+
*
|
|
54
|
+
* Both signals govern the colon-less shape alone. A colon is its own evidence
|
|
55
|
+
* of a label, and capping the colon form as well takes the break back from four
|
|
56
|
+
* markers between 31 and 50 characters that already have it, every one of them
|
|
57
|
+
* a genuine section marker.
|
|
58
|
+
*
|
|
59
|
+
* One marker in that review corpus is reached by neither signal, a 48-character
|
|
60
|
+
* heading a session wrote by hand rather than from the template. Widening to
|
|
61
|
+
* catch it means raising the ceiling back through the band, so it is left as
|
|
62
|
+
* the cheap error the asymmetry above already names.
|
|
63
|
+
*/
|
|
64
|
+
const BOLD_LINE = /^\*\*([^*]+)\*\*\s*$/
|
|
65
|
+
const SPAN_LABEL = /^`[^`]+`$/
|
|
66
|
+
const MARKER_WIDTH = 20
|
|
67
|
+
|
|
68
|
+
const LIST_ITEM = /^(\s*)([-*+]|\d+\.)\s+/
|
|
69
|
+
const TABLE_ROW = /^\s*\|/
|
|
70
|
+
const TABLE_SEPARATOR = /^\s*\|[\s:|-]+\|\s*$/
|
|
71
|
+
const BLOCKQUOTE = /^\s*>/
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* A sentence ends on terminal punctuation followed by the start of another
|
|
75
|
+
* sentence or by the end of the paragraph.
|
|
76
|
+
*
|
|
77
|
+
* Requiring an opening capital after the space is what keeps a version pin and
|
|
78
|
+
* a decimal from each reading as two sentences, since neither is followed by
|
|
79
|
+
* one. An abbreviation ahead of a capitalized word still counts, which
|
|
80
|
+
* over-reports by one on the sentence that carries it and is why this measure
|
|
81
|
+
* reports rather than gates.
|
|
82
|
+
*
|
|
83
|
+
* A code span opens a sentence that carries no capital at all, since a command
|
|
84
|
+
* name is lowercase and the requirement above would catch nothing there. The
|
|
85
|
+
* backtick therefore stands alone rather than joining the delimiters a capital
|
|
86
|
+
* follows, and the price is that a span opening a fragment mid-paragraph reads
|
|
87
|
+
* as a sentence start.
|
|
88
|
+
*/
|
|
89
|
+
const SENTENCE_END = /[.!?]["'’”)\]]*(?=\s+(?:["'“(\[]*[A-Z]|`)|\s*$)/g
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Checkpoints the structural measures run against.
|
|
93
|
+
*
|
|
94
|
+
* These are the definition rather than a fallback. Reading each number out of
|
|
95
|
+
* the sentence `standards/markdown.md` states it in was the alternative, and it
|
|
96
|
+
* put a parser contract on a document authored for people, where a rewording
|
|
97
|
+
* degraded a number and the report had to carry a legend saying which one. The
|
|
98
|
+
* standard still states every number for a reader, and moving one is an edit to
|
|
99
|
+
* both.
|
|
100
|
+
*
|
|
101
|
+
* The last three come from the `## Rhythm` section of the `write-human` skill
|
|
102
|
+
* rather than from a standard. That skill states what good rhythm is and this
|
|
103
|
+
* measures against the statement, so the numbers are read off the sentences
|
|
104
|
+
* that already carry them: a longest and shortest sentence within roughly five
|
|
105
|
+
* words of each other is one cadence, an opening word repeating twice is
|
|
106
|
+
* coincidence and three times is a pattern, and both rules are written about a
|
|
107
|
+
* paragraph, which needs a third sentence before either says anything.
|
|
108
|
+
*/
|
|
109
|
+
export const CHECKPOINTS = {
|
|
110
|
+
run: 40,
|
|
111
|
+
peerBullet: 130,
|
|
112
|
+
bullet: 400,
|
|
113
|
+
paragraph: 700,
|
|
114
|
+
sentences: 4,
|
|
115
|
+
renderWidth: 80,
|
|
116
|
+
cadence: 3,
|
|
117
|
+
spread: 5,
|
|
118
|
+
opener: 2,
|
|
119
|
+
} as const
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* The reading a cadence rate is compared against, rather than a checkpoint.
|
|
123
|
+
*
|
|
124
|
+
* A count with nothing beside it reads as a finding, and the two counts the
|
|
125
|
+
* cadence step reports have no range a reader can place them in: twelve flat
|
|
126
|
+
* paragraphs out of forty says the same as two without one. Naming that a
|
|
127
|
+
* healthy range differs by surface states that a range exists rather than what
|
|
128
|
+
* it looks like, so the reading travels with the command.
|
|
129
|
+
*
|
|
130
|
+
* This is an observation rather than a rule, which is why it sits apart from
|
|
131
|
+
* `CHECKPOINTS`. Nothing compares a run against it and no exit code reads it. A
|
|
132
|
+
* project whose corpus is entirely terse reference prose is expected to sit
|
|
133
|
+
* above the high end, and that is the measure working.
|
|
134
|
+
*
|
|
135
|
+
* Read across 483 markdown files at `6c273324` on 2026-08-20. A number here
|
|
136
|
+
* goes stale against the corpus it describes with nothing comparing the two, so
|
|
137
|
+
* re-measure before moving one.
|
|
138
|
+
*/
|
|
139
|
+
export const BASELINE = {
|
|
140
|
+
/** Share of every measured paragraph sitting at or under the spread checkpoint. */
|
|
141
|
+
flatShare: 8,
|
|
142
|
+
/** Measured paragraphs a file needs before its own rate means anything. */
|
|
143
|
+
floor: 10,
|
|
144
|
+
low: 0,
|
|
145
|
+
median: 6,
|
|
146
|
+
high: 21,
|
|
147
|
+
} as const
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Columns a source line wraps at when rendered.
|
|
151
|
+
*
|
|
152
|
+
* Nothing in this repository sets a line width and entries are authored one
|
|
153
|
+
* line per bullet, so the rendered width is the viewer's rather than the file's.
|
|
154
|
+
*/
|
|
155
|
+
export const RENDER_WIDTH: number = CHECKPOINTS.renderWidth
|
|
156
|
+
|
|
157
|
+
export interface Checkpoints {
|
|
158
|
+
readonly run: number
|
|
159
|
+
readonly peerBullet: number
|
|
160
|
+
readonly bullet: number
|
|
161
|
+
readonly paragraph: number
|
|
162
|
+
readonly sentences: number
|
|
163
|
+
readonly renderWidth: number
|
|
164
|
+
/** Sentences a paragraph needs before its cadence is measured at all. */
|
|
165
|
+
readonly cadence: number
|
|
166
|
+
/** Words between the longest and shortest sentence, at or under which the paragraph reads as one cadence. */
|
|
167
|
+
readonly spread: number
|
|
168
|
+
/** Times one opening word may open a sentence in a paragraph before it is a pattern. */
|
|
169
|
+
readonly opener: number
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export interface BulletFinding {
|
|
173
|
+
readonly line: number
|
|
174
|
+
/** Weight as folded, so a report says how far past the checkpoint it sits. */
|
|
175
|
+
readonly characters: number
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export interface ParagraphFinding {
|
|
179
|
+
readonly line: number
|
|
180
|
+
readonly sentences: number
|
|
181
|
+
readonly characters: number
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* One measured paragraph, carrying both cadence numbers rather than one.
|
|
186
|
+
*
|
|
187
|
+
* A paragraph reported for a narrow spread is usually worth reading for its
|
|
188
|
+
* openers too, and splitting the two into separate finding types would name the
|
|
189
|
+
* same line twice with half the picture on each.
|
|
190
|
+
*/
|
|
191
|
+
export interface CadenceFinding {
|
|
192
|
+
readonly line: number
|
|
193
|
+
readonly sentences: number
|
|
194
|
+
/** Words between the longest and shortest sentence. */
|
|
195
|
+
readonly spread: number
|
|
196
|
+
/** Times the most repeated opening word opens a sentence here. */
|
|
197
|
+
readonly repeats: number
|
|
198
|
+
/** The opening word behind `repeats`, lowercased and stripped of punctuation. */
|
|
199
|
+
readonly opener: string
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* A file's cadence distribution, reported rather than listed finding by finding.
|
|
204
|
+
*
|
|
205
|
+
* Both numbers are advisory in a way even the weight checkpoints are not. A
|
|
206
|
+
* healthy range differs by surface, so terse reference prose and a page written
|
|
207
|
+
* for a reader sit at different spreads and one range applied across the corpus
|
|
208
|
+
* would report the surfaces that are correct. The counts are what a reader
|
|
209
|
+
* compares against the stated range, and the two worst paragraphs are where
|
|
210
|
+
* they open the file.
|
|
211
|
+
*/
|
|
212
|
+
export interface CadenceReport {
|
|
213
|
+
/** Paragraphs carrying at least the floor sentence count. */
|
|
214
|
+
readonly measured: number
|
|
215
|
+
/** Measured paragraphs at or under the spread checkpoint. */
|
|
216
|
+
readonly flat: number
|
|
217
|
+
/** Measured paragraphs past the opener checkpoint. */
|
|
218
|
+
readonly repeating: number
|
|
219
|
+
/** Narrowest paragraph among the flat ones, or undefined when none is flat. */
|
|
220
|
+
readonly flattest: CadenceFinding | undefined
|
|
221
|
+
/** Most repetitive paragraph among the repeating ones, or undefined when none repeats. */
|
|
222
|
+
readonly mostRepeated: CadenceFinding | undefined
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export interface StructureReport {
|
|
226
|
+
readonly rel: string
|
|
227
|
+
/** Rendered lines at the render width, not source lines. */
|
|
228
|
+
readonly longestRun: number
|
|
229
|
+
/** First line of the longest run, or 0 when the file has no run at all. */
|
|
230
|
+
readonly longestRunLine: number
|
|
231
|
+
readonly heavyBullets: readonly BulletFinding[]
|
|
232
|
+
readonly heavyParagraphs: readonly ParagraphFinding[]
|
|
233
|
+
readonly cadence: CadenceReport
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Height a source line occupies once wrapped.
|
|
238
|
+
*
|
|
239
|
+
* A blank line renders as the gap it is rather than as nothing, which keeps it
|
|
240
|
+
* the distance the source measure already counted it as.
|
|
241
|
+
*
|
|
242
|
+
* The width is measured against what renders, since a rendered line shows a
|
|
243
|
+
* link's anchor text rather than its destination and a source measure
|
|
244
|
+
* over-counts exactly where this rule cares how far a reader travels.
|
|
245
|
+
*/
|
|
246
|
+
export function renderedHeight(text: string, width = RENDER_WIDTH): number {
|
|
247
|
+
return Math.max(1, Math.ceil(visibleText(text).length / width))
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Reports whether a run is the peer list the standard exempts.
|
|
252
|
+
*
|
|
253
|
+
* Every non-blank line has to be a list item at one indent. Prose mixed into
|
|
254
|
+
* the run or a nested level inside it ends the exemption, because either one
|
|
255
|
+
* means the block is no longer a flat set a reader can skim. Bullet count says
|
|
256
|
+
* nothing on its own, since a catalog of one-liners and a wall of paragraphs
|
|
257
|
+
* reach the same count and read nothing alike, so the average bullet decides.
|
|
258
|
+
*/
|
|
259
|
+
function isScannablePeerList(
|
|
260
|
+
run: readonly BodyLine[],
|
|
261
|
+
checkpoint: number,
|
|
262
|
+
): boolean {
|
|
263
|
+
const indents = new Set<number>()
|
|
264
|
+
let items = 0
|
|
265
|
+
let characters = 0
|
|
266
|
+
|
|
267
|
+
for (const line of run) {
|
|
268
|
+
const text = line.text.trim()
|
|
269
|
+
if (text === '') continue
|
|
270
|
+
|
|
271
|
+
const match = line.text.match(LIST_ITEM)
|
|
272
|
+
if (!match) return false
|
|
273
|
+
indents.add(match[1].length)
|
|
274
|
+
items++
|
|
275
|
+
characters += visibleText(text).length
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (indents.size !== 1) return false
|
|
279
|
+
|
|
280
|
+
return characters / items < checkpoint
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Reports whether a run is a table, the second shape the checkpoint cannot fix.
|
|
285
|
+
*
|
|
286
|
+
* The peer list above is exempt because it is already navigable. A table is
|
|
287
|
+
* exempt for the other reason: the remedy does not exist. A heading dropped
|
|
288
|
+
* inside one splits the table into two tables rather than breaking the run.
|
|
289
|
+
*
|
|
290
|
+
* Every non-blank line has to be a row, and a delimiter is required rather than
|
|
291
|
+
* assumed. A run holding a table between paragraphs is genuinely mixed and a
|
|
292
|
+
* heading breaks it at a seam either side, and a stack of lines opening with a
|
|
293
|
+
* pipe and no delimiter renders as paragraph text.
|
|
294
|
+
*/
|
|
295
|
+
function isTableRun(run: readonly BodyLine[]): boolean {
|
|
296
|
+
let separators = 0
|
|
297
|
+
|
|
298
|
+
for (const line of run) {
|
|
299
|
+
if (line.text.trim() === '') continue
|
|
300
|
+
if (!TABLE_ROW.test(line.text)) return false
|
|
301
|
+
if (TABLE_SEPARATOR.test(line.text)) separators++
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return separators > 0
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Reports whether a line is a section marker rather than emphasis.
|
|
309
|
+
*
|
|
310
|
+
* `BOLD_LINE` fixes the shape, and a colon-less label then answers to
|
|
311
|
+
* `SPAN_LABEL` or to `MARKER_WIDTH`, per the record above. Both read visible
|
|
312
|
+
* text, which reduces a link to its anchor text and leaves a backticked path
|
|
313
|
+
* counted whole, the same reading `isScannablePeerList` takes. The reduction is
|
|
314
|
+
* what lets a linked path reach the span test at all, since the markup around
|
|
315
|
+
* it would fail the pattern the destination is still attached.
|
|
316
|
+
*/
|
|
317
|
+
function isSectionMarker(text: string): boolean {
|
|
318
|
+
const marker = text.match(BOLD_LINE)
|
|
319
|
+
if (!marker) return false
|
|
320
|
+
|
|
321
|
+
const label = marker[1]
|
|
322
|
+
if (label.endsWith(':')) return true
|
|
323
|
+
|
|
324
|
+
const visible = visibleText(label)
|
|
325
|
+
|
|
326
|
+
return SPAN_LABEL.test(visible) || visible.length <= MARKER_WIDTH
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Measures the longest run of lines no signpost breaks, in rendered lines.
|
|
331
|
+
*
|
|
332
|
+
* A heading breaks a run and so does a section marker, which is the same
|
|
333
|
+
* signpost written the way a template asked for it. `isSectionMarker` above
|
|
334
|
+
* fixes which lines qualify.
|
|
335
|
+
*
|
|
336
|
+
* Fenced lines are skipped rather than treated as breaks, per the standard:
|
|
337
|
+
* they leave the count without ending the run, so prose either side of an
|
|
338
|
+
* example still measures as the one stretch a reader scrolls through. Blank
|
|
339
|
+
* lines do count, since the checkpoint is about how far a reader travels
|
|
340
|
+
* between signposts and a blank line is distance like any other.
|
|
341
|
+
*
|
|
342
|
+
* Height is what a reader travels, and source lines only stand in for it while
|
|
343
|
+
* lines stay short. A file authored one line per bullet puts a paragraph on
|
|
344
|
+
* each, so a block of fifteen bullets measures as fifteen and renders past
|
|
345
|
+
* sixty. Wrapping every line at a stated width is what closes that gap.
|
|
346
|
+
*/
|
|
347
|
+
export function longestRun(
|
|
348
|
+
lines: readonly BodyLine[],
|
|
349
|
+
checkpoints: Checkpoints,
|
|
350
|
+
): { length: number; line: number } {
|
|
351
|
+
let longest = 0
|
|
352
|
+
let longestLine = 0
|
|
353
|
+
let run: BodyLine[] = []
|
|
354
|
+
|
|
355
|
+
const close = (): void => {
|
|
356
|
+
// The reported line is the run's first non-blank one, since that is what an
|
|
357
|
+
// editor should open. A run of nothing but blank lines is the gap between
|
|
358
|
+
// two headings rather than a stretch a reader travels, so it never counts.
|
|
359
|
+
const first = run.find((line) => line.text.trim() !== '')
|
|
360
|
+
|
|
361
|
+
if (
|
|
362
|
+
first &&
|
|
363
|
+
!isScannablePeerList(run, checkpoints.peerBullet) &&
|
|
364
|
+
!isTableRun(run)
|
|
365
|
+
) {
|
|
366
|
+
const height = run.reduce(
|
|
367
|
+
(sum, line) => sum + renderedHeight(line.text, checkpoints.renderWidth),
|
|
368
|
+
0,
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
if (height > longest) {
|
|
372
|
+
longest = height
|
|
373
|
+
longestLine = first.number
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
run = []
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
for (const line of lines) {
|
|
380
|
+
if (line.fenced) continue
|
|
381
|
+
|
|
382
|
+
if (HEADING.test(line.text) || isSectionMarker(line.text)) {
|
|
383
|
+
close()
|
|
384
|
+
continue
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
run.push(line)
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
close()
|
|
391
|
+
|
|
392
|
+
return { length: longest, line: longestLine }
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Finds the top-level bullets carrying more than a decision.
|
|
397
|
+
*
|
|
398
|
+
* A nested item is left out rather than folded into its parent, since the
|
|
399
|
+
* checkpoint asks what one bullet carries and a child carries its own. Lines
|
|
400
|
+
* continuing a bullet do fold in, so a heavy bullet cannot fall under the
|
|
401
|
+
* checkpoint by being wrapped across two source lines. A fenced line closes the
|
|
402
|
+
* open bullet, which keeps a bullet from absorbing the example below it.
|
|
403
|
+
*/
|
|
404
|
+
export function heavyBullets(
|
|
405
|
+
lines: readonly BodyLine[],
|
|
406
|
+
checkpoints: Checkpoints,
|
|
407
|
+
): BulletFinding[] {
|
|
408
|
+
const findings: BulletFinding[] = []
|
|
409
|
+
let open: BulletFinding | null = null
|
|
410
|
+
|
|
411
|
+
const close = (): void => {
|
|
412
|
+
if (open && open.characters > checkpoints.bullet) findings.push(open)
|
|
413
|
+
open = null
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
for (const line of lines) {
|
|
417
|
+
if (line.fenced) {
|
|
418
|
+
close()
|
|
419
|
+
continue
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const item = line.text.match(LIST_ITEM)
|
|
423
|
+
const text = line.text.trim()
|
|
424
|
+
|
|
425
|
+
// Structure is read off the raw line and only the weight is masked. A line
|
|
426
|
+
// carrying nothing but an autolink has no visible text at all, and reading
|
|
427
|
+
// its masked form as blank would close the bullet it continues.
|
|
428
|
+
if (item) {
|
|
429
|
+
close()
|
|
430
|
+
if (item[1].length === 0) {
|
|
431
|
+
open = { line: line.number, characters: visibleText(text).length }
|
|
432
|
+
}
|
|
433
|
+
continue
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (text === '' || HEADING.test(line.text) || TABLE_ROW.test(line.text)) {
|
|
437
|
+
close()
|
|
438
|
+
continue
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// The joining space a wrapped line would have carried, so folding two
|
|
442
|
+
// source lines measures what one unwrapped line would have.
|
|
443
|
+
if (open)
|
|
444
|
+
open = {
|
|
445
|
+
...open,
|
|
446
|
+
characters: open.characters + visibleText(text).length + 1,
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
close()
|
|
451
|
+
|
|
452
|
+
return findings
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Splits a paragraph into the sentences terminal punctuation closes.
|
|
457
|
+
*
|
|
458
|
+
* A trailing span no punctuation closes is dropped rather than returned, which
|
|
459
|
+
* keeps this and the count below one definition. The pattern already matches a
|
|
460
|
+
* paragraph's final period through its end-of-text alternative, so the only
|
|
461
|
+
* span this drops is a paragraph genuinely ending without terminal punctuation,
|
|
462
|
+
* which is a fragment rather than a sentence to either measure.
|
|
463
|
+
*/
|
|
464
|
+
function splitSentences(text: string): string[] {
|
|
465
|
+
const sentences: string[] = []
|
|
466
|
+
let start = 0
|
|
467
|
+
|
|
468
|
+
for (const match of text.matchAll(SENTENCE_END)) {
|
|
469
|
+
const end = match.index + match[0].length
|
|
470
|
+
sentences.push(text.slice(start, end).trim())
|
|
471
|
+
start = end
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
return sentences
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function countSentences(text: string): number {
|
|
478
|
+
return splitSentences(text).length
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Splits the body into its prose paragraphs, each a run of consecutive lines.
|
|
483
|
+
*
|
|
484
|
+
* A heading, a list item, a table row, a blockquote, a blank line, and a fence
|
|
485
|
+
* each end one, so a bullet is measured by `heavyBullets` alone and never
|
|
486
|
+
* twice. Both paragraph measures walk this rather than one each, since two
|
|
487
|
+
* walks deciding what a paragraph is would drift apart.
|
|
488
|
+
*/
|
|
489
|
+
function paragraphBlocks(lines: readonly BodyLine[]): BodyLine[][] {
|
|
490
|
+
const blocks: BodyLine[][] = []
|
|
491
|
+
let block: BodyLine[] = []
|
|
492
|
+
|
|
493
|
+
const close = (): void => {
|
|
494
|
+
if (block.length > 0) blocks.push(block)
|
|
495
|
+
block = []
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
for (const line of lines) {
|
|
499
|
+
const text = line.text.trim()
|
|
500
|
+
const breaks =
|
|
501
|
+
line.fenced ||
|
|
502
|
+
text === '' ||
|
|
503
|
+
HEADING.test(line.text) ||
|
|
504
|
+
LIST_ITEM.test(line.text) ||
|
|
505
|
+
TABLE_ROW.test(line.text) ||
|
|
506
|
+
BLOCKQUOTE.test(line.text)
|
|
507
|
+
|
|
508
|
+
if (breaks) {
|
|
509
|
+
close()
|
|
510
|
+
continue
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
block.push(line)
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
close()
|
|
517
|
+
|
|
518
|
+
return blocks
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/** Folds a paragraph's source lines into the one line they would have wrapped from. */
|
|
522
|
+
function paragraphText(block: readonly BodyLine[]): string {
|
|
523
|
+
return block.map((line) => line.text.trim()).join(' ')
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Finds the prose paragraphs past either half of the standard's checkpoint.
|
|
528
|
+
*
|
|
529
|
+
* Both halves are stated in the standard and both are read from it. The weight
|
|
530
|
+
* half was added there rather than borrowed from the bullet checkpoint, which
|
|
531
|
+
* governs a different construct: one number feeding both would move the
|
|
532
|
+
* paragraph rule whenever the bullet rule was changed, and an author reading
|
|
533
|
+
* the sentence cap would find no weight rule to read at all.
|
|
534
|
+
*
|
|
535
|
+
* The two numbers coincide today because paragraph and bullet weight measure
|
|
536
|
+
* one population, sharing a median near 170 characters with no gap behind
|
|
537
|
+
* either candidate. They are separate checkpoints regardless, so either moves
|
|
538
|
+
* without dragging the other.
|
|
539
|
+
*/
|
|
540
|
+
export function heavyParagraphs(
|
|
541
|
+
lines: readonly BodyLine[],
|
|
542
|
+
checkpoints: Checkpoints,
|
|
543
|
+
): ParagraphFinding[] {
|
|
544
|
+
const findings: ParagraphFinding[] = []
|
|
545
|
+
|
|
546
|
+
for (const block of paragraphBlocks(lines)) {
|
|
547
|
+
const text = paragraphText(block)
|
|
548
|
+
const sentences = countSentences(text)
|
|
549
|
+
const characters = visibleText(text).length
|
|
550
|
+
|
|
551
|
+
if (
|
|
552
|
+
sentences > checkpoints.sentences ||
|
|
553
|
+
characters > checkpoints.paragraph
|
|
554
|
+
) {
|
|
555
|
+
findings.push({ line: block[0].number, sentences, characters })
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
return findings
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/** Punctuation either side of a word, so an opener is compared on its letters. */
|
|
563
|
+
const WORD_EDGE = /^[^\p{L}\p{N}]+|[^\p{L}\p{N}]+$/gu
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* The word a sentence opens on, lowercased and stripped of punctuation.
|
|
567
|
+
*
|
|
568
|
+
* A sentence opening on a code span yields the command inside it rather than a
|
|
569
|
+
* backtick, which is the word a reader hears. Casing is dropped because an
|
|
570
|
+
* opener repeating is a repetition whether or not one of the two sits mid-list
|
|
571
|
+
* and lost its capital.
|
|
572
|
+
*/
|
|
573
|
+
function openingWord(sentence: string): string {
|
|
574
|
+
const [first = ''] = sentence.split(/\s+/)
|
|
575
|
+
return first.replace(WORD_EDGE, '').toLowerCase()
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Measures one paragraph's spread and its most repeated opener.
|
|
580
|
+
*
|
|
581
|
+
* Words are counted off the text a reader is shown, so a link contributes its
|
|
582
|
+
* anchor text and a destination contributes nothing. The sentence boundaries do
|
|
583
|
+
* not move with that masking, since the pattern requires whitespace after the
|
|
584
|
+
* terminal punctuation and no destination or autolink carries any.
|
|
585
|
+
*/
|
|
586
|
+
function measureParagraph(block: readonly BodyLine[]): CadenceFinding {
|
|
587
|
+
const sentences = splitSentences(visibleText(paragraphText(block)))
|
|
588
|
+
const lengths = sentences.map(
|
|
589
|
+
(sentence) => sentence.split(/\s+/).filter(Boolean).length,
|
|
590
|
+
)
|
|
591
|
+
|
|
592
|
+
const counts = new Map<string, number>()
|
|
593
|
+
for (const sentence of sentences) {
|
|
594
|
+
const word = openingWord(sentence)
|
|
595
|
+
if (word !== '') counts.set(word, (counts.get(word) ?? 0) + 1)
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
let opener = ''
|
|
599
|
+
let repeats = 0
|
|
600
|
+
for (const [word, count] of counts) {
|
|
601
|
+
if (count > repeats) {
|
|
602
|
+
opener = word
|
|
603
|
+
repeats = count
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// A paragraph closing no sentence at all has no spread rather than a
|
|
608
|
+
// negative one, and the floor drops it before either number is read.
|
|
609
|
+
const spread =
|
|
610
|
+
lengths.length === 0 ? 0 : Math.max(...lengths) - Math.min(...lengths)
|
|
611
|
+
|
|
612
|
+
return {
|
|
613
|
+
line: block[0].number,
|
|
614
|
+
sentences: sentences.length,
|
|
615
|
+
spread,
|
|
616
|
+
repeats,
|
|
617
|
+
opener,
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* Reports how a file's prose is distributed across sentence length and opening
|
|
623
|
+
* word, which is the layer the ban sets cannot reach.
|
|
624
|
+
*
|
|
625
|
+
* A banned-word list expresses negatives and every symptom this answers is the
|
|
626
|
+
* absence of something, so no addition to that set catches a paragraph whose
|
|
627
|
+
* sentences are all one length. The measure stops at what is countable. A
|
|
628
|
+
* sentence's grammatical shape and whether it carries a finite verb are the two
|
|
629
|
+
* rules `write-human` states that this does not implement. Reporting them wrong
|
|
630
|
+
* is worse than not reporting them, since they name the exact failure this
|
|
631
|
+
* exists to measure.
|
|
632
|
+
*
|
|
633
|
+
* That was a prediction and it has now been measured, against two parsers
|
|
634
|
+
* rather than against the idea of one. Both ran over 11,389 paragraph sentences
|
|
635
|
+
* across 503 markdown files at `c7e92612` on 2026-08-20, and they disagree by
|
|
636
|
+
* a factor of four on a number one of them has to be wrong about.
|
|
637
|
+
*
|
|
638
|
+
* `compromise` reported 2 percent. It tags a fronted past participle as a
|
|
639
|
+
* finite past-tense verb, so all twelve of those sentences in
|
|
640
|
+
* `.claude/ARCHITECTURE.md` read as carrying one, eleven opening `Measured at`
|
|
641
|
+
* and one `Overturned by`, which is the exact shape the measure exists to
|
|
642
|
+
* catch. It is not even consistent with itself there: `Measured at` tags a verb
|
|
643
|
+
* and `Measured against` tags an adjective.
|
|
644
|
+
*
|
|
645
|
+
* `wink-pos-tagger` reported 9 percent and fixes that class, tagging the
|
|
646
|
+
* participle `VBN`. Roughly three quarters of what it flags is still wrong:
|
|
647
|
+
* hand-classifying every 21st of the 1,071 flagged sentences put 12 of 51
|
|
648
|
+
* genuinely verbless. Two classes split the other 39 almost evenly and neither
|
|
649
|
+
* is a tuning problem. Nineteen are imperatives, which this instructional
|
|
650
|
+
* corpus is full of and whose verb tags as a proper noun as often as a verb.
|
|
651
|
+
* Twenty are ordinary declaratives whose predicate is noun-ambiguous and tags
|
|
652
|
+
* as a noun outright, so `Each maps to a skill.` and `Nothing checks either
|
|
653
|
+
* one.` both report verbless. This corpus is built from exactly those words:
|
|
654
|
+
* reports, answers, checks, maps, holds, names, carries, records, measures.
|
|
655
|
+
*
|
|
656
|
+
* Separating them needs to know which token is the predicate, which is syntax
|
|
657
|
+
* rather than a tag. Neither tagger carries one and nothing was found worth
|
|
658
|
+
* taking for a number nothing gates on, so the rule the deferral set is met and
|
|
659
|
+
* still returns no: a parse was tried, twice, and neither produced a number
|
|
660
|
+
* worth printing beside two that hold.
|
|
661
|
+
*
|
|
662
|
+
* Where a shape sits is a different question and is already answered. A heading
|
|
663
|
+
* fragment, a list item, and a table cell are all legitimately verbless and
|
|
664
|
+
* none of them reaches this measure, because `paragraphBlocks` ends a paragraph
|
|
665
|
+
* on each. That exclusion is structural and needs no grammar. The imperative is
|
|
666
|
+
* the one that sits in paragraph prose, and it is what neither parser could
|
|
667
|
+
* separate out.
|
|
668
|
+
*
|
|
669
|
+
* A paragraph under the floor is skipped rather than scored. A two-sentence
|
|
670
|
+
* configuration note has no spread worth reading, and the opener rule is
|
|
671
|
+
* written about a third sentence turning a coincidence into a pattern, so
|
|
672
|
+
* neither measure says anything before the floor is reached.
|
|
673
|
+
*/
|
|
674
|
+
export function measureCadence(
|
|
675
|
+
lines: readonly BodyLine[],
|
|
676
|
+
checkpoints: Checkpoints,
|
|
677
|
+
): CadenceReport {
|
|
678
|
+
const measured = paragraphBlocks(lines)
|
|
679
|
+
.map(measureParagraph)
|
|
680
|
+
.filter((finding) => finding.sentences >= checkpoints.cadence)
|
|
681
|
+
|
|
682
|
+
const flat = measured.filter(
|
|
683
|
+
(finding) => finding.spread <= checkpoints.spread,
|
|
684
|
+
)
|
|
685
|
+
const repeating = measured.filter(
|
|
686
|
+
(finding) => finding.repeats > checkpoints.opener,
|
|
687
|
+
)
|
|
688
|
+
|
|
689
|
+
return {
|
|
690
|
+
measured: measured.length,
|
|
691
|
+
flat: flat.length,
|
|
692
|
+
repeating: repeating.length,
|
|
693
|
+
flattest: worst(flat, (finding) => -finding.spread),
|
|
694
|
+
mostRepeated: worst(repeating, (finding) => finding.repeats),
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/**
|
|
699
|
+
* The file's worst paragraph on one measure, earliest line breaking a tie.
|
|
700
|
+
*
|
|
701
|
+
* Only paragraphs already past their checkpoint are passed in, so a file
|
|
702
|
+
* reading healthy names nothing rather than naming its least healthy paragraph,
|
|
703
|
+
* which a reader would take for a finding.
|
|
704
|
+
*/
|
|
705
|
+
function worst(
|
|
706
|
+
findings: readonly CadenceFinding[],
|
|
707
|
+
rank: (finding: CadenceFinding) => number,
|
|
708
|
+
): CadenceFinding | undefined {
|
|
709
|
+
return findings.reduce<CadenceFinding | undefined>(
|
|
710
|
+
(held, finding) => (!held || rank(finding) > rank(held) ? finding : held),
|
|
711
|
+
undefined,
|
|
712
|
+
)
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
export function measureStructure(
|
|
716
|
+
rel: string,
|
|
717
|
+
lines: readonly BodyLine[],
|
|
718
|
+
checkpoints: Checkpoints,
|
|
719
|
+
): StructureReport {
|
|
720
|
+
const run = longestRun(lines, checkpoints)
|
|
721
|
+
|
|
722
|
+
return {
|
|
723
|
+
rel,
|
|
724
|
+
longestRun: run.length,
|
|
725
|
+
longestRunLine: run.line,
|
|
726
|
+
heavyBullets: heavyBullets(lines, checkpoints),
|
|
727
|
+
heavyParagraphs: heavyParagraphs(lines, checkpoints),
|
|
728
|
+
cadence: measureCadence(lines, checkpoints),
|
|
729
|
+
}
|
|
730
|
+
}
|