@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/sync/check.ts
CHANGED
|
@@ -531,12 +531,12 @@ export function parseNewSkills(paths: string): string[] {
|
|
|
531
531
|
|
|
532
532
|
/**
|
|
533
533
|
* What the target's `.claude/rules/` proves about its entitlement. `held` is
|
|
534
|
-
* every rule name it carries and `
|
|
534
|
+
* every rule name it carries and `folders` every rule folder those names sit in.
|
|
535
535
|
*
|
|
536
536
|
* Read off the installed tree rather than off a stack name, because
|
|
537
537
|
* `gov install` records file hashes and never the stack it resolved, so the
|
|
538
538
|
* chain a target consumed survives nowhere else. `installRules` copies each
|
|
539
|
-
* rule into the subdirectory it was authored in, which is what makes the
|
|
539
|
+
* rule into the subdirectory it was authored in, which is what makes the folder
|
|
540
540
|
* folders legible as evidence.
|
|
541
541
|
*
|
|
542
542
|
* `successorOf` adds the name a held rule was renamed to, which the next sync
|
|
@@ -547,12 +547,12 @@ export function readInstalledRules(
|
|
|
547
547
|
successorOf?: (name: string) => string | undefined,
|
|
548
548
|
): {
|
|
549
549
|
held: Set<string>
|
|
550
|
-
|
|
550
|
+
folders: Set<string>
|
|
551
551
|
} {
|
|
552
552
|
const dir = join(target, ...INSTALL_MARKERS.governance)
|
|
553
553
|
const held = new Set<string>()
|
|
554
|
-
const
|
|
555
|
-
if (!isDirectory(dir)) return { held,
|
|
554
|
+
const folders = new Set<string>()
|
|
555
|
+
if (!isDirectory(dir)) return { held, folders }
|
|
556
556
|
|
|
557
557
|
for (const rel of new Bun.Glob('**/*.md').scanSync({
|
|
558
558
|
cwd: dir,
|
|
@@ -566,34 +566,34 @@ export function readInstalledRules(
|
|
|
566
566
|
held.add(name)
|
|
567
567
|
const successor = successorOf?.(name)
|
|
568
568
|
if (successor !== undefined) held.add(successor)
|
|
569
|
-
if (boundary > 0)
|
|
569
|
+
if (boundary > 0) folders.add(posix.slice(0, boundary))
|
|
570
570
|
}
|
|
571
571
|
|
|
572
|
-
return { held,
|
|
572
|
+
return { held, folders }
|
|
573
573
|
}
|
|
574
574
|
|
|
575
575
|
/**
|
|
576
576
|
* Narrows rules added upstream to the ones this target could receive and does
|
|
577
577
|
* not already hold.
|
|
578
578
|
*
|
|
579
|
-
* The
|
|
579
|
+
* The folder test is the entitlement filter, and `folders` carries two sources: the
|
|
580
580
|
* folders the target already holds, and the folders holding a rule the base
|
|
581
581
|
* stack resolves to.
|
|
582
582
|
* A rule authored under `lang/` or `ui/` is named by an individual stack, so it
|
|
583
583
|
* belongs to some targets and not others, and listing every added file would
|
|
584
584
|
* tell a base consumer about rules it was never entitled to. The test
|
|
585
|
-
* over-reports inside a
|
|
585
|
+
* over-reports inside a folder a target already carries, since one folder can be
|
|
586
586
|
* reached by more than one stack, and that costs a line where under-reporting
|
|
587
587
|
* would cost the whole point of the section.
|
|
588
588
|
*
|
|
589
|
-
* The `held` test is what keeps a rule that moved
|
|
589
|
+
* The `held` test is what keeps a rule that moved folders upstream out. A rename
|
|
590
590
|
* reaches this diff as an addition, and the target already has the file under
|
|
591
591
|
* its old folder, so matching by name is what tells the two apart.
|
|
592
592
|
*/
|
|
593
593
|
export function selectNewRules(
|
|
594
594
|
paths: string,
|
|
595
595
|
held: ReadonlySet<string>,
|
|
596
|
-
|
|
596
|
+
folders: ReadonlySet<string>,
|
|
597
597
|
): string[] {
|
|
598
598
|
const prefix = 'governance/rules/'
|
|
599
599
|
const names = new Set<string>()
|
|
@@ -604,11 +604,11 @@ export function selectNewRules(
|
|
|
604
604
|
|
|
605
605
|
const rel = trimmed.slice(prefix.length)
|
|
606
606
|
const boundary = rel.indexOf('/')
|
|
607
|
-
const
|
|
607
|
+
const folder = boundary === -1 ? '' : rel.slice(0, boundary)
|
|
608
608
|
const name = basename(rel, '.md')
|
|
609
609
|
|
|
610
610
|
if (held.has(name)) continue
|
|
611
|
-
if (
|
|
611
|
+
if (folder !== '' && !folders.has(folder)) continue
|
|
612
612
|
|
|
613
613
|
names.add(name)
|
|
614
614
|
}
|
|
@@ -617,29 +617,29 @@ export function selectNewRules(
|
|
|
617
617
|
}
|
|
618
618
|
|
|
619
619
|
/**
|
|
620
|
-
*
|
|
620
|
+
* Rule folders holding a rule the base stack resolves to. Every governance
|
|
621
621
|
* stack extends base, so a new rule authored beside one of base's rules is
|
|
622
622
|
* reported to every target the fallback below reads.
|
|
623
623
|
*
|
|
624
|
-
* This is what covers a
|
|
625
|
-
* read off folders the target already holds, and a
|
|
624
|
+
* This is what covers a folder no target can carry yet. Entitlement is otherwise
|
|
625
|
+
* read off folders the target already holds, and a folder base reaches later
|
|
626
626
|
* exists in no installed tree, so without this the rules inside it would reach
|
|
627
627
|
* nobody. Read through `resolveRules` rather than off the stack's entries,
|
|
628
628
|
* since base names its rules one by one and an entry is a slug rather than a
|
|
629
629
|
* folder.
|
|
630
630
|
*
|
|
631
631
|
* A folder only a leaf stack reaches is deliberately absent. It is entitled to
|
|
632
|
-
* some targets and not others, which is the distinction the
|
|
632
|
+
* some targets and not others, which is the distinction the folder test makes and
|
|
633
633
|
* the installed tree is the only evidence of.
|
|
634
634
|
*/
|
|
635
|
-
export function
|
|
635
|
+
export function baseFolders(root: string): Set<string> {
|
|
636
636
|
const resolution = resolveRules(root, 'base')
|
|
637
637
|
if (!resolution.ok) return new Set()
|
|
638
638
|
|
|
639
639
|
return new Set(
|
|
640
640
|
lookupRules(root, resolution.rules)
|
|
641
641
|
.found.map((source) => source.subdir.split(sep)[0] ?? '')
|
|
642
|
-
.filter((
|
|
642
|
+
.filter((folder) => folder !== ''),
|
|
643
643
|
)
|
|
644
644
|
}
|
|
645
645
|
|
|
@@ -651,7 +651,7 @@ export function baseBands(root: string): Set<string> {
|
|
|
651
651
|
* `collectMissing` reports per file through the domain scan, so a rule the
|
|
652
652
|
* chain names and the target lacks reaches both surfaces the same way.
|
|
653
653
|
*
|
|
654
|
-
* The diff-and-
|
|
654
|
+
* The diff-and-folder path stays as the fallback for a target stamped before
|
|
655
655
|
* governance recorded a chain. Rules are domain-scoped there too, so it
|
|
656
656
|
* measures from governance's own anchor rather than from the oldest anchor
|
|
657
657
|
* across domains the way `readNewSkills` does. A shared anchor would let
|
|
@@ -670,7 +670,7 @@ export function baseBands(root: string): Set<string> {
|
|
|
670
670
|
* unmeasured result from a clean one.
|
|
671
671
|
*
|
|
672
672
|
* A recorded chain naming a stack the toolkit no longer ships falls through to
|
|
673
|
-
* the
|
|
673
|
+
* the folder-based path below rather than reporting the empty list an
|
|
674
674
|
* unresolved chain would otherwise produce. That empty list reads exactly
|
|
675
675
|
* like a target holding everything, which is the same failure this function
|
|
676
676
|
* exists to close, so a retired stack name is read the same as no chain at
|
|
@@ -702,8 +702,8 @@ export async function readNewRules(
|
|
|
702
702
|
'governance/rules/',
|
|
703
703
|
])
|
|
704
704
|
|
|
705
|
-
const { held,
|
|
706
|
-
return selectNewRules(paths, held,
|
|
705
|
+
const { held, folders } = readInstalledRules(target, successorResolver(root))
|
|
706
|
+
return selectNewRules(paths, held, folders.union(baseFolders(root)))
|
|
707
707
|
}
|
|
708
708
|
|
|
709
709
|
async function readUpstream(
|
package/src/sync/copy.ts
CHANGED
|
@@ -4,11 +4,10 @@ import { dirname } from 'node:path'
|
|
|
4
4
|
/**
|
|
5
5
|
* Copies with `cp` semantics. Two behaviors matter and neither is the default
|
|
6
6
|
* in the obvious alternatives. `Bun.write` drops the executable bit, which
|
|
7
|
-
* would land husky hooks
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* alone.
|
|
7
|
+
* would land husky hooks non-executable. Plain `copyFile` imposes the source
|
|
8
|
+
* mode on an existing destination, so a file the target deliberately
|
|
9
|
+
* restricted to 600, or an earlier stack set executable, comes back at the
|
|
10
|
+
* source's mode. `cp` leaves an existing destination's mode alone.
|
|
12
11
|
*
|
|
13
12
|
* Every domain that writes into a target routes through here. The tooling
|
|
14
13
|
* inject path found the trap first, and the sync engine inherits it for every
|
|
@@ -20,7 +20,6 @@ packages = [
|
|
|
20
20
|
"preview" = "WORKTREE_PORT_OFFSET=$(bash scripts/worktree-port.sh) && export WORKTREE_PORT_OFFSET && astro preview"
|
|
21
21
|
"astro" = "astro"
|
|
22
22
|
"typecheck" = "astro check"
|
|
23
|
-
"setup" = "./scripts/setup.sh"
|
|
24
23
|
"card:dev" = "WORKTREE_PORT_OFFSET=$(bash scripts/worktree-port.sh) && export WORKTREE_PORT_OFFSET && astro dev --config card.config.mjs"
|
|
25
24
|
"card:check" = "bash scripts/check-card-exclusion.sh"
|
|
26
25
|
|
|
@@ -20,7 +20,7 @@ The astro stack covers Astro + TypeScript projects: content sites, marketing sit
|
|
|
20
20
|
- `astro.config.mjs`: `@astrojs/react` integration, `@tailwindcss/vite` in `vite.plugins`, `@/` path alias via `vite.resolve.alias`, `ASTRO_SITE` env for the `site` field. Port `4321` plus `WORKTREE_PORT_OFFSET` at `server.port`, with `strictPort` under `vite.server` and `vite.preview`. Astro merges the user's `vite` block into the config backing both its dev and its static preview server, and feeds `server.port` through as the preview port, so the port sits at the top level while the bind guarantee sits under `vite`.
|
|
21
21
|
- `vitest.config.ts`: uses `getViteConfig` from `astro/config` (not `mergeConfig`). jsdom, globals, setup file, `passWithNoTests: true`, v8 coverage, `**/*.astro` in coverage excludes.
|
|
22
22
|
- `playwright.config.ts`: all browsers, `webServer` runs `bun run build && bun run preview` on port `4321` plus `WORKTREE_PORT_OFFSET`, `reuseExistingServer: false`. Astro's dev/prod gap is wide (MDX, island hydration, asset optimization), so E2E always tests the built `dist/`. `DIST_PREBUILT` set in the environment drops the `build` half, running `bun run preview` alone against a `dist/` a prior CI job already produced.
|
|
23
|
-
- `tsconfig.json`: extends `astro/tsconfigs/strict`, adds `skipLibCheck`, `vitest/globals` and `@testing-library/jest-dom` in types, `@/` paths.
|
|
23
|
+
- `tsconfig.json`: extends `astro/tsconfigs/strict`, adds `skipLibCheck`, `vitest/globals` and `@testing-library/jest-dom/vitest` in types, `@/` paths.
|
|
24
24
|
- `card.config.mjs`, `card-src/pages/og-card.astro`, and `scripts/check-card-exclusion.sh`: the social card route, its structural build exclusion, and the check enforcing it. See `## Social card route` below.
|
|
25
25
|
- `eslint.config.js`: overrides the web layer. Adds `eslint-plugin-astro` (`.astro` parser via `astro-eslint-parser`). React-hooks scoped to `.jsx`/`.tsx` only (`.astro` is not React). The shared block's `files` selector includes `.astro`, so `check-file/filename-naming-convention` reaches `.ts`, `.tsx`, and `.astro` under `KEBAB_CASE`, overriding Astro's own PascalCase component convention deliberately, on the ground that a component's name in markup comes from the import binding rather than the filename. `.js` and `.jsx` stay out of the rule's own pattern, matching `web` and `nextjs`. `check-file/folder-naming-convention` reaches every `src/**` folder except `__tests__` and the whole `pages/` subtree, which carries its own off-block for the bracket-named dynamic routes and nested slug folders Astro's file-based routing produces. See `canon/context/tooling/stacks.md` for the measurement.
|
|
26
26
|
|
|
@@ -32,15 +32,7 @@ The astro stack covers Astro + TypeScript projects: content sites, marketing sit
|
|
|
32
32
|
|
|
33
33
|
Unit-test React island components (`.tsx`) only. Do not test `.astro` files. Page-level behavior is verified by Playwright against rendered output.
|
|
34
34
|
|
|
35
|
-
Vitest is pinned to `^
|
|
36
|
-
|
|
37
|
-
## Setup script
|
|
38
|
-
|
|
39
|
-
- File: `scripts/setup.sh`. Destructive: deletes `.git` and self-removes after running. Run once immediately after scaffolding.
|
|
40
|
-
- Prompt for project name, normalize to kebab-case, derive title-cased display name.
|
|
41
|
-
- Update `package.json` name and version, inject verify/clean/update scripts, remove setup.
|
|
42
|
-
- Update `astro.config.mjs`: set `site` if provided.
|
|
43
|
-
- Update `<title>` and `<meta name="description">` in the default layout.
|
|
35
|
+
Vitest is pinned to `^5` in the web manifest, which supports the Vite 8 that Astro 7 bundles. The earlier `^3` pin avoided `^4` bundling rolldown-vite, which ignored the esbuild JSX config in `@vitejs/plugin-react`. Astro now bundles Vite 8 itself, so that conflict no longer applies.
|
|
44
36
|
|
|
45
37
|
## Prettier (extend)
|
|
46
38
|
|
|
@@ -27,8 +27,12 @@ interface Phase {
|
|
|
27
27
|
readonly title: string
|
|
28
28
|
readonly failed: string
|
|
29
29
|
readonly passed: string
|
|
30
|
+
readonly tools?: readonly Tool[]
|
|
30
31
|
}
|
|
31
32
|
|
|
33
|
+
const SHELL_TOOLS_HINT =
|
|
34
|
+
"Install it with 'sudo apt-get install shfmt shellcheck' or 'brew install shfmt shellcheck'"
|
|
35
|
+
|
|
32
36
|
const PHASES: readonly Phase[] = [
|
|
33
37
|
{
|
|
34
38
|
script: 'typecheck',
|
|
@@ -42,16 +46,10 @@ const PHASES: readonly Phase[] = [
|
|
|
42
46
|
failed: 'Lint failed',
|
|
43
47
|
passed: 'Lint passed',
|
|
44
48
|
},
|
|
45
|
-
{
|
|
46
|
-
script: 'format',
|
|
47
|
-
title: 'Formatting',
|
|
48
|
-
failed: 'Format failed',
|
|
49
|
-
passed: 'Format applied',
|
|
50
|
-
},
|
|
51
49
|
{
|
|
52
50
|
script: 'check:format',
|
|
53
51
|
title: 'Format check',
|
|
54
|
-
failed:
|
|
52
|
+
failed: "Format check failed. Run 'bun run format' to fix it",
|
|
55
53
|
passed: 'Format check passed',
|
|
56
54
|
},
|
|
57
55
|
{
|
|
@@ -59,6 +57,10 @@ const PHASES: readonly Phase[] = [
|
|
|
59
57
|
title: 'Shell',
|
|
60
58
|
failed: 'Shell check failed',
|
|
61
59
|
passed: 'Shell check passed',
|
|
60
|
+
tools: [
|
|
61
|
+
{ name: 'shfmt', hint: SHELL_TOOLS_HINT },
|
|
62
|
+
{ name: 'shellcheck', hint: SHELL_TOOLS_HINT },
|
|
63
|
+
],
|
|
62
64
|
},
|
|
63
65
|
{
|
|
64
66
|
script: 'test:run',
|
|
@@ -77,7 +79,11 @@ const PHASES: readonly Phase[] = [
|
|
|
77
79
|
// A subfolder synced with --skip base declares none of these, since the
|
|
78
80
|
// repository root owns them, so a list that does not require one runs it only
|
|
79
81
|
// where the folder declares it.
|
|
80
|
-
const BASE_PHASES = new Set(['
|
|
82
|
+
const BASE_PHASES = new Set(['check:format'])
|
|
83
|
+
|
|
84
|
+
// An add-on stack declares these, keyed to the stack that ships each, so a
|
|
85
|
+
// folder that never synced the add-on skips the phase rather than failing it.
|
|
86
|
+
const ADD_ON_PHASES = new Map([['check:shell', 'shell']])
|
|
81
87
|
|
|
82
88
|
const PHASE_LIST = 'scripts/verify.json'
|
|
83
89
|
const CARD_EXCLUSION = 'scripts/check-card-exclusion.sh'
|
|
@@ -226,8 +232,8 @@ function readDeclaredScripts(): Set<string> {
|
|
|
226
232
|
)
|
|
227
233
|
}
|
|
228
234
|
|
|
229
|
-
function checkTools(
|
|
230
|
-
for (const tool of
|
|
235
|
+
function checkTools(tools: readonly Tool[]): void {
|
|
236
|
+
for (const tool of tools) {
|
|
231
237
|
if (onPath(tool.name)) continue
|
|
232
238
|
fail(
|
|
233
239
|
tool.hint === undefined
|
|
@@ -250,7 +256,8 @@ function runPhase(
|
|
|
250
256
|
declared: Set<string>,
|
|
251
257
|
): void {
|
|
252
258
|
const isBase = BASE_PHASES.has(phase.script)
|
|
253
|
-
|
|
259
|
+
const addOn = ADD_ON_PHASES.get(phase.script)
|
|
260
|
+
if (!required && !isBase && addOn === undefined) return
|
|
254
261
|
|
|
255
262
|
logStep(phase.title)
|
|
256
263
|
if (!declared.has(phase.script)) {
|
|
@@ -260,10 +267,13 @@ function runPhase(
|
|
|
260
267
|
)
|
|
261
268
|
}
|
|
262
269
|
logSkip(
|
|
263
|
-
|
|
270
|
+
addOn === undefined
|
|
271
|
+
? `Skipped: ${phase.script} is not declared here, so the repository root runs it`
|
|
272
|
+
: `Skipped: ${phase.script} is not declared here. Run 'canon tooling sync ${addOn} . --write' to add it`,
|
|
264
273
|
)
|
|
265
274
|
return
|
|
266
275
|
}
|
|
276
|
+
checkTools(phase.tools ?? [])
|
|
267
277
|
runCheck(`bun run ${phase.script}`, phase.failed)
|
|
268
278
|
logInfo(phase.passed)
|
|
269
279
|
}
|
|
@@ -332,7 +342,7 @@ function checkMarkdownBans(): void {
|
|
|
332
342
|
function main(): void {
|
|
333
343
|
const declared = readDeclaredScripts()
|
|
334
344
|
const list = readPhaseList()
|
|
335
|
-
checkTools(list)
|
|
345
|
+
checkTools(list.tools)
|
|
336
346
|
checkPreconditions(list)
|
|
337
347
|
|
|
338
348
|
if (!isNested) print(`${GREY}┌${NC}`)
|
|
@@ -6,7 +6,7 @@ scaffold = ""
|
|
|
6
6
|
|
|
7
7
|
[sync]
|
|
8
8
|
source = "configs"
|
|
9
|
-
per_root = ["scripts/verify.
|
|
9
|
+
per_root = ["scripts/verify.ts", "scripts/verify.json"]
|
|
10
10
|
|
|
11
11
|
[dependencies.dev]
|
|
12
12
|
packages = [
|
|
@@ -17,11 +17,12 @@ packages = [
|
|
|
17
17
|
]
|
|
18
18
|
|
|
19
19
|
[scripts]
|
|
20
|
-
"check:format" = "prettier --check --log-level warn --ignore-path .gitignore --ignore-path .prettierignore .
|
|
21
|
-
"
|
|
22
|
-
"format" = "prettier --write --log-level warn --ignore-path .gitignore --ignore-path .prettierignore . && find scripts -name '*.sh' -exec shfmt --write --indent 2 {} +"
|
|
20
|
+
"check:format" = "prettier --check --log-level warn --ignore-path .gitignore --ignore-path .prettierignore ."
|
|
21
|
+
"format" = "prettier --write --log-level warn --ignore-path .gitignore --ignore-path .prettierignore ."
|
|
23
22
|
"prepare" = "husky"
|
|
24
|
-
|
|
23
|
+
|
|
24
|
+
[scripts.override]
|
|
25
|
+
"check" = "bun scripts/verify.ts"
|
|
25
26
|
|
|
26
27
|
[gitignore]
|
|
27
28
|
"# System" = [".DS_Store"]
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## Overview
|
|
4
4
|
|
|
5
|
-
The base layer covers every project the toolkit scaffolds, whatever language sits on top. It ships formatting,
|
|
5
|
+
The base layer covers every project the toolkit scaffolds, whatever language sits on top. It ships formatting, conventional commits, git hooks, CI, and the verify runner. Every other stack extends it, so a decision made here is one every stack inherits. Shell formatting and linting live in the `shell` add-on stack, which a project syncs once it writes shell.
|
|
6
6
|
|
|
7
7
|
A repository with several language roots layers this stack once at the root and skips it per subtree, since git honors only one `core.hooksPath` and a re-dropped husky silently breaks. `canon docs scenarios` carries the recipe.
|
|
8
8
|
|
|
@@ -11,13 +11,10 @@ A repository with several language roots layers this stack once at the root and
|
|
|
11
11
|
Golden config files live in `tooling/base/configs/` and are copied into the target on `canon tooling sync base . --write`, replacing whatever sits at those paths. They are the source of truth. The reference covers rationale and tradeoffs. Configs show the concrete setup.
|
|
12
12
|
|
|
13
13
|
- `.prettierrc`: `semi: false`, `singleQuote: true`, plus a parser override per non-standard extension (`.mdx` to `markdown`).
|
|
14
|
-
- `.shellcheckrc`: `external-sources=true`. Required for shellcheck to follow `source` directives.
|
|
15
|
-
- `.editorconfig`: `root = true`, with an `[*.sh]` block setting `indent_style = space` and `indent_size = 2`.
|
|
16
14
|
- `commitlint.config.js`: ESM default export extending `@commitlint/config-conventional`. Rules are `header-max-length: 72`, `scope-case: lower-case`, `subject-full-stop: never`, `subject-case` disabled, and a local `no-claude-co-author` rule refusing a `Co-authored-by` trailer that names Claude or Anthropic.
|
|
17
15
|
- `.husky/`: `pre-commit`, `commit-msg`, `pre-push`, `post-merge`, `post-rewrite`.
|
|
18
|
-
- `scripts/verify.
|
|
19
|
-
- `scripts/verify.
|
|
20
|
-
- `scripts/verify.json`: the phases a stack requires, plus any tools and preconditions it probes first. The nearest stack's copy wins. A required phase whose script `package.json` lacks fails the run. A base phase the list leaves out runs only where the folder declares its script. Base requires its three phases, so a base-only target still fails on a missing one. These three files are base's `[sync] per_root` set, which a `--skip base` subfolder still receives.
|
|
16
|
+
- `scripts/verify.ts`: the one verify runner every stack shares, and the command behind `check`. It runs typecheck, lint, format check, shell, unit tests, build, markdown bans, then card exclusion, always in that order, and skips any phase the folder's `scripts/verify.json` leaves out. Node modules only, since a target's own lint and typecheck read it.
|
|
17
|
+
- `scripts/verify.json`: the phases a stack requires, plus any tools and preconditions it probes first. The nearest stack's copy wins. A required phase whose script `package.json` lacks fails the run. A base phase the list leaves out runs only where the folder declares its script. Base requires `check:format`, so a base-only target still fails on a missing one. The shell phase runs only where `package.json` declares `check:shell`, which the `shell` stack adds, and probes for `shfmt` and `shellcheck` first. These two files are base's `[sync] per_root` set, which a `--skip base` subfolder still receives.
|
|
21
18
|
|
|
22
19
|
## What ships as user-owned seeds
|
|
23
20
|
|
|
@@ -32,14 +29,12 @@ Seeds live in `tooling/base/seeds/`. Sync drops each once on first install and n
|
|
|
32
29
|
## Tool pairing
|
|
33
30
|
|
|
34
31
|
- Runtime: `bun` as package manager and script runner, `bunx` over `npx` for a one-off executable.
|
|
35
|
-
- Formatting: Prettier for what it parses
|
|
36
|
-
- Shell: shfmt formats and shellcheck lints at warning severity. Both take their files from `find scripts -name '*.sh'`, since `scripts/` can mix shell with TypeScript and shellcheck has no directory mode. A shell script with a shebang and no `.sh` extension is covered by neither, so give it the extension.
|
|
32
|
+
- Formatting: Prettier for what it parses. Prettier has no shell parser, so shell formatting is the `shell` stack's.
|
|
37
33
|
- Commits: commitlint against conventional commits, wired through the husky `commit-msg` hook. Format is `<type>(<scope>): <subject>` in imperative mood with no trailing period.
|
|
38
34
|
- Dev dependencies: `prettier`, `husky`, `@commitlint/cli`, `@commitlint/config-conventional`. Install via `bun add -D`.
|
|
39
35
|
|
|
40
36
|
## File layout
|
|
41
37
|
|
|
42
|
-
- All shell scripts live in `scripts/`. Do not place a `.sh` file outside it.
|
|
43
38
|
- Hooks live in `.husky/`, seeded context docs in `canon/context/`.
|
|
44
39
|
- The `canon/context/` location matches the three-tier context model: project-wide invariants in `CLAUDE.md` and `canon/REQUIREMENTS.md`, path-scoped rules in `.claude/rules/`, and on-demand domain narrative in `canon/context/`. Indexes stay opt-in.
|
|
45
40
|
|
|
@@ -57,11 +52,10 @@ Seeded baseline globs:
|
|
|
57
52
|
|
|
58
53
|
- `**/*.{json,md,mdc}` runs `prettier --write --ignore-path .gitignore --ignore-path .prettierignore`
|
|
59
54
|
- `**/*.md` runs `canon indexes regen`
|
|
60
|
-
- `**/*.sh` runs `shfmt --write --indent 2` then `shellcheck --severity=warning`
|
|
61
55
|
|
|
62
56
|
## CI
|
|
63
57
|
|
|
64
|
-
- Steps: checkout, setup Bun at latest, `bun install --frozen-lockfile`,
|
|
58
|
+
- Steps: checkout, setup Bun at latest, `bun install --frozen-lockfile`, then `check:format`.
|
|
65
59
|
- CI asserts and never writes. Format must be clean before push.
|
|
66
60
|
|
|
67
61
|
## Gitignore
|
|
@@ -78,19 +72,18 @@ Sticky negative knowledge. Do not relearn.
|
|
|
78
72
|
- Do NOT invoke prettier without both `--ignore-path .gitignore --ignore-path .prettierignore`. Passing one drops the other, since the flag replaces the default rather than adding to it.
|
|
79
73
|
- Do NOT omit `--log-level warn` from a prettier invocation. The default prints a line per unchanged file.
|
|
80
74
|
- Do NOT put logic in a husky hook and expect its shebang to hold. Husky runs hooks as `sh -e`, so a hook carrying logic is POSIX sh under errexit whatever the first line says.
|
|
81
|
-
- Do NOT drop the `[*.sh]` block from `.editorconfig` because shfmt already sets indentation. The editor and shfmt then disagree and produce spurious git diffs.
|
|
82
|
-
- Do NOT copy lint-staged's `**/*.sh` glob into a package.json script. lint-staged expands its own globs and passes matched files as arguments, which a bare shell script does not.
|
|
83
75
|
|
|
84
76
|
## CLI
|
|
85
77
|
|
|
86
|
-
| Script | What it does
|
|
87
|
-
| ---------------------- |
|
|
88
|
-
| `bun run check` |
|
|
89
|
-
| `bun run check:format` | Asserts prettier
|
|
90
|
-
| `bun run
|
|
91
|
-
| `bun run
|
|
92
|
-
| `bun run prepare` | Initializes husky hooks, run automatically on `bun install` |
|
|
78
|
+
| Script | What it does |
|
|
79
|
+
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
80
|
+
| `bun run check` | Gates a checkout through `scripts/verify.ts` and writes nothing. Honors `VERIFY_NESTED=true` to suppress timeline boundaries when another script calls it. |
|
|
81
|
+
| `bun run check:format` | Asserts prettier formatting without writing |
|
|
82
|
+
| `bun run format` | Writes prettier formatting in place |
|
|
83
|
+
| `bun run prepare` | Initializes husky hooks, run automatically on `bun install` |
|
|
93
84
|
|
|
94
|
-
|
|
85
|
+
Base writes `check` as a `[scripts.override]`, so a sync replaces whatever `check` a target declared, including one that chained further checks after the runner. Re-add a chained check as its own script after a sync.
|
|
95
86
|
|
|
96
|
-
`
|
|
87
|
+
`bun run check` gates a checkout and writes nothing. Formatting drift fails it at `check:format`, and the failure line names `bun run format`, the one command that writes. It once ran `format` first to auto-fix drift, which rewrote files in a checkout several sessions share on every push and left a gate that could not fail. That was rejected, so the writing pass stays in `bun run format` and a push fails until it runs. The pull request workflow calls `check:format` directly as well.
|
|
88
|
+
|
|
89
|
+
`scripts/verify.ts`'s `Markdown bans` stage gates on banned characters. It runs `canon markdown audit` over every tracked markdown file except `CHANGELOG.md`, since a generated changelog carries commit subjects nobody wrote against the ban set, and exits `2` on a finding or `3` on a shipped-empty ban set, failing `bun run check` and the `pre-push` hook that calls it. A refusal to measure exits `1` and is logged as a skip rather than a failure, as does a corpus with no file left to check once `CHANGELOG.md` is set aside. The stage needs `canon` on PATH. `canon docs projects` names the install command. Without it, the stage is skipped and logged, and coverage falls back to the format check above.
|
|
@@ -22,11 +22,5 @@ jobs:
|
|
|
22
22
|
- name: Install Dependencies
|
|
23
23
|
run: bun install --frozen-lockfile
|
|
24
24
|
|
|
25
|
-
- name: Install Shell Tools
|
|
26
|
-
run: sudo apt-get install -y shfmt shellcheck
|
|
27
|
-
|
|
28
25
|
- name: Check Formatting
|
|
29
26
|
run: bun run check:format
|
|
30
|
-
|
|
31
|
-
- name: Check Shell
|
|
32
|
-
run: bun run check:shell
|
|
@@ -13,20 +13,19 @@ Owns the GitHub Actions workflow that gates a merge: which events start a run, a
|
|
|
13
13
|
|
|
14
14
|
- `.github/workflows/` owns the workflow definitions a trigger below starts
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
### Triggers
|
|
17
17
|
|
|
18
18
|
- Pull requests targeting `main`
|
|
19
19
|
- `workflow_dispatch` (manual run from the Actions tab)
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
### Checks
|
|
22
22
|
|
|
23
23
|
Defined in `.github/workflows/verify.yml`. All jobs must pass before merge.
|
|
24
24
|
|
|
25
|
-
| Check | Command | What it asserts
|
|
26
|
-
| ------ | ---------------------- |
|
|
27
|
-
| Format | `bun run check:format` | prettier
|
|
28
|
-
| Shell | `bun run check:shell` | shellcheck passes at warning level |
|
|
25
|
+
| Check | Command | What it asserts |
|
|
26
|
+
| ------ | ---------------------- | ----------------- |
|
|
27
|
+
| Format | `bun run check:format` | prettier is clean |
|
|
29
28
|
|
|
30
|
-
|
|
29
|
+
### Running CI locally
|
|
31
30
|
|
|
32
|
-
`bun run check` runs the same
|
|
31
|
+
`bun run check` runs the same assert and writes nothing. If CI fails on format, run `bun run format` locally and commit the diff.
|
|
@@ -14,24 +14,24 @@ Owns how the project runs on a developer machine: installing the toolchain, the
|
|
|
14
14
|
- `scripts/` owns the shell scripts the package scripts below call
|
|
15
15
|
- `.husky/` owns the git hooks
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
### Setup
|
|
18
18
|
|
|
19
19
|
- Install [Bun](https://bun.sh): `curl -fsSL https://bun.sh/install | bash`
|
|
20
20
|
- Install dependencies: `bun install`
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
### Scripts
|
|
23
23
|
|
|
24
|
-
| Command | Purpose
|
|
25
|
-
| ---------------- |
|
|
26
|
-
| `bun run check` |
|
|
27
|
-
| `bun run format` | Auto-fix prettier
|
|
24
|
+
| Command | Purpose |
|
|
25
|
+
| ---------------- | ---------------------------------------------------------------------------------------- |
|
|
26
|
+
| `bun run check` | Gates locally and writes nothing. Fails on formatting drift until `bun run format` runs. |
|
|
27
|
+
| `bun run format` | Auto-fix prettier formatting. |
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
### Shell scripts
|
|
30
30
|
|
|
31
31
|
All `.sh` files live under `scripts/`. Do not place shell scripts outside `scripts/`.
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
### Husky hooks
|
|
34
34
|
|
|
35
|
-
- `pre-commit` runs `lint-staged` (prettier
|
|
35
|
+
- `pre-commit` runs `lint-staged` (prettier on staged files).
|
|
36
36
|
- `commit-msg` runs `commitlint` against the conventional commit format.
|
|
37
|
-
- `pre-push` runs `bun run check`. When a markdown-bans audit tool is on PATH, it also gates on banned characters across every tracked markdown file except `CHANGELOG.md`.
|
|
37
|
+
- `pre-push` runs `bun run check`. When a markdown-bans audit tool is on PATH, it also gates on banned characters across every tracked markdown file except `CHANGELOG.md`. A push fails on formatting drift until `bun run format` runs, since the hook writes nothing.
|
|
@@ -26,6 +26,14 @@ case "$file_path" in
|
|
|
26
26
|
*/.claude/.tmp/* | */.canon/tmp/*) exit 0 ;;
|
|
27
27
|
esac
|
|
28
28
|
|
|
29
|
+
# A background session's temp folder is the harness's own, told to the session
|
|
30
|
+
# and located under the home folder, outside every project. Matching its shape
|
|
31
|
+
# rather than reading CLAUDE_JOB_DIR holds whether or not this hook's
|
|
32
|
+
# environment carries the variable.
|
|
33
|
+
case "$file_path" in
|
|
34
|
+
*/.claude/jobs/*/tmp/*) exit 0 ;;
|
|
35
|
+
esac
|
|
36
|
+
|
|
29
37
|
# A project whose own root sits under a path carrying a tmp segment is not
|
|
30
38
|
# writing to system temp, and the bare pattern below would trip on every source
|
|
31
39
|
# file it holds. The project root is stripped before the match rather than
|
|
@@ -57,5 +65,5 @@ marker="$marker_dir/$key"
|
|
|
57
65
|
mkdir -p "$marker_dir"
|
|
58
66
|
: >"$marker"
|
|
59
67
|
|
|
60
|
-
msg='Temporary file write outside the project scratch folder. Write temp files to .canon/tmp/<slug>/ in the project root, not system temp. See the Scratch rule (
|
|
68
|
+
msg='Temporary file write outside the project scratch folder. Write temp files to .canon/tmp/<slug>/ in the project root, not system temp. See the Scratch rule (scratch.md, under canon/ in your installed governance rules).'
|
|
61
69
|
jq -nc --arg msg "$msg" '{hookSpecificOutput:{hookEventName:"PreToolUse",additionalContext:$msg}}'
|
|
@@ -62,13 +62,28 @@ fi
|
|
|
62
62
|
hits=$(printf '%s' "$record" |
|
|
63
63
|
jq -r '.entries[]?.bans[]? | ":\(.line):\(.column + 1) \(.kind) \(.term)"' 2>/dev/null)
|
|
64
64
|
|
|
65
|
+
# Two advisory measures ride in the same record and never gate. A heading's
|
|
66
|
+
# case is read for the whole file, since it is cheap to fix where it stands. A
|
|
67
|
+
# colon is kept only where the word before it and the colon appear in the text
|
|
68
|
+
# this write carried, since an edit often rewrites part of a line and a
|
|
69
|
+
# whole-line match would drop exactly that case. The verb names that word as
|
|
70
|
+
# `lead`, since slicing `text` at `column` miscounts after an emoji, and a
|
|
71
|
+
# binary predating the field falls back to the slice. A file holds colons nobody is
|
|
72
|
+
# asking this edit to answer for.
|
|
73
|
+
new=$(printf '%s' "$input" |
|
|
74
|
+
jq -r '[.tool_input.new_string?, .tool_input.content?, (.tool_input.edits[]?.new_string?)] | map(select(. != null)) | join("\n")' 2>/dev/null)
|
|
75
|
+
headings=$(printf '%s' "$record" |
|
|
76
|
+
jq -r '.entries[]?.headingCase[]? | ":\(.line) \(.text)"' 2>/dev/null)
|
|
77
|
+
colons=$(printf '%s' "$record" |
|
|
78
|
+
jq -r --arg new "$new" '.entries[]?.colons[]? | (.lead // (.text[:.column] | sub("^.*\\s"; ""))) as $lead | select($new | contains($lead + ":")) | ":\(.line):\(.column + 1) \(.text)"' 2>/dev/null)
|
|
79
|
+
|
|
65
80
|
# A set the verb shipped empty measures nothing and would report a clean file.
|
|
66
81
|
# Reading the findings alone turns that narrowed check into a pass, so the field
|
|
67
82
|
# is read beside them.
|
|
68
83
|
empty=$(printf '%s' "$record" |
|
|
69
84
|
jq -r '[.bans.emptySets[]?] | join(", ")' 2>/dev/null)
|
|
70
85
|
|
|
71
|
-
[ -z "$hits" ] && [ -z "$unread" ] && [ -z "$empty" ] && exit 0
|
|
86
|
+
[ -z "$hits" ] && [ -z "$headings" ] && [ -z "$colons" ] && [ -z "$unread" ] && [ -z "$empty" ] && exit 0
|
|
72
87
|
|
|
73
88
|
nl=$'\n'
|
|
74
89
|
msg=""
|
|
@@ -82,7 +97,17 @@ elif [ -n "$empty" ]; then
|
|
|
82
97
|
fi
|
|
83
98
|
|
|
84
99
|
if [ -n "$hits" ]; then
|
|
85
|
-
found=$(printf 'Standards-audit: markdown.md violations in %s.
|
|
100
|
+
found=$(printf 'Standards-audit: markdown.md violations in %s. Split the sentence or rewrite it. A colon or a comma standing where the em dash was is the same sentence. A code span is the answer only where the token is genuinely an identifier under discussion.\n%s' "$file" "$hits")
|
|
101
|
+
msg="${msg:+$msg$nl}$found"
|
|
102
|
+
fi
|
|
103
|
+
|
|
104
|
+
if [ -n "$headings" ]; then
|
|
105
|
+
found=$(printf 'Standards-audit: headings in %s are not sentence case. Lowercase every word after the first unless it is a proper noun, and leave a proper noun as it is.\n%s' "$file" "$headings")
|
|
106
|
+
msg="${msg:+$msg$nl}$found"
|
|
107
|
+
fi
|
|
108
|
+
|
|
109
|
+
if [ -n "$colons" ]; then
|
|
110
|
+
found=$(printf 'Standards-audit: this write added a colon in the middle of a sentence in %s. A colon belongs after a label or at the end of a line introducing a list. Split the sentence or rewrite it.\n%s' "$file" "$colons")
|
|
86
111
|
msg="${msg:+$msg$nl}$found"
|
|
87
112
|
fi
|
|
88
113
|
|