@erclx/canon 4.61.0 → 4.62.1
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/setup-smoke/REQUIREMENT.md +38 -0
- package/claude/skills/setup-smoke/SKILL.md +53 -0
- package/claude/skills/setup-verify/REQUIREMENT.md +4 -4
- package/claude/skills/setup-verify/SKILL.md +3 -3
- package/docs/agents/commands.md +83 -80
- package/docs/target-projects.md +2 -0
- package/docs/workflow/ai-workflow.md +11 -10
- package/package.json +1 -1
- package/src/claude/cases/setup.ts +5 -0
- package/src/commands/migrate.ts +136 -0
- package/src/migrate/scratch-evidence.ts +326 -0
- package/tooling/astro/configs/eslint.config.js +1 -1
- package/tooling/astro/reference.md +1 -1
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: setup-smoke
|
|
3
|
+
description: Why the heavy scaffold stages need an owner and what heuristic judges a server smoke pass
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Setup smoke requirement
|
|
7
|
+
|
|
8
|
+
## Gap
|
|
9
|
+
|
|
10
|
+
Without this skill, a freshly scaffolded project's dev server, preview server, end-to-end suite, and screenshot harness never run anywhere. `setup-verify` excludes all three for the flakiness reasons that make them wrong for an unattended chain, and names no destination for any of them, so a broken dev server or a missing end-to-end dependency surfaces only once the user hits it by hand.
|
|
11
|
+
|
|
12
|
+
A dev or preview server carries no exit code to read, since it runs until stopped rather than finishing on its own, unlike the leaf scripts `setup-verify` runs. Judging it needs a heuristic rather than a pass/fail read.
|
|
13
|
+
|
|
14
|
+
## Must
|
|
15
|
+
|
|
16
|
+
- Read the scripts the project declares and run only those, skip absent ones
|
|
17
|
+
- Run `dev`, `preview`, `test:e2e`, `screenshot` in that fixed order, stopping at the first failure
|
|
18
|
+
- Judge a server script by running it in the background, waiting a fixed window, checking the process is alive and its output carries no fatal error string, then killing it regardless of the outcome
|
|
19
|
+
- Install dependencies itself when missing, so it runs standalone without `setup-verify` having run first
|
|
20
|
+
|
|
21
|
+
## Must not
|
|
22
|
+
|
|
23
|
+
- Invent a fallback command for an absent script
|
|
24
|
+
- Run a composite script
|
|
25
|
+
- Require `setup-verify` to have passed first
|
|
26
|
+
- Duplicate a leaf script `setup-verify` already runs
|
|
27
|
+
- Get invoked by anything other than a person or a chain naming it directly. Review this at each future change, since nothing can answer it before the skill has run. `canon docs target-projects` states the scaffold chain that keeps it outside the unattended path, for the same flakiness reasons that keep it out of `setup-verify`'s own leaf-script order.
|
|
28
|
+
|
|
29
|
+
## Guards
|
|
30
|
+
|
|
31
|
+
- No `package.json` at the project root stops, since there are no declared scripts to read
|
|
32
|
+
- A missing dependency folder installs first rather than letting every script fail on the same cause
|
|
33
|
+
|
|
34
|
+
## Out of scope
|
|
35
|
+
|
|
36
|
+
- The leaf scripts `setup-verify` already runs: `lint:fix`, `typecheck`, `check`, `test:run`, `build`
|
|
37
|
+
- Generating the configs these scripts drive, which the tooling stack reference owns
|
|
38
|
+
- A true port-probe readiness check, since no manifest key exposes a generic port across stacks
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: setup-smoke
|
|
3
|
+
description: Runs the heavy scaffold checks setup-verify skips - dev and preview server smoke, Playwright end-to-end tests, and the screenshot harness - on the same declared-scripts contract. Use after setup-verify passes, or when asked to "run the smoke tests", "check the dev server", "run e2e against the scaffold", or "run the heavy checks". Do NOT use in place of setup-verify, or on a project without package.json.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Smoke scaffold
|
|
7
|
+
|
|
8
|
+
Runs the local checks a scaffold's leaf scripts cannot cover: whether the dev and preview servers actually start, whether the end-to-end suite passes against a real server, and whether the screenshot harness produces output. Standalone from `setup-verify`. Neither skill requires the other to have run first.
|
|
9
|
+
|
|
10
|
+
## Guards
|
|
11
|
+
|
|
12
|
+
- If `package.json` does not exist at the project root, stop: `❌ No package.json found. Cannot smoke test.`
|
|
13
|
+
- If `node_modules/` does not exist, run `bun install` first, then proceed.
|
|
14
|
+
|
|
15
|
+
## Step 1: read scripts
|
|
16
|
+
|
|
17
|
+
Read `package.json` from the project root and extract the `scripts` block. Do not hardcode script names.
|
|
18
|
+
|
|
19
|
+
## Step 2: run the chain
|
|
20
|
+
|
|
21
|
+
Run the scripts below in order. Stop on the first failure and surface the error.
|
|
22
|
+
|
|
23
|
+
| Order | Script | Skip if | How judged |
|
|
24
|
+
| ----- | ------------ | ------- | ----------------------- |
|
|
25
|
+
| 1 | `dev` | absent | server smoke, see below |
|
|
26
|
+
| 2 | `preview` | absent | server smoke, see below |
|
|
27
|
+
| 3 | `test:e2e` | absent | leaf script: exit code |
|
|
28
|
+
| 4 | `screenshot` | absent | leaf script: exit code |
|
|
29
|
+
|
|
30
|
+
Skip any script not present in `package.json`. Do not invent a fallback command. Run each leaf script as `bun run <script>` from the project root.
|
|
31
|
+
|
|
32
|
+
### Server smoke
|
|
33
|
+
|
|
34
|
+
`dev` and `preview` run until stopped, so there is no exit code to read. Start the script backgrounded, wait 5 seconds, then check two things: the process is still running, and its captured output carries no string matching `error`, `Error`, or `fatal`. Kill the process either way once judged. Report a failure on either check failing.
|
|
35
|
+
|
|
36
|
+
This is weaker than a real readiness probe. No manifest key exposes a generic port to poll across stacks, so port-probing is out of reach here.
|
|
37
|
+
|
|
38
|
+
## Step 3: report
|
|
39
|
+
|
|
40
|
+
For each script run, report one of:
|
|
41
|
+
|
|
42
|
+
- `✅ <script>`
|
|
43
|
+
- `❌ <script>` followed by the failing output (last 40 lines) or, for a server script, the reason judged (process died, or a fatal string in its output)
|
|
44
|
+
|
|
45
|
+
End with a summary line:
|
|
46
|
+
|
|
47
|
+
- On pass: `✅ Scaffold smoke-tested (<n> scripts passed).`
|
|
48
|
+
- On fail: `❌ Scaffold failed at <script>. Fix the error and re-run setup-smoke.`
|
|
49
|
+
|
|
50
|
+
## Out of scope
|
|
51
|
+
|
|
52
|
+
- `lint:fix`, `typecheck`, `check`, `test:run`, `build`. Those are `canon:setup-verify`'s leaf-script chain.
|
|
53
|
+
- Generating the configs these scripts drive, which the tooling stack reference owns.
|
|
@@ -11,7 +11,7 @@ Without this skill, a freshly scaffolded project is handed over on the strength
|
|
|
11
11
|
|
|
12
12
|
The naive check fails two ways. A session that assumes script names runs `test` against a stack that exposes `test:run`, or invents a fallback command when a script is absent, and either result reports on something the project never declared. A session that runs a composite script gets one failure covering several stages, so the output names the wrapper rather than the break.
|
|
13
13
|
|
|
14
|
-
The stages left out are the ones that lie. A dev server or a preview reads as failed when it starts slowly, a browser test needs an install and a running server before it can even fail correctly, and a CI workflow cannot be judged locally at all. Including any of them turns a scaffold check into a flaky one, and a flaky check gets ignored.
|
|
14
|
+
The stages left out are the ones that lie, and each now has an owner in `canon:setup-smoke` rather than nowhere at all. A dev server or a preview reads as failed when it starts slowly, a browser test needs an install and a running server before it can even fail correctly, and a CI workflow cannot be judged locally at all. Including any of them turns a scaffold check into a flaky one, and a flaky check gets ignored.
|
|
15
15
|
|
|
16
16
|
## Must
|
|
17
17
|
|
|
@@ -33,7 +33,7 @@ The stages left out are the ones that lie. A dev server or a preview reads as fa
|
|
|
33
33
|
|
|
34
34
|
## Out of scope
|
|
35
35
|
|
|
36
|
-
- Dev and preview smoke tests, which `project-commands` starts on request
|
|
37
|
-
- Browser end-to-end tests, which need a browser install and a running server
|
|
38
|
-
- CI workflow validation, which runs on the pull request rather than locally
|
|
36
|
+
- Dev and preview smoke tests, which `project-commands` starts on request and `canon:setup-smoke` checks automatically
|
|
37
|
+
- Browser end-to-end tests, which need a browser install and a running server, covered by `canon:setup-smoke`
|
|
38
|
+
- CI workflow validation, which runs on the pull request rather than locally. `canon:setup-smoke` runs the same stages locally as the closest proxy
|
|
39
39
|
- Generating the configs it checks, which the tooling stack reference owns
|
|
@@ -46,6 +46,6 @@ End with a summary line:
|
|
|
46
46
|
|
|
47
47
|
## Out of scope
|
|
48
48
|
|
|
49
|
-
- Dev server and preview server smoke tests. Too flaky for a scaffold check, where a slow start reads as a failure. Use `project-commands` to start one on request.
|
|
50
|
-
- Playwright E2E tests. Require browser install and a running server.
|
|
51
|
-
- CI workflow validation. Runs in GitHub Actions on PR, not locally.
|
|
49
|
+
- Dev server and preview server smoke tests. Too flaky for a scaffold check, where a slow start reads as a failure. Use `project-commands` to start one on request, or `canon:setup-smoke` to check both automatically.
|
|
50
|
+
- Playwright E2E tests. Require browser install and a running server. Use `canon:setup-smoke`.
|
|
51
|
+
- CI workflow validation. Runs in GitHub Actions on PR, not locally. `canon:setup-smoke` runs the same end-to-end and screenshot stages locally, as the closest proxy.
|
package/docs/agents/commands.md
CHANGED
|
@@ -9,86 +9,87 @@ Full help: `canon <command> --help`. Behavior notes for the install and sync ver
|
|
|
9
9
|
|
|
10
10
|
## Project-level
|
|
11
11
|
|
|
12
|
-
| Command
|
|
13
|
-
|
|
|
14
|
-
| `canon init [path]`
|
|
15
|
-
| `canon sync [path]`
|
|
16
|
-
| `canon sync --check`
|
|
17
|
-
| `canon sandbox [cat:cmd]`
|
|
18
|
-
| `canon sandbox reset`
|
|
19
|
-
| `canon sandbox clean`
|
|
20
|
-
| `canon sandbox check`
|
|
21
|
-
| `canon sandbox coverage`
|
|
22
|
-
| `canon indexes regen`
|
|
23
|
-
| `canon indexes list [path]`
|
|
24
|
-
| `canon docs [topic]`
|
|
25
|
-
| `canon design render`
|
|
26
|
-
| `canon design regen`
|
|
27
|
-
| `canon design css`
|
|
28
|
-
| `canon design install`
|
|
29
|
-
| `canon design sync`
|
|
30
|
-
| `canon slides render`
|
|
31
|
-
| `canon slides list`
|
|
32
|
-
| `canon feedback`
|
|
33
|
-
| `canon transcripts <url>`
|
|
34
|
-
| `canon tasks archive`
|
|
35
|
-
| `canon tasks pull-request`
|
|
36
|
-
| `canon tasks plan-link`
|
|
37
|
-
| `canon tasks outcome`
|
|
38
|
-
| `canon tasks validate`
|
|
39
|
-
| `canon tasks next-label`
|
|
40
|
-
| `canon intake list`
|
|
41
|
-
| `canon intake answer`
|
|
42
|
-
| `canon teach list`
|
|
43
|
-
| `canon teach open`
|
|
44
|
-
| `canon teach resource`
|
|
45
|
-
| `canon teach glossary`
|
|
46
|
-
| `canon teach stylesheet`
|
|
47
|
-
| `canon teach nav`
|
|
48
|
-
| `canon records validate`
|
|
49
|
-
| `canon records migrate`
|
|
50
|
-
| `canon records size`
|
|
51
|
-
| `canon records push`
|
|
52
|
-
| `canon records pull`
|
|
53
|
-
| `canon migrate rename`
|
|
54
|
-
| `canon migrate records`
|
|
55
|
-
| `canon migrate record-tree`
|
|
56
|
-
| `canon migrate rule-layout`
|
|
57
|
-
| `canon
|
|
58
|
-
| `canon
|
|
59
|
-
| `canon worktrees
|
|
60
|
-
| `canon
|
|
61
|
-
| `canon
|
|
62
|
-
| `canon
|
|
63
|
-
| `canon
|
|
64
|
-
| `canon
|
|
65
|
-
| `canon
|
|
66
|
-
| `canon claude skills
|
|
67
|
-
| `canon claude skills
|
|
68
|
-
| `canon claude
|
|
69
|
-
| `canon
|
|
70
|
-
| `canon gov
|
|
71
|
-
| `canon gov
|
|
72
|
-
| `canon gov
|
|
73
|
-
| `canon
|
|
74
|
-
| `canon
|
|
75
|
-
| `canon
|
|
76
|
-
| `canon labels
|
|
77
|
-
| `canon
|
|
78
|
-
| `canon
|
|
79
|
-
| `canon pr
|
|
80
|
-
| `canon pr
|
|
81
|
-
| `canon
|
|
82
|
-
| `canon repo metadata
|
|
83
|
-
| `canon
|
|
84
|
-
| `canon
|
|
85
|
-
| `canon audits
|
|
86
|
-
| `canon
|
|
87
|
-
| `canon
|
|
88
|
-
| `canon
|
|
89
|
-
| `canon
|
|
90
|
-
| `canon
|
|
91
|
-
| `canon
|
|
12
|
+
| Command | Purpose |
|
|
13
|
+
| -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
14
|
+
| `canon init [path]` | Bootstrap a project with selected toolkit domains |
|
|
15
|
+
| `canon sync [path]` | Sync all installed domains in a target project |
|
|
16
|
+
| `canon sync --check` | Report toolkit drift and the installed version against the newest published (`--json`, `--exit-code`) |
|
|
17
|
+
| `canon sandbox [cat:cmd]` | Run sandbox scenarios (interactive or routed), toolkit-only like the tree it reads |
|
|
18
|
+
| `canon sandbox reset` | Reset sandbox to baseline |
|
|
19
|
+
| `canon sandbox clean` | Wipe the sandbox |
|
|
20
|
+
| `canon sandbox check` | Score a provisioned sandbox against a scenario expectation (`--json` for the verdict) |
|
|
21
|
+
| `canon sandbox coverage` | Report which scenarios declare expectations (`--json`, `--strict`, `--skills`) |
|
|
22
|
+
| `canon indexes regen` | Regenerate `index.md` files from sibling frontmatter |
|
|
23
|
+
| `canon indexes list [path]` | Flatten every folder index under a path into one `{path, title, description}` catalog (`--json`) |
|
|
24
|
+
| `canon docs [topic]` | Emit toolkit reference docs (`list`, or a topic by name) |
|
|
25
|
+
| `canon design render` | Render `.claude/DESIGN.md` tokens to HTML and CSS |
|
|
26
|
+
| `canon design regen` | Rewrite this repository's `.claude/DESIGN.md` and `src/design/base.css` from `src/design/tokens.ts` |
|
|
27
|
+
| `canon design css` | Emit the design tokens and components as CSS on stdout (`--no-components` for properties alone) |
|
|
28
|
+
| `canon design install` | Install the base stylesheet into a project at `.claude/design/base.css` |
|
|
29
|
+
| `canon design sync` | Update a base stylesheet already installed under `.claude/design/` |
|
|
30
|
+
| `canon slides render` | Render a `.claude/SLIDES.md` source into a PowerPoint deck, reporting any unrecognized layout name on stderr |
|
|
31
|
+
| `canon slides list` | List the available slide layouts (`--json` for the catalog) |
|
|
32
|
+
| `canon feedback` | Write toolkit feedback from stdin to `.canon/review/feedback/`, or open a GitHub issue with `--github`, refusing either when a required field is absent |
|
|
33
|
+
| `canon transcripts <url>` | Fetch a YouTube transcript with metadata frontmatter (needs `yt-dlp`) |
|
|
34
|
+
| `canon tasks archive` | Move a shipped task and its plan off the board, clear its ordering row, and regenerate the index |
|
|
35
|
+
| `canon tasks pull-request` | Record a pull request number on the task a branch closes, by stem or `--plan` (`--json`) |
|
|
36
|
+
| `canon tasks plan-link` | Write or correct a task's `Plan:` line to point at a plan, by stem and plan path or slug (`--json`) |
|
|
37
|
+
| `canon tasks outcome` | Mark outcomes `[x]` on a task by position, repeating `--close` (`--json`) |
|
|
38
|
+
| `canon tasks validate` | Report board rows whose shape, order, plan, task file, group, file set, or blocker does not hold (`--json`) |
|
|
39
|
+
| `canon tasks next-label` | Report the next unused phase label across the board and its archive (`--json`) |
|
|
40
|
+
| `canon intake list` | Report intake folder counts, or one folder's items, keeping what is unread with `--unread` (`--json`) |
|
|
41
|
+
| `canon intake answer` | Write selections into one cluster's answer slots, repeating `--set <item>=<answer>` (`--json`) |
|
|
42
|
+
| `canon teach list` | Report learning workspaces and the ordinal a new one takes, or what one workspace holds and what its records schedule (`--json`) |
|
|
43
|
+
| `canon teach open` | Open a workspace at the next ordinal with its mission, resources, and glossary files (`--json`) |
|
|
44
|
+
| `canon teach resource` | Record sources and leads in a workspace, repeating `--read` or `--lead` as `<title>=<url>` (`--json`) |
|
|
45
|
+
| `canon teach glossary` | Add terms to a workspace glossary alphabetically, repeating `--term <term>=<definition>` (`--json`) |
|
|
46
|
+
| `canon teach stylesheet` | Seed a workspace stylesheet from the design source, leaving an existing one alone without `--force` (`--json`) |
|
|
47
|
+
| `canon teach nav` | Rewrite the teach-root listing, a workspace's contents page, and each lesson's chrome from its four marker regions (`--json`) |
|
|
48
|
+
| `canon records validate` | Report a session record or a standard against the standard governing it, per kind (`--json`) |
|
|
49
|
+
| `canon records migrate` | Rewrite the records a validate finding names a recoverable transform for (`--write`, `--json`) |
|
|
50
|
+
| `canon records size` | Report what each record folder holds and how much of it is recent, heaviest first (`--json`) |
|
|
51
|
+
| `canon records push` | Commit the nine backed record folders and push them to a private records remote (`--json`) |
|
|
52
|
+
| `canon records pull` | Fetch the records remote and write it back, refusing rather than discarding unpushed records (`--json`) |
|
|
53
|
+
| `canon migrate rename` | Rewrite every unprotected `aitk` token to `canon` and move the paths that carry the name, reporting the plan without `--write` (`--scope`, `--json`) |
|
|
54
|
+
| `canon migrate records` | Move the gitignored session records to `.canon/` and repoint every tracked citation, reporting the plan without `--write` (`--root`, `--json`) |
|
|
55
|
+
| `canon migrate record-tree` | Repoint the old-root citations inside the records themselves, scoped to the live folders and reporting every line without `--write` (`--root`, `--json`) |
|
|
56
|
+
| `canon migrate rule-layout` | Move a target's installed rules from the flat `.claude/rules/<subdir>/` layout to `.claude/rules/canon/<subdir>/`, reporting the plan without `--write` (`--root`, `--json`) |
|
|
57
|
+
| `canon migrate scratch-evidence` | Move a folder under `.tmp/` that a durable record cites as evidence to `.canon/review/evidence/`, repointing every citation live or archived, reporting the plan without `--write` (`--root`, `--json`) |
|
|
58
|
+
| `canon sessions list` | Resolve live sessions to the worktree and branch each holds, filtered by `--branch` (`--json`) |
|
|
59
|
+
| `canon worktrees list` | Report which worktrees are reclaimable, keyed on the pull request having merged, with every refusal and the removal route named (`--json`) |
|
|
60
|
+
| `canon worktrees reclaim` | Remove every reclaimable worktree and the branch behind it, reporting without acting under `--dry-run` (`--json`) |
|
|
61
|
+
| `canon comments scan` | Measure comment density by language and comment kind, with a trend recomputed from git |
|
|
62
|
+
| `canon context audit` | Report required sections, length, cited paths, reference form, catalog tables, provenance, superseded-decision narration, and index drift |
|
|
63
|
+
| `canon markdown audit` | Fail any markdown path on a banned character, word, or spelling, or a dead relative link, and report the structural checkpoints |
|
|
64
|
+
| `canon claude skills audit` | Report both skill corpora against the mechanical rules in `standards/skill.md` |
|
|
65
|
+
| `canon standards audit` | Report the corpus against the success-criterion gate `standards/standard.md` states, failing only on a standard new to the branch (`--json`, `--arrivals-only`) |
|
|
66
|
+
| `canon claude skills drift` | Name the shipped skill bodies rewritten between a given ref and `HEAD`, and the installed version against the newest published (`--json`) |
|
|
67
|
+
| `canon claude skills reach` | Report the bodies in either skill corpus citing a toolkit path no target project receives, exiting 2 on an unqualified one |
|
|
68
|
+
| `canon claude skills rank` | Score either skill corpus's descriptions against a case corpus by TF-IDF cosine similarity, reporting rank-one and top-three (`--cases <path>`) |
|
|
69
|
+
| `canon claude routing` | Report per `CLAUDE.md` section how many bullets name a path and how many of those a path-scoped rule already covers (`--json`) |
|
|
70
|
+
| `canon gov test-order` | Report where an implementation reached history ahead of the test covering it (`--json`) |
|
|
71
|
+
| `canon gov superseded` | Report where the tree still asserts a value a changed convention no longer produces, keyed on the value and on the family stem behind a templated citation (`--json`) |
|
|
72
|
+
| `canon gov restated` | Report every instruction the always-loaded file or a rule shares with the seed, a shipped skill body, or another rule, classed and with its anchors named (`--json`) |
|
|
73
|
+
| `canon gov citations` | Resolve every path a rule cites and every internal frontmatter glob, failing on one reaching nothing (`--json`) |
|
|
74
|
+
| `canon secrets scan` | Report credential-shaped values in the tree the package ships, keyed on issued values rather than on words (`--json`) |
|
|
75
|
+
| `canon deps audit` | Report published advisories against the resolved dependency set, refusing rather than reporting clean when the index is unreachable (`--json`) |
|
|
76
|
+
| `canon labels audit` | Report the labels a changed set earns from the pull request label map and the paths no row reaches (`--json`) |
|
|
77
|
+
| `canon labels scan` | Fail a pull request or a posted review whose title, body, or review comment carries a phase label, a label a code span quotes, a gitignored record path, a session link, a title word no dictionary holds, or a title breaking `standards/pr.md`'s format, casing, or length rule (`--event`, `--body-file`, `--json`) |
|
|
78
|
+
| `canon autoship classify` | Decide whether a changed set needs the review pass, naming the file and the test that decided it (`--json`) |
|
|
79
|
+
| `canon pr key-changes` | Compare the files a pull request body's Key Changes names against its own diff, in both directions (`--body`, `--base`, `--json`) |
|
|
80
|
+
| `canon pr head` | Compare the head a pull request object reports against the branch tip the remote carries, naming both shas (`--root`, `--json`) |
|
|
81
|
+
| `canon pr checks` | Report the check runs belonging to the branch tip, reading pending for a tip carrying none rather than clean (`--root`, `--json`) |
|
|
82
|
+
| `canon repo metadata propose` | Compare a description, homepage, and topic set computed from the README and `package.json` against what the remote carries, writing nothing (`--root`, `--json`) |
|
|
83
|
+
| `canon repo metadata apply` | Write an explicitly supplied description, homepage, or topic set to the remote through `gh repo edit` (`--description`, `--homepage`, `--topics`, `--root`, `--json`) |
|
|
84
|
+
| `canon census [path]` | Report tracked file count, a breakdown by extension, and a line total that skips whatever reads as binary (`--json`) |
|
|
85
|
+
| `canon audits run` | Run every audit as one set, report per check under one verdict, and compare each count to the recorded baseline (`--json`, `--record`, `--corpus`) |
|
|
86
|
+
| `canon audits list` | List every audit the set runs, with the corpus each reads and whether it gates (`--json`) |
|
|
87
|
+
| `canon gate run` | Run every stage that guards a branch here, scoping shell, types, and tests to the changed set (`--all`, `--no-write`, `--nested`, `--json`) |
|
|
88
|
+
| `canon inventory [subject]` | Walk every route a project declares and group its elements by the property each computes, as a listing rather than a gate (`--json`) |
|
|
89
|
+
| `canon drive <url> <run>` | Walk a page through named interactions and measure each state it reaches, reporting findings rather than gating (`--json`) |
|
|
90
|
+
| `canon capture [source]` | Render HTML sources to PNG and prove the font each one declares resolved (`--selector` required, `--out`) |
|
|
91
|
+
| `canon serve [dir]` | Serve a directory on the loopback interface and print the link that opens it, running until interrupted (`--port`, `--entry`, `--json`) |
|
|
92
|
+
| `canon upgrade` | Reinstall the CLI globally with the package manager the install path names (`--json`) |
|
|
92
93
|
|
|
93
94
|
`canon serve` drives no browser, which is what separates it from the four that do. All four ship now that `capture` does, so the line between them is the engine rather than the package. A generated page loses its script to an editor preview and to a `file://` open, so the link is the delivery rather than a convenience, and every generated surface here reaches a reader through one. A teach lesson's stylesheet is embedded rather than linked, so only its script still needs a server. It binds `127.0.0.1` and never a wildcard, because what it is pointed at is routinely a gitignored record tree. It sends `cache-control: no-store`, since a preview exists to be edited and reloaded and a cached stylesheet reads as a fix that did not work.
|
|
94
95
|
|
|
@@ -162,6 +163,8 @@ Every citation in scope is reported with its file, its line number, and the line
|
|
|
162
163
|
|
|
163
164
|
`migrate rule-layout` moves a target still on the flat installed-rule layout onto the `canon/`-wrapped one. It classifies each flat file against the target's own recorded stamp: an unchanged hash is clean, a changed one is edited, and a name neither the stamp nor the current rule catalog can vouch for is unclaimed and left in place. Clean and edited files both relocate with their content untouched, since moving a rule is a different decision from re-syncing it. The one renumbered rule reads its destination from a table rather than from its own current name, so it never lands as a second copy under the retired number.
|
|
164
165
|
|
|
166
|
+
`migrate scratch-evidence` moves a folder out of `.tmp/` for good rather than repointing a citation in place. A promoted folder is one a durable record, live or archived, cites as its evidence, and no source file under `claude/`, `scripts/`, `src/`, `governance/`, or `standards/` names by path, which is what a script still writing into it fails. It walks every `canon records push` folder rather than the live ones alone, since most of what it repoints sits in `tasks/archive/`, `plans/archive/`, and `groundwork/`. A destination already occupied is a refusal, and a second run rewriting nothing is the same idempotence check `migrate record-tree` uses.
|
|
167
|
+
|
|
165
168
|
A destination that already holds the same bytes marks the flat file a duplicate, which `--write` deletes rather than moves. One holding different bytes is a real collision: neither file is touched, it is reported by name, and every other planned move still applies independently. Run this before `canon gov install` or `canon gov sync` against a target still on the flat layout, since neither bootstrap verb detects or clears it on its own.
|
|
166
169
|
|
|
167
170
|
## Version skew
|
package/docs/target-projects.md
CHANGED
|
@@ -63,6 +63,8 @@ The chain is:
|
|
|
63
63
|
|
|
64
64
|
The chain stops at the project edge. `repo-metadata` and `git-commit` also ship, reaching a remote and the project's history respectively, and neither runs as part of it. The chain serves a fresh scaffold and names a destination for the three states it does not. An existing project goes to `canon:canon-operator`, which reads what the project already carries before naming a per-domain command. An install wanting the Claude layer without the tooling chain runs `canon claude init` for the seed docs and then `canon:setup-indexes` for the index system. A language the toolkit ships no stack for is the one of the three the chain still runs for, on `base`, with the fallback marked in the preview so it can be declined there.
|
|
65
65
|
|
|
66
|
+
Run `canon:setup-smoke` by hand once `setup-verify` passes, for the heavier server smoke, end-to-end, and screenshot pass, since the same flakiness reasons that excluded those stages from `setup-verify` keep it out of this unattended chain too.
|
|
67
|
+
|
|
66
68
|
Keep the `## Scripts` table in `.claude/context/development.md` current as scripts are added. Base tooling seeds that entry with the commands it installs, and each stack reference extends the table. `project-commands` reads it to start the app or run a check on request, so a command missing from the table cannot be run that way. A project whose entry outgrew one file and split into `.claude/context/development/` keeps the table in `overview.md`, which is where the skill looks next. <!-- audit-ignore-citations: .claude/context/development.md -->
|
|
67
69
|
|
|
68
70
|
### From scaffold to first feature
|
|
@@ -193,16 +193,17 @@ This section is the corpus the coverage claim is measured against: every name `c
|
|
|
193
193
|
|
|
194
194
|
### Set up a project
|
|
195
195
|
|
|
196
|
-
| Skill | When to use
|
|
197
|
-
| ----------------------------- |
|
|
198
|
-
| `canon:setup-init` | On a fresh scaffold, to detect the stack and run the whole install chain in one pass
|
|
199
|
-
| `canon:canon-operator` | On a project that already exists, to read what it carries before an install is picked
|
|
200
|
-
| `canon:setup-gov` | When the governance rules are wanted without the tooling chain
|
|
201
|
-
| `canon:setup-indexes` | When a markdown-heavy folder needs an `index.md` a session can browse
|
|
202
|
-
| `canon:setup-plugins` | On a new machine, to install the community and official plugins user-scoped
|
|
203
|
-
| `canon:setup-verify` | After the agent generates configs, to run the installed scripts and report pass or fail
|
|
204
|
-
| `canon:
|
|
205
|
-
| `canon:claude-
|
|
196
|
+
| Skill | When to use |
|
|
197
|
+
| ----------------------------- | --------------------------------------------------------------------------------------------------------------- |
|
|
198
|
+
| `canon:setup-init` | On a fresh scaffold, to detect the stack and run the whole install chain in one pass |
|
|
199
|
+
| `canon:canon-operator` | On a project that already exists, to read what it carries before an install is picked |
|
|
200
|
+
| `canon:setup-gov` | When the governance rules are wanted without the tooling chain |
|
|
201
|
+
| `canon:setup-indexes` | When a markdown-heavy folder needs an `index.md` a session can browse |
|
|
202
|
+
| `canon:setup-plugins` | On a new machine, to install the community and official plugins user-scoped |
|
|
203
|
+
| `canon:setup-verify` | After the agent generates configs, to run the installed scripts and report pass or fail |
|
|
204
|
+
| `canon:setup-smoke` | After `setup-verify` passes, to check the dev and preview servers, end-to-end tests, and the screenshot harness |
|
|
205
|
+
| `canon:claude-design-extract` | Before the first UI feature, to draft `.claude/DESIGN.md` |
|
|
206
|
+
| `canon:claude-diagram` | Once the architecture is written, to draft per-kind entries under `.canon/diagrams/` |
|
|
206
207
|
|
|
207
208
|
### Decide what to build
|
|
208
209
|
|
package/package.json
CHANGED
|
@@ -30,6 +30,11 @@ export const SETUP_CASES: readonly SkillCase[] = [
|
|
|
30
30
|
"Run through the generated scaffold's scripts and confirm each one passes.",
|
|
31
31
|
expect: 'setup-verify',
|
|
32
32
|
},
|
|
33
|
+
{
|
|
34
|
+
prompt:
|
|
35
|
+
'Check that the dev server actually starts and the end-to-end suite passes against the scaffold.',
|
|
36
|
+
expect: 'setup-smoke',
|
|
37
|
+
},
|
|
33
38
|
{
|
|
34
39
|
prompt:
|
|
35
40
|
'This CLAUDE.md file has grown huge, break it apart into the tiered context model.',
|
package/src/commands/migrate.ts
CHANGED
|
@@ -24,6 +24,13 @@ import {
|
|
|
24
24
|
type RuleLayoutPlan,
|
|
25
25
|
walkFlatRules,
|
|
26
26
|
} from '@/migrate/rule-layout'
|
|
27
|
+
import {
|
|
28
|
+
applyScratchEvidence,
|
|
29
|
+
planScratchEvidence,
|
|
30
|
+
readScratchEvidenceCorpus,
|
|
31
|
+
type ScratchEvidencePlan,
|
|
32
|
+
walkScratchEvidenceCorpus,
|
|
33
|
+
} from '@/migrate/scratch-evidence'
|
|
27
34
|
import { PROJECT_ROOT } from '@/project-root'
|
|
28
35
|
import { readStamp, stampedHashes } from '@/sync/stamp'
|
|
29
36
|
import { logError, logInfo, logStep, logWarn, pipeOutput, plural } from '@/ui'
|
|
@@ -470,6 +477,96 @@ function toRecordTreeRecord(
|
|
|
470
477
|
}
|
|
471
478
|
}
|
|
472
479
|
|
|
480
|
+
interface ScratchEvidenceOptions {
|
|
481
|
+
readonly json?: boolean
|
|
482
|
+
readonly write?: boolean
|
|
483
|
+
readonly root?: string
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Moves the nine cited-and-unwritten folders under `.canon/tmp/` to
|
|
488
|
+
* `.canon/review/evidence/`, and repoints the citations that name them,
|
|
489
|
+
* archives included.
|
|
490
|
+
*/
|
|
491
|
+
async function runScratchEvidence(
|
|
492
|
+
opts: ScratchEvidenceOptions,
|
|
493
|
+
): Promise<number> {
|
|
494
|
+
const root = opts.root ?? process.cwd()
|
|
495
|
+
|
|
496
|
+
const files = await walkScratchEvidenceCorpus(root)
|
|
497
|
+
const sources = await readScratchEvidenceCorpus(files)
|
|
498
|
+
const plan = planScratchEvidence(root, sources)
|
|
499
|
+
|
|
500
|
+
if (opts.json) {
|
|
501
|
+
process.stdout.write(
|
|
502
|
+
`${JSON.stringify(toScratchEvidenceRecord(plan, opts.write))}\n`,
|
|
503
|
+
)
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
reportScratchEvidence(plan)
|
|
507
|
+
|
|
508
|
+
if (plan.collisions.length > 0) {
|
|
509
|
+
logError(
|
|
510
|
+
`${plural(plan.collisions.length, 'destination')} already occupied. Neither side moved.`,
|
|
511
|
+
)
|
|
512
|
+
for (const collision of plan.collisions) logError(` ${collision}`)
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
if (plan.moves.length === 0 && plan.entries.length === 0) {
|
|
516
|
+
return plan.collisions.length > 0 ? 1 : 0
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
if (!opts.write) {
|
|
520
|
+
logWarn('Nothing was written. Pass --write to apply this plan.')
|
|
521
|
+
return 2
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const applied = await applyScratchEvidence(plan)
|
|
525
|
+
logStep(
|
|
526
|
+
`Moved ${plural(applied.moved, 'folder')} and rewrote ${plural(applied.written, 'file')}.`,
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
if (applied.failed.length > 0) {
|
|
530
|
+
logError(`Could not write ${plural(applied.failed.length, 'file')}.`)
|
|
531
|
+
for (const path of applied.failed) logError(` ${path}`)
|
|
532
|
+
return 1
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
return plan.collisions.length > 0 ? 1 : 0
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function reportScratchEvidence(plan: ScratchEvidencePlan): void {
|
|
539
|
+
logInfo(`${plural(plan.moves.length, 'folder')} to move.`)
|
|
540
|
+
for (const move of plan.moves) {
|
|
541
|
+
logInfo(` ${move.from} -> ${move.to}`)
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
logInfo(
|
|
545
|
+
`${plural(plan.entries.length, 'file')} to change, ${plural(plan.rewritten, 'citation')} to rewrite.`,
|
|
546
|
+
)
|
|
547
|
+
for (const entry of plan.entries) {
|
|
548
|
+
logInfo(` ${entry.path}: ${plural(entry.rewritten, 'citation')}`)
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function toScratchEvidenceRecord(
|
|
553
|
+
plan: ScratchEvidencePlan,
|
|
554
|
+
wrote: boolean | undefined,
|
|
555
|
+
): unknown {
|
|
556
|
+
return {
|
|
557
|
+
ok: true,
|
|
558
|
+
wrote: wrote === true,
|
|
559
|
+
moves: plan.moves,
|
|
560
|
+
collisions: plan.collisions,
|
|
561
|
+
files: plan.entries.length,
|
|
562
|
+
rewritten: plan.rewritten,
|
|
563
|
+
paths: plan.entries.map((entry) => ({
|
|
564
|
+
path: entry.path,
|
|
565
|
+
rewritten: entry.rewritten,
|
|
566
|
+
})),
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
473
570
|
interface RuleLayoutOptions {
|
|
474
571
|
readonly json?: boolean
|
|
475
572
|
readonly write?: boolean
|
|
@@ -673,6 +770,45 @@ export function register(program: Command): void {
|
|
|
673
770
|
process.exitCode = await runRecordTree(opts)
|
|
674
771
|
})
|
|
675
772
|
|
|
773
|
+
migrate
|
|
774
|
+
.command('scratch-evidence')
|
|
775
|
+
.description('Promote cited measurement folders out of tmp into review')
|
|
776
|
+
.helpOption('-h, --help', 'Show this help message')
|
|
777
|
+
.option('--json', 'Add a machine-readable record on stdout')
|
|
778
|
+
.option('--write', 'Apply the plan rather than reporting it')
|
|
779
|
+
.option(
|
|
780
|
+
'--root <path>',
|
|
781
|
+
'Project root, defaulting to the working directory',
|
|
782
|
+
)
|
|
783
|
+
.addHelpText(
|
|
784
|
+
'after',
|
|
785
|
+
[
|
|
786
|
+
'',
|
|
787
|
+
'Moves nine folders cited as measurement evidence from .canon/tmp/ to',
|
|
788
|
+
'.canon/review/evidence/, which canon records push already backs, and',
|
|
789
|
+
'repoints every citation that names one, live or archived.',
|
|
790
|
+
'',
|
|
791
|
+
'A promoted folder is one a durable record cites and no source file',
|
|
792
|
+
'under claude/, scripts/, src/, governance/, or standards/ names by',
|
|
793
|
+
'path. A folder a script still writes into stays in scratch, since',
|
|
794
|
+
'moving it needs a code change first.',
|
|
795
|
+
'',
|
|
796
|
+
'Exit codes:',
|
|
797
|
+
' 0 nothing to move, or --write applied the whole plan',
|
|
798
|
+
' 1 a write failed, or an unresolved collision remains',
|
|
799
|
+
' 2 a plan exists and --write was not passed',
|
|
800
|
+
'',
|
|
801
|
+
'Examples:',
|
|
802
|
+
' canon migrate scratch-evidence',
|
|
803
|
+
' canon migrate scratch-evidence --write',
|
|
804
|
+
' canon migrate scratch-evidence --json',
|
|
805
|
+
'',
|
|
806
|
+
].join('\n'),
|
|
807
|
+
)
|
|
808
|
+
.action(async (opts: ScratchEvidenceOptions) => {
|
|
809
|
+
process.exitCode = await runScratchEvidence(opts)
|
|
810
|
+
})
|
|
811
|
+
|
|
676
812
|
migrate
|
|
677
813
|
.command('rename')
|
|
678
814
|
.description('Rewrite every unprotected aitk token to canon')
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The promotion of nine cited measurement folders out of `.canon/tmp/` into
|
|
3
|
+
* `.canon/review/evidence/`, which `canon records push` already backs.
|
|
4
|
+
*
|
|
5
|
+
* The scratch root is the one record root a disk loss takes with it, and a
|
|
6
|
+
* durable record naming a folder under it as its evidence is a citation into
|
|
7
|
+
* something the backup never covers. The nine promoted here are every folder
|
|
8
|
+
* under scratch that a live or archived record cites and no source file under
|
|
9
|
+
* `claude/`, `scripts/`, `src/`, `governance/`, or `standards/` names by path,
|
|
10
|
+
* which is what separates them from a folder a script still writes into on its
|
|
11
|
+
* own schedule.
|
|
12
|
+
*
|
|
13
|
+
* Distinct from `record-tree.ts`, which repoints a citation of the `.claude/`
|
|
14
|
+
* to `.canon/` root move and prunes every `archive` segment on the way in,
|
|
15
|
+
* since an archived record describes work that already closed. This move
|
|
16
|
+
* reaches into an archive on purpose: `tasks/archive/`, `plans/archive/`, and
|
|
17
|
+
* `groundwork/` are where most of the citations broken here already sit, and
|
|
18
|
+
* a folder promoted out from under them stays gone whether the citing record
|
|
19
|
+
* is open or closed.
|
|
20
|
+
*
|
|
21
|
+
* Honors the same `canon-keep-record-root` marker `records.ts` reads, since a
|
|
22
|
+
* sentence describing what no target holds is not a live pointer this checkout
|
|
23
|
+
* has to keep resolving.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { existsSync } from 'node:fs'
|
|
27
|
+
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises'
|
|
28
|
+
import { dirname, join } from 'node:path'
|
|
29
|
+
import { BACKED_FOLDERS } from '@/records/backup'
|
|
30
|
+
import { recordDir, SCRATCH } from '@/record-root'
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Every folder this promotion moves, at the name it carries under scratch.
|
|
34
|
+
*
|
|
35
|
+
* Derived by the two-clause test `canon migrate scratch-evidence` exists to
|
|
36
|
+
* apply mechanically: a durable record under a `BACKED_FOLDERS` entry, live
|
|
37
|
+
* or archived, names the folder as its evidence, and no file under `claude/`,
|
|
38
|
+
* `scripts/`, `src/`, `governance/`, or `standards/` names that path. Measured
|
|
39
|
+
* 2026-09-06 against nine folders holding thirteen files.
|
|
40
|
+
*
|
|
41
|
+
* `verify-astro` and `verify-vite-react` pass the same test and are excluded
|
|
42
|
+
* by name: both are scaffolds a command generates rather than records a
|
|
43
|
+
* session wrote, and each is 100+ MB, which the review remote is not sized
|
|
44
|
+
* for. `ablation`, `eval-runs`, `sandbox-runs`, `memory-archive`,
|
|
45
|
+
* `groundwork-fixtures`, `precompact-handoff`, `pr-poll`, `pr`,
|
|
46
|
+
* `address-review`, and `memory-routing` fail the second clause: a script or
|
|
47
|
+
* a skill body names each of those paths, so moving one needs a code change
|
|
48
|
+
* first rather than a promotion.
|
|
49
|
+
*/
|
|
50
|
+
export const PROMOTED_FOLDERS: readonly string[] = [
|
|
51
|
+
'hero-probe',
|
|
52
|
+
'markdown-corpus-sweep',
|
|
53
|
+
'orchestrator-output',
|
|
54
|
+
'orchestrator-watch',
|
|
55
|
+
'review-calibration',
|
|
56
|
+
'sandbox-drift',
|
|
57
|
+
'skill-requirement-pass',
|
|
58
|
+
'system-map',
|
|
59
|
+
'target-survey',
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
const EVIDENCE_ROOT = ['review', 'evidence'] as const
|
|
63
|
+
|
|
64
|
+
/** Where a promoted folder sits before the move. */
|
|
65
|
+
export function sourcePath(root: string, folder: string): string {
|
|
66
|
+
return recordDir(root, SCRATCH, folder)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Where it lands after, always under whichever root `review/` resolves at. */
|
|
70
|
+
export function destinationPath(root: string, folder: string): string {
|
|
71
|
+
return recordDir(root, ...EVIDENCE_ROOT, folder)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function escape(value: string): string {
|
|
75
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The prefixes a citation of a promoted folder is spelled with, absolute at
|
|
80
|
+
* either record root and relative from one directory below it. A tail
|
|
81
|
+
* rejecting a following name character is what keeps `target-survey` from
|
|
82
|
+
* swallowing a sibling folder whose name extends it.
|
|
83
|
+
*/
|
|
84
|
+
const OLD_PREFIXES = [
|
|
85
|
+
'.claude/.tmp/',
|
|
86
|
+
'.canon/tmp/',
|
|
87
|
+
'../.tmp/',
|
|
88
|
+
'../tmp/',
|
|
89
|
+
'../../.tmp/',
|
|
90
|
+
'../../tmp/',
|
|
91
|
+
] as const
|
|
92
|
+
|
|
93
|
+
function citationPattern(folder: string): RegExp {
|
|
94
|
+
const alternation = OLD_PREFIXES.map(escape).join('|')
|
|
95
|
+
return new RegExp(
|
|
96
|
+
`(?:${alternation})${escape(folder)}(?![A-Za-z0-9._-])`,
|
|
97
|
+
'g',
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
interface Rewrite {
|
|
102
|
+
readonly pattern: RegExp
|
|
103
|
+
readonly folder: string
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** One rewrite per folder, paired with its citation pattern. */
|
|
107
|
+
function buildRewrites(folders: readonly string[]): readonly Rewrite[] {
|
|
108
|
+
return folders.map((folder) => ({
|
|
109
|
+
pattern: citationPattern(folder),
|
|
110
|
+
folder,
|
|
111
|
+
}))
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Marks a line naming a promoted folder's old path on purpose, the same
|
|
116
|
+
* marker `records.ts` reads: on the line itself or on the nearest non-blank
|
|
117
|
+
* line above it. A sentence describing what no target holds, rather than
|
|
118
|
+
* pointing a reader at this checkout's own evidence, needs the old spelling
|
|
119
|
+
* kept, and a mechanical rewrite cannot tell that apart from a live citation.
|
|
120
|
+
*/
|
|
121
|
+
const KEEP_MARKER = 'canon-keep-record-root'
|
|
122
|
+
|
|
123
|
+
function isKept(lines: readonly string[], index: number): boolean {
|
|
124
|
+
if (lines[index]?.includes(KEEP_MARKER)) return true
|
|
125
|
+
|
|
126
|
+
let above = index - 1
|
|
127
|
+
while (above >= 0 && lines[above]?.trim() === '') above -= 1
|
|
128
|
+
|
|
129
|
+
return above >= 0 && (lines[above]?.includes(KEEP_MARKER) ?? false)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function rewriteLine(line: string, rewrites: readonly Rewrite[]): string {
|
|
133
|
+
return rewrites.reduce(
|
|
134
|
+
(current, { pattern, folder }) =>
|
|
135
|
+
current.replace(pattern, `.canon/review/evidence/${folder}`),
|
|
136
|
+
line,
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
interface RewriteOutcome {
|
|
141
|
+
readonly text: string
|
|
142
|
+
readonly count: number
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Rewrites every unmarked citation of a folder `rewrites` covers into its
|
|
147
|
+
* destination under `.canon/review/evidence/`, absolute regardless of how the
|
|
148
|
+
* source citation was spelled, counting each as it goes. A file naming no
|
|
149
|
+
* such folder returns byte-identical with a count of zero, and a marked line
|
|
150
|
+
* is returned unchanged and uncounted.
|
|
151
|
+
*/
|
|
152
|
+
function applyRewrites(
|
|
153
|
+
text: string,
|
|
154
|
+
rewrites: readonly Rewrite[],
|
|
155
|
+
): RewriteOutcome {
|
|
156
|
+
const lines = text.split('\n')
|
|
157
|
+
let count = 0
|
|
158
|
+
|
|
159
|
+
const rewritten = lines.map((line, index) => {
|
|
160
|
+
if (isKept(lines, index)) return line
|
|
161
|
+
|
|
162
|
+
for (const { pattern } of rewrites) {
|
|
163
|
+
count += [...line.matchAll(pattern)].length
|
|
164
|
+
}
|
|
165
|
+
return rewriteLine(line, rewrites)
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
return { text: rewritten.join('\n'), count }
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** The files under every `BACKED_FOLDERS` entry, archives included. */
|
|
172
|
+
export async function walkScratchEvidenceCorpus(
|
|
173
|
+
root: string,
|
|
174
|
+
): Promise<string[]> {
|
|
175
|
+
const files: string[] = []
|
|
176
|
+
|
|
177
|
+
for (const folder of BACKED_FOLDERS) {
|
|
178
|
+
const dir = recordDir(root, folder)
|
|
179
|
+
if (!existsSync(dir)) continue
|
|
180
|
+
|
|
181
|
+
const glob = new Bun.Glob('**/*')
|
|
182
|
+
for await (const path of glob.scan({
|
|
183
|
+
cwd: dir,
|
|
184
|
+
onlyFiles: true,
|
|
185
|
+
dot: true,
|
|
186
|
+
})) {
|
|
187
|
+
files.push(join(dir, path))
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return files.sort()
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export interface ScratchEvidenceSource {
|
|
195
|
+
readonly path: string
|
|
196
|
+
readonly text: string
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Reads every file the walk found, skipping one that carries a NUL byte. */
|
|
200
|
+
export async function readScratchEvidenceCorpus(
|
|
201
|
+
paths: readonly string[],
|
|
202
|
+
): Promise<ScratchEvidenceSource[]> {
|
|
203
|
+
const sources: ScratchEvidenceSource[] = []
|
|
204
|
+
|
|
205
|
+
for (const path of paths) {
|
|
206
|
+
const bytes = await readFile(path).catch(() => undefined)
|
|
207
|
+
if (bytes === undefined || bytes.includes(0)) continue
|
|
208
|
+
|
|
209
|
+
sources.push({ path, text: bytes.toString('utf8') })
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return sources
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export interface FolderMove {
|
|
216
|
+
readonly folder: string
|
|
217
|
+
readonly from: string
|
|
218
|
+
readonly to: string
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export interface CitationEntry {
|
|
222
|
+
readonly path: string
|
|
223
|
+
readonly text: string
|
|
224
|
+
readonly rewritten: number
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export interface ScratchEvidencePlan {
|
|
228
|
+
readonly moves: readonly FolderMove[]
|
|
229
|
+
readonly collisions: readonly string[]
|
|
230
|
+
readonly entries: readonly CitationEntry[]
|
|
231
|
+
readonly rewritten: number
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Every promoted folder found on disk, with its destination, refusing a
|
|
236
|
+
* folder whose destination is already occupied rather than merging into it.
|
|
237
|
+
*/
|
|
238
|
+
export function planFolderMoves(root: string): {
|
|
239
|
+
moves: FolderMove[]
|
|
240
|
+
collisions: string[]
|
|
241
|
+
} {
|
|
242
|
+
const moves: FolderMove[] = []
|
|
243
|
+
const collisions: string[] = []
|
|
244
|
+
|
|
245
|
+
for (const folder of PROMOTED_FOLDERS) {
|
|
246
|
+
const from = sourcePath(root, folder)
|
|
247
|
+
if (!existsSync(from)) continue
|
|
248
|
+
|
|
249
|
+
const to = destinationPath(root, folder)
|
|
250
|
+
if (existsSync(to)) {
|
|
251
|
+
collisions.push(to)
|
|
252
|
+
continue
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
moves.push({ folder, from, to })
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return { moves, collisions }
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* What the promotion would do, without doing it. Pure over the sources it is
|
|
263
|
+
* handed, the way `planRecordTree` is, so a caller reports and applies from
|
|
264
|
+
* the same value. A file whose text does not change is dropped.
|
|
265
|
+
*/
|
|
266
|
+
export function planScratchEvidence(
|
|
267
|
+
root: string,
|
|
268
|
+
sources: readonly ScratchEvidenceSource[],
|
|
269
|
+
): ScratchEvidencePlan {
|
|
270
|
+
const { moves, collisions } = planFolderMoves(root)
|
|
271
|
+
const folders = PROMOTED_FOLDERS.filter(
|
|
272
|
+
(folder) => !collisions.includes(destinationPath(root, folder)),
|
|
273
|
+
)
|
|
274
|
+
const rewrites = buildRewrites(folders)
|
|
275
|
+
const entries: CitationEntry[] = []
|
|
276
|
+
|
|
277
|
+
for (const source of sources) {
|
|
278
|
+
const { text, count } = applyRewrites(source.text, rewrites)
|
|
279
|
+
if (count === 0) continue
|
|
280
|
+
|
|
281
|
+
entries.push({ path: source.path, text, rewritten: count })
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return {
|
|
285
|
+
moves,
|
|
286
|
+
collisions,
|
|
287
|
+
entries,
|
|
288
|
+
rewritten: entries.reduce((sum, entry) => sum + entry.rewritten, 0),
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export interface ScratchEvidenceResult {
|
|
293
|
+
readonly moved: number
|
|
294
|
+
readonly written: number
|
|
295
|
+
readonly failed: readonly string[]
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** Writes the plan: every folder move, then every citation rewrite. */
|
|
299
|
+
export async function applyScratchEvidence(
|
|
300
|
+
plan: ScratchEvidencePlan,
|
|
301
|
+
): Promise<ScratchEvidenceResult> {
|
|
302
|
+
let moved = 0
|
|
303
|
+
let written = 0
|
|
304
|
+
const failed: string[] = []
|
|
305
|
+
|
|
306
|
+
for (const move of plan.moves) {
|
|
307
|
+
await mkdir(dirname(move.to), { recursive: true })
|
|
308
|
+
const done = await rename(move.from, move.to)
|
|
309
|
+
.then(() => true)
|
|
310
|
+
.catch(() => false)
|
|
311
|
+
|
|
312
|
+
if (done) moved += 1
|
|
313
|
+
else failed.push(move.from)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
for (const entry of plan.entries) {
|
|
317
|
+
const done = await writeFile(entry.path, entry.text)
|
|
318
|
+
.then(() => true)
|
|
319
|
+
.catch(() => false)
|
|
320
|
+
|
|
321
|
+
if (done) written += 1
|
|
322
|
+
else failed.push(entry.path)
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return { moved, written, failed }
|
|
326
|
+
}
|
|
@@ -26,7 +26,7 @@ export default defineConfig([
|
|
|
26
26
|
...tseslint.configs.recommended,
|
|
27
27
|
...astro.configs.recommended,
|
|
28
28
|
{
|
|
29
|
-
files: ['**/*.{ts,tsx,js,jsx}'],
|
|
29
|
+
files: ['**/*.{ts,tsx,js,jsx,astro}'],
|
|
30
30
|
plugins: {
|
|
31
31
|
'simple-import-sort': simpleImportSort,
|
|
32
32
|
'check-file': checkFile,
|
|
@@ -21,7 +21,7 @@ The astro stack covers Astro + TypeScript projects: content sites, marketing sit
|
|
|
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
23
|
- `tsconfig.json`: extends `astro/tsconfigs/strict`, adds `skipLibCheck`, `vitest/globals` and `@testing-library/jest-dom` in types, `@/` paths.
|
|
24
|
-
- `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). `
|
|
24
|
+
- `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`'s `src/**/!(__tests__|pages)` pattern reaches no folder under a real ESLint run, `.astro`-only or otherwise. See `.claude/context/tooling.md` for the measurement.
|
|
25
25
|
|
|
26
26
|
## Typecheck
|
|
27
27
|
|