@erclx/canon 4.102.0 → 4.104.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/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/canon-cli/SKILL.md +3 -74
- package/claude/skills/canon-operator/REQUIREMENT.md +2 -2
- package/claude/skills/canon-operator/SKILL.md +5 -5
- package/claude/skills/canon-rollout/SKILL.md +7 -6
- package/claude/skills/design-taste/references/craft.md +1 -1
- package/claude/skills/design-taste/references/preflight.md +2 -2
- package/claude/skills/design-taste/references/tells.md +1 -1
- package/claude/skills/draft-readme/REQUIREMENT.md +2 -0
- package/claude/skills/draft-readme/SKILL.md +9 -1
- package/claude/skills/git-followup/SKILL.md +1 -1
- package/claude/skills/git-issue/SKILL.md +2 -2
- package/{standards → claude/skills/git-issue/references}/issue.md +3 -8
- package/claude/skills/git-pr/SKILL.md +2 -2
- package/claude/skills/index-lookup/REQUIREMENT.md +2 -2
- package/claude/skills/project-commands/REQUIREMENT.md +1 -1
- package/claude/skills/seed-sync/REQUIREMENT.md +1 -1
- package/claude/skills/target-check/REQUIREMENT.md +42 -0
- package/claude/skills/target-check/SKILL.md +91 -0
- package/claude/skills/target-check/references/domains.md +88 -0
- package/claude/skills/target-setup/REQUIREMENT.md +56 -0
- package/claude/skills/target-setup/SKILL.md +163 -0
- package/claude/skills/target-setup/references/detect.md +80 -0
- package/claude/skills/{setup-indexes/SKILL.md → target-setup/references/indexes.md} +59 -23
- package/claude/skills/target-setup/references/verify.md +95 -0
- package/claude/skills/ui-checklist/SKILL.md +17 -0
- package/docs/agents/commands.md +1 -1
- package/docs/agents/sandbox.md +1 -1
- package/docs/agents/scripting.md +3 -2
- package/docs/target-projects.md +13 -11
- package/docs/workflow/ai-workflow.md +19 -23
- package/governance/rules/ci/700-ci-workflow.md +11 -0
- package/governance/rules/ui/400-ui.md +0 -5
- package/governance/rules/ui/460-design-taste.md +1 -1
- package/governance/rules/ui/465-interface-casing.md +31 -0
- package/governance/stacks/astro.toml +1 -1
- package/governance/stacks/react.toml +1 -1
- package/package.json +1 -1
- package/scripts/core/regen-hero.sh +1 -1
- package/scripts/core/regen-web-previews.ts +1 -0
- package/src/claude/cases/setup.ts +20 -11
- package/src/commands/design.ts +8 -2
- package/src/commands/sync.ts +1 -1
- package/src/design/css.ts +5 -2
- package/src/design/render.ts +46 -6
- package/src/gate/measures.ts +57 -0
- package/src/gate/stages.ts +10 -14
- package/src/gov/list.ts +1 -1
- package/src/sandbox/census.ts +3 -3
- package/src/shipped/references.ts +1 -1
- package/standards/index.md +0 -1
- package/standards/readme.md +22 -3
- package/tooling/astro/manifest.toml +1 -0
- package/tooling/nextjs/manifest.toml +1 -0
- package/tooling/web/configs/.github/workflows/readme-screenshot.yml +110 -0
- package/tooling/web/configs/e2e/screenshot.ts +69 -15
- package/tooling/web/configs/scripts/lib/preview-server.sh +31 -0
- package/tooling/web/configs/scripts/readme-screenshot.sh +26 -0
- package/tooling/web/configs/scripts/screenshot.sh +1 -24
- package/tooling/web/manifest.toml +1 -0
- package/tooling/web/reference.md +6 -1
- package/claude/skills/setup-gov/REQUIREMENT.md +0 -37
- package/claude/skills/setup-gov/SKILL.md +0 -77
- package/claude/skills/setup-indexes/REQUIREMENT.md +0 -48
- package/claude/skills/setup-init/REQUIREMENT.md +0 -46
- package/claude/skills/setup-init/SKILL.md +0 -133
- package/claude/skills/setup-plugins/REQUIREMENT.md +0 -42
- package/claude/skills/setup-plugins/SKILL.md +0 -81
- package/claude/skills/setup-plugins/references/plugin-catalog.md +0 -53
- package/claude/skills/setup-smoke/REQUIREMENT.md +0 -38
- package/claude/skills/setup-smoke/SKILL.md +0 -53
- package/claude/skills/setup-verify/REQUIREMENT.md +0 -39
- package/claude/skills/setup-verify/SKILL.md +0 -51
- package/scripts/core/regen-tooling-paths.sh +0 -61
|
@@ -210,7 +210,7 @@ const PHASE_LABEL = /\bv\d+\.\d+(?!\.\d)\b/g
|
|
|
210
210
|
*
|
|
211
211
|
* Anchored on the trailing `\d{3}-[\w-]+\.md` rather than on the bare
|
|
212
212
|
* `.claude/rules/` prefix, which is what keeps a folder mention carrying no
|
|
213
|
-
* number, such as `create-rule`, `memory-review`, and `setup
|
|
213
|
+
* number, such as `create-rule`, `memory-review`, and `target-setup` already
|
|
214
214
|
* write correctly, from matching. The segment group between `rules/` and the
|
|
215
215
|
* numbered file admits both a governance-namespace path
|
|
216
216
|
* (`canon/core/055-scratch.md`) and a project-namespace one
|
package/standards/index.md
CHANGED
|
@@ -19,7 +19,6 @@ Reference docs for consistent authoring across the toolkit and target projects.
|
|
|
19
19
|
- [Glossary reference](glossary.md): Frontmatter, entry shape, ordering, and the rules deciding which terms a glossary carries
|
|
20
20
|
- [Groundwork reference](groundwork.md): Folder layout, ordinal naming, reserved numbering, frontmatter and dating, required file contents, and conventions for a measurement track
|
|
21
21
|
- [Intake reference](intake.md): Folder layout, ordinal naming, reserved index number, frontmatter and dating, the item template, the answer contract, and retrieval
|
|
22
|
-
- [Issue reference](issue.md): GitHub issue title, labels, and body conventions
|
|
23
22
|
- [Markdown reference](markdown.md): Headings, paragraph and list structure, code spans, the date form, punctuation, emphasis, file references, banned words, and frontmatter wording
|
|
24
23
|
- [Memory reference](memory.md): Filename and type prefix, frontmatter, the body shape per type, links between entries, and the lifecycle from write to retire
|
|
25
24
|
- [Mermaid reference](mermaid.md): Direction and layout, node and edge budgets, accessibility fields, label punctuation, and render verification for a Mermaid diagram
|
package/standards/readme.md
CHANGED
|
@@ -11,7 +11,7 @@ The reader is what changes. Reference prose serves someone who already committed
|
|
|
11
11
|
|
|
12
12
|
## Scope
|
|
13
13
|
|
|
14
|
-
Governs every `README.md`: voice, heading structure, required and optional sections, badge selection, and what the page links out to instead of carrying.
|
|
14
|
+
Governs every `README.md`: voice, heading structure, required and optional sections, the header block, badge selection, and what the page links out to instead of carrying.
|
|
15
15
|
|
|
16
16
|
Does not govern:
|
|
17
17
|
|
|
@@ -34,14 +34,14 @@ Scoped to the README at a repository root. A nested README documenting a folder,
|
|
|
34
34
|
|
|
35
35
|
- H1 title, H2 major sections, H3 subsections. Maintain proper hierarchy for GitHub's auto-generated table of contents.
|
|
36
36
|
- Use sentence case for all headings (proper nouns and product names retain their casing)
|
|
37
|
-
- Project description in plain text directly under H1. Keep it to 2-3 sentences.
|
|
37
|
+
- Project description in plain text below the header block, or directly under the H1 when the block holds nothing beneath the title. Keep it to 2-3 sentences.
|
|
38
38
|
- Do not create deeply nested heading structures that harm scannability
|
|
39
39
|
- Do not use horizontal rules or dividers (`---`)
|
|
40
40
|
|
|
41
41
|
## Sections
|
|
42
42
|
|
|
43
43
|
- Required: project description, installation/setup, usage examples, support/help resources
|
|
44
|
-
- Optional:
|
|
44
|
+
- Optional: the header block below, features, contributing (link to `CONTRIBUTING.md`), license (link to `LICENSE`)
|
|
45
45
|
- Do not include full API documentation. Link to separate docs instead.
|
|
46
46
|
- Do not include license text. Reference the `LICENSE` file.
|
|
47
47
|
- Do not include detailed contribution guidelines. Reference `CONTRIBUTING.md`.
|
|
@@ -49,6 +49,25 @@ Scoped to the README at a repository root. A nested README documenting a folder,
|
|
|
49
49
|
- A README that points to the project's own context entries instead of restating them satisfies the required list by routing rather than carrying. A repository documented in `docs/` or `canon/context/` names the entry point and the listing command, then stops. The reader is one hop away, the same way `## Badges` treats zero as complete.
|
|
50
50
|
- A README missing an H1, or whose headings restate the tool that scaffolded it rather than the project, is unedited generator output rather than a page anyone wrote. Draft over it. Nothing here asks a session to preserve a section a scaffold wrote and nobody replaced.
|
|
51
51
|
|
|
52
|
+
### Header block
|
|
53
|
+
|
|
54
|
+
The opening of a root README is a block of up to six elements in a fixed order. Every element is read off the repository rather than written from scratch, and each one is a slot: a project that holds nothing for it leaves it out.
|
|
55
|
+
|
|
56
|
+
1. Mark: the project's logo or icon, from an image the repository already commits. Omit it when the project has none.
|
|
57
|
+
2. Title: the H1, naming the project.
|
|
58
|
+
3. Badges: the block `### Badges` below governs, placed directly under the title.
|
|
59
|
+
4. Claim: one line saying what the project does, taken from the manifest description or the repository description.
|
|
60
|
+
5. Live link: the URL of the running surface, from a `homepage` field or a deploy config. Omit it when the project has no page or no known URL.
|
|
61
|
+
6. Product screenshot: an image the project already commits and references, shown with alt text naming what it depicts. Present only when the project has a page. Omit it when no such image exists rather than writing a placeholder path.
|
|
62
|
+
|
|
63
|
+
The description of 2-3 sentences follows the block and expands the claim rather than repeating it.
|
|
64
|
+
|
|
65
|
+
- A project with no page omits the link and the screenshot. A library or a CLI reads as a complete block with a title, badges, and a claim.
|
|
66
|
+
- The order is the block's rather than the page's. The screenshot sits last inside the block, which puts it in the first row of content a reader meets below the title, badges, and claim.
|
|
67
|
+
- A themed light and dark pair counts as one element in the slot rather than two. The block does not require a pair and leaves the handling of themes to the project.
|
|
68
|
+
- Centering the block in an HTML wrapper is optional. Plain markdown reaches every host a README renders on, and a wrapper is styling a project can add.
|
|
69
|
+
- Never invent an element. A mark or a screenshot the project does not hold stays out of the page.
|
|
70
|
+
|
|
52
71
|
### Badges
|
|
53
72
|
|
|
54
73
|
A badge earns its place by carrying a fact the reader cannot get from the page they are already on. Three classes pass that test.
|
|
@@ -24,6 +24,7 @@ packages = [
|
|
|
24
24
|
|
|
25
25
|
[scripts.override]
|
|
26
26
|
"screenshot" = "PREVIEW_PORT=$(bash scripts/worktree-port.sh 4321) && export PREVIEW_PORT && bash scripts/screenshot.sh"
|
|
27
|
+
"readme-screenshot" = "PREVIEW_PORT=$(bash scripts/worktree-port.sh 4321) && export PREVIEW_PORT && bash scripts/readme-screenshot.sh"
|
|
27
28
|
"dev" = "WORKTREE_PORT_OFFSET=$(bash scripts/worktree-port.sh) && export WORKTREE_PORT_OFFSET && astro dev"
|
|
28
29
|
"preview" = "WORKTREE_PORT_OFFSET=$(bash scripts/worktree-port.sh) && export WORKTREE_PORT_OFFSET && astro preview"
|
|
29
30
|
|
|
@@ -17,6 +17,7 @@ packages = [
|
|
|
17
17
|
[scripts.override]
|
|
18
18
|
"dev" = "WORKTREE_PORT_OFFSET=$(bash scripts/worktree-port.sh) && export WORKTREE_PORT_OFFSET && next dev --port $((3000 + WORKTREE_PORT_OFFSET))"
|
|
19
19
|
"screenshot" = "PREVIEW_PORT=$(bash scripts/worktree-port.sh 3000) && export PREVIEW_PORT && bash scripts/screenshot.sh"
|
|
20
|
+
"readme-screenshot" = "PREVIEW_PORT=$(bash scripts/worktree-port.sh 3000) && export PREVIEW_PORT && bash scripts/readme-screenshot.sh"
|
|
20
21
|
|
|
21
22
|
[gitignore]
|
|
22
23
|
"# Next" = [".next/"]
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
name: README Screenshot
|
|
2
|
+
# canon-no-seed: a stack-specific workflow with no root counterpart by design, since this repository reports its own capture in pr-visual-checks.yml and never commits it.
|
|
3
|
+
|
|
4
|
+
# No `workflow_dispatch`: the job commits onto a pull request's head branch,
|
|
5
|
+
# and a manual run has no pull request to commit onto.
|
|
6
|
+
on:
|
|
7
|
+
pull_request:
|
|
8
|
+
paths:
|
|
9
|
+
# Project-specific. Replace the placeholder with the paths whose change
|
|
10
|
+
# moves the README frame, such as the component and styles it renders.
|
|
11
|
+
# The three entries below the placeholder keep the job firing on this
|
|
12
|
+
# file's own installation, which is what lets the first step refuse an
|
|
13
|
+
# unfilled template instead of the job never running.
|
|
14
|
+
- 'REPLACE_WITH_PROJECT_PATHS'
|
|
15
|
+
- 'scripts/readme-screenshot.sh'
|
|
16
|
+
- 'scripts/lib/preview-server.sh'
|
|
17
|
+
- '.github/workflows/readme-screenshot.yml'
|
|
18
|
+
|
|
19
|
+
concurrency:
|
|
20
|
+
group: readme-screenshot-${{ github.ref }}
|
|
21
|
+
cancel-in-progress: true
|
|
22
|
+
|
|
23
|
+
jobs:
|
|
24
|
+
capture:
|
|
25
|
+
name: 🖼️ Refresh README Screenshot
|
|
26
|
+
runs-on: ubuntu-latest
|
|
27
|
+
# The default GITHUB_TOKEN cannot push to a fork's branch, so a PR from
|
|
28
|
+
# outside this repository is skipped rather than left to fail on the push
|
|
29
|
+
# step below.
|
|
30
|
+
if: github.event.pull_request.head.repo.full_name == github.repository
|
|
31
|
+
permissions:
|
|
32
|
+
contents: write
|
|
33
|
+
steps:
|
|
34
|
+
- name: Checkout
|
|
35
|
+
uses: actions/checkout@v4
|
|
36
|
+
with:
|
|
37
|
+
ref: ${{ github.head_ref }}
|
|
38
|
+
|
|
39
|
+
- name: Refuse An Unfilled Template
|
|
40
|
+
# A path list left at its placeholder never fires on the pages it was
|
|
41
|
+
# installed to watch, and that failure leaves no run, no diff, and no
|
|
42
|
+
# complaint. Failing here is what tells the project it has not
|
|
43
|
+
# finished installing.
|
|
44
|
+
run: |
|
|
45
|
+
if grep -q "^ *- 'REPLACE_WITH_PROJECT_PATHS'" .github/workflows/readme-screenshot.yml; then
|
|
46
|
+
echo "::error file=.github/workflows/readme-screenshot.yml::The paths list still holds REPLACE_WITH_PROJECT_PATHS. Replace it with the paths that move the README frame."
|
|
47
|
+
exit 1
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
- name: Setup Node
|
|
51
|
+
uses: actions/setup-node@v4
|
|
52
|
+
with:
|
|
53
|
+
node-version: '22'
|
|
54
|
+
|
|
55
|
+
- name: Setup Bun
|
|
56
|
+
uses: oven-sh/setup-bun@v2
|
|
57
|
+
with:
|
|
58
|
+
bun-version: latest
|
|
59
|
+
|
|
60
|
+
- name: Install Dependencies
|
|
61
|
+
run: bun install --frozen-lockfile
|
|
62
|
+
|
|
63
|
+
- name: Cache Playwright Browser
|
|
64
|
+
id: playwright-cache
|
|
65
|
+
uses: actions/cache@v4
|
|
66
|
+
with:
|
|
67
|
+
path: ~/.cache/ms-playwright
|
|
68
|
+
key: playwright-${{ runner.os }}-chromium-${{ hashFiles('**/package.json') }}
|
|
69
|
+
|
|
70
|
+
- name: Install Playwright System Dependencies
|
|
71
|
+
# Unconditional: the cache holds browser binaries and nothing apt
|
|
72
|
+
# wrote, so gating this on a cache hit leaves a warm run launching
|
|
73
|
+
# chromium without the libraries it needs.
|
|
74
|
+
run: bunx playwright install-deps chromium
|
|
75
|
+
|
|
76
|
+
- name: Install Playwright Browser
|
|
77
|
+
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
|
78
|
+
run: bunx playwright install chromium
|
|
79
|
+
|
|
80
|
+
- name: Capture README Screenshot
|
|
81
|
+
run: bun run readme-screenshot
|
|
82
|
+
|
|
83
|
+
- name: Check For Changes
|
|
84
|
+
id: diff
|
|
85
|
+
run: |
|
|
86
|
+
if git status --porcelain assets/evidence/readme | grep -q .; then
|
|
87
|
+
echo "changed=true" >> "$GITHUB_OUTPUT"
|
|
88
|
+
else
|
|
89
|
+
echo "changed=false" >> "$GITHUB_OUTPUT"
|
|
90
|
+
fi
|
|
91
|
+
|
|
92
|
+
- name: Commit And Push
|
|
93
|
+
# Self-terminating: this push retriggers this same workflow on the PR,
|
|
94
|
+
# which captures again, finds no diff against what was just pushed,
|
|
95
|
+
# and stops here without pushing a second time.
|
|
96
|
+
if: steps.diff.outputs.changed == 'true'
|
|
97
|
+
# The hooks are off because this commit carries nothing they can read.
|
|
98
|
+
# `pre-push` runs the full verify, whose format step shells out to
|
|
99
|
+
# `shfmt`, which the runner does not install, so a push through them
|
|
100
|
+
# exits 127. The jobs in verify.yml already run those checks against
|
|
101
|
+
# the source, and what this step pushes is PNG bytes a capture
|
|
102
|
+
# produced.
|
|
103
|
+
env:
|
|
104
|
+
HUSKY: 0
|
|
105
|
+
run: |
|
|
106
|
+
git config user.name "github-actions[bot]"
|
|
107
|
+
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
108
|
+
git add assets/evidence/readme
|
|
109
|
+
git commit -m "chore(readme): refresh README screenshot"
|
|
110
|
+
git push origin "HEAD:${{ github.head_ref }}"
|
|
@@ -9,12 +9,33 @@ interface CaptureSection {
|
|
|
9
9
|
selector: string
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
interface Viewport {
|
|
13
|
+
readonly name: string
|
|
14
|
+
readonly width: number
|
|
15
|
+
readonly height: number
|
|
16
|
+
/** Whether a case flagged `evidence: true` also writes its frames at this width. */
|
|
17
|
+
readonly evidence: boolean
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The breakpoint buckets every case is crossed with, named by width. 320 is
|
|
22
|
+
* the base a page has to hold without wrapping or clipping. Committing every
|
|
23
|
+
* width multiplies the tracked frames by the width count, so only the base and
|
|
24
|
+
* the desktop width carry `evidence`.
|
|
25
|
+
*/
|
|
26
|
+
const VIEWPORTS: readonly Viewport[] = [
|
|
27
|
+
{ name: '320', width: 320, height: 800, evidence: true },
|
|
28
|
+
{ name: '768', width: 768, height: 800, evidence: false },
|
|
29
|
+
{ name: '1280', width: 1280, height: 800, evidence: true },
|
|
30
|
+
{ name: '1536', width: 1536, height: 800, evidence: false },
|
|
31
|
+
]
|
|
32
|
+
|
|
12
33
|
interface CaptureCase {
|
|
13
34
|
section: string
|
|
14
35
|
theme: string
|
|
15
36
|
route: string
|
|
16
|
-
width
|
|
17
|
-
|
|
37
|
+
/** Names of viewports this case skips, for a route that reflows rather than breaks at that width. */
|
|
38
|
+
excludeViewports?: readonly string[]
|
|
18
39
|
evidence?: boolean
|
|
19
40
|
sections?: CaptureSection[]
|
|
20
41
|
setup?: (page: Page) => Promise<void>
|
|
@@ -50,8 +71,6 @@ const CASES: CaptureCase[] = [
|
|
|
50
71
|
section: 'home',
|
|
51
72
|
theme: 'default',
|
|
52
73
|
route: '/',
|
|
53
|
-
width: 1280,
|
|
54
|
-
height: 800,
|
|
55
74
|
evidence: true,
|
|
56
75
|
sections: HOME_SECTIONS,
|
|
57
76
|
},
|
|
@@ -59,13 +78,28 @@ const CASES: CaptureCase[] = [
|
|
|
59
78
|
section: 'home',
|
|
60
79
|
theme: 'dark',
|
|
61
80
|
route: '/',
|
|
62
|
-
width: 1280,
|
|
63
|
-
height: 800,
|
|
64
81
|
sections: HOME_SECTIONS,
|
|
65
82
|
setup: (page) => page.emulateMedia({ colorScheme: 'dark' }),
|
|
66
83
|
},
|
|
67
84
|
]
|
|
68
85
|
|
|
86
|
+
/** One case at one viewport, which is the unit a filter selects and a context is built for. */
|
|
87
|
+
interface CaptureRun {
|
|
88
|
+
readonly captureCase: CaptureCase
|
|
89
|
+
readonly viewport: Viewport
|
|
90
|
+
readonly label: string
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const ALL_RUNS: readonly CaptureRun[] = CASES.flatMap((captureCase) =>
|
|
94
|
+
VIEWPORTS.filter(
|
|
95
|
+
(viewport) => !captureCase.excludeViewports?.includes(viewport.name),
|
|
96
|
+
).map((viewport) => ({
|
|
97
|
+
captureCase,
|
|
98
|
+
viewport,
|
|
99
|
+
label: `${captureCase.section}/${viewport.name}--${captureCase.theme}`,
|
|
100
|
+
})),
|
|
101
|
+
)
|
|
102
|
+
|
|
69
103
|
const args = process.argv.slice(2)
|
|
70
104
|
const checkConsoleClean = args.includes('--check-console-clean')
|
|
71
105
|
const requireBaseUrl = args.includes('--require-base-url')
|
|
@@ -87,6 +121,28 @@ try {
|
|
|
87
121
|
|
|
88
122
|
const OUT_DIR = path.join('screenshots', hostname)
|
|
89
123
|
|
|
124
|
+
const SCREENSHOT_FILTER = process.env.SCREENSHOT_FILTER?.trim() ?? ''
|
|
125
|
+
|
|
126
|
+
const filterTerms = SCREENSHOT_FILTER.split(',')
|
|
127
|
+
.map((term) => term.trim())
|
|
128
|
+
.filter(Boolean)
|
|
129
|
+
|
|
130
|
+
const RUNS =
|
|
131
|
+
filterTerms.length > 0
|
|
132
|
+
? ALL_RUNS.filter((run) =>
|
|
133
|
+
filterTerms.some((term) => run.label.includes(term)),
|
|
134
|
+
)
|
|
135
|
+
: ALL_RUNS
|
|
136
|
+
|
|
137
|
+
if (filterTerms.length > 0 && RUNS.length === 0) {
|
|
138
|
+
console.error(
|
|
139
|
+
`No cases matched SCREENSHOT_FILTER=${SCREENSHOT_FILTER}. Available labels:\n${ALL_RUNS.map(
|
|
140
|
+
(run) => ` ${run.label}`,
|
|
141
|
+
).join('\n')}`,
|
|
142
|
+
)
|
|
143
|
+
process.exit(1)
|
|
144
|
+
}
|
|
145
|
+
|
|
90
146
|
/** Every wait on a lazy load is bounded explicitly. */
|
|
91
147
|
const SETTLE_TIMEOUT = 15_000
|
|
92
148
|
|
|
@@ -198,12 +254,12 @@ const browser = await chromium.launch()
|
|
|
198
254
|
const consoleErrors: string[] = []
|
|
199
255
|
let ranCases = 0
|
|
200
256
|
|
|
201
|
-
for (const captureCase of
|
|
257
|
+
for (const { captureCase, viewport, label } of RUNS) {
|
|
202
258
|
if (captureCase.evidence && requireBaseUrl) continue
|
|
203
259
|
|
|
204
260
|
ranCases++
|
|
205
261
|
const context = await browser.newContext({
|
|
206
|
-
viewport: { width:
|
|
262
|
+
viewport: { width: viewport.width, height: viewport.height },
|
|
207
263
|
// A page honoring the preference lands its own motion at the end state,
|
|
208
264
|
// which is what a still frame has to show. Playwright reads it as a
|
|
209
265
|
// context option, so it reaches no page written beside `viewport` above.
|
|
@@ -214,9 +270,7 @@ for (const captureCase of CASES) {
|
|
|
214
270
|
if (checkConsoleClean) {
|
|
215
271
|
page.on('console', (msg) => {
|
|
216
272
|
if (msg.type() === 'error') {
|
|
217
|
-
consoleErrors.push(
|
|
218
|
-
`${captureCase.section}/${captureCase.theme}: ${msg.text()}`,
|
|
219
|
-
)
|
|
273
|
+
consoleErrors.push(`${label}: ${msg.text()}`)
|
|
220
274
|
}
|
|
221
275
|
})
|
|
222
276
|
}
|
|
@@ -226,21 +280,21 @@ for (const captureCase of CASES) {
|
|
|
226
280
|
await page.goto(`${BASE_URL}${captureCase.route}`)
|
|
227
281
|
await page.waitForLoadState('networkidle')
|
|
228
282
|
|
|
229
|
-
const label = `${captureCase.section}/${captureCase.theme}`
|
|
230
283
|
const sections = await settle(page, label, captureCase.sections ?? [])
|
|
231
284
|
|
|
232
285
|
const sweepDir = path.join(OUT_DIR, captureCase.section)
|
|
233
286
|
const evidenceDir = path.join('evidence', captureCase.section)
|
|
234
|
-
const file = `${captureCase.theme}.png`
|
|
287
|
+
const file = `${viewport.name}--${captureCase.theme}.png`
|
|
288
|
+
const writesEvidence = captureCase.evidence && viewport.evidence
|
|
235
289
|
|
|
236
290
|
await shoot(page, sweepDir, file)
|
|
237
|
-
if (
|
|
291
|
+
if (writesEvidence) await shoot(page, evidenceDir, file)
|
|
238
292
|
|
|
239
293
|
if (sections.length > 0) await markFloatingChrome(page)
|
|
240
294
|
|
|
241
295
|
for (const { name, selector } of sections) {
|
|
242
296
|
await shoot(page, path.join(sweepDir, name), file, selector)
|
|
243
|
-
if (
|
|
297
|
+
if (writesEvidence) {
|
|
244
298
|
await shoot(page, path.join(evidenceDir, name), file, selector)
|
|
245
299
|
}
|
|
246
300
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Sourced, not executed. Builds the site, serves it in the background on
|
|
3
|
+
# PREVIEW_PORT, and blocks until it answers or exits with an error. Shared by
|
|
4
|
+
# scripts/screenshot.sh and scripts/readme-screenshot.sh so the two don't
|
|
5
|
+
# carry two copies of the same startup boilerplate.
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
|
|
8
|
+
: "${PREVIEW_PORT:=4173}"
|
|
9
|
+
|
|
10
|
+
if curl -sSf "http://localhost:$PREVIEW_PORT/" >/dev/null 2>&1; then
|
|
11
|
+
echo "Port $PREVIEW_PORT already in use. Stop the listening process or set PREVIEW_PORT." >&2
|
|
12
|
+
exit 1
|
|
13
|
+
fi
|
|
14
|
+
|
|
15
|
+
bun run build
|
|
16
|
+
bun run preview >/dev/null 2>&1 &
|
|
17
|
+
PREVIEW_PID=$!
|
|
18
|
+
# shellcheck disable=SC2154 # pid is bound by the for loop inside the single-quoted trap body, which shellcheck does not track there
|
|
19
|
+
trap 'kill "$PREVIEW_PID" 2>/dev/null || true; if command -v lsof >/dev/null 2>&1; then for pid in $(lsof -ti tcp:"$PREVIEW_PORT" 2>/dev/null); do kill "$pid" 2>/dev/null || true; done; else echo "lsof not found on PATH; cannot confirm the preview port is clear of a detached grandchild." >&2; fi; wait "$PREVIEW_PID" 2>/dev/null || true' EXIT
|
|
20
|
+
|
|
21
|
+
for _ in $(seq 1 40); do
|
|
22
|
+
if curl -sSf "http://localhost:$PREVIEW_PORT/" >/dev/null 2>&1; then
|
|
23
|
+
break
|
|
24
|
+
fi
|
|
25
|
+
sleep 0.25
|
|
26
|
+
done
|
|
27
|
+
|
|
28
|
+
if ! curl -sSf "http://localhost:$PREVIEW_PORT/" >/dev/null 2>&1; then
|
|
29
|
+
echo "Preview server not responding on port $PREVIEW_PORT after 10s." >&2
|
|
30
|
+
exit 1
|
|
31
|
+
fi
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# Project-specific. Both are placeholders the project fills in, and the script
|
|
5
|
+
# refuses to run until it does, since a shipped default would capture the wrong
|
|
6
|
+
# part of the page in every project but the one it was written for.
|
|
7
|
+
README_FILTER='' # case label to capture, such as header/1280
|
|
8
|
+
README_EVIDENCE_DIR='assets/evidence/readme'
|
|
9
|
+
|
|
10
|
+
if [[ -z "$README_FILTER" ]]; then
|
|
11
|
+
echo "README_FILTER is unset in scripts/readme-screenshot.sh. Name the case label the README frame is captured from, such as header/1280." >&2
|
|
12
|
+
exit 1
|
|
13
|
+
fi
|
|
14
|
+
|
|
15
|
+
source "$(dirname "${BASH_SOURCE[0]}")/lib/preview-server.sh"
|
|
16
|
+
|
|
17
|
+
SCREENSHOT_BASE_URL="http://localhost:$PREVIEW_PORT" SCREENSHOT_FILTER="$README_FILTER" bun e2e/screenshot.ts
|
|
18
|
+
|
|
19
|
+
# The filter's section and viewport name the folder and the file prefix the
|
|
20
|
+
# capture wrote under screenshots/<hostname>/.
|
|
21
|
+
frame_dir="screenshots/localhost/$(dirname "$README_FILTER")"
|
|
22
|
+
frame_prefix="$(basename "$README_FILTER")"
|
|
23
|
+
|
|
24
|
+
mkdir -p "$README_EVIDENCE_DIR"
|
|
25
|
+
cp "$frame_dir/$frame_prefix--default.png" "$README_EVIDENCE_DIR/light.png"
|
|
26
|
+
cp "$frame_dir/$frame_prefix--dark.png" "$README_EVIDENCE_DIR/dark.png"
|
|
@@ -1,29 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
set -euo pipefail
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
if curl -sSf "http://localhost:$PREVIEW_PORT/" >/dev/null 2>&1; then
|
|
7
|
-
echo "Port $PREVIEW_PORT already in use. Stop the listening process or set PREVIEW_PORT." >&2
|
|
8
|
-
exit 1
|
|
9
|
-
fi
|
|
10
|
-
|
|
11
|
-
bun run build
|
|
12
|
-
bun run preview >/dev/null 2>&1 &
|
|
13
|
-
PREVIEW_PID=$!
|
|
14
|
-
# shellcheck disable=SC2154 # pid is bound by the for loop inside the single-quoted trap body, which shellcheck does not track there
|
|
15
|
-
trap 'kill "$PREVIEW_PID" 2>/dev/null || true; if command -v lsof >/dev/null 2>&1; then for pid in $(lsof -ti tcp:"$PREVIEW_PORT" 2>/dev/null); do kill "$pid" 2>/dev/null || true; done; else echo "lsof not found on PATH; cannot confirm the preview port is clear of a detached grandchild." >&2; fi; wait "$PREVIEW_PID" 2>/dev/null || true' EXIT
|
|
16
|
-
|
|
17
|
-
for _ in $(seq 1 40); do
|
|
18
|
-
if curl -sSf "http://localhost:$PREVIEW_PORT/" >/dev/null 2>&1; then
|
|
19
|
-
break
|
|
20
|
-
fi
|
|
21
|
-
sleep 0.25
|
|
22
|
-
done
|
|
23
|
-
|
|
24
|
-
if ! curl -sSf "http://localhost:$PREVIEW_PORT/" >/dev/null 2>&1; then
|
|
25
|
-
echo "Preview server not responding on port $PREVIEW_PORT after 10s." >&2
|
|
26
|
-
exit 1
|
|
27
|
-
fi
|
|
4
|
+
source "$(dirname "${BASH_SOURCE[0]}")/lib/preview-server.sh"
|
|
28
5
|
|
|
29
6
|
SCREENSHOT_BASE_URL="http://localhost:$PREVIEW_PORT" bun e2e/screenshot.ts
|
|
@@ -52,6 +52,7 @@ packages = [
|
|
|
52
52
|
[scripts.override]
|
|
53
53
|
"lint" = "eslint . --max-warnings 0"
|
|
54
54
|
"screenshot" = "PREVIEW_PORT=$(bash scripts/worktree-port.sh 4173) && export PREVIEW_PORT && bash scripts/screenshot.sh"
|
|
55
|
+
"readme-screenshot" = "PREVIEW_PORT=$(bash scripts/worktree-port.sh 4173) && export PREVIEW_PORT && bash scripts/readme-screenshot.sh"
|
|
55
56
|
|
|
56
57
|
[gitignore]
|
|
57
58
|
"# Build" = ["dist/"]
|
package/tooling/web/reference.md
CHANGED
|
@@ -12,7 +12,11 @@ Golden config files live in `tooling/web/configs/` and are copied into the targe
|
|
|
12
12
|
|
|
13
13
|
- `eslint.config.js`: flat config with `@eslint/js`, `typescript-eslint`, React hooks, import sort, check-file, vitest rules scoped to test files, `eslint-config-prettier` last.
|
|
14
14
|
- `src/test/setup.ts`: `@testing-library/jest-dom` import, `cleanup` after each test.
|
|
15
|
-
- `e2e/screenshot.ts`: capture template. A single `CASES` record at the top carries one entry per output file, each naming a section, a theme, a route, and
|
|
15
|
+
- `e2e/screenshot.ts`: capture template. A single `CASES` record at the top carries one entry per output file, each naming a section, a theme, and a route, and the loop below crosses every case with the `VIEWPORTS` array and writes `screenshots/<hostname>/<section>/<width>--<theme>.png`, keyed on `SCREENSHOT_BASE_URL`'s hostname so a local and a deployed run land in different folders. `VIEWPORTS` holds four breakpoint buckets named by width, 320, 768, 1280 and 1536, with 320 as the base a page has to hold without wrapping or clipping. A case flagged `evidence: true` additionally writes `evidence/<section>/<width>--<theme>.png`, with no hostname segment, at the viewports flagged `evidence` alone, which are 320 and 1280. Committing all four multiplies the tracked frames by the width count, so the flag is what keeps the tracked set at twice the sections rather than four times. A case may name `excludeViewports` to skip a width, for a route whose long-form prose reflows at 320 instead of breaking. `SCREENSHOT_FILTER` narrows a run to the cases whose label, `<section>/<width>--<theme>`, contains any comma-separated term, exits 1 when nothing matches and lists the labels that exist, and leaves the rest of the sweep alone. `--require-base-url` skips a flagged case entirely, so a production smoke run neither writes to the committed path nor counts an evidence route in its console-clean check, and exits 1 when that leaves zero cases run, so flagging every case cannot silence the production check without saying so. Per-project cases extend the one record. A route's widths and themes sit together under its section folder, so the filename carries both. A case may also name `sections`, one `{ name, selector }` per addressable part of the route, and each writes its own frame at `screenshots/<hostname>/<section>/<name>/<width>--<theme>.png` beside the whole-page one, plus `evidence/<section>/<name>/<width>--<theme>.png` where the case is flagged and the width carries `evidence`. The field is optional, so a case that names none captures the page alone as before, and a selector matching nothing is reported and skipped rather than raised, since `CASES` is a template a project rewrites and a target that syncs this file before editing its own list would otherwise capture nothing at all. Testing that every named section resolves belongs to the project that owns the selectors, beside its own page. Every case settles before anything is shot: the context is created with `reducedMotion: 'reduce'`, each named section is scrolled into view so an `IntersectionObserver` fires and a frame marked `loading="lazy"` loads, the page returns to the top, and fonts, images and same-origin frames are waited out on a bounded condition rather than on a second `networkidle`, which is a lifecycle event of the navigation and does not re-arm for a load a scroll started. A section frame drops the page's floating chrome, being whatever computes to `position: sticky` or `fixed`, because a section taller than the viewport is scrolled into view to be cut and a sticky bar otherwise rides that scroll into the middle of the frame. The whole-page frame is shot first and keeps its chrome. `--check-console-clean` collects `console`-level error messages per case and exits 1 with the list if any fired, turning the capture into a smoke check. `--require-base-url` exits 1 before launching a browser when `SCREENSHOT_BASE_URL` is unset, guarding a script meant to run against a real deployment from silently capturing `localhost`.
|
|
16
|
+
- `scripts/screenshot.sh`: sources `scripts/lib/preview-server.sh`, then runs the capture against the served preview.
|
|
17
|
+
- `scripts/lib/preview-server.sh`: sourced, never executed. Builds, serves on `PREVIEW_PORT`, blocks until the port answers, and traps the whole process group on exit, including the detached grandchild `lsof` finds. Both capture scripts source it so the startup lives once.
|
|
18
|
+
- `scripts/readme-screenshot.sh`: captures one case under `SCREENSHOT_FILTER` and copies its light and dark frames to `assets/evidence/readme/`. `README_FILTER` ships empty with `header/1280` as its example, and the script exits 1 until a project fills it, since a shipped default would capture the wrong part of the page in every project but one. Its copy reads from `screenshots/<hostname>/`, which is where the harness writes.
|
|
19
|
+
- `.github/workflows/readme-screenshot.yml`: refreshes that frame on a pull request and commits it back to the branch. The path list ships as `REPLACE_WITH_PROJECT_PATHS`, and the job's first step after checkout exits 1 while it stays, since a job that never fires leaves no run and no diff to notice. The workflow's own path is in the list so the installing pull request reaches that step. When a commit-back is right and when a report is, `governance/rules/ci/700-ci-workflow.md` states the test.
|
|
16
20
|
- `.vscode/extensions.json` and `.vscode/settings.json`: editor wiring for ESLint, Tailwind, Playwright, Vitest.
|
|
17
21
|
- `.github/workflows/verify.yml`: `static-checks`, `unit-tests`, `build-verify`, and `e2e-tests` jobs.
|
|
18
22
|
- `scripts/verify.sh`: extends base verify with typecheck, lint, unit tests, and build in the full order.
|
|
@@ -106,6 +110,7 @@ Append rows:
|
|
|
106
110
|
| `bun run test:e2e` | Run Playwright E2E tests. |
|
|
107
111
|
| `bun run test:e2e:changed` | Run Playwright E2E tests for specs the import graph reaches from the current diff. |
|
|
108
112
|
| `bun run screenshot` | Build, preview, then capture screenshots. |
|
|
113
|
+
| `bun run readme-screenshot` | Build, preview, then capture the README frame into `assets/evidence/readme/`. |
|
|
109
114
|
| `bun run smoke:prod` | Capture against `SCREENSHOT_BASE_URL`, requiring it set and failing on any console error. |
|
|
110
115
|
|
|
111
116
|
`canon tooling verify <stack>` is the only automated caller of `bun run screenshot`, running it for any stack whose `package.json` declares the script and asserting that PNG files land under `screenshots/`. It counts them with a recursive find carrying no depth limit, so the section folders the seed writes satisfy the assertion without a change to it. Do not flatten the layout to protect that check. No ship chain captures a screenshot, so the output path the seed writes is a contract that one verifier reads rather than a default a ship step depends on.
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: setup-gov
|
|
3
|
-
description: Scope boundary for governance rule install and the authoring it refuses to do in a target
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Setup gov requirement
|
|
7
|
-
|
|
8
|
-
## Gap
|
|
9
|
-
|
|
10
|
-
Without this skill, a session picks a stack name from memory and installs rules written for a framework the project does not use, passes an extra the catalog does not carry so the install warns on a file it cannot resolve, and adds a rule the picked stack already pulls in.
|
|
11
|
-
|
|
12
|
-
The two costly failures are quieter. An unmatched technology gets the nearest rule instead of a stop, so the project carries governance authored for something adjacent and nothing marks it as a guess. A rule written directly into the target lands where no sync will ever reach it, so it drifts from the toolkit copy that owns it.
|
|
13
|
-
|
|
14
|
-
The last failure sits at a boundary rather than inside a run. `setup-init` turns away a project whose language it carries no stack for and sends it here, and a body stating only what this skill does not cover leaves that route running one way, so a person arriving on it reads a skill that never says it is the destination.
|
|
15
|
-
|
|
16
|
-
## Must
|
|
17
|
-
|
|
18
|
-
- Read the catalog at run time and resolve both the stack and every extra against it
|
|
19
|
-
- Name the evidence file behind each detected technology in the preview, so a wrong match is visible before the install rather than after
|
|
20
|
-
- Dedupe extras against the rules the picked stack already resolves
|
|
21
|
-
- Run the install non-interactively, since the CLI picker blocks where no terminal is attached
|
|
22
|
-
- State the inbound route from `setup-init`, so a project sent here for the language-neutral rule layer can tell it landed at the destination the chain named
|
|
23
|
-
|
|
24
|
-
## Must not
|
|
25
|
-
|
|
26
|
-
- Author a rule inside the target project. Rules are authored in the toolkit and reach a project by install or sync.
|
|
27
|
-
- Install a stack or extra whose name did not come from the catalog read
|
|
28
|
-
|
|
29
|
-
## Guards
|
|
30
|
-
|
|
31
|
-
- An unmatched technology stops and presents the options rather than resolving to a nearby rule. Authoring the missing rule in the toolkit, installing the matching layers without the technology-specific one, and aborting are the three, and this skill picks none of them.
|
|
32
|
-
|
|
33
|
-
## Out of scope
|
|
34
|
-
|
|
35
|
-
- The rest of the onboarding chain. This skill installs governance and nothing else, and `setup-init` is the one-shot path that installs governance beside tooling, snippets, and the seeds.
|
|
36
|
-
- Scaffolding the rule file an unmatched technology needs. `create-rule` writes one into a target project, and a rule the toolkit should ship is authored under `governance/rules/` against `canon standards rule`.
|
|
37
|
-
- Updating rules a project already has, which `canon gov sync` owns
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: setup-gov
|
|
3
|
-
description: Detects a project's stack from its files and installs matching toolkit governance rules into `.claude/rules/`. Use after scaffolding a new project, when asked to "install gov rules", "install governance", "set up governance", or when a target project has no `.claude/rules/` yet. Assumes the `canon` CLI is on PATH.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Gov install
|
|
7
|
-
|
|
8
|
-
Automates `canon gov install` by inferring the stack and extras from the current project, then shelling out to the CLI with the resolved arguments.
|
|
9
|
-
|
|
10
|
-
## Scope
|
|
11
|
-
|
|
12
|
-
- Governance rules and nothing else. `setup-init` is the one-shot chain that installs governance beside tooling, standards, snippets, and the seeds.
|
|
13
|
-
- This is where `setup-init` sends a project whose language the toolkit carries no stack for. Rules install without the `base` development dependencies the tooling layer would drop on it. Resolve and stop exactly as below, since a language with no stack is the unmatched case `## Gap handling` already owns and arriving by that route changes none of it.
|
|
14
|
-
|
|
15
|
-
## Read the catalog
|
|
16
|
-
|
|
17
|
-
Run this first to load the current stacks and rules. Never hardcode names. The catalog is the source of truth.
|
|
18
|
-
|
|
19
|
-
```bash
|
|
20
|
-
canon gov list --json 2>/dev/null
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
## Detect the stack
|
|
24
|
-
|
|
25
|
-
Read these from the project root in parallel:
|
|
26
|
-
|
|
27
|
-
- `package.json`: dependencies and devDependencies
|
|
28
|
-
- Root config files: `astro.config.*`, `next.config.*`, `vite.config.*`, `tailwind.config.*`, `tsconfig.json`, `pyproject.toml`, `go.mod`, `Cargo.toml`
|
|
29
|
-
- `canon/REQUIREMENTS.md` and `canon/ARCHITECTURE.md` if present
|
|
30
|
-
- Directory structure via `ls -1` of the project root and `src/` if present
|
|
31
|
-
|
|
32
|
-
## Match
|
|
33
|
-
|
|
34
|
-
Match the detected evidence to the catalog:
|
|
35
|
-
|
|
36
|
-
- Pick the closest stack. Match detected runtime or framework against stack names in the catalog.
|
|
37
|
-
- Identify additional technologies not already covered by the picked stack. For each, find a rule whose `description` or `paths` points at that technology, then add it via `--add`.
|
|
38
|
-
- Dedupe extras against stack rules. Do not add a rule the stack already pulls in.
|
|
39
|
-
|
|
40
|
-
## Gap handling
|
|
41
|
-
|
|
42
|
-
If a detected technology has no matching rule, stop and surface the gap. Do not guess.
|
|
43
|
-
|
|
44
|
-
Present four options:
|
|
45
|
-
|
|
46
|
-
1. Author a new rule in the toolkit at `governance/rules/<domain>/<num>-<name>.md` following `${CLAUDE_SKILL_DIR}/../../standards/rule.md`, commit, then re-run install. Take this route when the toolkit should ship the rule to every project.
|
|
47
|
-
2. Invoke `create-rule`, which scaffolds the file into the target project at a non-colliding number. Take this route when the rule is specific to this project and no sync should ever overwrite it.
|
|
48
|
-
3. Install the matching non-<tech> rules and skip the tech-specific layer.
|
|
49
|
-
4. Abort.
|
|
50
|
-
|
|
51
|
-
This skill authors no rule itself. Option 1 writes in the toolkit repository, and option 2 hands the project-local file to `create-rule`.
|
|
52
|
-
|
|
53
|
-
## Preview
|
|
54
|
-
|
|
55
|
-
Before executing, output:
|
|
56
|
-
|
|
57
|
-
- **Detected:** each technology with the file that evidenced it
|
|
58
|
-
- **Stack:** picked stack name and total resolved rule count
|
|
59
|
-
- **Extras:** each `--add` rule with a one-line reason
|
|
60
|
-
- **Target:** resolved target path
|
|
61
|
-
- **Command:** the exact shell command to run
|
|
62
|
-
|
|
63
|
-
## Execute
|
|
64
|
-
|
|
65
|
-
Run with `CANON_NON_INTERACTIVE=1` so no CLI picker prompts appear. Claude Code's tool permission dialog is the confirmation gate.
|
|
66
|
-
|
|
67
|
-
```bash
|
|
68
|
-
CANON_NON_INTERACTIVE=1 canon gov install <stack> --add <extras> <target>
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
## Response
|
|
72
|
-
|
|
73
|
-
After execution, report:
|
|
74
|
-
|
|
75
|
-
- Rule count installed
|
|
76
|
-
- Target path
|
|
77
|
-
- Any rules that failed to resolve (the CLI warns on missing rule files)
|