@erclx/canon 5.35.0 → 5.37.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 +11 -11
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/api-design/SKILL.md +6 -0
- package/claude/skills/backlog-triage/SKILL.md +7 -7
- package/claude/skills/build-in-slices/SKILL.md +6 -0
- package/claude/skills/build-in-slices/references/adopted.md +3 -0
- package/claude/skills/canon-cli/REQUIREMENT.md +2 -2
- package/claude/skills/canon-cli/SKILL.md +5 -1
- package/claude/skills/canon-feedback/SKILL.md +5 -0
- package/claude/skills/canon-feedback-triage/SKILL.md +3 -0
- package/claude/skills/canvas-board/REQUIREMENT.md +1 -1
- package/claude/skills/canvas-board/SKILL.md +5 -2
- package/claude/skills/canvas-board/references/canvas.md +12 -0
- package/claude/skills/canvas-board/references/design.md +53 -36
- package/claude/skills/ci-workflow/SKILL.md +5 -0
- package/claude/skills/code-craft/SKILL.md +6 -0
- package/claude/skills/codebase-layout/REQUIREMENT.md +1 -1
- package/claude/skills/codebase-layout/SKILL.md +2 -2
- package/claude/skills/context-fold/references/context.md +48 -34
- package/claude/skills/create-rule/REQUIREMENT.md +6 -8
- package/claude/skills/create-rule/SKILL.md +19 -17
- package/claude/skills/create-rule/references/rule.md +17 -23
- package/claude/skills/create-skill/SKILL.md +11 -2
- package/claude/skills/create-skill/references/skill-disclosure.md +26 -0
- package/claude/skills/create-skill/references/skill-paths.md +3 -0
- package/claude/skills/create-skill/references/skill-reference.md +12 -0
- package/claude/skills/create-skill/references/skill.md +26 -25
- package/claude/skills/deprecation-migration/SKILL.md +6 -0
- package/claude/skills/deprecation-migration/references/adopted.md +3 -0
- package/claude/skills/design-taste/REQUIREMENT.md +6 -6
- package/claude/skills/design-taste/SKILL.md +8 -2
- package/claude/skills/design-taste/references/craft.md +4 -1
- package/claude/skills/design-taste/references/preflight.md +18 -2
- package/claude/skills/design-taste/references/tells.md +1 -1
- package/claude/skills/docs-sync/references/docs.md +6 -0
- package/claude/skills/docs-sync/references/readme.md +15 -0
- package/claude/skills/draft-and-pick/SKILL.md +12 -0
- package/claude/skills/draft-doc/references/context.md +1 -1
- package/claude/skills/draft-doc/references/readme.md +3 -0
- package/claude/skills/draft-figure/references/mermaid.md +6 -0
- package/claude/skills/draft-identity/SKILL.md +3 -2
- package/claude/skills/draft-slides/SKILL.md +3 -0
- package/claude/skills/git-branch/references/branch.md +16 -11
- package/claude/skills/git-commit/references/commit.md +19 -11
- package/claude/skills/git-commit/references/versioning.md +9 -0
- package/claude/skills/git-issue/references/issue.md +12 -9
- package/claude/skills/git-pr/SKILL.md +5 -0
- package/claude/skills/git-pr/references/pr.md +24 -6
- package/claude/skills/git-ship/SKILL.md +3 -0
- package/claude/skills/git-split/SKILL.md +6 -0
- package/claude/skills/git-stage/SKILL.md +3 -0
- package/claude/skills/git-worktree/SKILL.md +6 -6
- package/claude/skills/markdown-craft/references/markdown.md +27 -8
- package/claude/skills/plan-feature/SKILL.md +10 -1
- package/claude/skills/plan-feature/references/plan.md +20 -8
- package/claude/skills/plan-groundwork/SKILL.md +6 -0
- package/claude/skills/plan-groundwork/references/groundwork.md +10 -7
- package/claude/skills/plan-intake/references/intake.md +6 -6
- package/claude/skills/read-frames/SKILL.md +3 -0
- package/claude/skills/review-craft/SKILL.md +3 -0
- package/claude/skills/review-craft/references/ui.md +1 -1
- package/claude/skills/review-ui/SKILL.md +6 -3
- package/claude/skills/role-orchestrator/SKILL.md +3 -0
- package/claude/skills/role-orchestrator/references/orchestrator-dispatch.md +7 -4
- package/claude/skills/role-orchestrator/references/orchestrator-refill.md +3 -0
- package/claude/skills/role-planner/SKILL.md +3 -0
- package/claude/skills/role-worker/SKILL.md +12 -0
- package/claude/skills/search-craft/SKILL.md +3 -0
- package/claude/skills/systematic-debugging/SKILL.md +6 -0
- package/claude/skills/target-check/references/domains.md +5 -0
- package/claude/skills/target-setup/SKILL.md +5 -0
- package/claude/skills/task-board/references/tasks.md +9 -6
- package/claude/skills/task-complete/references/archive.md +3 -0
- package/claude/skills/teach-workspace/SKILL.md +3 -0
- package/claude/skills/teach-workspace/references/glossary.md +3 -0
- package/claude/skills/teach-workspace/references/lesson-build.md +3 -0
- package/claude/skills/teach-workspace/references/lesson-craft.md +3 -0
- package/claude/skills/teach-workspace/references/teach.md +8 -8
- package/claude/skills/test-craft/SKILL.md +7 -2
- package/claude/skills/ux-audit/REQUIREMENT.md +1 -1
- package/claude/skills/ux-audit/SKILL.md +2 -2
- package/claude/skills/ux-measure/REQUIREMENT.md +1 -1
- package/claude/skills/video-craft/SKILL.md +6 -0
- package/claude/skills/write-human/SKILL.md +9 -0
- package/claude/skills/write-human/references/machine-tells.md +12 -0
- package/docs/agents/capture.md +2 -0
- package/docs/agents/commands.md +59 -56
- package/docs/agents/comments.md +1 -1
- package/docs/agents/context-audit-checks.md +6 -0
- package/docs/agents/context-audit.md +8 -8
- package/docs/agents/context-classify.md +2 -0
- package/docs/agents/drift-surfaces.md +5 -5
- package/docs/agents/gate.md +6 -7
- package/docs/agents/gov-detect.md +1 -1
- package/docs/agents/index.md +2 -2
- package/docs/agents/install-and-sync.md +6 -6
- package/docs/agents/markdown-audit-weight.md +11 -3
- package/docs/agents/markdown-audit.md +14 -2
- package/docs/agents/output-shape.md +1 -1
- package/docs/agents/overview.md +2 -2
- package/docs/agents/pr-frames.md +17 -3
- package/docs/agents/rule-citations.md +5 -10
- package/docs/agents/scripting.md +1 -1
- package/docs/agents/sessions-transfer.md +8 -6
- package/docs/agents/skills-audit.md +9 -1
- package/docs/target/projects.md +2 -2
- package/docs/target/scenarios.md +5 -5
- package/docs/target/standards-moved.md +20 -18
- package/docs/workflow/ai-workflow.md +4 -1
- package/docs/workflow/visual-design-workflow.md +8 -3
- package/governance/detect.toml +32 -32
- package/governance/renames.toml +44 -0
- package/governance/rules/lib/{305-e2e-reliability.md → e2e-reliability.md} +1 -1
- package/governance/rules/lib/{306-test-scope.md → test-scope.md} +2 -2
- package/governance/rules/ui/{410-a11y.md → a11y.md} +1 -1
- package/governance/rules/ui/{440-surface-capture.md → surface-capture.md} +3 -3
- package/governance/stacks/base.toml +6 -6
- package/governance/stacks/go.toml +1 -1
- package/governance/stacks/node.toml +1 -1
- package/governance/stacks/php.toml +1 -1
- package/governance/stacks/python.toml +1 -1
- package/package.json +2 -2
- package/scripts/core/install-check.sh +2 -7
- package/src/capture/frame-values.ts +3 -6
- package/src/cli/help-all.ts +96 -0
- package/src/cli/help.ts +28 -27
- package/src/cli.ts +8 -1
- package/src/commands/claude.ts +29 -0
- package/src/commands/context.ts +29 -4
- package/src/commands/design.ts +22 -1
- package/src/commands/gate.ts +1 -8
- package/src/commands/gov.ts +25 -3
- package/src/commands/init.ts +10 -1
- package/src/commands/markdown.ts +148 -3
- package/src/commands/pr.ts +17 -10
- package/src/commands/records.ts +1 -1
- package/src/commands/sync.ts +11 -0
- package/src/commands/tooling.ts +8 -0
- package/src/context/audit.ts +83 -0
- package/src/context/classify/prompts.ts +6 -1
- package/src/context/gate.ts +6 -3
- package/src/design/budget.ts +14 -10
- package/src/design/document.ts +5 -5
- package/src/design/parse.ts +21 -4
- package/src/design/render.ts +3 -3
- package/src/gate/measures.ts +187 -2
- package/src/gate/sequencer.ts +0 -4
- package/src/gate/stages.ts +32 -27
- package/src/git/files.ts +30 -0
- package/src/gov/adapter.ts +2 -2
- package/src/gov/citations.ts +10 -33
- package/src/gov/install.ts +2 -2
- package/src/gov/list.ts +1 -1
- package/src/gov/payload.ts +1 -1
- package/src/gov/stacks.ts +4 -4
- package/src/inventory/subjects.ts +1 -1
- package/src/markdown/ceiling.ts +6 -1
- package/src/markdown/colons.ts +86 -0
- package/src/markdown/gate.ts +25 -0
- package/src/markdown/growth.ts +43 -0
- package/src/markdown/heading-case.ts +74 -0
- package/src/markdown/stacks.ts +74 -0
- package/src/markdown/structure.ts +72 -4
- package/src/migrate/rule-layout.ts +12 -12
- package/src/pr/frames.ts +66 -1
- package/src/shipped/references.ts +14 -14
- package/src/sync/check.ts +23 -23
- package/src/sync/copy.ts +4 -5
- package/tooling/astro/configs/tsconfig.json +1 -1
- package/tooling/astro/manifest.toml +0 -1
- package/tooling/astro/reference.md +2 -10
- package/tooling/base/configs/scripts/verify.json +1 -1
- package/tooling/base/configs/scripts/verify.ts +23 -13
- package/tooling/base/manifest.toml +6 -5
- package/tooling/base/reference.md +15 -22
- package/tooling/base/seeds/.github/workflows/verify.yml +0 -6
- package/tooling/base/seeds/.lintstagedrc +1 -2
- package/tooling/base/seeds/canon/context/ci.md +7 -8
- package/tooling/base/seeds/canon/context/development.md +10 -10
- package/tooling/claude/seeds/.claude/hooks/scratch-guard.sh +9 -1
- package/tooling/claude/seeds/.claude/hooks/standards-audit.sh +27 -2
- package/tooling/claude/seeds/CLAUDE.md +1 -1
- package/tooling/claude/seeds/canon/DESIGN.md +24 -37
- package/tooling/shell/configs/.github/workflows/shell.yml +30 -0
- package/tooling/shell/manifest.toml +12 -0
- package/tooling/shell/reference.md +46 -0
- package/tooling/vite-react/configs/tsconfig.json +1 -1
- package/tooling/vite-react/configs/vitest.config.ts +1 -1
- package/tooling/vite-react/manifest.toml +0 -1
- package/tooling/vite-react/reference.md +2 -10
- package/tooling/web/configs/.github/workflows/readme-screenshot.yml +3 -5
- package/tooling/web/configs/.github/workflows/verify.yml +0 -6
- package/tooling/web/configs/src/test/setup.ts +1 -1
- package/tooling/web/manifest.toml +4 -4
- package/tooling/web/reference.md +4 -4
- package/tooling/base/configs/scripts/verify.sh +0 -2
- /package/governance/rules/canon/{606-git.md → git.md} +0 -0
- /package/governance/rules/canon/{611-indexes.md → indexes.md} +0 -0
- /package/governance/rules/canon/{604-scratch.md → scratch.md} +0 -0
- /package/governance/rules/canon/{602-tasks.md → task-scope.md} +0 -0
- /package/governance/rules/canon/{605-worktrees.md → worktrees.md} +0 -0
- /package/governance/rules/claude/{566-output.md → output.md} +0 -0
- /package/governance/rules/code/{000-code.md → routing.md} +0 -0
- /package/governance/rules/code/{010-testing.md → testing.md} +0 -0
- /package/governance/rules/framework/{210-astro.md → astro.md} +0 -0
- /package/governance/rules/framework/{220-fastapi.md → fastapi.md} +0 -0
- /package/governance/rules/framework/{230-nextjs.md → nextjs.md} +0 -0
- /package/governance/rules/framework/{200-react.md → react.md} +0 -0
- /package/governance/rules/framework/{260-shadcn.md → shadcn.md} +0 -0
- /package/governance/rules/framework/{250-tailwind.md → tailwind.md} +0 -0
- /package/governance/rules/lang/{130-go.md → go.md} +0 -0
- /package/governance/rules/lang/{140-php.md → php.md} +0 -0
- /package/governance/rules/lang/{110-python.md → python.md} +0 -0
- /package/governance/rules/lang/{100-typescript.md → typescript.md} +0 -0
- /package/governance/rules/lib/{370-database.md → database.md} +0 -0
- /package/governance/rules/lib/{340-pydantic.md → pydantic.md} +0 -0
- /package/governance/rules/lib/{360-security-server.md → security-server.md} +0 -0
- /package/governance/rules/lib/{350-security-web.md → security-web.md} +0 -0
- /package/governance/rules/lib/{320-tanstack-query.md → tanstack-query.md} +0 -0
- /package/governance/rules/lib/{335-testing-go.md → testing-go.md} +0 -0
- /package/governance/rules/lib/{336-testing-php.md → testing-php.md} +0 -0
- /package/governance/rules/lib/{330-testing-py.md → testing-py.md} +0 -0
- /package/governance/rules/lib/{300-testing-ts.md → testing-ts.md} +0 -0
- /package/governance/rules/lib/{310-zod.md → zod.md} +0 -0
- /package/governance/rules/tooling/{700-ci-workflow.md → ci-workflow.md} +0 -0
- /package/governance/rules/tooling/{790-code-comments.md → code-comments.md} +0 -0
- /package/governance/rules/tooling/{780-config-comments.md → config-comments.md} +0 -0
- /package/governance/rules/tooling/{775-dependencies.md → dependencies.md} +0 -0
- /package/governance/rules/ui/{480-canvas.md → canvas.md} +0 -0
- /package/governance/rules/ui/{400-ui.md → copy.md} +0 -0
- /package/governance/rules/ui/{460-design-taste.md → design-taste.md} +0 -0
- /package/governance/rules/ui/{420-forms.md → forms.md} +0 -0
- /package/governance/rules/ui/{465-interface-casing.md → interface-casing.md} +0 -0
- /package/governance/rules/ui/{450-link-behavior.md → link-behavior.md} +0 -0
- /package/governance/rules/ui/{470-motion.md → motion.md} +0 -0
- /package/governance/rules/ui/{430-ux-completeness.md → ux-completeness.md} +0 -0
- /package/tooling/{base → shell}/configs/.editorconfig +0 -0
- /package/tooling/{base → shell}/configs/.shellcheckrc +0 -0
package/src/design/parse.ts
CHANGED
|
@@ -25,6 +25,23 @@ export interface DesignDoc {
|
|
|
25
25
|
iconography: string
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* The three headings the standard renamed toward Google's DESIGN.md, each read
|
|
30
|
+
* as the section it became. Records the seed and `canvas-board` wrote before
|
|
31
|
+
* the rename sit in targets and will not rename themselves, and an unmatched
|
|
32
|
+
* heading parses to nothing without saying so. The field names on `DesignDoc`
|
|
33
|
+
* keep the old words, since the preview reading them is retired separately.
|
|
34
|
+
*/
|
|
35
|
+
export const LEGACY_HEADINGS: Readonly<Record<string, string>> = {
|
|
36
|
+
Personality: 'Overview',
|
|
37
|
+
Color: 'Colors',
|
|
38
|
+
Borders: 'Shapes',
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function canonicalHeading(heading: string): string {
|
|
42
|
+
return LEGACY_HEADINGS[heading] ?? heading
|
|
43
|
+
}
|
|
44
|
+
|
|
28
45
|
const VERIFY_TAG = /\s*\?\s*verify\s*$/
|
|
29
46
|
|
|
30
47
|
/** A cell whose whole content is one balanced code span and nothing else. */
|
|
@@ -34,11 +51,11 @@ export function parseDesignDoc(path: string): DesignDoc {
|
|
|
34
51
|
const raw = readFileSync(path, 'utf8')
|
|
35
52
|
const sections = splitSections(raw)
|
|
36
53
|
return {
|
|
37
|
-
personality: prose(sections['
|
|
38
|
-
color: table(sections['
|
|
54
|
+
personality: prose(sections['Overview']),
|
|
55
|
+
color: table(sections['Colors']),
|
|
39
56
|
typography: table(sections['Typography']),
|
|
40
57
|
spacing: table(sections['Spacing']),
|
|
41
|
-
borders: table(sections['
|
|
58
|
+
borders: table(sections['Shapes']),
|
|
42
59
|
motion: prose(sections['Motion']),
|
|
43
60
|
iconography: prose(sections['Iconography']),
|
|
44
61
|
}
|
|
@@ -53,7 +70,7 @@ function splitSections(raw: string): Record<string, string> {
|
|
|
53
70
|
const match = line.match(/^##\s+(.+?)\s*$/)
|
|
54
71
|
if (match) {
|
|
55
72
|
if (current) out[current] = buffer.join('\n')
|
|
56
|
-
current = match[1]
|
|
73
|
+
current = canonicalHeading(match[1])
|
|
57
74
|
buffer = []
|
|
58
75
|
} else if (current) {
|
|
59
76
|
buffer.push(line)
|
package/src/design/render.ts
CHANGED
|
@@ -268,7 +268,7 @@ ${sections.join('\n')}
|
|
|
268
268
|
|
|
269
269
|
function sectionPersonality(text: string): string {
|
|
270
270
|
if (!text) return ''
|
|
271
|
-
return `<h2>
|
|
271
|
+
return `<h2>Overview</h2>\n<p>${escape(text)}</p>`
|
|
272
272
|
}
|
|
273
273
|
|
|
274
274
|
function sectionColor(rows: Row[]): string {
|
|
@@ -283,7 +283,7 @@ function sectionColor(rows: Row[]): string {
|
|
|
283
283
|
return `<tr><td>${swatch}${cellText(r, 'Role')}</td><td>${cellText(r, 'Intent')}</td><td><code>${shown}</code>${mark(r, 'Value')}</td></tr>`
|
|
284
284
|
})
|
|
285
285
|
.join('\n')
|
|
286
|
-
return `<h2>
|
|
286
|
+
return `<h2>Colors</h2>\n<table><thead><tr><th>Role</th><th>Intent</th><th>Value</th></tr></thead><tbody>${body}</tbody></table>`
|
|
287
287
|
}
|
|
288
288
|
|
|
289
289
|
function sectionTypography(rows: Row[]): string {
|
|
@@ -325,7 +325,7 @@ function sectionBorders(rows: Row[]): string {
|
|
|
325
325
|
return `<tr><td>${cellText(r, 'Role')}</td><td><code>${escape(radius)}</code>${mark(r, 'Radius')}</td><td><code>${escape(width)}</code>${mark(r, 'Width')}</td><td>${cellText(r, 'When used')}</td><td>${sample}</td></tr>`
|
|
326
326
|
})
|
|
327
327
|
.join('\n')
|
|
328
|
-
return `<h2>
|
|
328
|
+
return `<h2>Shapes</h2>\n<table><thead><tr><th>Role</th><th>Radius</th><th>Width</th><th>When used</th><th>Sample</th></tr></thead><tbody>${body}</tbody></table>`
|
|
329
329
|
}
|
|
330
330
|
|
|
331
331
|
/** A section the record writes as one-line rules draws them as a list. */
|
package/src/gate/measures.ts
CHANGED
|
@@ -18,9 +18,18 @@ import {
|
|
|
18
18
|
import { familyKeys } from '@/claude/skills-families'
|
|
19
19
|
import { measureDesignProse } from '@/design/budget'
|
|
20
20
|
import { DESIGN_DOCUMENT } from '@/design/regen'
|
|
21
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
listChangedFiles,
|
|
23
|
+
listFilesAtRef,
|
|
24
|
+
listRepositoryFiles,
|
|
25
|
+
readFileAtRef,
|
|
26
|
+
resolveBaseRef,
|
|
27
|
+
} from '@/git/files'
|
|
22
28
|
import { ceilingFindings } from '@/markdown/ceiling'
|
|
23
29
|
import { resolveMarkdown } from '@/markdown/files'
|
|
30
|
+
import { grownBlocks, normalizeProse } from '@/markdown/growth'
|
|
31
|
+
import { bodyLines } from '@/markdown/scan'
|
|
32
|
+
import { bulletStacks as findBulletStacks } from '@/markdown/stacks'
|
|
24
33
|
import { CHECKPOINTS } from '@/markdown/structure'
|
|
25
34
|
import {
|
|
26
35
|
isShippedCorpus,
|
|
@@ -501,6 +510,182 @@ export const documentCeiling: Measure = async (ctx) => {
|
|
|
501
510
|
}
|
|
502
511
|
}
|
|
503
512
|
|
|
513
|
+
/**
|
|
514
|
+
* The tracked documents a bullet run is held against: the context entries, the
|
|
515
|
+
* docs pages, and the skill bodies and references of both skill trees.
|
|
516
|
+
*
|
|
517
|
+
* Rules and the requirement records stay out, since a rule is one imperative
|
|
518
|
+
* per bullet and a requirement is a record of its own. Named here and nowhere
|
|
519
|
+
* else, so the stage and its tests read one scope.
|
|
520
|
+
*/
|
|
521
|
+
const STACK_SCOPE = [
|
|
522
|
+
/^canon\/context\/.+\.md$/,
|
|
523
|
+
/^docs\/.+\.md$/,
|
|
524
|
+
/^(?:claude|\.claude)\/skills\/[^/]+\/SKILL\.md$/,
|
|
525
|
+
/^(?:claude|\.claude)\/skills\/[^/]+\/references\/.+\.md$/,
|
|
526
|
+
]
|
|
527
|
+
|
|
528
|
+
/** Whether a path is one the bullet cap holds. */
|
|
529
|
+
export function inStackScope(rel: string): boolean {
|
|
530
|
+
return STACK_SCOPE.some((pattern) => pattern.test(rel))
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/** The context entries alone, where length drifts because `context-fold` appends after every merge. */
|
|
534
|
+
const GROWTH_SCOPE = /^canon\/context\/.+\.md$/
|
|
535
|
+
|
|
536
|
+
/** Whether a path is one the growth check reads. */
|
|
537
|
+
export function inGrowthScope(rel: string): boolean {
|
|
538
|
+
return GROWTH_SCOPE.test(rel)
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Every in-scope document against the five-bullet cap.
|
|
543
|
+
*
|
|
544
|
+
* The failure names each file with its longest run and where it opens. Read
|
|
545
|
+
* in-process rather than through `canon markdown audit`, whose exit code stays
|
|
546
|
+
* bans and dead links only, for the reason `documentCeiling` gives: the plan
|
|
547
|
+
* and groundwork skills run that audit on gitignored records, where a long
|
|
548
|
+
* list is not a defect.
|
|
549
|
+
*/
|
|
550
|
+
export const bulletStacks: Measure = async (ctx) => {
|
|
551
|
+
const files = await listRepositoryFiles(ctx.root)
|
|
552
|
+
if (files === undefined) {
|
|
553
|
+
return {
|
|
554
|
+
emissions: [],
|
|
555
|
+
unmeasured: 'git could not list the tree, so no bullet run was measured.',
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
const scoped = files.filter(inStackScope)
|
|
560
|
+
const failing: { rel: string; count: number; line: number }[] = []
|
|
561
|
+
|
|
562
|
+
for (const rel of scoped) {
|
|
563
|
+
let source: string
|
|
564
|
+
try {
|
|
565
|
+
source = readFileSync(join(ctx.root, rel), 'utf8')
|
|
566
|
+
} catch {
|
|
567
|
+
continue
|
|
568
|
+
}
|
|
569
|
+
const runs = findBulletStacks(bodyLines(source))
|
|
570
|
+
if (runs.length === 0) continue
|
|
571
|
+
const longest = runs.reduce((a, b) => (b.count > a.count ? b : a))
|
|
572
|
+
failing.push({ rel, count: longest.count, line: longest.line })
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
if (failing.length === 0) {
|
|
576
|
+
return {
|
|
577
|
+
emissions: [
|
|
578
|
+
info(
|
|
579
|
+
`No run of more than 5 plain bullets across ${plural(scoped.length, 'document')}`,
|
|
580
|
+
),
|
|
581
|
+
],
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const where = failing
|
|
586
|
+
.sort((a, b) => b.count - a.count)
|
|
587
|
+
.map((f) => `${f.rel}:${f.line} ${f.count} bullets`)
|
|
588
|
+
.join(', ')
|
|
589
|
+
return {
|
|
590
|
+
emissions: [],
|
|
591
|
+
failure: `${plural(failing.length, 'document')} carrying a run of more than 5 plain bullets: ${where}. Split each run under H3 headings with a few bullets or prose beneath, or open every item with a bold label or a link.`,
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* A context entry's paragraph or top-level bullet past its checkpoint that
|
|
597
|
+
* this branch added or changed.
|
|
598
|
+
*
|
|
599
|
+
* A block fails when its text is absent from every context entry the merge
|
|
600
|
+
* base held, so an unchanged long block passes wherever it now sits and an
|
|
601
|
+
* edited or new one fails. Reading the whole base folder rather than the same
|
|
602
|
+
* path is what lets a block move into a sibling entry. No resolvable base
|
|
603
|
+
* reports unmeasured rather than a pass, since a missing base would otherwise
|
|
604
|
+
* read every block as new or none as new.
|
|
605
|
+
*
|
|
606
|
+
* Only context entries are held to it. Length drifts there because
|
|
607
|
+
* `context-fold` appends after every merge, and in docs and skills it stays the
|
|
608
|
+
* audit's advisory warning.
|
|
609
|
+
*/
|
|
610
|
+
export const paragraphGrowth: Measure = async (ctx) => {
|
|
611
|
+
const base = await resolveBaseRef(ctx.root)
|
|
612
|
+
if (base === undefined) {
|
|
613
|
+
return {
|
|
614
|
+
emissions: [],
|
|
615
|
+
unmeasured:
|
|
616
|
+
'no merge base resolved, so no paragraph or bullet was compared against it.',
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
const changed = await listChangedFiles(ctx.root, base)
|
|
621
|
+
if (changed === undefined) {
|
|
622
|
+
return {
|
|
623
|
+
emissions: [],
|
|
624
|
+
unmeasured:
|
|
625
|
+
'git could not list the branch range, so no block was compared.',
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
const grown: {
|
|
630
|
+
rel: string
|
|
631
|
+
kind: string
|
|
632
|
+
line: number
|
|
633
|
+
characters: number
|
|
634
|
+
}[] = []
|
|
635
|
+
let corpus: string | undefined
|
|
636
|
+
|
|
637
|
+
for (const rel of changed.filter(inGrowthScope)) {
|
|
638
|
+
let source: string
|
|
639
|
+
try {
|
|
640
|
+
source = readFileSync(join(ctx.root, rel), 'utf8')
|
|
641
|
+
} catch {
|
|
642
|
+
continue
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
const own = normalizeProse((await readFileAtRef(ctx.root, base, rel)) ?? '')
|
|
646
|
+
const first = grownBlocks(source, own)
|
|
647
|
+
if (first.length === 0) continue
|
|
648
|
+
|
|
649
|
+
if (corpus === undefined) {
|
|
650
|
+
const paths =
|
|
651
|
+
(await listFilesAtRef(ctx.root, base, 'canon/context')) ?? []
|
|
652
|
+
const texts = await Promise.all(
|
|
653
|
+
paths
|
|
654
|
+
.filter(inGrowthScope)
|
|
655
|
+
.map((path) => readFileAtRef(ctx.root, base, path)),
|
|
656
|
+
)
|
|
657
|
+
corpus = normalizeProse(texts.filter((t) => t !== undefined).join('\n'))
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
for (const block of grownBlocks(source, corpus)) {
|
|
661
|
+
grown.push({
|
|
662
|
+
rel,
|
|
663
|
+
kind: block.kind,
|
|
664
|
+
line: block.line,
|
|
665
|
+
characters: block.characters,
|
|
666
|
+
})
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
if (grown.length === 0) {
|
|
671
|
+
return {
|
|
672
|
+
emissions: [
|
|
673
|
+
info(
|
|
674
|
+
`No paragraph past ${CHECKPOINTS.paragraph} characters and no bullet past ${CHECKPOINTS.bullet} grew in a context entry`,
|
|
675
|
+
),
|
|
676
|
+
],
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
const where = grown
|
|
681
|
+
.map((g) => `${g.rel}:${g.line} ${g.kind} ${g.characters} characters`)
|
|
682
|
+
.join(', ')
|
|
683
|
+
return {
|
|
684
|
+
emissions: [],
|
|
685
|
+
failure: `${plural(grown.length, 'block')} past the checkpoint added or changed on this branch: ${where}. Split each at a seam, or cut it under ${CHECKPOINTS.paragraph} characters for a paragraph and ${CHECKPOINTS.bullet} for a bullet.`,
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
504
689
|
/**
|
|
505
690
|
* Whether a path's stem, its basename up to the first `.`, repeats its
|
|
506
691
|
* immediate parent folder, either whole or as a `<folder>-` prefix.
|
|
@@ -659,7 +844,7 @@ export const markdownBans: Measure = async (ctx) => {
|
|
|
659
844
|
return {
|
|
660
845
|
emissions: [output(frame.all)],
|
|
661
846
|
failure:
|
|
662
|
-
'Markdown prose carries a banned character, or a relative link resolves to nothing on disk. Rewrite the sentence or fix the link, and reach for a code span only where the token is genuinely an identifier under discussion.',
|
|
847
|
+
'Markdown prose carries a banned character, or a relative link resolves to nothing on disk. Rewrite the sentence, since a colon or a comma standing where the em dash was is the same sentence, or fix the link, and reach for a code span only where the token is genuinely an identifier under discussion.',
|
|
663
848
|
}
|
|
664
849
|
}
|
|
665
850
|
|
package/src/gate/sequencer.ts
CHANGED
|
@@ -113,8 +113,6 @@ export function readProcLoad(): LoadReading | undefined {
|
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
export interface GateContext extends MeasureContext {
|
|
116
|
-
/** Whether the format stage writes or checks. */
|
|
117
|
-
readonly write: boolean
|
|
118
116
|
/**
|
|
119
117
|
* The changed set a scoped stage is read against, or `undefined` where
|
|
120
118
|
* scoping is off and every stage runs.
|
|
@@ -444,8 +442,6 @@ export async function runStages(
|
|
|
444
442
|
const results: StageResult[] = []
|
|
445
443
|
|
|
446
444
|
for (const stage of stages) {
|
|
447
|
-
if (stage.when !== undefined && !stage.when({ write: ctx.write })) continue
|
|
448
|
-
|
|
449
445
|
const result = await runStage(stage, ctx)
|
|
450
446
|
results.push(result)
|
|
451
447
|
onResult?.(result)
|
package/src/gate/stages.ts
CHANGED
|
@@ -11,10 +11,12 @@ import {
|
|
|
11
11
|
captureStamps,
|
|
12
12
|
clientCommandCitations,
|
|
13
13
|
designProse,
|
|
14
|
+
bulletStacks,
|
|
14
15
|
documentCeiling,
|
|
15
16
|
folderEchoedNames,
|
|
16
17
|
markdownBans,
|
|
17
18
|
type Measure,
|
|
19
|
+
paragraphGrowth,
|
|
18
20
|
pluginManifests,
|
|
19
21
|
readmeCitations,
|
|
20
22
|
recordIdempotence,
|
|
@@ -81,11 +83,6 @@ export interface Stage {
|
|
|
81
83
|
readonly scope?: RegExp
|
|
82
84
|
/** What a scoped-out stage says, so a skip never reads as a pass. */
|
|
83
85
|
readonly skipped?: string
|
|
84
|
-
/**
|
|
85
|
-
* Whether this stage belongs in the run at all, read off the write grant
|
|
86
|
-
* rather than the changed set. Absent means it always belongs.
|
|
87
|
-
*/
|
|
88
|
-
readonly when?: (options: { readonly write: boolean }) => boolean
|
|
89
86
|
readonly checks: readonly Check[]
|
|
90
87
|
/**
|
|
91
88
|
* The machine lock this stage holds while its checks run, so one box runs it
|
|
@@ -144,28 +141,15 @@ const script = (name: string, failure: string): Check => ({
|
|
|
144
141
|
* whole set at once.
|
|
145
142
|
*/
|
|
146
143
|
export const STAGES: readonly Stage[] = [
|
|
147
|
-
{
|
|
148
|
-
id: 'format',
|
|
149
|
-
label: 'Formatting',
|
|
150
|
-
when: ({ write }) => write,
|
|
151
|
-
checks: [
|
|
152
|
-
{
|
|
153
|
-
kind: 'command',
|
|
154
|
-
argv: ['bun', 'run', 'format'],
|
|
155
|
-
failure: 'Format failed',
|
|
156
|
-
},
|
|
157
|
-
],
|
|
158
|
-
success: 'Format applied',
|
|
159
|
-
},
|
|
160
144
|
{
|
|
161
145
|
id: 'format-check',
|
|
162
146
|
label: 'Format check',
|
|
163
|
-
when: ({ write }) => !write,
|
|
164
147
|
checks: [
|
|
165
148
|
{
|
|
166
149
|
kind: 'command',
|
|
167
150
|
argv: ['bun', 'run', 'check:format'],
|
|
168
|
-
failure:
|
|
151
|
+
failure:
|
|
152
|
+
'Format check failed. Run bun run format and commit the result.',
|
|
169
153
|
},
|
|
170
154
|
],
|
|
171
155
|
success: 'Format check passed',
|
|
@@ -335,19 +319,23 @@ export const STAGES: readonly Stage[] = [
|
|
|
335
319
|
checks: [{ kind: 'measure', measure: unreferencedRules }],
|
|
336
320
|
},
|
|
337
321
|
{
|
|
338
|
-
//
|
|
339
|
-
//
|
|
340
|
-
//
|
|
341
|
-
//
|
|
342
|
-
//
|
|
322
|
+
// The facts the audit can answer from the files gate: a cited path that
|
|
323
|
+
// does not resolve, a required section an entry does not declare, an H2
|
|
324
|
+
// outside the closed set of four, and an index disagreeing with its folder.
|
|
325
|
+
// Its length, depth, bullet, table, and provenance findings are judgment
|
|
326
|
+
// thresholds, and failing a push on one would make the stage something to
|
|
327
|
+
// route around. The length readings that gate sit in their own stages
|
|
328
|
+
// below, apart from the audit's exit code: the whole-document ceiling, the
|
|
329
|
+
// bullet cap, and the growth of a paragraph or a bullet past its
|
|
330
|
+
// checkpoint.
|
|
343
331
|
id: 'context-citations',
|
|
344
332
|
label: 'Context citations',
|
|
345
333
|
checks: [
|
|
346
334
|
{
|
|
347
335
|
kind: 'cli',
|
|
348
|
-
argv: ['context', 'audit', '--
|
|
336
|
+
argv: ['context', 'audit', '--gate'],
|
|
349
337
|
failure:
|
|
350
|
-
'A cited context path does not resolve. Run bun src/cli.ts context audit.',
|
|
338
|
+
'A cited context path does not resolve, an entry lacks a required section or carries an H2 outside Overview, Layout, Decisions, and Gotchas, or an index disagrees with its folder. Run bun src/cli.ts context audit --gate.',
|
|
351
339
|
},
|
|
352
340
|
],
|
|
353
341
|
success: 'Context citations resolve',
|
|
@@ -366,6 +354,23 @@ export const STAGES: readonly Stage[] = [
|
|
|
366
354
|
label: 'Document ceiling',
|
|
367
355
|
checks: [{ kind: 'measure', measure: documentCeiling }],
|
|
368
356
|
},
|
|
357
|
+
{
|
|
358
|
+
// Unscoped, since any branch can add a bullet to any entry. A run of more
|
|
359
|
+
// than 5 plain bullets fails the push in the context entries, the docs
|
|
360
|
+
// pages, and the skill bodies and references. Apart from the audit's exit
|
|
361
|
+
// code for the reason the Document ceiling stage above states.
|
|
362
|
+
id: 'bullet-stacks',
|
|
363
|
+
label: 'Bullet stacks',
|
|
364
|
+
checks: [{ kind: 'measure', measure: bulletStacks }],
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
// Context entries only, where length drifts. A paragraph past 700
|
|
368
|
+
// characters or a top-level bullet past 250 fails when this branch added or
|
|
369
|
+
// changed it, and an unchanged one passes until it is edited.
|
|
370
|
+
id: 'paragraph-growth',
|
|
371
|
+
label: 'Paragraph growth',
|
|
372
|
+
checks: [{ kind: 'measure', measure: paragraphGrowth }],
|
|
373
|
+
},
|
|
369
374
|
{
|
|
370
375
|
// Unscoped, since any branch can add a file anywhere and one git ls-files
|
|
371
376
|
// costs nothing. Reads paths rather than markdown, so it sits here rather
|
package/src/git/files.ts
CHANGED
|
@@ -165,3 +165,33 @@ export async function listRepositoryFiles(
|
|
|
165
165
|
|
|
166
166
|
return [...new Set([...tracked, ...untracked])].sort()
|
|
167
167
|
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* A file as `ref` holds it, or undefined where the ref holds no such path or
|
|
171
|
+
* git cannot answer. A path absent at the ref and a failed read are the same
|
|
172
|
+
* answer for a caller asking what the copy before this branch said.
|
|
173
|
+
*/
|
|
174
|
+
export async function readFileAtRef(
|
|
175
|
+
root: string,
|
|
176
|
+
ref: string,
|
|
177
|
+
rel: string,
|
|
178
|
+
): Promise<string | undefined> {
|
|
179
|
+
return git(root, ['show', `${ref}:${rel}`])
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Every path under `prefix` that `ref` holds, or undefined where git cannot answer. */
|
|
183
|
+
export async function listFilesAtRef(
|
|
184
|
+
root: string,
|
|
185
|
+
ref: string,
|
|
186
|
+
prefix: string,
|
|
187
|
+
): Promise<string[] | undefined> {
|
|
188
|
+
const listed = await git(root, [
|
|
189
|
+
'ls-tree',
|
|
190
|
+
'-r',
|
|
191
|
+
'--name-only',
|
|
192
|
+
ref,
|
|
193
|
+
'--',
|
|
194
|
+
prefix,
|
|
195
|
+
])
|
|
196
|
+
return listed === undefined ? undefined : listed.split('\n').filter(Boolean)
|
|
197
|
+
}
|
package/src/gov/adapter.ts
CHANGED
|
@@ -98,8 +98,8 @@ export function createGovAdapter(root: string): SyncAdapter {
|
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
/**
|
|
101
|
-
* Where the target already holds a rule, in whichever
|
|
102
|
-
* successor held outside the source's
|
|
101
|
+
* Where the target already holds a rule, in whichever rule folder. A
|
|
102
|
+
* successor held outside the source's folder stays where it is, the same way
|
|
103
103
|
* `locateSource` syncs a moved rule in place, rather than gaining a second copy.
|
|
104
104
|
*
|
|
105
105
|
* Holding the predecessor is the whole entitlement test, so no stamped chain
|
package/src/gov/citations.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs'
|
|
2
2
|
import { readFile } from 'node:fs/promises'
|
|
3
|
-
import {
|
|
3
|
+
import { join, resolve } from 'node:path'
|
|
4
4
|
import { $ } from 'bun'
|
|
5
5
|
import { isMarked } from '@/sweep/exempt-marker'
|
|
6
6
|
import { gitEnv } from '@/git/env'
|
|
@@ -41,14 +41,11 @@ export const RULE_DIRS: readonly string[] = [
|
|
|
41
41
|
export const GLOB_CORPUS = join('internal', 'rules')
|
|
42
42
|
|
|
43
43
|
/**
|
|
44
|
-
* How the citation was written,
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* `path` is a backticked repository path. `sibling` is a rule naming another
|
|
48
|
-
* rule by filename alone, which resolves inside the folder the citing rule
|
|
49
|
-
* sits in.
|
|
44
|
+
* How the citation was written, being a backticked repository path. A bare
|
|
45
|
+
* filename is no form, since with no folder around it nothing tells a rule
|
|
46
|
+
* from a convention such as `index.md`.
|
|
50
47
|
*/
|
|
51
|
-
export type CitationForm = 'path'
|
|
48
|
+
export type CitationForm = 'path'
|
|
52
49
|
|
|
53
50
|
/**
|
|
54
51
|
* What the sweep decided about one citation.
|
|
@@ -115,9 +112,6 @@ const FENCE = /^\s*(?:```|~~~)/
|
|
|
115
112
|
|
|
116
113
|
const FRONTMATTER_DELIMITER = /^---\s*$/
|
|
117
114
|
|
|
118
|
-
/** A rule filename, which is how a rule names a sibling with no folder around it. */
|
|
119
|
-
const SIBLING_RULE = /^\d{3}-[a-z0-9-]+\.md$/
|
|
120
|
-
|
|
121
115
|
/**
|
|
122
116
|
* A character that puts the span outside this repository, or outside paths
|
|
123
117
|
* altogether.
|
|
@@ -160,24 +154,7 @@ function namesAFile(span: string): boolean {
|
|
|
160
154
|
export function classifySpan(span: string): CitationForm | undefined {
|
|
161
155
|
if (isNotRepositoryPath(span)) return undefined
|
|
162
156
|
if (!namesAFile(span)) return undefined
|
|
163
|
-
|
|
164
|
-
return SIBLING_RULE.test(span) ? 'sibling' : undefined
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* Where a citation could answer, which is wherever the reader's own tools look
|
|
169
|
-
* and nowhere else.
|
|
170
|
-
*
|
|
171
|
-
* A sibling resolves inside the folder the citing rule sits in, since that is
|
|
172
|
-
* the only place a bare rule filename means anything.
|
|
173
|
-
*/
|
|
174
|
-
function candidatesFor(
|
|
175
|
-
form: CitationForm,
|
|
176
|
-
cited: string,
|
|
177
|
-
ruleFile: string,
|
|
178
|
-
): string[] {
|
|
179
|
-
if (form === 'sibling') return [join(dirname(ruleFile), cited)]
|
|
180
|
-
return [cited]
|
|
157
|
+
return span.includes('/') ? 'path' : undefined
|
|
181
158
|
}
|
|
182
159
|
|
|
183
160
|
interface RawCitation {
|
|
@@ -268,7 +245,7 @@ function isGoverned(cited: string, declared: readonly string[]): boolean {
|
|
|
268
245
|
*
|
|
269
246
|
* Anchored on the entry shape rather than searched for as a substring. A bare
|
|
270
247
|
* scan for the glob text finds it in the `description:` line first wherever a
|
|
271
|
-
* rule names what it governs in prose, which is how `
|
|
248
|
+
* rule names what it governs in prose, which is how `seed-mirror.md` reported
|
|
272
249
|
* its `CLAUDE.md` glob against line 2 instead of line 4.
|
|
273
250
|
*/
|
|
274
251
|
const LIST_ENTRY = /^\s*-\s*(?:'([^']*)'|"([^"]*)"|(\S.*?))\s*$/
|
|
@@ -410,7 +387,7 @@ export async function readCitations(root: string): Promise<CitationReport> {
|
|
|
410
387
|
}
|
|
411
388
|
|
|
412
389
|
for (const raw of collectCitations(text)) {
|
|
413
|
-
const candidates =
|
|
390
|
+
const candidates = [raw.cited]
|
|
414
391
|
const resolved = candidates.find((path) =>
|
|
415
392
|
existsSync(resolve(root, path)),
|
|
416
393
|
)
|
|
@@ -439,7 +416,7 @@ function classifyStatus(
|
|
|
439
416
|
): CitationStatus {
|
|
440
417
|
if (resolved !== undefined) return 'resolved'
|
|
441
418
|
if (raw.marked) return 'exempt'
|
|
442
|
-
if (
|
|
419
|
+
if (isGoverned(raw.cited, declared)) return 'governed'
|
|
443
420
|
return 'dead'
|
|
444
421
|
}
|
|
445
422
|
|
|
@@ -456,7 +433,7 @@ async function applyIgnored(
|
|
|
456
433
|
measured: Extract<CitationReport, { kind: 'measured' }>,
|
|
457
434
|
): Promise<CitationReport> {
|
|
458
435
|
const pending = measured.citations.filter(
|
|
459
|
-
(citation) => citation.status === 'dead'
|
|
436
|
+
(citation) => citation.status === 'dead',
|
|
460
437
|
)
|
|
461
438
|
if (pending.length === 0) return measured
|
|
462
439
|
|
package/src/gov/install.ts
CHANGED
|
@@ -28,7 +28,7 @@ export function canonRulesDir(target: string): string {
|
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
30
|
* This repository's own toolkit-only rules, installed beside `canon/` rather
|
|
31
|
-
* than interleaved into its
|
|
31
|
+
* than interleaved into its folders. No target ever holds this folder.
|
|
32
32
|
*/
|
|
33
33
|
export function installedInternalRulesDir(target: string): string {
|
|
34
34
|
return join(installedRulesDir(target), 'internal')
|
|
@@ -134,7 +134,7 @@ export function lookupRules(
|
|
|
134
134
|
|
|
135
135
|
/**
|
|
136
136
|
* Copies each rule into the subdirectory it was authored in, so the installed
|
|
137
|
-
* tree keeps the
|
|
137
|
+
* tree keeps the rule folder structure `governance/rules/` carries. Returns the
|
|
138
138
|
* target-relative paths the timeline prints.
|
|
139
139
|
*
|
|
140
140
|
* `destSubdir` picks which wrapper the rule lands under: `canon` for every
|
package/src/gov/list.ts
CHANGED
|
@@ -74,7 +74,7 @@ export function buildStackEntries(root: string): StackEntry[] {
|
|
|
74
74
|
|
|
75
75
|
/**
|
|
76
76
|
* Reads the catalog straight off the source tree. A rule's domain is the
|
|
77
|
-
* subdirectory it sits in, which is the
|
|
77
|
+
* subdirectory it sits in, which is the rule folder grouping install preserves, and
|
|
78
78
|
* its `detect` entry is the evidence that proposes it, null for a rule
|
|
79
79
|
* `detect.toml` does not name.
|
|
80
80
|
*/
|
package/src/gov/payload.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { stripFrontmatter } from '@/markdown/frontmatter'
|
|
|
5
5
|
/**
|
|
6
6
|
* Lists installed rules in the order the payload concatenates them. Sorting by
|
|
7
7
|
* path keeps the numeric filename prefixes ordered inside each subdirectory,
|
|
8
|
-
* which is how the rule
|
|
8
|
+
* which is how the rule folders are meant to read.
|
|
9
9
|
*/
|
|
10
10
|
export function listRuleFiles(rulesDir: string): string[] {
|
|
11
11
|
return [
|
package/src/gov/stacks.ts
CHANGED
|
@@ -78,9 +78,9 @@ export function loadGovStack(
|
|
|
78
78
|
* entry resolves to itself, so a folder and a slug reach the caller as one
|
|
79
79
|
* shape rather than two the caller has to tell apart.
|
|
80
80
|
*
|
|
81
|
-
* The directory wins over a rule file of the same name
|
|
82
|
-
*
|
|
83
|
-
*
|
|
81
|
+
* The directory wins over a rule file of the same name, which would leave
|
|
82
|
+
* that rule unreachable from a stack entry. `src/gov/rule-layout.test.ts`
|
|
83
|
+
* fails on any rule basename equal to a folder name, so the two never meet.
|
|
84
84
|
*/
|
|
85
85
|
export function expandStackEntry(root: string, entry: string): string[] {
|
|
86
86
|
const dir = join(rulesSourceDir(root), entry)
|
|
@@ -163,7 +163,7 @@ export function unreferencedRules(root: string): string[] {
|
|
|
163
163
|
* does not hold. `resolveRules` already walks a stack's `extends` ancestors,
|
|
164
164
|
* so reading its leaf entry is enough; no second walk resolves the chain
|
|
165
165
|
* itself. A stack the toolkit no longer ships resolves to nothing rather than
|
|
166
|
-
* throwing, the same way `readNewRules`'s
|
|
166
|
+
* throwing, the same way `readNewRules`'s folder fallback already treats it.
|
|
167
167
|
*
|
|
168
168
|
* A held rule's declared successor counts as held, since the sync that
|
|
169
169
|
* renames the old file installs it.
|
|
@@ -27,7 +27,7 @@ export interface Subject {
|
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
29
|
* Reports what each element changes about itself when it takes focus, which is
|
|
30
|
-
* the reading `governance/rules/ui/
|
|
30
|
+
* the reading `governance/rules/ui/a11y.md` states three rules against and
|
|
31
31
|
* nothing measures.
|
|
32
32
|
*
|
|
33
33
|
* The difference between rest and focus is the treatment, rather than the
|
package/src/markdown/ceiling.ts
CHANGED
|
@@ -34,9 +34,14 @@ export interface CeilingFinding {
|
|
|
34
34
|
readonly exempt: string | null
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
/** Whether the release tool writes this document, which no measure reads. */
|
|
38
|
+
export function isChangelog(rel: string): boolean {
|
|
39
|
+
return basename(rel) === CHANGELOG
|
|
40
|
+
}
|
|
41
|
+
|
|
37
42
|
/** The reason a document is exempt from the ceiling, or null where it is not. */
|
|
38
43
|
export function lengthExemption(rel: string, source: string): string | null {
|
|
39
|
-
if (
|
|
44
|
+
if (isChangelog(rel)) return CHANGELOG_REASON
|
|
40
45
|
|
|
41
46
|
for (const line of bodyLines(source)) {
|
|
42
47
|
if (line.fenced) continue
|