@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/commands/init.ts
CHANGED
|
@@ -37,10 +37,19 @@ export function register(program: Command): void {
|
|
|
37
37
|
'',
|
|
38
38
|
'Passing any option skips the confirmation prompt.',
|
|
39
39
|
'',
|
|
40
|
+
'Runs `canon tooling sync base --write` first, which replaces every',
|
|
41
|
+
'base golden config the target holds, then `canon claude init`,',
|
|
42
|
+
'`canon gov install`, and `canon wiki init`. Refuses a target that is',
|
|
43
|
+
'not a directory or is the toolkit root.',
|
|
44
|
+
'',
|
|
45
|
+
'Exit codes:',
|
|
46
|
+
' 0 every domain installed, or the prompt was cancelled',
|
|
47
|
+
' 1 the target was refused, or a domain failed, named on stderr',
|
|
48
|
+
'',
|
|
40
49
|
'Examples:',
|
|
41
50
|
' canon init',
|
|
42
51
|
' canon init ../my-app',
|
|
43
|
-
' canon init --stack node --add
|
|
52
|
+
' canon init --stack node --add astro,shadcn ../my-app',
|
|
44
53
|
' canon init --skip governance ../my-app',
|
|
45
54
|
'',
|
|
46
55
|
].join('\n'),
|
package/src/commands/markdown.ts
CHANGED
|
@@ -3,9 +3,14 @@ import { readFile } from 'node:fs/promises'
|
|
|
3
3
|
import { dirname, relative, resolve } from 'node:path'
|
|
4
4
|
import type { Command } from 'commander'
|
|
5
5
|
import { BAN_SETS, emptyBanSets } from '@/markdown/bans'
|
|
6
|
-
import { lengthExemption } from '@/markdown/ceiling'
|
|
6
|
+
import { isChangelog, lengthExemption } from '@/markdown/ceiling'
|
|
7
|
+
import { type ColonFinding, findColons } from '@/markdown/colons'
|
|
7
8
|
import { type MarkdownAuditRefusal, resolveMarkdown } from '@/markdown/files'
|
|
8
9
|
import { isGating } from '@/markdown/gate'
|
|
10
|
+
import {
|
|
11
|
+
findHeadingCase,
|
|
12
|
+
type HeadingCaseFinding,
|
|
13
|
+
} from '@/markdown/heading-case'
|
|
9
14
|
import { decodePath, findBrokenLinks, type LinkFinding } from '@/markdown/links'
|
|
10
15
|
import {
|
|
11
16
|
type BanFinding,
|
|
@@ -13,6 +18,7 @@ import {
|
|
|
13
18
|
bodyLines,
|
|
14
19
|
scanBans,
|
|
15
20
|
} from '@/markdown/scan'
|
|
21
|
+
import { bulletStacks, CAP, type StackFinding } from '@/markdown/stacks'
|
|
16
22
|
import {
|
|
17
23
|
BASELINE,
|
|
18
24
|
CHECKPOINTS,
|
|
@@ -52,6 +58,9 @@ interface FileReport {
|
|
|
52
58
|
readonly rel: string
|
|
53
59
|
readonly bans: readonly BanFinding[]
|
|
54
60
|
readonly links: readonly LinkFinding[]
|
|
61
|
+
readonly colons: readonly ColonFinding[]
|
|
62
|
+
readonly headingCase: readonly HeadingCaseFinding[]
|
|
63
|
+
readonly bulletStacks: readonly StackFinding[]
|
|
55
64
|
readonly structure: StructureReport
|
|
56
65
|
readonly renderedLines: number
|
|
57
66
|
/** The stated reason, or null where the document is not exempt. */
|
|
@@ -90,6 +99,14 @@ export function register(program: Command): void {
|
|
|
90
99
|
'Bullet, paragraph, and depth weight are judgments a reader settles,',
|
|
91
100
|
'so all three report and none of them fails a run.',
|
|
92
101
|
'',
|
|
102
|
+
'A mid-sentence colon and a heading with a capital after its first word',
|
|
103
|
+
'report the same way. A proper noun is the one case the second cannot',
|
|
104
|
+
'tell from title case, and the first is sometimes right.',
|
|
105
|
+
'',
|
|
106
|
+
'Bullet stacks report a run of more than 5 plain bullets under one',
|
|
107
|
+
'heading and never exit 2. The Bullet stacks gate stage owns that',
|
|
108
|
+
'verdict, for the reason the length line below states.',
|
|
109
|
+
'',
|
|
93
110
|
'Length reports a whole document past the ceiling and never exits 2.',
|
|
94
111
|
'The Document ceiling gate stage owns that verdict, since this audit also',
|
|
95
112
|
'runs on a record kept out of version control, where a long plan is not',
|
|
@@ -100,7 +117,7 @@ export function register(program: Command): void {
|
|
|
100
117
|
'it correctly and a flat paragraph there is not a defect.',
|
|
101
118
|
'',
|
|
102
119
|
'Rewrite the sentence carrying a hit rather than swapping the token for',
|
|
103
|
-
'a near-synonym. A code span clears the report and is the answer only',
|
|
120
|
+
'a near-synonym or a colon or a comma. A code span clears the report and is the answer only',
|
|
104
121
|
'where the token is genuinely an identifier under discussion, which is',
|
|
105
122
|
'what markdown.md reserves the span for.',
|
|
106
123
|
'',
|
|
@@ -160,9 +177,14 @@ async function runAudit(
|
|
|
160
177
|
const abs = resolve(root, rel)
|
|
161
178
|
const source = await readFile(abs, 'utf8')
|
|
162
179
|
const lines = bodyLines(source)
|
|
180
|
+
// The release tool writes a changelog, so neither advisory reads it.
|
|
181
|
+
const advisory = !isChangelog(rel)
|
|
163
182
|
return {
|
|
164
183
|
rel,
|
|
165
184
|
bans: scanBans(lines, bans),
|
|
185
|
+
colons: advisory ? findColons(lines) : [],
|
|
186
|
+
headingCase: advisory ? findHeadingCase(source) : [],
|
|
187
|
+
bulletStacks: advisory ? bulletStacks(lines) : [],
|
|
166
188
|
links: findBrokenLinks(lines, (path) =>
|
|
167
189
|
existsSync(resolve(dirname(abs), path)),
|
|
168
190
|
),
|
|
@@ -177,6 +199,9 @@ async function runAudit(
|
|
|
177
199
|
reportScope(scope.files, scope.unmatched)
|
|
178
200
|
reportBans(reports, bans, empty)
|
|
179
201
|
reportLinks(reports, root)
|
|
202
|
+
reportColons(reports)
|
|
203
|
+
reportHeadingCase(reports)
|
|
204
|
+
reportStacks(reports)
|
|
180
205
|
reportBullets(reports, checkpoints)
|
|
181
206
|
reportParagraphs(reports, checkpoints)
|
|
182
207
|
reportCadence(reports, checkpoints)
|
|
@@ -210,6 +235,9 @@ async function runAudit(
|
|
|
210
235
|
path: report.rel,
|
|
211
236
|
bans: report.bans,
|
|
212
237
|
links: report.links,
|
|
238
|
+
colons: report.colons,
|
|
239
|
+
headingCase: report.headingCase,
|
|
240
|
+
bulletStacks: report.bulletStacks,
|
|
213
241
|
longestRun: report.structure.longestRun,
|
|
214
242
|
longestRunLine: report.structure.longestRunLine,
|
|
215
243
|
heavyBullets: report.structure.heavyBullets,
|
|
@@ -230,6 +258,9 @@ async function runAudit(
|
|
|
230
258
|
bans: reports.flatMap((report) => report.bans),
|
|
231
259
|
links: reports.flatMap((report) => report.links),
|
|
232
260
|
structure: reports.map((report) => report.structure),
|
|
261
|
+
colons: reports.flatMap((report) => report.colons),
|
|
262
|
+
headingCase: reports.flatMap((report) => report.headingCase),
|
|
263
|
+
bulletStacks: reports.flatMap((report) => report.bulletStacks),
|
|
233
264
|
})
|
|
234
265
|
|
|
235
266
|
return gating ? EXIT_GATE : 0
|
|
@@ -310,7 +341,7 @@ function reportBans(
|
|
|
310
341
|
logWarn(`${plural(total, 'hit')} across ${plural(carrying.length, 'file')}`)
|
|
311
342
|
logWarn('This fails the run. Every other check below reports.')
|
|
312
343
|
logInfo(
|
|
313
|
-
|
|
344
|
+
"Rewrite the sentence rather than swapping the token for a near-synonym, or a colon or a comma in the dash's place. That is the same sentence.",
|
|
314
345
|
)
|
|
315
346
|
logInfo(
|
|
316
347
|
'A code span clears the report and is the answer only where the token is genuinely an identifier under discussion, which is what markdown.md reserves the span for.',
|
|
@@ -383,6 +414,120 @@ function reportLinks(reports: readonly FileReport[], root: string): void {
|
|
|
383
414
|
)
|
|
384
415
|
}
|
|
385
416
|
|
|
417
|
+
function reportStacks(reports: readonly FileReport[]): void {
|
|
418
|
+
logStep('Bullet stacks')
|
|
419
|
+
logInfo(
|
|
420
|
+
`A run of more than ${CAP} top-level bullets, with no paragraph or heading between them.`,
|
|
421
|
+
)
|
|
422
|
+
logInfo(
|
|
423
|
+
'A run whose every item opens with a bold label or a link is exempt. This never fails a run. The Bullet stacks gate stage owns that verdict.',
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
const carrying = reports
|
|
427
|
+
.filter((report) => report.bulletStacks.length > 0)
|
|
428
|
+
.sort((a, b) => longestStack(b) - longestStack(a))
|
|
429
|
+
|
|
430
|
+
if (carrying.length === 0) {
|
|
431
|
+
logInfo(`No run past ${CAP} bullets.`)
|
|
432
|
+
return
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const total = carrying.reduce(
|
|
436
|
+
(sum, report) => sum + report.bulletStacks.length,
|
|
437
|
+
0,
|
|
438
|
+
)
|
|
439
|
+
logWarn(`${plural(total, 'stack')} across ${plural(carrying.length, 'file')}`)
|
|
440
|
+
logInfo(
|
|
441
|
+
'Fold the run into prose, give each item an H3, or split the list at its seam.',
|
|
442
|
+
)
|
|
443
|
+
pipeOutput(
|
|
444
|
+
carrying
|
|
445
|
+
.map(
|
|
446
|
+
(report) =>
|
|
447
|
+
`${report.rel} ${plural(report.bulletStacks.length, 'stack')}\n${report.bulletStacks
|
|
448
|
+
.map((found) => ` :${found.line} ${found.count} bullets`)
|
|
449
|
+
.join('\n')}`,
|
|
450
|
+
)
|
|
451
|
+
.join('\n'),
|
|
452
|
+
)
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function longestStack(report: FileReport): number {
|
|
456
|
+
return Math.max(0, ...report.bulletStacks.map((found) => found.count))
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function reportColons(reports: readonly FileReport[]): void {
|
|
460
|
+
logStep('Colons')
|
|
461
|
+
logInfo(
|
|
462
|
+
'A colon followed by more text in a prose line, outside a label, a code span, and a link.',
|
|
463
|
+
)
|
|
464
|
+
logInfo(
|
|
465
|
+
'A heading, a table row, a fenced block, and a line-final colon are excluded. This never fails a run.',
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
const carrying = reports
|
|
469
|
+
.filter((report) => report.colons.length > 0)
|
|
470
|
+
.sort((a, b) => b.colons.length - a.colons.length)
|
|
471
|
+
|
|
472
|
+
if (carrying.length === 0) {
|
|
473
|
+
logInfo('No colon joining two clauses.')
|
|
474
|
+
return
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const total = carrying.reduce((sum, report) => sum + report.colons.length, 0)
|
|
478
|
+
logWarn(`${plural(total, 'colon')} across ${plural(carrying.length, 'file')}`)
|
|
479
|
+
logInfo(
|
|
480
|
+
'Split the sentence or rewrite it. A colon belongs after a label or at the end of a line introducing a list.',
|
|
481
|
+
)
|
|
482
|
+
pipeOutput(
|
|
483
|
+
carrying
|
|
484
|
+
.map(
|
|
485
|
+
(report) =>
|
|
486
|
+
`${report.rel} ${plural(report.colons.length, 'colon')}\n${report.colons
|
|
487
|
+
.map((found) => ` :${found.line}:${found.column + 1}`)
|
|
488
|
+
.join('\n')}`,
|
|
489
|
+
)
|
|
490
|
+
.join('\n'),
|
|
491
|
+
)
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function reportHeadingCase(reports: readonly FileReport[]): void {
|
|
495
|
+
logStep('Heading case')
|
|
496
|
+
logInfo(
|
|
497
|
+
'A heading or frontmatter title with a capitalized word after its first, outside code spans and all-caps tokens.',
|
|
498
|
+
)
|
|
499
|
+
logInfo(
|
|
500
|
+
'A proper noun reports too and is left as it is. This never fails a run.',
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
const carrying = reports
|
|
504
|
+
.filter((report) => report.headingCase.length > 0)
|
|
505
|
+
.sort((a, b) => b.headingCase.length - a.headingCase.length)
|
|
506
|
+
|
|
507
|
+
if (carrying.length === 0) {
|
|
508
|
+
logInfo('No heading carrying a capital after its first word.')
|
|
509
|
+
return
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const total = carrying.reduce(
|
|
513
|
+
(sum, report) => sum + report.headingCase.length,
|
|
514
|
+
0,
|
|
515
|
+
)
|
|
516
|
+
logWarn(
|
|
517
|
+
`${plural(total, 'heading')} across ${plural(carrying.length, 'file')}`,
|
|
518
|
+
)
|
|
519
|
+
pipeOutput(
|
|
520
|
+
carrying
|
|
521
|
+
.map(
|
|
522
|
+
(report) =>
|
|
523
|
+
`${report.rel} ${plural(report.headingCase.length, 'heading')}\n${report.headingCase
|
|
524
|
+
.map((found) => ` :${found.line} ${found.text}`)
|
|
525
|
+
.join('\n')}`,
|
|
526
|
+
)
|
|
527
|
+
.join('\n'),
|
|
528
|
+
)
|
|
529
|
+
}
|
|
530
|
+
|
|
386
531
|
function reportBullets(
|
|
387
532
|
reports: readonly FileReport[],
|
|
388
533
|
checkpoints: Checkpoints,
|
package/src/commands/pr.ts
CHANGED
|
@@ -44,7 +44,7 @@ import {
|
|
|
44
44
|
settleChecklist,
|
|
45
45
|
} from '@/pr/evidence'
|
|
46
46
|
import {
|
|
47
|
-
|
|
47
|
+
dropFramesWithLegacy,
|
|
48
48
|
FRAMES_BRANCH,
|
|
49
49
|
type FramesApi,
|
|
50
50
|
type FramesRefusal,
|
|
@@ -53,7 +53,8 @@ import {
|
|
|
53
53
|
isFrameHead,
|
|
54
54
|
isFramePass,
|
|
55
55
|
isFrameTheme,
|
|
56
|
-
|
|
56
|
+
LEGACY_FRAMES_BRANCH,
|
|
57
|
+
pruneFramesWithLegacy,
|
|
57
58
|
pushFrame,
|
|
58
59
|
readFrame,
|
|
59
60
|
type TreeEntry,
|
|
@@ -933,7 +934,7 @@ export function register(program: Command): void {
|
|
|
933
934
|
'after',
|
|
934
935
|
[
|
|
935
936
|
'',
|
|
936
|
-
`
|
|
937
|
+
`An add writes only refs/heads/${FRAMES_BRANCH}, through the GitHub git data API,`,
|
|
937
938
|
'so nothing is checked out and no other branch is touched. A frame lands',
|
|
938
939
|
'at pr-<number>/<short-head>/<pass>/box-<n>.png, or box-<n>-<theme>.png',
|
|
939
940
|
'with --theme, and `link` names that',
|
|
@@ -948,6 +949,11 @@ export function register(program: Command): void {
|
|
|
948
949
|
'request still open, reopened, closed exactly the given days ago, or one',
|
|
949
950
|
'whose state could not be read, listing that last kind under `unread`.',
|
|
950
951
|
'',
|
|
952
|
+
`Both also drain the ${LEGACY_FRAMES_BRANCH} branch a rename left behind,`,
|
|
953
|
+
'removing only, and carry `legacy: true` when it changed. --prune empties',
|
|
954
|
+
'it as its pull requests expire, and --drop clears one pull request at',
|
|
955
|
+
'once. An add never writes it.',
|
|
956
|
+
'',
|
|
951
957
|
'Read `reason` on the JSON record:',
|
|
952
958
|
' ok an add pushed, or a drop or prune finished',
|
|
953
959
|
' bad-mode not exactly one of --add, --drop, --prune',
|
|
@@ -2876,13 +2882,13 @@ function isTreeEntry(value: unknown): value is TreeEntry {
|
|
|
2876
2882
|
* that already exists or no longer does, which is another writer's doing and
|
|
2877
2883
|
* worth a re-read rather than a failure.
|
|
2878
2884
|
*/
|
|
2879
|
-
function ghFramesApi(call: ApiCaller): FramesApi {
|
|
2880
|
-
const ref = `git/refs/heads/${
|
|
2885
|
+
function ghFramesApi(call: ApiCaller, branch: string): FramesApi {
|
|
2886
|
+
const ref = `git/refs/heads/${branch}`
|
|
2881
2887
|
const refWrite = (result: ApiCall) =>
|
|
2882
2888
|
result.ok ? 'ok' : result.status === 422 ? 'conflict' : 'failed'
|
|
2883
2889
|
return {
|
|
2884
2890
|
async readTip() {
|
|
2885
|
-
const head = await call('GET', `git/ref/heads/${
|
|
2891
|
+
const head = await call('GET', `git/ref/heads/${branch}`)
|
|
2886
2892
|
if (!head.ok) {
|
|
2887
2893
|
return head.status === 404
|
|
2888
2894
|
? { kind: 'missing' }
|
|
@@ -2937,7 +2943,7 @@ function ghFramesApi(call: ApiCaller): FramesApi {
|
|
|
2937
2943
|
async createRef(commit) {
|
|
2938
2944
|
return refWrite(
|
|
2939
2945
|
await call('POST', 'git/refs', {
|
|
2940
|
-
ref: `refs/heads/${
|
|
2946
|
+
ref: `refs/heads/${branch}`,
|
|
2941
2947
|
sha: commit,
|
|
2942
2948
|
}),
|
|
2943
2949
|
)
|
|
@@ -3034,7 +3040,8 @@ async function runFrames(
|
|
|
3034
3040
|
|
|
3035
3041
|
if (Bun.which('gh') === null) return refuseFrames('gh-missing')
|
|
3036
3042
|
const runner = ghApiRunner(root)
|
|
3037
|
-
const api = ghFramesApi(runner.call)
|
|
3043
|
+
const api = ghFramesApi(runner.call, FRAMES_BRANCH)
|
|
3044
|
+
const legacyApi = ghFramesApi(runner.call, LEGACY_FRAMES_BRANCH)
|
|
3038
3045
|
const refuseWrite = (reason: FramesRefusal) =>
|
|
3039
3046
|
refuseFrames(
|
|
3040
3047
|
reason === 'push-failed' && runner.refusedWrites.length > 0
|
|
@@ -3059,7 +3066,7 @@ async function runFrames(
|
|
|
3059
3066
|
|
|
3060
3067
|
if (opts.prune !== undefined) {
|
|
3061
3068
|
const days = Number(opts.prune)
|
|
3062
|
-
const result = await
|
|
3069
|
+
const result = await pruneFramesWithLegacy(api, legacyApi, days, new Date())
|
|
3063
3070
|
if (result.kind === 'refused') return refuseWrite(result.reason)
|
|
3064
3071
|
const { kind: _kind, ...rest } = result
|
|
3065
3072
|
return finish({ days, ...rest }, [
|
|
@@ -3072,7 +3079,7 @@ async function runFrames(
|
|
|
3072
3079
|
|
|
3073
3080
|
const pull = Number(number)
|
|
3074
3081
|
if (opts.drop === true) {
|
|
3075
|
-
const result = await
|
|
3082
|
+
const result = await dropFramesWithLegacy(api, legacyApi, [pull])
|
|
3076
3083
|
if (result.kind === 'refused') return refuseWrite(result.reason)
|
|
3077
3084
|
const { kind: _kind, ...rest } = result
|
|
3078
3085
|
return finish({ number: pull, ...rest }, [
|
package/src/commands/records.ts
CHANGED
|
@@ -81,7 +81,7 @@ export function register(program: Command): void {
|
|
|
81
81
|
const records = program
|
|
82
82
|
.command('records')
|
|
83
83
|
.description(
|
|
84
|
-
'Check a governed corpus against its standard, and back up the session records under .
|
|
84
|
+
'Check a governed corpus against its standard, and back up the session records under .canon/',
|
|
85
85
|
)
|
|
86
86
|
.helpOption('-h, --help', 'Show this help message')
|
|
87
87
|
|
package/src/commands/sync.ts
CHANGED
|
@@ -57,6 +57,17 @@ export function register(program: Command): void {
|
|
|
57
57
|
.addHelpText(
|
|
58
58
|
'after',
|
|
59
59
|
[
|
|
60
|
+
'',
|
|
61
|
+
'Runs `canon gov sync`, `canon design sync`, and `canon claude sync`',
|
|
62
|
+
'on each domain installed, so each carries its own replace behavior:',
|
|
63
|
+
'governance replaces an edited rule and deletes a retired one, design',
|
|
64
|
+
'replaces its shipped files, and claude writes .gitignore alone.',
|
|
65
|
+
'Tooling is not synced. Refuses a dirty working tree.',
|
|
66
|
+
'',
|
|
67
|
+
'Exit codes:',
|
|
68
|
+
' 0 synced, no domain installed, or --check without --exit-code',
|
|
69
|
+
' 1 the target or a dirty tree was refused, opening the pull request',
|
|
70
|
+
' failed, or --check --exit-code found drift',
|
|
60
71
|
'',
|
|
61
72
|
'Examples:',
|
|
62
73
|
' canon sync',
|
package/src/commands/tooling.ts
CHANGED
|
@@ -106,6 +106,14 @@ export function register(program: Command): void {
|
|
|
106
106
|
'what `canon sync --check --exit-code` spells with a flag. Pass --check',
|
|
107
107
|
'to report the same list and always exit 0.',
|
|
108
108
|
'',
|
|
109
|
+
'--write replaces every golden config that differs from the stack.',
|
|
110
|
+
'Seeds are copy-once, so a seed the target already holds is left alone.',
|
|
111
|
+
'',
|
|
112
|
+
'Exit codes:',
|
|
113
|
+
' 0 synced, up to date, cancelled, or run with --check',
|
|
114
|
+
' 1 refused, or run headlessly with neither flag while a file would',
|
|
115
|
+
' be replaced',
|
|
116
|
+
'',
|
|
109
117
|
].join('\n'),
|
|
110
118
|
)
|
|
111
119
|
.action(
|
package/src/context/audit.ts
CHANGED
|
@@ -63,6 +63,22 @@ export const REQUIRED_SECTIONS_BY_FOLDER: Readonly<
|
|
|
63
63
|
context: ['Overview', 'Layout'],
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
/**
|
|
67
|
+
* The closed set of H2 sections per audited folder name, which a required
|
|
68
|
+
* section belongs to. An entry carrying another H2 is a finding under
|
|
69
|
+
* `--gate`, since the set is what makes a context entry cheap to find a fact
|
|
70
|
+
* in: a domain's own topic names go one level down as H3 items under
|
|
71
|
+
* `Decisions` and `Gotchas`.
|
|
72
|
+
*
|
|
73
|
+
* Held beside the required lists for the same reason they are held here, and
|
|
74
|
+
* keyed on the folder whose standard states it.
|
|
75
|
+
*/
|
|
76
|
+
export const ALLOWED_SECTIONS_BY_FOLDER: Readonly<
|
|
77
|
+
Record<string, readonly string[]>
|
|
78
|
+
> = {
|
|
79
|
+
context: ['Overview', 'Layout', 'Decisions', 'Gotchas'],
|
|
80
|
+
}
|
|
81
|
+
|
|
66
82
|
/**
|
|
67
83
|
* The context standard's list, kept as its own export because the audit record
|
|
68
84
|
* has published it flat under `checkpoints.requiredSections` since before any
|
|
@@ -225,6 +241,12 @@ export interface EntryReport {
|
|
|
225
241
|
* is `missingSections`, since one entry answers for its siblings.
|
|
226
242
|
*/
|
|
227
243
|
readonly sections: readonly string[]
|
|
244
|
+
/**
|
|
245
|
+
* H2 headings outside the folder's closed set, in document order, and empty
|
|
246
|
+
* outside a folder whose standard states one. What the folder carries in
|
|
247
|
+
* all is `extraSections`.
|
|
248
|
+
*/
|
|
249
|
+
readonly otherSections: readonly string[]
|
|
228
250
|
/**
|
|
229
251
|
* Whether the file declares itself a skeleton, which excludes it from the
|
|
230
252
|
* section check alone. Every other measure still reads it, since a stub is
|
|
@@ -281,6 +303,13 @@ export interface LengthFinding {
|
|
|
281
303
|
readonly causes: readonly LengthCause[]
|
|
282
304
|
}
|
|
283
305
|
|
|
306
|
+
export interface ExtraSectionFinding {
|
|
307
|
+
/** Repo-relative path of the entry carrying the headings. */
|
|
308
|
+
readonly rel: string
|
|
309
|
+
/** H2 headings outside the closed set, never empty. */
|
|
310
|
+
readonly headings: readonly string[]
|
|
311
|
+
}
|
|
312
|
+
|
|
284
313
|
export interface SectionFinding {
|
|
285
314
|
/**
|
|
286
315
|
* Repo-relative path of whatever owes the sections: the entry itself in the
|
|
@@ -442,6 +471,33 @@ function declaredSections(
|
|
|
442
471
|
return required.filter((section) => found.has(section))
|
|
443
472
|
}
|
|
444
473
|
|
|
474
|
+
const H2_TEXT = /^##\s+(.+?)\s*$/
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Finds the H2 headings outside the allowed set.
|
|
478
|
+
*
|
|
479
|
+
* H2 only, since the set closes the sections of an entry and says nothing of
|
|
480
|
+
* where its H3 items sit. Fenced blocks are skipped, so a template quoted
|
|
481
|
+
* inside an example declares nothing about the entry quoting it.
|
|
482
|
+
*/
|
|
483
|
+
function otherSections(
|
|
484
|
+
lines: readonly BodyLine[],
|
|
485
|
+
allowed: readonly string[],
|
|
486
|
+
): string[] {
|
|
487
|
+
if (allowed.length === 0) return []
|
|
488
|
+
|
|
489
|
+
const found: string[] = []
|
|
490
|
+
|
|
491
|
+
for (const line of lines) {
|
|
492
|
+
if (line.fenced) continue
|
|
493
|
+
|
|
494
|
+
const match = line.text.match(H2_TEXT)
|
|
495
|
+
if (match && !allowed.includes(match[1])) found.push(match[1])
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
return found
|
|
499
|
+
}
|
|
500
|
+
|
|
445
501
|
/**
|
|
446
502
|
* Finds the references naming a sibling entry by bare filename.
|
|
447
503
|
*
|
|
@@ -500,6 +556,7 @@ export function measureEntry(
|
|
|
500
556
|
governsContent = true,
|
|
501
557
|
siblings: readonly string[] = [],
|
|
502
558
|
required: readonly string[] = governsContent ? REQUIRED_SECTIONS : [],
|
|
559
|
+
allowed: readonly string[] = [],
|
|
503
560
|
): EntryReport {
|
|
504
561
|
const lines = bodyLines(source)
|
|
505
562
|
|
|
@@ -510,6 +567,7 @@ export function measureEntry(
|
|
|
510
567
|
provenance: governsContent ? provenance(lines) : [],
|
|
511
568
|
bareReferences: bareReferences(lines, siblings),
|
|
512
569
|
sections: declaredSections(lines, required),
|
|
570
|
+
otherSections: otherSections(lines, allowed),
|
|
513
571
|
stub: isStubSeed(source),
|
|
514
572
|
governed: governsContent,
|
|
515
573
|
}
|
|
@@ -589,6 +647,7 @@ export async function measureFolders(
|
|
|
589
647
|
governsContent(folder),
|
|
590
648
|
names.filter((name) => name !== self),
|
|
591
649
|
requiredSections(folder),
|
|
650
|
+
allowedSections(folder),
|
|
592
651
|
),
|
|
593
652
|
)
|
|
594
653
|
}
|
|
@@ -609,6 +668,13 @@ export function requiredSections(folder: AuditedFolder): readonly string[] {
|
|
|
609
668
|
: []
|
|
610
669
|
}
|
|
611
670
|
|
|
671
|
+
/** Names the H2 sections the folder's standard allows, empty where it states no set. */
|
|
672
|
+
export function allowedSections(folder: AuditedFolder): readonly string[] {
|
|
673
|
+
return Object.hasOwn(ALLOWED_SECTIONS_BY_FOLDER, folder.name)
|
|
674
|
+
? ALLOWED_SECTIONS_BY_FOLDER[folder.name]
|
|
675
|
+
: []
|
|
676
|
+
}
|
|
677
|
+
|
|
612
678
|
/**
|
|
613
679
|
* Reports whether a bare sibling name here is a reference by construction.
|
|
614
680
|
*
|
|
@@ -688,3 +754,20 @@ export function missingSections(
|
|
|
688
754
|
|
|
689
755
|
return findings
|
|
690
756
|
}
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* Names each entry carrying an H2 outside its folder's closed set.
|
|
760
|
+
*
|
|
761
|
+
* Per entry rather than per folder, unlike `missingSections`: a split folder's
|
|
762
|
+
* entries each answer for their own headings, since an extra section is a fact
|
|
763
|
+
* about the file it sits in. A stub seed is dropped for the reason
|
|
764
|
+
* `missingSections` drops one, a skeleton being exempt from the shape rather
|
|
765
|
+
* than from being well formed.
|
|
766
|
+
*/
|
|
767
|
+
export function extraSections(
|
|
768
|
+
entries: readonly EntryReport[],
|
|
769
|
+
): ExtraSectionFinding[] {
|
|
770
|
+
return entries
|
|
771
|
+
.filter((entry) => !entry.stub && entry.otherSections.length > 0)
|
|
772
|
+
.map((entry) => ({ rel: entry.rel, headings: entry.otherSections }))
|
|
773
|
+
}
|
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
* System prompts and user-message builders for the model layer, ported
|
|
3
3
|
* verbatim as data from the groundwork spike at
|
|
4
4
|
* `.canon/groundwork/93-canonical-doc-drift/scripts/prompts/v3.md` (diff
|
|
5
|
-
* mode) and `sweep-v1.md` (sweep mode).
|
|
5
|
+
* mode) and `sweep-v1.md` (sweep mode). The diff prompt is verbatim. The sweep
|
|
6
|
+
* prompt was tuned past `sweep-v1.md` with one decision check, measured
|
|
7
|
+
* against the groundwork's labelled set in
|
|
8
|
+
* `canon/context/context-model/overview.md`.
|
|
6
9
|
*
|
|
7
10
|
* Each prompt already states its own rules per canonical doc type inside one
|
|
8
11
|
* string, which is the shape the spike measured, so there is one prompt per
|
|
@@ -45,6 +48,8 @@ Decision checks, in order:
|
|
|
45
48
|
1. Judge what the text says, not how long it is. A long section that states current design once is KEEP.
|
|
46
49
|
2. A trailing "Measured at <sha> on <date>" does not make a paragraph current if another paragraph in the section states an older value on the same subject.
|
|
47
50
|
3. Short rationale for a current rule ("the form checks length itself because...") is KEEP. A story of the rounds that produced the rule is REWRITE.
|
|
51
|
+
4. Rationale is KEEP even when it is told in the past tense. A reason naming why an alternative was left out, what an earlier approach got wrong, what a measurement caught, or what a past run cost is the evidence for the current rule, so a sentence such as "the first form timed out" or "two runs took 1281s and 1924s" or "reading figures from real data caught three wrong claims" supports a rule that stands and stays KEEP. Past tense alone is never the test.
|
|
52
|
+
5. REWRITE needs the section to restate its own subject as a sequence: a superseded figure kept beside its replacement, "now" or "no longer" marking a change, branch or PR narration, or rounds of candidates. A maintenance step or an operator decision stated as a rule is current, not history.
|
|
48
53
|
|
|
49
54
|
Return only JSON: {"verdict": "...", "quotes": ["<up to three short phrases copied from the section that decided it>"], "reason": "<one sentence>"}
|
|
50
55
|
`
|
package/src/context/gate.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { SectionFinding } from '@/context/audit'
|
|
1
|
+
import type { ExtraSectionFinding, SectionFinding } from '@/context/audit'
|
|
2
2
|
import type { FolderDrift } from '@/context/index-drift'
|
|
3
3
|
|
|
4
4
|
export interface GateInput {
|
|
@@ -13,6 +13,8 @@ export interface GateInput {
|
|
|
13
13
|
*/
|
|
14
14
|
readonly requirementsOverWords: boolean
|
|
15
15
|
readonly sections: readonly SectionFinding[]
|
|
16
|
+
/** H2 headings outside a folder's closed set, gating under `--gate` alone. */
|
|
17
|
+
readonly extraSections: readonly ExtraSectionFinding[]
|
|
16
18
|
readonly drift: readonly FolderDrift[]
|
|
17
19
|
/**
|
|
18
20
|
* Whether the caller asked for the widened gate. False leaves a missing
|
|
@@ -36,7 +38,7 @@ export function hasDrift(drift: readonly FolderDrift[]): boolean {
|
|
|
36
38
|
* does a requirements record past its own word cap: the record states its rule
|
|
37
39
|
* for itself, which makes it a fact rather than a threshold a reader weighs. The findings `--gate` adds are
|
|
38
40
|
* the ones answerable from the file itself: a required section it does not
|
|
39
|
-
* declare, and an index disagreeing with its folder. Entry length, depth, bullet,
|
|
41
|
+
* declare, an H2 outside the closed set, and an index disagreeing with its folder. Entry length, depth, bullet,
|
|
40
42
|
* table, provenance, and the record's claim coverage are judgments, so they
|
|
41
43
|
* stay out under both modes.
|
|
42
44
|
*/
|
|
@@ -44,6 +46,7 @@ export function isGating({
|
|
|
44
46
|
unresolvedCitations,
|
|
45
47
|
requirementsOverWords,
|
|
46
48
|
sections,
|
|
49
|
+
extraSections,
|
|
47
50
|
drift,
|
|
48
51
|
widened,
|
|
49
52
|
}: GateInput): boolean {
|
|
@@ -51,5 +54,5 @@ export function isGating({
|
|
|
51
54
|
if (requirementsOverWords) return true
|
|
52
55
|
if (!widened) return false
|
|
53
56
|
|
|
54
|
-
return sections.length > 0 || hasDrift(drift)
|
|
57
|
+
return sections.length > 0 || extraSections.length > 0 || hasDrift(drift)
|
|
55
58
|
}
|
package/src/design/budget.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { canonicalHeading } from '@/design/parse'
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* The prose budget `claude/skills/canvas-board/references/design.md` sets for a design record, and the
|
|
3
5
|
* counter the `design` gate stage reads it through.
|
|
@@ -11,21 +13,23 @@
|
|
|
11
13
|
* `<` is dropped the way `prose()` drops it, and so is a fenced block or an
|
|
12
14
|
* HTML comment spanning several lines, which `prose()` would read in part. A
|
|
13
15
|
* `###` subsection counts toward the `##` section holding it, since the
|
|
14
|
-
* standard fixes the sections and a subheading is no way past the cap.
|
|
16
|
+
* standard fixes the sections and a subheading is no way past the cap. A
|
|
17
|
+
* legacy `## Personality` takes the Overview cap through the parser's alias,
|
|
18
|
+
* and a finding names the heading as the record wrote it.
|
|
15
19
|
*/
|
|
16
20
|
export const DESIGN_BUDGET = {
|
|
17
21
|
/** Words before the first `##` section, the title excluded. */
|
|
18
22
|
leadWords: 40,
|
|
19
|
-
/** Words in `##
|
|
20
|
-
|
|
21
|
-
|
|
23
|
+
/** Words in `## Overview`, held to one paragraph. */
|
|
24
|
+
overviewWords: 60,
|
|
25
|
+
overviewParagraphs: 1,
|
|
22
26
|
/** Bullets or paragraphs in any other section, beside its table. */
|
|
23
27
|
rules: 3,
|
|
24
28
|
/** Words in one of those rules, the proxy for one line. */
|
|
25
29
|
ruleWords: 25,
|
|
26
30
|
} as const
|
|
27
31
|
|
|
28
|
-
const
|
|
32
|
+
const OVERVIEW = 'Overview'
|
|
29
33
|
|
|
30
34
|
/** A list item at any depth, which starts a rule of its own. */
|
|
31
35
|
const BULLET = /^\s*(?:[-*+]|\d+[.)])\s+/
|
|
@@ -52,15 +56,15 @@ export function measureDesignProse(source: string): string[] {
|
|
|
52
56
|
`The lead holds ${total} words against a cap of ${DESIGN_BUDGET.leadWords}`,
|
|
53
57
|
)
|
|
54
58
|
}
|
|
55
|
-
} else if (section.heading ===
|
|
56
|
-
if (rules.length > DESIGN_BUDGET.
|
|
59
|
+
} else if (canonicalHeading(section.heading) === OVERVIEW) {
|
|
60
|
+
if (rules.length > DESIGN_BUDGET.overviewParagraphs) {
|
|
57
61
|
findings.push(
|
|
58
|
-
`${
|
|
62
|
+
`${section.heading} holds ${rules.length} paragraphs against a cap of ${DESIGN_BUDGET.overviewParagraphs}`,
|
|
59
63
|
)
|
|
60
64
|
}
|
|
61
|
-
if (total > DESIGN_BUDGET.
|
|
65
|
+
if (total > DESIGN_BUDGET.overviewWords) {
|
|
62
66
|
findings.push(
|
|
63
|
-
`${
|
|
67
|
+
`${section.heading} holds ${total} words against a cap of ${DESIGN_BUDGET.overviewWords}`,
|
|
64
68
|
)
|
|
65
69
|
}
|
|
66
70
|
} else {
|
package/src/design/document.ts
CHANGED
|
@@ -114,10 +114,10 @@ export function renderDesignDocument(tokens: DesignTokens = TOKENS): string {
|
|
|
114
114
|
'',
|
|
115
115
|
LEAD,
|
|
116
116
|
'',
|
|
117
|
-
section('
|
|
117
|
+
section('Overview', tokens.personality),
|
|
118
118
|
'',
|
|
119
119
|
section(
|
|
120
|
-
'
|
|
120
|
+
'Colors',
|
|
121
121
|
bullets(tokens.colorRules),
|
|
122
122
|
formatTable(['Role', 'Intent', 'Value'], colorRows(tokens.color)),
|
|
123
123
|
),
|
|
@@ -137,8 +137,10 @@ export function renderDesignDocument(tokens: DesignTokens = TOKENS): string {
|
|
|
137
137
|
formatTable(['Step', 'Multiplier', 'Value'], spaceRows(tokens.spacing)),
|
|
138
138
|
),
|
|
139
139
|
'',
|
|
140
|
+
section('Layout', bullets(tokens.layout)),
|
|
141
|
+
'',
|
|
140
142
|
section(
|
|
141
|
-
'
|
|
143
|
+
'Shapes',
|
|
142
144
|
bullets(tokens.bordersRules),
|
|
143
145
|
formatTable(
|
|
144
146
|
['Role', 'Radius', 'Width', 'When used'],
|
|
@@ -146,8 +148,6 @@ export function renderDesignDocument(tokens: DesignTokens = TOKENS): string {
|
|
|
146
148
|
),
|
|
147
149
|
),
|
|
148
150
|
'',
|
|
149
|
-
section('Layout', bullets(tokens.layout)),
|
|
150
|
-
'',
|
|
151
151
|
section('Motion', bullets(tokens.motion)),
|
|
152
152
|
'',
|
|
153
153
|
section('Iconography', bullets(tokens.iconography)),
|