@erclx/canon 5.30.0 → 5.32.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/README.md +1 -1
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/api-design/references/adopted.md +1 -1
- package/claude/skills/auto-ship/REQUIREMENT.md +4 -16
- package/claude/skills/auto-ship/SKILL.md +13 -58
- package/claude/skills/auto-ship/references/draft-mark.md +2 -2
- package/claude/skills/auto-ship/references/failure-recovery.md +1 -5
- package/claude/skills/auto-ship/references/tier-failures.md +1 -1
- package/claude/skills/auto-ship/references/ui-checklist.md +2 -2
- package/claude/skills/auto-ship/references/verb-absent.md +3 -27
- package/claude/skills/backlog-triage/SKILL.md +1 -1
- package/claude/skills/build-in-slices/REQUIREMENT.md +1 -1
- package/claude/skills/build-in-slices/references/adopted.md +1 -1
- package/claude/skills/canon-cli/REQUIREMENT.md +4 -6
- package/claude/skills/canon-cli/SKILL.md +2 -2
- package/claude/skills/canon-feedback/REQUIREMENT.md +1 -1
- package/claude/skills/canon-feedback/SKILL.md +10 -13
- package/claude/skills/canon-feedback-triage/REQUIREMENT.md +13 -14
- package/claude/skills/canon-feedback-triage/SKILL.md +50 -29
- package/claude/skills/canvas/REQUIREMENT.md +3 -4
- package/claude/skills/canvas/SKILL.md +5 -5
- package/{standards → claude/skills/canvas/references}/canvas.md +12 -1
- package/claude/skills/ci-workflow/REQUIREMENT.md +0 -1
- package/claude/skills/context-fold/REQUIREMENT.md +8 -20
- package/claude/skills/context-fold/SKILL.md +21 -45
- package/claude/skills/context-fold/references/classify.md +7 -8
- package/claude/skills/context-fold/references/completion-report.md +2 -6
- package/claude/skills/context-fold/references/context-refresh.md +5 -15
- package/claude/skills/context-fold/references/promoted-pages.md +3 -3
- package/claude/skills/context-fold/references/receipt-sweep.md +6 -17
- package/claude/skills/context-fold/references/unusable-baseline.md +4 -6
- package/claude/skills/create-rule/REQUIREMENT.md +0 -1
- package/claude/skills/create-skill/REQUIREMENT.md +6 -4
- package/claude/skills/create-skill/SKILL.md +9 -8
- package/{standards → claude/skills/create-skill/references}/skill-paths.md +1 -0
- package/claude/skills/decision-escalate/REQUIREMENT.md +1 -1
- package/claude/skills/decision-escalate/SKILL.md +1 -1
- package/claude/skills/design-taste/REQUIREMENT.md +0 -2
- package/claude/skills/design-taste/SKILL.md +2 -4
- package/claude/skills/design-taste/references/vocabulary.md +1 -1
- package/claude/skills/docs-sync/REQUIREMENT.md +3 -3
- package/claude/skills/docs-sync/SKILL.md +1 -1
- package/claude/skills/document-health/REQUIREMENT.md +2 -2
- package/claude/skills/document-health/SKILL.md +4 -5
- package/claude/skills/document-health/references/axes.md +4 -6
- package/claude/skills/draft-and-pick/REQUIREMENT.md +16 -14
- package/claude/skills/draft-and-pick/SKILL.md +40 -32
- package/claude/skills/draft-and-pick/references/live-arms.md +9 -9
- package/claude/skills/draft-doc/REQUIREMENT.md +5 -5
- package/claude/skills/draft-doc/SKILL.md +10 -11
- package/claude/skills/draft-doc/references/requirements.md +61 -0
- package/claude/skills/draft-doc/references/wiki-kind.md +53 -0
- package/claude/skills/draft-doc/references/wiki.md +50 -30
- package/claude/skills/draft-figure/REQUIREMENT.md +2 -2
- package/claude/skills/draft-figure/SKILL.md +5 -5
- package/claude/skills/draft-figure/references/architecture-set.md +4 -4
- package/claude/skills/draft-figure/references/hand-drawn.md +1 -1
- package/claude/skills/draft-figure/references/mermaid-path.md +55 -0
- package/claude/skills/draft-figure/references/mermaid.md +43 -34
- package/claude/skills/draft-identity/REQUIREMENT.md +1 -1
- package/claude/skills/draft-identity/SKILL.md +9 -7
- package/claude/skills/draft-ready/REQUIREMENT.md +1 -1
- package/claude/skills/draft-ready/SKILL.md +6 -6
- package/claude/skills/draft-ready/references/assembly.md +1 -1
- package/{standards → claude/skills/draft-ready/references}/ready.md +13 -5
- package/claude/skills/git-issue/SKILL.md +1 -1
- package/claude/skills/git-issue/references/issue.md +3 -3
- package/claude/skills/git-pr/SKILL.md +1 -1
- package/claude/skills/git-ship/REQUIREMENT.md +0 -2
- package/claude/skills/git-ship/SKILL.md +16 -24
- package/claude/skills/git-stage/SKILL.md +1 -1
- package/claude/skills/markdown-craft/REQUIREMENT.md +23 -0
- package/claude/skills/markdown-craft/SKILL.md +17 -0
- package/{standards → claude/skills/markdown-craft/references}/markdown.md +14 -7
- package/claude/skills/plan-feature/REQUIREMENT.md +2 -1
- package/claude/skills/plan-feature/SKILL.md +14 -8
- package/{standards → claude/skills/plan-feature/references}/plan.md +33 -21
- package/claude/skills/plan-groundwork/SKILL.md +5 -6
- package/{standards → claude/skills/plan-groundwork/references}/groundwork.md +17 -4
- package/claude/skills/plan-intake/SKILL.md +2 -3
- package/{standards → claude/skills/plan-intake/references}/intake.md +17 -2
- package/claude/skills/plan-intake-answer/REQUIREMENT.md +1 -1
- package/claude/skills/plan-intake-answer/SKILL.md +1 -1
- package/claude/skills/review-address/SKILL.md +2 -2
- package/claude/skills/review-address/references/post-review-findings.md +1 -1
- package/claude/skills/review-craft/REQUIREMENT.md +3 -3
- package/claude/skills/review-craft/SKILL.md +2 -2
- package/claude/skills/review-craft/references/ui.md +4 -4
- package/claude/skills/review-pr/REQUIREMENT.md +1 -1
- package/claude/skills/review-pr/SKILL.md +5 -7
- package/claude/skills/review-pr/references/post.md +2 -2
- package/claude/skills/review-ui/REQUIREMENT.md +0 -1
- package/claude/skills/review-ui/SKILL.md +1 -1
- package/claude/skills/review-ui/references/post.md +1 -1
- package/claude/skills/role-orchestrator/REQUIREMENT.md +0 -3
- package/claude/skills/role-orchestrator/SKILL.md +8 -8
- package/claude/skills/role-orchestrator/references/orchestrator-dispatch.md +4 -3
- package/claude/skills/role-orchestrator/references/orchestrator-handoff.md +7 -10
- package/claude/skills/role-orchestrator/references/orchestrator-parked.md +2 -2
- package/claude/skills/role-orchestrator/references/orchestrator-refill.md +0 -3
- package/claude/skills/role-orchestrator/references/orchestrator-resume.md +1 -1
- package/claude/skills/role-planner/SKILL.md +2 -2
- package/claude/skills/role-reviewer/SKILL.md +1 -1
- package/claude/skills/role-worker/SKILL.md +2 -2
- package/claude/skills/session-compact/REQUIREMENT.md +1 -4
- package/claude/skills/session-compact/SKILL.md +3 -13
- package/claude/skills/session-compact/references/handoff-note.md +2 -2
- package/claude/skills/session-map/REQUIREMENT.md +1 -9
- package/claude/skills/session-map/SKILL.md +8 -16
- package/{standards → claude/skills/session-map/references}/session.md +17 -11
- package/claude/skills/session-relay/REQUIREMENT.md +1 -1
- package/claude/skills/session-resume/REQUIREMENT.md +4 -6
- package/claude/skills/session-resume/SKILL.md +3 -9
- package/claude/skills/session-worktree/SKILL.md +1 -1
- package/claude/skills/session-worktree/references/main-root-writes.md +2 -2
- package/claude/skills/systematic-debugging/REQUIREMENT.md +0 -1
- package/claude/skills/target-check/REQUIREMENT.md +3 -3
- package/claude/skills/target-check/SKILL.md +1 -1
- package/claude/skills/target-check/references/domains.md +1 -1
- package/claude/skills/target-setup/REQUIREMENT.md +4 -4
- package/claude/skills/target-setup/SKILL.md +3 -3
- package/claude/skills/target-setup/references/detect.md +1 -1
- package/claude/skills/target-setup/references/indexes.md +1 -1
- package/claude/skills/task-board/SKILL.md +4 -4
- package/claude/skills/task-board/references/archive.md +1 -1
- package/{standards → claude/skills/task-board/references}/board.md +9 -0
- package/claude/skills/task-board/references/decline.md +1 -1
- package/{standards → claude/skills/task-board/references}/tasks.md +13 -8
- package/claude/skills/teach-workspace/SKILL.md +1 -1
- package/claude/skills/teach-workspace/references/revisit.md +1 -1
- package/{standards → claude/skills/teach-workspace/references}/teach.md +17 -5
- package/claude/skills/test-first/REQUIREMENT.md +1 -1
- package/claude/skills/ux-audit/REQUIREMENT.md +2 -2
- package/claude/skills/ux-audit/SKILL.md +2 -3
- package/claude/skills/write-human/REQUIREMENT.md +0 -1
- package/claude/skills/write-human/SKILL.md +2 -2
- package/docs/agents/audits.md +1 -1
- package/docs/agents/canvas.md +1 -1
- package/docs/agents/commands.md +6 -10
- package/docs/agents/context-audit-checks.md +10 -24
- package/docs/agents/context-audit.md +7 -15
- package/docs/agents/context-classify.md +5 -5
- package/docs/agents/counts.md +5 -5
- package/docs/agents/drift-surfaces.md +2 -2
- package/docs/agents/index.md +3 -4
- package/docs/agents/install-and-sync.md +6 -6
- package/docs/agents/intake.md +1 -1
- package/docs/agents/markdown-audit.md +1 -1
- package/docs/agents/records-stale.md +5 -5
- package/docs/agents/records-validate.md +1 -1
- package/docs/agents/records.md +2 -2
- package/docs/agents/restated.md +1 -1
- package/docs/agents/routing.md +1 -1
- package/docs/agents/rule-citations.md +1 -1
- package/docs/agents/skills-audit.md +2 -2
- package/docs/agents/state-scoped-risk.md +2 -2
- package/docs/agents/superseded.md +2 -2
- package/docs/agents/tasks-plans.md +2 -2
- package/docs/agents/tasks-validate.md +1 -1
- package/docs/agents/tasks.md +1 -1
- package/docs/agents/teach.md +1 -1
- package/docs/agents/test-order.md +1 -1
- package/docs/agents/upstream.md +94 -0
- package/docs/target/migrations.md +24 -0
- package/docs/target/projects.md +5 -7
- package/docs/target/scenarios.md +2 -2
- package/docs/target/sync.md +3 -3
- package/docs/workflow/ai-workflow.md +7 -38
- package/docs/workflow/index.md +1 -1
- package/docs/workflow/operating-model.md +8 -8
- package/docs/workflow/parallel-features.md +7 -7
- package/docs/workflow/skill-map-on-request.md +13 -16
- package/docs/workflow/skill-map.md +20 -32
- package/docs/workflow/visual-design-workflow.md +11 -14
- package/governance/renames.toml +0 -23
- package/governance/rules/canon/605-worktrees.md +1 -1
- package/governance/rules/{standards → canon}/plan.md +5 -5
- package/governance/rules/canon/tasks.md +18 -0
- package/governance/rules/writing/markdown.md +23 -0
- package/governance/stacks/base.toml +1 -1
- package/package.json +1 -1
- package/scripts/core/install-check.sh +2 -2
- package/scripts/core/regen-web-previews.ts +1 -1
- package/src/audits/catalog.ts +5 -97
- package/src/claude/cases/authoring.ts +5 -18
- package/src/claude/cases/misc.ts +0 -17
- package/src/claude/cases/setup.ts +3 -20
- package/src/claude/cases/workflow.ts +0 -33
- package/src/claude/seeds.ts +1 -7
- package/src/claude/skills-audit.ts +1 -1
- package/src/claude/skills-reach.ts +1 -1
- package/src/cli/help.ts +3 -4
- package/src/cli.ts +2 -4
- package/src/commands/claude.ts +9 -15
- package/src/commands/context.ts +18 -280
- package/src/commands/feedback.ts +22 -91
- package/src/commands/gov.ts +2 -2
- package/src/commands/records.ts +24 -6
- package/src/commands/sync.ts +5 -3
- package/src/commands/upstream.ts +430 -0
- package/src/context/audit.ts +6 -15
- package/src/context/classify/extract.ts +6 -13
- package/src/context/classify/patterns.ts +2 -41
- package/src/context/classify/prompts.ts +2 -6
- package/src/context/classify/run.ts +3 -3
- package/src/context/folders.ts +7 -12
- package/src/context/gate.ts +11 -91
- package/src/context/requirements.ts +13 -4
- package/src/counts/scan.ts +3 -3
- package/src/design/components.ts +2 -2
- package/src/feedback/body.ts +0 -12
- package/src/gate/measures.ts +6 -57
- package/src/gate/stages.ts +0 -1
- package/src/gov/citations.ts +9 -3
- package/src/gov/restated.ts +3 -3
- package/src/markdown/structure.ts +3 -3
- package/src/migrate/record-layout.ts +3 -3
- package/src/records/backup.ts +1 -0
- package/src/records/ordinal.ts +1 -1
- package/src/records/prune.ts +2 -1
- package/src/records/stale-canonical.ts +2 -2
- package/src/records/validate.ts +55 -0
- package/src/roots/record.ts +2 -1
- package/src/sandbox/scenario.ts +115 -11
- package/src/shipped/references.ts +3 -3
- package/src/standards/read.ts +52 -1
- package/src/sync/seeds-report.ts +1 -1
- package/src/tasks/answers.ts +14 -1
- package/src/tasks/reach.ts +2 -2
- package/src/tasks/stack.ts +176 -0
- package/src/tasks/validate.ts +1 -1
- package/src/teach/workspace.ts +1 -1
- package/src/upstream/catalog.ts +112 -0
- package/src/upstream/cursor.ts +120 -0
- package/src/upstream/digest.ts +75 -0
- package/src/upstream/due.ts +128 -0
- package/src/upstream/releases.ts +170 -0
- package/standards/context.md +3 -5
- package/standards/design.md +2 -3
- package/standards/index.md +0 -23
- package/standards/publish.md +1 -1
- package/standards/rule.md +1 -1
- package/standards/skill.md +1 -1
- package/tooling/base/reference.md +1 -1
- package/tooling/claude/reference.md +6 -20
- package/tooling/claude/seeds/.claude/hooks/index-reminder.sh +1 -1
- package/tooling/claude/seeds/.claude/hooks/records-index.sh +8 -20
- package/tooling/claude/seeds/.claude/hooks/standards-audit.sh +3 -3
- package/tooling/claude/seeds/CLAUDE.md +1 -3
- package/tooling/claude/seeds/canon/REQUIREMENTS.md +2 -9
- package/tooling/web/configs/e2e/screenshot.ts +0 -1
- package/claude/skills/auto-ship/references/review-findings.md +0 -20
- package/claude/skills/bash-cli-script/REQUIREMENT.md +0 -43
- package/claude/skills/bash-cli-script/SKILL.md +0 -53
- package/claude/skills/bash-cli-script/references/template.md +0 -19
- package/claude/skills/canon-operator/REQUIREMENT.md +0 -62
- package/claude/skills/canon-operator/SKILL.md +0 -113
- package/claude/skills/canon-rollout/REQUIREMENT.md +0 -59
- package/claude/skills/canon-rollout/SKILL.md +0 -140
- package/claude/skills/canon-rollout/references/failure-recovery.md +0 -20
- package/claude/skills/context-fold/references/anchor-sweep.md +0 -58
- package/claude/skills/context-fold/references/wireframe-sweep.md +0 -45
- package/claude/skills/create-standard/REQUIREMENT.md +0 -33
- package/claude/skills/create-standard/SKILL.md +0 -31
- package/claude/skills/design-extract/REQUIREMENT.md +0 -42
- package/claude/skills/design-extract/SKILL.md +0 -108
- package/claude/skills/draft-and-pick/references/frame.html +0 -118
- package/claude/skills/draft-doc/references/wireframes.md +0 -48
- package/claude/skills/index-lookup/REQUIREMENT.md +0 -35
- package/claude/skills/index-lookup/SKILL.md +0 -44
- package/claude/skills/markdown-propose/REQUIREMENT.md +0 -47
- package/claude/skills/markdown-propose/SKILL.md +0 -122
- package/claude/skills/markdown-propose/references/format.md +0 -118
- package/claude/skills/memory-capture/REQUIREMENT.md +0 -50
- package/claude/skills/memory-capture/SKILL.md +0 -117
- package/claude/skills/memory-review/REQUIREMENT.md +0 -52
- package/claude/skills/memory-review/SKILL.md +0 -129
- package/claude/skills/memory-review/references/later-phases.md +0 -118
- package/claude/skills/memory-review/references/receipt-format.md +0 -64
- package/claude/skills/project-commands/REQUIREMENT.md +0 -42
- package/claude/skills/project-commands/SKILL.md +0 -87
- package/claude/skills/repo-metadata/REQUIREMENT.md +0 -37
- package/claude/skills/repo-metadata/SKILL.md +0 -54
- package/claude/skills/restate-plainly/REQUIREMENT.md +0 -41
- package/claude/skills/restate-plainly/SKILL.md +0 -41
- package/claude/skills/review-branch/REQUIREMENT.md +0 -40
- package/claude/skills/review-branch/SKILL.md +0 -137
- package/claude/skills/seed-sync/REQUIREMENT.md +0 -45
- package/claude/skills/seed-sync/SKILL.md +0 -158
- package/claude/skills/sketch-design/REQUIREMENT.md +0 -38
- package/claude/skills/sketch-design/SKILL.md +0 -94
- package/claude/skills/ux-walkthrough/REQUIREMENT.md +0 -56
- package/claude/skills/ux-walkthrough/SKILL.md +0 -53
- package/claude/skills/ux-walkthrough/references/builds.md +0 -15
- package/claude/skills/ux-walkthrough/references/candidate-pages.md +0 -11
- package/claude/skills/ux-walkthrough/references/measuring.md +0 -22
- package/claude/skills/ux-walkthrough/references/record.md +0 -36
- package/claude/skills/ux-walkthrough/references/relay.md +0 -25
- package/docs/agents/architecture-record.md +0 -36
- package/docs/agents/review-classification.md +0 -76
- package/governance/rules/canon/601-indexes.md +0 -9
- package/governance/rules/canon/603-memory.md +0 -11
- package/governance/rules/claude/565-behavior.md +0 -27
- package/governance/rules/claude/567-planning.md +0 -16
- package/governance/rules/claude/575-hooks.md +0 -16
- package/governance/rules/claude/576-settings.md +0 -13
- package/governance/rules/claude/592-claude-md.md +0 -19
- package/governance/rules/lang/120-bash.md +0 -18
- package/governance/rules/standards/architecture.md +0 -13
- package/governance/rules/standards/groundwork.md +0 -13
- package/governance/rules/standards/intake.md +0 -12
- package/governance/rules/standards/markdown.md +0 -16
- package/governance/rules/standards/memory.md +0 -12
- package/governance/rules/standards/ready.md +0 -13
- package/governance/rules/standards/requirements.md +0 -12
- package/governance/rules/standards/session.md +0 -13
- package/governance/rules/standards/tasks.md +0 -15
- package/governance/rules/standards/teach.md +0 -14
- package/governance/rules/standards/wiki.md +0 -13
- package/governance/rules/standards/wireframes.md +0 -13
- package/governance/rules/writing/800-prose.md +0 -15
- package/src/autoship/classify.ts +0 -75
- package/src/autoship/paths.ts +0 -50
- package/src/commands/autoship.ts +0 -129
- package/src/commands/repo.ts +0 -393
- package/src/context/architecture.ts +0 -654
- package/src/context/wireframe-states.ts +0 -238
- package/src/repo/metadata.ts +0 -206
- package/standards/architecture.md +0 -127
- package/standards/memory.md +0 -158
- package/standards/mermaid.md +0 -64
- package/standards/requirements.md +0 -79
- package/standards/wiki.md +0 -77
- package/standards/wireframes.md +0 -143
- package/tooling/claude/seeds/.claude/memory/index.md +0 -8
- package/tooling/claude/seeds/canon/ARCHITECTURE.md +0 -23
- package/tooling/claude/seeds/canon/wireframes/index.md +0 -8
- /package/{standards → claude/skills/create-skill/references}/skill-practice.md +0 -0
- /package/{standards → claude/skills/create-skill/references}/skill-requirement.md +0 -0
- /package/{standards → claude/skills/plan-feature/references}/plan-lifecycle.md +0 -0
- /package/{standards → claude/skills/plan-groundwork/references}/groundwork-spikes.md +0 -0
- /package/{standards → claude/skills/task-board/references}/tasks-decline.md +0 -0
- /package/{standards → claude/skills/teach-workspace/references}/glossary.md +0 -0
|
@@ -1,654 +0,0 @@
|
|
|
1
|
-
import { access, readFile } from 'node:fs/promises'
|
|
2
|
-
import { join, relative } from 'node:path'
|
|
3
|
-
import { AUDITS } from '@/audits/catalog'
|
|
4
|
-
import { type BodyLine, bodyLines } from '@/markdown/scan'
|
|
5
|
-
import { surfaceDir } from '@/roots/surface'
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* The record this measures, relative to `root`.
|
|
9
|
-
*
|
|
10
|
-
* A function rather than a fixed constant, because the path now resolves at
|
|
11
|
-
* either surface root and a constant naming one of them would be believed to
|
|
12
|
-
* be true of a project that has moved. One fixed name rather than a folder
|
|
13
|
-
* walk otherwise, because the standard governing it names one document, and
|
|
14
|
-
* the length rule this measures is stated by whichever record sits there
|
|
15
|
-
* rather than by the standard or by this file.
|
|
16
|
-
*/
|
|
17
|
-
export function architectureRel(root: string): string {
|
|
18
|
-
return relative(root, surfaceDir(root, 'ARCHITECTURE.md'))
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* The line allowances a record states for itself, absent when it states none.
|
|
23
|
-
*
|
|
24
|
-
* No standard sets a length rule for this document, so the numbers belong to
|
|
25
|
-
* whichever record declares them rather than to the toolkit. Holding a pair in
|
|
26
|
-
* code and gating every project against it audits a target against a rule it
|
|
27
|
-
* never adopted, which is the failure `canResolveAtRoot` already answers on the
|
|
28
|
-
* folder side. A record stating no rule is measured and never gated.
|
|
29
|
-
*/
|
|
30
|
-
export interface Allowances {
|
|
31
|
-
readonly frame: number
|
|
32
|
-
readonly perDecision: number
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* What a machine could do with the entry's reasoning, which is a candidate
|
|
37
|
-
* rather than a verdict.
|
|
38
|
-
*
|
|
39
|
-
* - `countable`: carries a figure over a tree, so a run could recompute it.
|
|
40
|
-
* - `invariant`: quantifies over a named tree, so a walk could falsify it.
|
|
41
|
-
* - `neither`: says why a choice was made, which nothing tests.
|
|
42
|
-
*/
|
|
43
|
-
export type ClaimKind = 'countable' | 'invariant' | 'neither'
|
|
44
|
-
|
|
45
|
-
export interface DecisionReport {
|
|
46
|
-
readonly heading: string
|
|
47
|
-
/** Line of the `###` heading, so a report line opens at the entry. */
|
|
48
|
-
readonly line: number
|
|
49
|
-
readonly claim: ClaimKind
|
|
50
|
-
/** The digit-spelled figures behind a `countable` reading, in order. */
|
|
51
|
-
readonly figures: readonly string[]
|
|
52
|
-
/** The first sentence behind an `invariant` reading, absent otherwise. */
|
|
53
|
-
readonly quantified?: string
|
|
54
|
-
/**
|
|
55
|
-
* Executable checks the entry names, which is the only coverage signal the
|
|
56
|
-
* record carries. Empty on an entry that names none, including one whose
|
|
57
|
-
* claim some check happens to cover without the entry saying so.
|
|
58
|
-
*/
|
|
59
|
-
readonly checks: readonly string[]
|
|
60
|
-
/**
|
|
61
|
-
* The entry's prose word count, its heading and fenced lines left out. Gates
|
|
62
|
-
* only against a per-decision cap the record states, so the report and the
|
|
63
|
-
* stage read one number.
|
|
64
|
-
*/
|
|
65
|
-
readonly words: number
|
|
66
|
-
/**
|
|
67
|
-
* Whether a sentence outside code opens with `Revisit when`. Read on every
|
|
68
|
-
* record and required only by one stating the clause, so a project that
|
|
69
|
-
* never adopted it sees the reading without failing on it.
|
|
70
|
-
*/
|
|
71
|
-
readonly revisit: boolean
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export interface ArchitectureReport {
|
|
75
|
-
readonly rel: string
|
|
76
|
-
readonly lines: number
|
|
77
|
-
/** The whole record's word count, reported for the same reason. */
|
|
78
|
-
readonly words: number
|
|
79
|
-
/**
|
|
80
|
-
* Word count of the `## Risks / open questions` section, absent when the
|
|
81
|
-
* record carries no such heading. The standard asks that section to hold
|
|
82
|
-
* only what is still open, so its weight is read the same way the file's is.
|
|
83
|
-
*/
|
|
84
|
-
readonly risksWords?: number
|
|
85
|
-
/**
|
|
86
|
-
* List items in the Risks section, nested ones included, since a nested
|
|
87
|
-
* bullet is weight the reader pays for. Absent alongside `risksWords`, so a
|
|
88
|
-
* record with no section never reads as one holding zero bullets.
|
|
89
|
-
*/
|
|
90
|
-
readonly risksBullets?: number
|
|
91
|
-
/** What the record declared, absent when it states no length rule. */
|
|
92
|
-
readonly allowances?: Allowances
|
|
93
|
-
/** The frame plus the per-decision allowance, absent alongside it. */
|
|
94
|
-
readonly ceiling?: number
|
|
95
|
-
/**
|
|
96
|
-
* The most decisions the record says it holds, absent when it states no
|
|
97
|
-
* cap. Declared by the record for the reason the allowances are, so a
|
|
98
|
-
* project that never adopted a cap is measured and never gated.
|
|
99
|
-
*/
|
|
100
|
-
readonly entryCap?: number
|
|
101
|
-
/** The most words a decision may hold, absent when the record states none. */
|
|
102
|
-
readonly wordCap?: number
|
|
103
|
-
/** The most Risks bullets the record allows, absent when it states none. */
|
|
104
|
-
readonly riskCap?: number
|
|
105
|
-
/**
|
|
106
|
-
* Whether the record states that every decision closes with a revisit
|
|
107
|
-
* sentence. Declared by the record for the reason the cap is, so a record
|
|
108
|
-
* that never wrote the clause is reported and never gated.
|
|
109
|
-
*/
|
|
110
|
-
readonly revisitRequired: boolean
|
|
111
|
-
readonly decisions: readonly DecisionReport[]
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const DECISION_HEADING = /^###\s+(.+?)\s*$/
|
|
115
|
-
const SECTION_HEADING = /^##\s+\S/
|
|
116
|
-
/** A markdown list item at any depth, which is what a risk bullet is. */
|
|
117
|
-
const LIST_ITEM = /^\s*(?:[-*+]|\d+[.)])\s+\S/
|
|
118
|
-
/** The H2 whose bullets the standard's `## Caps` section counts. */
|
|
119
|
-
const RISKS_HEADING = /^##\s+Risks\s*\/\s*open questions\s*$/i
|
|
120
|
-
const CODE_SPAN = /`[^`]*`/g
|
|
121
|
-
/** Dropped ahead of the figure scan, since an anchor date is not a claim. */
|
|
122
|
-
const ISO_DATE = /\b\d{4}-\d{2}-\d{2}\b/g
|
|
123
|
-
const FIGURE = /\b\d+(?:,\d{3})*\b/g
|
|
124
|
-
const SENTENCE_SPLIT = /(?<=[.])\s+/
|
|
125
|
-
|
|
126
|
-
/** A code span naming a path, which is what a quantifier has to govern. */
|
|
127
|
-
const PATH_SPAN = String.raw`\`[^\`]*(?:/|\.md|\.ts|\.sh|\.json)[^\`]*\``
|
|
128
|
-
const QUANTIFIER = String.raw`\b(?:every|each|no|nothing|any|all|never|only)\b`
|
|
129
|
-
/** Six words is the widest gap the corpus puts between the two. */
|
|
130
|
-
const WINDOW = String.raw`(?:\s+\S+){0,6}?\s+`
|
|
131
|
-
|
|
132
|
-
const QUANTIFIES_PATH = new RegExp(
|
|
133
|
-
`${QUANTIFIER}${WINDOW}${PATH_SPAN}|${PATH_SPAN}${WINDOW}${QUANTIFIER}`,
|
|
134
|
-
'i',
|
|
135
|
-
)
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* A code span naming a check this repository could run: a shell script, or a
|
|
139
|
-
* gate module whose measures read the tree in-process.
|
|
140
|
-
*/
|
|
141
|
-
const SCRIPT_SPAN = /^(?:scripts\/[\w./-]+\.sh|src\/gate\/[\w./-]+\.ts)$/
|
|
142
|
-
/** A code span invoking the CLI, which may or may not name a registered audit. */
|
|
143
|
-
const CANON_SPAN = /^canon\s+(.+)$/
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* The audit invocations a decision could name, spelled as a reader writes them.
|
|
147
|
-
*
|
|
148
|
-
* Read off the catalog rather than listed, so a verb renamed at its source
|
|
149
|
-
* stops matching here instead of going on matching a string nobody maintains.
|
|
150
|
-
*/
|
|
151
|
-
const AUDIT_INVOCATIONS: readonly string[] = AUDITS.map((audit) =>
|
|
152
|
-
audit.argv.filter((arg) => arg !== '--json').join(' '),
|
|
153
|
-
)
|
|
154
|
-
|
|
155
|
-
function codeSpans(text: string): string[] {
|
|
156
|
-
return (text.match(CODE_SPAN) ?? []).map((span) => span.slice(1, -1))
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* Names the executable checks an entry spells.
|
|
161
|
-
*
|
|
162
|
-
* A script has to exist on disk and a `canon` invocation has to match a
|
|
163
|
-
* registered audit, because an entry naming a check that was removed is an
|
|
164
|
-
* uncovered claim wearing a covered one's words.
|
|
165
|
-
*/
|
|
166
|
-
async function namedChecks(root: string, body: string): Promise<string[]> {
|
|
167
|
-
const spans = [...new Set(codeSpans(body))]
|
|
168
|
-
|
|
169
|
-
const verbs = spans.filter((span) => {
|
|
170
|
-
const invocation = span.match(CANON_SPAN)?.[1]
|
|
171
|
-
return invocation !== undefined && AUDIT_INVOCATIONS.includes(invocation)
|
|
172
|
-
})
|
|
173
|
-
|
|
174
|
-
const scripts = spans.filter((span) => SCRIPT_SPAN.test(span))
|
|
175
|
-
const present = await Promise.all(
|
|
176
|
-
scripts.map((span) =>
|
|
177
|
-
access(join(root, span)).then(
|
|
178
|
-
() => true,
|
|
179
|
-
() => false,
|
|
180
|
-
),
|
|
181
|
-
),
|
|
182
|
-
)
|
|
183
|
-
|
|
184
|
-
return [...verbs, ...scripts.filter((_, index) => present[index])]
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* Reads the figures a decision carries, which is the countable-claim signal.
|
|
189
|
-
*
|
|
190
|
-
* Digits alone. A cardinal spelled in words reads as pronominal far more often
|
|
191
|
-
* than as measured in this corpus, where "the alternative and it is one nobody
|
|
192
|
-
* passes" outnumbers "eleven copies", and admitting the spelled form classified
|
|
193
|
-
* 22 of 24 entries as countable, which distinguishes nothing. The cost is that
|
|
194
|
-
* a measured claim written in words reads as uncounted, which the report says.
|
|
195
|
-
*/
|
|
196
|
-
function figuresIn(body: string): string[] {
|
|
197
|
-
return body.replace(ISO_DATE, ' ').replace(CODE_SPAN, ' ').match(FIGURE) ?? []
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
/**
|
|
201
|
-
* The first sentence quantifying over a named tree, or undefined.
|
|
202
|
-
*
|
|
203
|
-
* The quantifier has to sit within a short window of the path so a sentence
|
|
204
|
-
* mentioning both without relating them does not read as a claim about the
|
|
205
|
-
* tree. Both orders are matched, since the corpus writes the property before
|
|
206
|
-
* the path as readily as after it.
|
|
207
|
-
*/
|
|
208
|
-
function quantifiedSentence(body: string): string | undefined {
|
|
209
|
-
return body
|
|
210
|
-
.replace(ISO_DATE, ' ')
|
|
211
|
-
.split(SENTENCE_SPLIT)
|
|
212
|
-
.map((sentence) => sentence.replace(/\n/g, ' ').trim())
|
|
213
|
-
.find((sentence) => QUANTIFIES_PATH.test(sentence))
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
/**
|
|
217
|
-
* Classifies one entry, with the countable reading taking precedence.
|
|
218
|
-
*
|
|
219
|
-
* An entry carrying both a figure and a quantified tree is recomputable, which
|
|
220
|
-
* is the stronger test, and the two readings are reported as one kind because
|
|
221
|
-
* the coverage split a reader wants is testable against unverifiable rather
|
|
222
|
-
* than a per-entry inventory of every claim in it.
|
|
223
|
-
*/
|
|
224
|
-
export function classifyDecision(body: string): {
|
|
225
|
-
claim: ClaimKind
|
|
226
|
-
figures: string[]
|
|
227
|
-
quantified?: string
|
|
228
|
-
} {
|
|
229
|
-
const figures = figuresIn(body)
|
|
230
|
-
if (figures.length > 0) return { claim: 'countable', figures }
|
|
231
|
-
|
|
232
|
-
const quantified = quantifiedSentence(body)
|
|
233
|
-
if (quantified !== undefined) {
|
|
234
|
-
return { claim: 'invariant', figures: [], quantified }
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
return { claim: 'neither', figures: [] }
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
interface RawDecision {
|
|
241
|
-
readonly heading: string
|
|
242
|
-
readonly line: number
|
|
243
|
-
readonly body: string
|
|
244
|
-
/** The body with its fenced lines dropped, which is where prose is read. */
|
|
245
|
-
readonly prose: string
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
/**
|
|
249
|
-
* Splits the record into its `###` entries.
|
|
250
|
-
*
|
|
251
|
-
* A heading inside a fenced block is skipped, since the seed template shows the
|
|
252
|
-
* shape it asks a project to write and a template entry is not a decision.
|
|
253
|
-
*
|
|
254
|
-
* A heading carrying several decisions counts once, so the total reads low by
|
|
255
|
-
* however many it holds. The report states that rather than parsing for it,
|
|
256
|
-
* because splitting a decision from its heading needs a marker the standard
|
|
257
|
-
* does not ask a record to carry.
|
|
258
|
-
*/
|
|
259
|
-
export function splitDecisions(source: string): RawDecision[] {
|
|
260
|
-
const lines = bodyLines(source)
|
|
261
|
-
const decisions: RawDecision[] = []
|
|
262
|
-
let open:
|
|
263
|
-
| { heading: string; line: number; body: string[]; prose: string[] }
|
|
264
|
-
| undefined
|
|
265
|
-
|
|
266
|
-
const close = (): void => {
|
|
267
|
-
if (!open) return
|
|
268
|
-
decisions.push({
|
|
269
|
-
heading: open.heading,
|
|
270
|
-
line: open.line,
|
|
271
|
-
body: open.body.join('\n'),
|
|
272
|
-
prose: open.prose.join('\n'),
|
|
273
|
-
})
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
for (const line of lines) {
|
|
277
|
-
if (line.fenced) {
|
|
278
|
-
open?.body.push(line.text)
|
|
279
|
-
continue
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
const heading = line.text.match(DECISION_HEADING)?.[1]
|
|
283
|
-
if (heading !== undefined) {
|
|
284
|
-
close()
|
|
285
|
-
open = { heading, line: line.number, body: [], prose: [] }
|
|
286
|
-
continue
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
// A decision runs to the next `###` or to the section that follows the
|
|
290
|
-
// decision list, so the risks below never read as the last entry's body.
|
|
291
|
-
if (SECTION_HEADING.test(line.text)) {
|
|
292
|
-
close()
|
|
293
|
-
open = undefined
|
|
294
|
-
continue
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
open?.body.push(line.text)
|
|
298
|
-
open?.prose.push(line.text)
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
close()
|
|
302
|
-
return decisions
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
/**
|
|
306
|
-
* Counts whitespace-delimited tokens, the unit a record's word caps are stated
|
|
307
|
-
* in. A code span counts as the tokens it holds, so a decision citing a long
|
|
308
|
-
* path pays for it the way a reader does. A caller gating on the count hands
|
|
309
|
-
* in prose with fenced lines already dropped.
|
|
310
|
-
*/
|
|
311
|
-
export function wordCount(text: string): number {
|
|
312
|
-
return text.match(/\S+/g)?.length ?? 0
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
/**
|
|
316
|
-
* Extracts the `## Risks / open questions` section, or nothing when the
|
|
317
|
-
* record carries no such heading.
|
|
318
|
-
*
|
|
319
|
-
* The section is read the same way `splitDecisions` reads a decision: capture
|
|
320
|
-
* starts at the heading and stops at the next H2, or at the end of the file
|
|
321
|
-
* when the section is last, which is where the standard's template puts it.
|
|
322
|
-
*/
|
|
323
|
-
export function risksSection(source: string): string | undefined {
|
|
324
|
-
return risksLines(source)
|
|
325
|
-
?.map((line) => line.text)
|
|
326
|
-
.join('\n')
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
/**
|
|
330
|
-
* Counts the list items in the Risks section, nested ones included, or
|
|
331
|
-
* nothing when the record carries no such heading. A fenced line is skipped,
|
|
332
|
-
* since a template bullet is not a risk.
|
|
333
|
-
*/
|
|
334
|
-
export function riskBullets(source: string): number | undefined {
|
|
335
|
-
return risksLines(source)?.filter(
|
|
336
|
-
(line) => !line.fenced && LIST_ITEM.test(line.text),
|
|
337
|
-
).length
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
function risksLines(source: string): BodyLine[] | undefined {
|
|
341
|
-
const body: BodyLine[] = []
|
|
342
|
-
let capturing = false
|
|
343
|
-
let found = false
|
|
344
|
-
|
|
345
|
-
for (const line of bodyLines(source)) {
|
|
346
|
-
if (line.fenced) {
|
|
347
|
-
if (capturing) body.push(line)
|
|
348
|
-
continue
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
if (RISKS_HEADING.test(line.text)) {
|
|
352
|
-
capturing = true
|
|
353
|
-
found = true
|
|
354
|
-
continue
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
if (capturing && SECTION_HEADING.test(line.text)) {
|
|
358
|
-
capturing = false
|
|
359
|
-
continue
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
if (capturing) body.push(line)
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
return found ? body : undefined
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
/** Cardinals a record spells rather than writes, which the corpus does for both. */
|
|
369
|
-
const SPELLED: Record<string, number> = {
|
|
370
|
-
one: 1,
|
|
371
|
-
two: 2,
|
|
372
|
-
three: 3,
|
|
373
|
-
four: 4,
|
|
374
|
-
five: 5,
|
|
375
|
-
six: 6,
|
|
376
|
-
seven: 7,
|
|
377
|
-
eight: 8,
|
|
378
|
-
nine: 9,
|
|
379
|
-
ten: 10,
|
|
380
|
-
eleven: 11,
|
|
381
|
-
twelve: 12,
|
|
382
|
-
thirteen: 13,
|
|
383
|
-
fourteen: 14,
|
|
384
|
-
fifteen: 15,
|
|
385
|
-
sixteen: 16,
|
|
386
|
-
seventeen: 17,
|
|
387
|
-
eighteen: 18,
|
|
388
|
-
nineteen: 19,
|
|
389
|
-
twenty: 20,
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
// Longest first, so `fourteen` is tried ahead of the `four` it starts with.
|
|
393
|
-
const SPELLED_ALTERNATION = Object.keys(SPELLED)
|
|
394
|
-
.sort((a, b) => b.length - a.length)
|
|
395
|
-
.join('|')
|
|
396
|
-
const CARDINAL = String.raw`(\d+|${SPELLED_ALTERNATION})`
|
|
397
|
-
const FRAME_CLAUSE = new RegExp(String.raw`${CARDINAL}-line frame`, 'i')
|
|
398
|
-
const PER_DECISION_CLAUSE = new RegExp(
|
|
399
|
-
String.raw`${CARDINAL}\s+lines?\s+a\s+decision`,
|
|
400
|
-
'i',
|
|
401
|
-
)
|
|
402
|
-
const ENTRY_CAP_CLAUSE = new RegExp(
|
|
403
|
-
String.raw`\bat\s+most\s+${CARDINAL}\s+decisions?\b`,
|
|
404
|
-
'i',
|
|
405
|
-
)
|
|
406
|
-
const WORD_CAP_CLAUSE = new RegExp(
|
|
407
|
-
String.raw`\bat\s+most\s+${CARDINAL}\s+words\s+a\s+decision\b`,
|
|
408
|
-
'i',
|
|
409
|
-
)
|
|
410
|
-
const RISK_CAP_CLAUSE = new RegExp(
|
|
411
|
-
String.raw`\bat\s+most\s+${CARDINAL}\s+risk\s+bullets?\b`,
|
|
412
|
-
'i',
|
|
413
|
-
)
|
|
414
|
-
|
|
415
|
-
function readCardinal(token: string | undefined): number | undefined {
|
|
416
|
-
if (token === undefined) return undefined
|
|
417
|
-
const spelled = SPELLED[token.toLowerCase()]
|
|
418
|
-
if (spelled !== undefined) return spelled
|
|
419
|
-
|
|
420
|
-
const digits = Number.parseInt(token, 10)
|
|
421
|
-
return Number.isNaN(digits) ? undefined : digits
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
/**
|
|
425
|
-
* Reads the allowances a record declares for itself, or nothing.
|
|
426
|
-
*
|
|
427
|
-
* Both clauses have to be present, because half a formula is not one. A record
|
|
428
|
-
* whose wording drifts past these clauses falls back to reporting rather than
|
|
429
|
-
* to a stale ceiling held here, so the failure is visible in the run's own
|
|
430
|
-
* output instead of gating a project on a rule nobody can point at.
|
|
431
|
-
*/
|
|
432
|
-
export function readAllowances(source: string): Allowances | undefined {
|
|
433
|
-
const frame = readCardinal(source.match(FRAME_CLAUSE)?.[1])
|
|
434
|
-
const perDecision = readCardinal(source.match(PER_DECISION_CLAUSE)?.[1])
|
|
435
|
-
|
|
436
|
-
if (frame === undefined || perDecision === undefined) return undefined
|
|
437
|
-
return { frame, perDecision }
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
/**
|
|
441
|
-
* Reads the entry cap a record declares for itself, or nothing.
|
|
442
|
-
*
|
|
443
|
-
* The clause is the record's own, like the allowances above, so a record
|
|
444
|
-
* whose wording drifts past it falls back to reporting rather than to a cap
|
|
445
|
-
* held here that no project wrote.
|
|
446
|
-
*/
|
|
447
|
-
export function readEntryCap(source: string): number | undefined {
|
|
448
|
-
return readCardinal(source.match(ENTRY_CAP_CLAUSE)?.[1])
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
/**
|
|
452
|
-
* Reads the per-decision word cap a record declares, spelled `at most <n>
|
|
453
|
-
* words a decision`, or nothing. One spelling, the way the line allowance
|
|
454
|
-
* reads `<n> lines a decision`, so a drift falls back to reporting.
|
|
455
|
-
*/
|
|
456
|
-
export function readWordCap(source: string): number | undefined {
|
|
457
|
-
return readCardinal(source.match(WORD_CAP_CLAUSE)?.[1])
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
/** Reads the Risks cap a record declares, spelled `at most <n> risk bullets`. */
|
|
461
|
-
export function readRiskCap(source: string): number | undefined {
|
|
462
|
-
return readCardinal(source.match(RISK_CAP_CLAUSE)?.[1])
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
const REVISIT_CLAUSE =
|
|
466
|
-
/\bevery\s+decision\s+closes\s+with\s+a\s+revisit\s+sentence\b/i
|
|
467
|
-
/**
|
|
468
|
-
* `Revisit when` opening a sentence: at the start of the text, after a line
|
|
469
|
-
* break, or after a sentence's closing stop. Mid-sentence it is not the
|
|
470
|
-
* sentence the standard asks for.
|
|
471
|
-
*/
|
|
472
|
-
const REVISIT_SENTENCE = /(?:^|\n|[.!?]\s+)\s*(?:[-*]\s+)?Revisit when\b/
|
|
473
|
-
|
|
474
|
-
/**
|
|
475
|
-
* Whether the record states the revisit clause for itself, read off its prose
|
|
476
|
-
* alone so a template quoting the clause inside a fence does not adopt it.
|
|
477
|
-
*
|
|
478
|
-
* The clause is the record's own, like the cap, so wording that drifts past it
|
|
479
|
-
* falls back to reporting rather than to a requirement held here.
|
|
480
|
-
*/
|
|
481
|
-
export function readRevisitRule(source: string): boolean {
|
|
482
|
-
const prose = bodyLines(source)
|
|
483
|
-
.filter((line) => !line.fenced)
|
|
484
|
-
.map((line) => line.text)
|
|
485
|
-
.join('\n')
|
|
486
|
-
.replace(CODE_SPAN, ' ')
|
|
487
|
-
return REVISIT_CLAUSE.test(prose)
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
/**
|
|
491
|
-
* Whether a decision's prose carries a sentence opening `Revisit when`.
|
|
492
|
-
*
|
|
493
|
-
* Presence only, never content, so a vacuous sentence passes. A code span is
|
|
494
|
-
* dropped first, since a standard quoting the form is not stating one.
|
|
495
|
-
*/
|
|
496
|
-
export function hasRevisitSentence(prose: string): boolean {
|
|
497
|
-
return REVISIT_SENTENCE.test(prose.replace(CODE_SPAN, ' '))
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
/** Whether a read failed because nothing sits at the path. */
|
|
501
|
-
function isMissing(error: unknown): boolean {
|
|
502
|
-
const code = (error as { code?: unknown }).code
|
|
503
|
-
return code === 'ENOENT' || code === 'ENOTDIR'
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
/** The ceiling the record's own formula derives from its decision count. */
|
|
507
|
-
export function ceilingFor(allowances: Allowances, decisions: number): number {
|
|
508
|
-
return allowances.frame + allowances.perDecision * decisions
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
/**
|
|
512
|
-
* Measures the record, or reports nothing when the project carries none.
|
|
513
|
-
*
|
|
514
|
-
* Absent rather than empty, for the reason the sibling checks state: a project
|
|
515
|
-
* with no record and one whose record holds no decision are different answers,
|
|
516
|
-
* and a zeroed report reads as the second.
|
|
517
|
-
*/
|
|
518
|
-
export async function measureArchitecture(
|
|
519
|
-
root: string,
|
|
520
|
-
): Promise<ArchitectureReport | undefined> {
|
|
521
|
-
const rel = architectureRel(root)
|
|
522
|
-
const path = join(root, rel)
|
|
523
|
-
|
|
524
|
-
let source: string
|
|
525
|
-
try {
|
|
526
|
-
source = await readFile(path, 'utf8')
|
|
527
|
-
} catch (error) {
|
|
528
|
-
// Only a record that is not there reads as absent. A record present and
|
|
529
|
-
// unreadable propagates the way every sibling reader here lets one
|
|
530
|
-
// propagate, since swallowing it reports a project with no record and the
|
|
531
|
-
// length gate passes over a file nobody opened.
|
|
532
|
-
if (!isMissing(error)) throw error
|
|
533
|
-
return undefined
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
const allowances = readAllowances(source)
|
|
537
|
-
const entryCap = readEntryCap(source)
|
|
538
|
-
const wordCap = readWordCap(source)
|
|
539
|
-
const riskCap = readRiskCap(source)
|
|
540
|
-
const revisitRequired = readRevisitRule(source)
|
|
541
|
-
const raw = splitDecisions(source)
|
|
542
|
-
const decisions = await Promise.all(
|
|
543
|
-
raw.map(async (entry) => {
|
|
544
|
-
const { claim, figures, quantified } = classifyDecision(entry.body)
|
|
545
|
-
return {
|
|
546
|
-
heading: entry.heading,
|
|
547
|
-
line: entry.line,
|
|
548
|
-
claim,
|
|
549
|
-
figures,
|
|
550
|
-
...(quantified !== undefined && { quantified }),
|
|
551
|
-
checks: await namedChecks(root, entry.body),
|
|
552
|
-
words: wordCount(entry.prose),
|
|
553
|
-
revisit: hasRevisitSentence(entry.prose),
|
|
554
|
-
}
|
|
555
|
-
}),
|
|
556
|
-
)
|
|
557
|
-
|
|
558
|
-
const risks = risksSection(source)
|
|
559
|
-
const bullets = riskBullets(source)
|
|
560
|
-
|
|
561
|
-
return {
|
|
562
|
-
rel,
|
|
563
|
-
lines: source.replace(/\n$/, '').split('\n').length,
|
|
564
|
-
words: wordCount(
|
|
565
|
-
bodyLines(source)
|
|
566
|
-
.map((line) => line.text)
|
|
567
|
-
.join('\n'),
|
|
568
|
-
),
|
|
569
|
-
...(risks !== undefined && { risksWords: wordCount(risks) }),
|
|
570
|
-
...(bullets !== undefined && { risksBullets: bullets }),
|
|
571
|
-
...(allowances !== undefined && {
|
|
572
|
-
allowances,
|
|
573
|
-
ceiling: ceilingFor(allowances, raw.length),
|
|
574
|
-
}),
|
|
575
|
-
...(entryCap !== undefined && { entryCap }),
|
|
576
|
-
...(wordCap !== undefined && { wordCap }),
|
|
577
|
-
...(riskCap !== undefined && { riskCap }),
|
|
578
|
-
revisitRequired,
|
|
579
|
-
decisions,
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
/**
|
|
584
|
-
* Whether the record holds more decisions than the cap it states.
|
|
585
|
-
*
|
|
586
|
-
* False for a record stating no cap, for the reason `isOverLength` gives. The
|
|
587
|
-
* count is by heading, so a heading carrying several decisions counts once,
|
|
588
|
-
* which is the undercount `splitDecisions` already names.
|
|
589
|
-
*/
|
|
590
|
-
export function isOverCount(report: ArchitectureReport): boolean {
|
|
591
|
-
return (
|
|
592
|
-
report.entryCap !== undefined && report.decisions.length > report.entryCap
|
|
593
|
-
)
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
/**
|
|
597
|
-
* The decisions holding more prose words than the cap the record states.
|
|
598
|
-
*
|
|
599
|
-
* Empty for a record stating no word cap, for the reason `isOverCount` gives.
|
|
600
|
-
*/
|
|
601
|
-
export function overWords(report: ArchitectureReport): DecisionReport[] {
|
|
602
|
-
const cap = report.wordCap
|
|
603
|
-
if (cap === undefined) return []
|
|
604
|
-
return report.decisions.filter((entry) => entry.words > cap)
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
/**
|
|
608
|
-
* Whether the Risks section holds more bullets than the cap the record states.
|
|
609
|
-
*
|
|
610
|
-
* False for a record stating no cap, and false for one carrying no Risks
|
|
611
|
-
* section, which the stage reports as absent rather than as zero bullets.
|
|
612
|
-
*/
|
|
613
|
-
export function isOverRisks(report: ArchitectureReport): boolean {
|
|
614
|
-
return (
|
|
615
|
-
report.riskCap !== undefined &&
|
|
616
|
-
report.risksBullets !== undefined &&
|
|
617
|
-
report.risksBullets > report.riskCap
|
|
618
|
-
)
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
/**
|
|
622
|
-
* The headings of decisions lacking a revisit sentence the record requires.
|
|
623
|
-
*
|
|
624
|
-
* Empty for a record stating no clause, for the reason `isOverCount` gives.
|
|
625
|
-
*/
|
|
626
|
-
export function missingRevisit(report: ArchitectureReport): string[] {
|
|
627
|
-
if (!report.revisitRequired) return []
|
|
628
|
-
return report.decisions
|
|
629
|
-
.filter((entry) => !entry.revisit)
|
|
630
|
-
.map((entry) => entry.heading)
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
/**
|
|
634
|
-
* Whether the record is longer than the ceiling it derives for itself.
|
|
635
|
-
*
|
|
636
|
-
* False for a record declaring no allowances, which has no ceiling to be past.
|
|
637
|
-
* That is the answer rather than a gap, since the length rule is the record's
|
|
638
|
-
* own and a project that never wrote one owes nothing to it.
|
|
639
|
-
*/
|
|
640
|
-
export function isOverLength(report: ArchitectureReport): boolean {
|
|
641
|
-
return report.ceiling !== undefined && report.lines > report.ceiling
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
/** How many entries carry a claim a machine could test. */
|
|
645
|
-
export function testableCount(report: ArchitectureReport): number {
|
|
646
|
-
return report.decisions.filter((entry) => entry.claim !== 'neither').length
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
/** How many testable entries name a check that exists. */
|
|
650
|
-
export function coveredCount(report: ArchitectureReport): number {
|
|
651
|
-
return report.decisions.filter(
|
|
652
|
-
(entry) => entry.claim !== 'neither' && entry.checks.length > 0,
|
|
653
|
-
).length
|
|
654
|
-
}
|