@erclx/canon 4.27.1 → 4.29.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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "canon",
3
3
  "description": "Automated governance, versioning, and discovery tools for Claude Code.",
4
- "version": "4.27.1",
4
+ "version": "4.29.0",
5
5
  "author": {
6
6
  "name": "Eric Le",
7
7
  "url": "https://github.com/erclx"
@@ -165,7 +165,7 @@ gh pr ready --undo <number>
165
165
  gh pr view <number> --json isDraft
166
166
  ```
167
167
 
168
- Name the number `git-ship`'s pull request step returned on both calls rather than leaving either to resolve by branch, since each matches a head ref and ignores state, so a reused branch name reaches a merged namesake. The read is where that costs the most, answering with the wrong record's flag, and the write above it errors rather than mutating one.
168
+ Name the number `git-ship`'s pull request step returned on both calls rather than leaving either to resolve by branch. `${CLAUDE_SKILL_DIR}/../git-pr/REQUIREMENT.md` states why: a lookup that resolves by branch alone can return a closed pull request sharing that head, so the number is resolved once and reused rather than re-derived.
169
169
 
170
170
  Report what the read returned rather than what the command printed, since the exit says the call ran and says nothing about the state. A `true` reports a draft. A `false` reports the pull request as opened ready and unsupervised, and the chain stops there. Never re-issue the undo on a disagreeing read, which fights whoever readied it instead of guarding anything.
171
171
 
@@ -119,7 +119,7 @@ Write no shape for a correction. A correction is a sentence, and a format for ad
119
119
  - Resolve the target at the moment of sending with `canon sessions list --branch`, never from a mapping written down earlier, since names rotate as sessions end and one recorded earlier in a session has failed inside the hour. The runbook read at step 5 routes on the count and the confidence it answers with
120
120
  - Open the message with the worktree and branch the sender believes the reader holds, asking to be corrected, whenever that mapping is inferred rather than confirmed
121
121
  - Name the skill for the reader to run rather than writing an invocation, which arrives as text
122
- - Read the pull request's own draft flag rather than the state a worker reports, since two reported a draft that read ready inside ninety seconds, and report what the read returned and when rather than the state alone. The flag settles the question only once the worker's chain has run its undo, and nothing marks that moment, so a read taken between the pull request opening and that call sees a genuinely ready pull request about to become a draft. That is how a poll on `erclx/canon#1307` told an operator the opposite of what the worker had said.
122
+ - Read the pull request's own draft flag rather than the state a worker reports, and report what the read returned and when rather than the state alone. `canon docs pr-reads` states why a reported field can lag and say nothing about it. The flag settles the question only once the worker's chain has run its undo, and nothing marks that moment, so a read taken between the pull request opening and that call sees a genuinely ready pull request about to become a draft. That is how a poll on `erclx/canon#1307` told an operator the opposite of what the worker had said.
123
123
 
124
124
  A session is reachable when it appears in a live listing, which reads what each session registered on disk rather than probing it, and a message carries plain text and no authority. When no live session holds the branch, report the invocation for the human, naming the branch, the pull request, and the skill to run, then stop. Retrying or waiting leaves the loop believing it is open while nothing acts on it. Every dispatch in the trial behind this step found a live session, so this branch stands on reasoning rather than on observation.
125
125
 
@@ -264,6 +264,10 @@ Hand the row to the human-launch line in step 4 instead of dispatching when any
264
264
 
265
265
  The first of those five is the one that reaches a person rather than the board. A row held for a collision or for a serialize reason waits on the wave clearing, where a row held on its plan waits on an answer only the operator can give, so hand that one over with the question label and its stated reason attached rather than as a name and a refusal.
266
266
 
267
+ Hand the person one command: `/canon:claude-autoship <plan>`, naming the row, the plan path, and the branch together, with no worktree call ahead of it. A leading worktree call adds nothing beyond what `claude-autoship` Step 0 already reaches for itself, by the same judgment the template above calls a residual risk rather than a settled contract. A second command also risks a client folding two commands into one message, which reads everything after the first command's name as its own argument and drops the second: that happened in four dispatches out of four before the fix became one message carrying the autoship call alone.
268
+
269
+ Suggest, as one line to the operator, that they rename their own session to the row's id, so a process listing shows what the session is for without a cross-reference to the board.
270
+
267
271
  ## Stop the loop
268
272
 
269
273
  Wrapped in `/loop`, re-run the check against `## Run now` on each wake. Stop rather than firing again once the group is empty or every row in it reads `claimed: true`. Report that once, on the wake that finds it, and let the loop end rather than continuing to poll a board nobody is clearing. `orchestrator-poll.md` already carries this reasoning for the review trigger, and it binds a dispatcher the same way.
@@ -137,7 +137,7 @@ printf 'number=%s\nurl=%s\n' "$pr_number" "$pr_url"
137
137
 
138
138
  ### Record the number on the task
139
139
 
140
- Write the `number` the final command printed onto the task the branch is closing. Do not resolve it again. A head branch that carried an earlier pull request now has two, and a second `gh pr view` would pick between them by a precedence rule nothing here states. Reading what created or edited the pull request needs no such rule.
140
+ Write the `number` the final command printed onto the task the branch is closing. Do not resolve it again. `${CLAUDE_SKILL_DIR}/REQUIREMENT.md` states why: a lookup that resolves by branch alone can return a closed pull request sharing that head, so the number is resolved once and reused rather than re-derived.
141
141
 
142
142
  The task is the one whose `Plan:` line names the plan this branch implemented. Name that plan by its file, which is `.canon/plans/feature-<slug>.md` at the main worktree root with `<slug>` derived per `${CLAUDE_SKILL_DIR}/../../standards/slug.md`. `claude-feature` writes the plan under the branch slug, so the two correspond on any branch that came through the plan-to-execute path. When the session already knows which plan it implemented, because a caller read it earlier in the chain, use that filename instead of re-deriving.
143
143
 
@@ -9,81 +9,81 @@ Full help: `canon <command> --help`. Behavior notes for the install and sync ver
9
9
 
10
10
  ## Project-level
11
11
 
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 docs [topic]` | Emit toolkit reference docs (`list`, or a topic by name) |
24
- | `canon design render` | Render `.claude/DESIGN.md` tokens to HTML and CSS |
25
- | `canon design regen` | Rewrite this repository's `.claude/DESIGN.md` and `src/design/base.css` from `src/design/tokens.ts` |
26
- | `canon design css` | Emit the design tokens and components as CSS on stdout (`--no-components` for properties alone) |
27
- | `canon design install` | Install the base stylesheet into a project at `.claude/design/base.css` |
28
- | `canon design sync` | Update a base stylesheet already installed under `.claude/design/` |
29
- | `canon slides render` | Render a `.claude/SLIDES.md` source into a PowerPoint deck, reporting any unrecognized layout name on stderr |
30
- | `canon slides list` | List the available slide layouts (`--json` for the catalog) |
31
- | `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 |
32
- | `canon transcripts <url>` | Fetch a YouTube transcript with metadata frontmatter (needs `yt-dlp`) |
33
- | `canon tasks archive` | Move a shipped task and its plan off the board, clear its ordering row, and regenerate the index |
34
- | `canon tasks pull-request` | Record a pull request number on the task a branch closes, by stem or `--plan` (`--json`) |
35
- | `canon tasks outcome` | Mark outcomes `[x]` on a task by position, repeating `--close` (`--json`) |
36
- | `canon tasks validate` | Report board rows whose shape, order, plan, task file, group, file set, or blocker does not hold (`--json`) |
37
- | `canon intake list` | Report intake folder counts, or one folder's items, keeping what is unread with `--unread` (`--json`) |
38
- | `canon intake answer` | Write selections into one cluster's answer slots, repeating `--set <item>=<answer>` (`--json`) |
39
- | `canon teach list` | Report learning workspaces and the ordinal a new one takes, or what one workspace holds (`--json`) |
40
- | `canon teach open` | Open a workspace at the next ordinal with its mission, resources, and glossary files (`--json`) |
41
- | `canon teach resource` | Record sources and leads in a workspace, repeating `--read` or `--lead` as `<title>=<url>` (`--json`) |
42
- | `canon teach glossary` | Add terms to a workspace glossary alphabetically, repeating `--term <term>=<definition>` (`--json`) |
43
- | `canon teach stylesheet` | Seed a workspace stylesheet from the design source, leaving an existing one alone without `--force` (`--json`) |
44
- | `canon records validate` | Report a session record or a standard against the standard governing it, per kind (`--json`) |
45
- | `canon records migrate` | Rewrite the records a validate finding names a recoverable transform for (`--write`, `--json`) |
46
- | `canon records size` | Report what each record folder holds and how much of it is recent, heaviest first (`--json`) |
47
- | `canon records push` | Commit the nine backed record folders and push them to a private records remote (`--json`) |
48
- | `canon records pull` | Fetch the records remote and write it back, refusing rather than discarding unpushed records (`--json`) |
49
- | `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`) |
50
- | `canon migrate records` | Move the gitignored session records to `.canon/` and repoint every tracked citation, reporting the plan without `--write` (`--root`, `--json`) |
51
- | `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`) |
52
- | `canon sessions list` | Resolve live sessions to the worktree and branch each holds, filtered by `--branch` (`--json`) |
53
- | `canon worktrees list` | Report which worktrees are reclaimable, keyed on the pull request having merged, with every refusal and the removal route named (`--json`) |
54
- | `canon worktrees reclaim` | Remove every reclaimable worktree and the branch behind it, reporting without acting under `--dry-run` (`--json`) |
55
- | `canon comments scan` | Measure comment density by language and comment kind, with a trend recomputed from git |
56
- | `canon context audit` | Report required sections, length, cited paths, reference form, catalog tables, provenance, superseded-decision narration, and index drift |
57
- | `canon markdown audit` | Fail any markdown path on a banned character, word, or spelling, and report the structural checkpoints |
58
- | `canon claude skills audit` | Report both skill corpora against the mechanical rules in `standards/skill.md` |
59
- | `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`) |
60
- | `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`) |
61
- | `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 |
62
- | `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>`) |
63
- | `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`) |
64
- | `canon gov test-order` | Report where an implementation reached history ahead of the test covering it (`--json`) |
65
- | `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`) |
66
- | `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`) |
67
- | `canon gov citations` | Resolve every path a rule cites and every internal frontmatter glob, failing on one reaching nothing (`--json`) |
68
- | `canon secrets scan` | Report credential-shaped values in the tree the package ships, keyed on issued values rather than on words (`--json`) |
69
- | `canon deps audit` | Report published advisories against the resolved dependency set, refusing rather than reporting clean when the index is unreachable (`--json`) |
70
- | `canon labels audit` | Report the labels a changed set earns from the pull request label map and the paths no row reaches (`--json`) |
71
- | `canon labels scan` | Fail a pull request whose title or body carries a phase label, a label a code span quotes, a gitignored record path, or a session link (`--event`, `--json`) |
72
- | `canon autoship classify` | Decide whether a changed set needs the review pass, naming the file and the test that decided it (`--json`) |
73
- | `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`) |
74
- | `canon pr head` | Compare the head a pull request object reports against the branch tip the remote carries, naming both shas (`--root`, `--json`) |
75
- | `canon pr checks` | Report the check runs belonging to the branch tip, reading pending for a tip carrying none rather than clean (`--root`, `--json`) |
76
- | `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`) |
77
- | `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`) |
78
- | `canon census [path]` | Report tracked file count, a breakdown by extension, and a line total that skips whatever reads as binary (`--json`) |
79
- | `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`) |
80
- | `canon audits list` | List every audit the set runs, with the corpus each reads and whether it gates (`--json`) |
81
- | `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`) |
82
- | `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`) |
83
- | `canon drive <url> <run>` | Walk a page through named interactions and measure each state it reaches, reporting findings rather than gating (`--json`) |
84
- | `canon capture [source]` | Render HTML sources to PNG and prove the font each one declares resolved (`--selector` required, `--out`) |
85
- | `canon serve [dir]` | Serve a directory on the loopback interface and print the link that opens it, running until interrupted (`--port`, `--entry`, `--json`) |
86
- | `canon upgrade` | Reinstall the CLI globally with the package manager the install path names (`--json`) |
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 docs [topic]` | Emit toolkit reference docs (`list`, or a topic by name) |
24
+ | `canon design render` | Render `.claude/DESIGN.md` tokens to HTML and CSS |
25
+ | `canon design regen` | Rewrite this repository's `.claude/DESIGN.md` and `src/design/base.css` from `src/design/tokens.ts` |
26
+ | `canon design css` | Emit the design tokens and components as CSS on stdout (`--no-components` for properties alone) |
27
+ | `canon design install` | Install the base stylesheet into a project at `.claude/design/base.css` |
28
+ | `canon design sync` | Update a base stylesheet already installed under `.claude/design/` |
29
+ | `canon slides render` | Render a `.claude/SLIDES.md` source into a PowerPoint deck, reporting any unrecognized layout name on stderr |
30
+ | `canon slides list` | List the available slide layouts (`--json` for the catalog) |
31
+ | `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 |
32
+ | `canon transcripts <url>` | Fetch a YouTube transcript with metadata frontmatter (needs `yt-dlp`) |
33
+ | `canon tasks archive` | Move a shipped task and its plan off the board, clear its ordering row, and regenerate the index |
34
+ | `canon tasks pull-request` | Record a pull request number on the task a branch closes, by stem or `--plan` (`--json`) |
35
+ | `canon tasks outcome` | Mark outcomes `[x]` on a task by position, repeating `--close` (`--json`) |
36
+ | `canon tasks validate` | Report board rows whose shape, order, plan, task file, group, file set, or blocker does not hold (`--json`) |
37
+ | `canon intake list` | Report intake folder counts, or one folder's items, keeping what is unread with `--unread` (`--json`) |
38
+ | `canon intake answer` | Write selections into one cluster's answer slots, repeating `--set <item>=<answer>` (`--json`) |
39
+ | `canon teach list` | Report learning workspaces and the ordinal a new one takes, or what one workspace holds (`--json`) |
40
+ | `canon teach open` | Open a workspace at the next ordinal with its mission, resources, and glossary files (`--json`) |
41
+ | `canon teach resource` | Record sources and leads in a workspace, repeating `--read` or `--lead` as `<title>=<url>` (`--json`) |
42
+ | `canon teach glossary` | Add terms to a workspace glossary alphabetically, repeating `--term <term>=<definition>` (`--json`) |
43
+ | `canon teach stylesheet` | Seed a workspace stylesheet from the design source, leaving an existing one alone without `--force` (`--json`) |
44
+ | `canon records validate` | Report a session record or a standard against the standard governing it, per kind (`--json`) |
45
+ | `canon records migrate` | Rewrite the records a validate finding names a recoverable transform for (`--write`, `--json`) |
46
+ | `canon records size` | Report what each record folder holds and how much of it is recent, heaviest first (`--json`) |
47
+ | `canon records push` | Commit the nine backed record folders and push them to a private records remote (`--json`) |
48
+ | `canon records pull` | Fetch the records remote and write it back, refusing rather than discarding unpushed records (`--json`) |
49
+ | `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`) |
50
+ | `canon migrate records` | Move the gitignored session records to `.canon/` and repoint every tracked citation, reporting the plan without `--write` (`--root`, `--json`) |
51
+ | `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`) |
52
+ | `canon sessions list` | Resolve live sessions to the worktree and branch each holds, filtered by `--branch` (`--json`) |
53
+ | `canon worktrees list` | Report which worktrees are reclaimable, keyed on the pull request having merged, with every refusal and the removal route named (`--json`) |
54
+ | `canon worktrees reclaim` | Remove every reclaimable worktree and the branch behind it, reporting without acting under `--dry-run` (`--json`) |
55
+ | `canon comments scan` | Measure comment density by language and comment kind, with a trend recomputed from git |
56
+ | `canon context audit` | Report required sections, length, cited paths, reference form, catalog tables, provenance, superseded-decision narration, and index drift |
57
+ | `canon markdown audit` | Fail any markdown path on a banned character, word, or spelling, and report the structural checkpoints |
58
+ | `canon claude skills audit` | Report both skill corpora against the mechanical rules in `standards/skill.md` |
59
+ | `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`) |
60
+ | `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`) |
61
+ | `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 |
62
+ | `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>`) |
63
+ | `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`) |
64
+ | `canon gov test-order` | Report where an implementation reached history ahead of the test covering it (`--json`) |
65
+ | `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`) |
66
+ | `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`) |
67
+ | `canon gov citations` | Resolve every path a rule cites and every internal frontmatter glob, failing on one reaching nothing (`--json`) |
68
+ | `canon secrets scan` | Report credential-shaped values in the tree the package ships, keyed on issued values rather than on words (`--json`) |
69
+ | `canon deps audit` | Report published advisories against the resolved dependency set, refusing rather than reporting clean when the index is unreachable (`--json`) |
70
+ | `canon labels audit` | Report the labels a changed set earns from the pull request label map and the paths no row reaches (`--json`) |
71
+ | `canon labels scan` | Fail a pull request whose title or body carries a phase label, a label a code span quotes, a gitignored record path, a session link, or a title word no dictionary holds (`--event`, `--json`) |
72
+ | `canon autoship classify` | Decide whether a changed set needs the review pass, naming the file and the test that decided it (`--json`) |
73
+ | `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`) |
74
+ | `canon pr head` | Compare the head a pull request object reports against the branch tip the remote carries, naming both shas (`--root`, `--json`) |
75
+ | `canon pr checks` | Report the check runs belonging to the branch tip, reading pending for a tip carrying none rather than clean (`--root`, `--json`) |
76
+ | `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`) |
77
+ | `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`) |
78
+ | `canon census [path]` | Report tracked file count, a breakdown by extension, and a line total that skips whatever reads as binary (`--json`) |
79
+ | `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`) |
80
+ | `canon audits list` | List every audit the set runs, with the corpus each reads and whether it gates (`--json`) |
81
+ | `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`) |
82
+ | `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`) |
83
+ | `canon drive <url> <run>` | Walk a page through named interactions and measure each state it reaches, reporting findings rather than gating (`--json`) |
84
+ | `canon capture [source]` | Render HTML sources to PNG and prove the font each one declares resolved (`--selector` required, `--out`) |
85
+ | `canon serve [dir]` | Serve a directory on the loopback interface and print the link that opens it, running until interrupted (`--port`, `--entry`, `--json`) |
86
+ | `canon upgrade` | Reinstall the CLI globally with the package manager the install path names (`--json`) |
87
87
 
88
88
  `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 stylesheet and 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. 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.
89
89
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@erclx/canon",
3
3
  "type": "module",
4
- "version": "4.27.1",
4
+ "version": "4.29.0",
5
5
  "description": "Infrastructure and quality tooling for developer workflows",
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -4,6 +4,7 @@ import type { Command } from 'commander'
4
4
  import { type LabelAuditRefusal, auditLabels } from '@/labels/audit'
5
5
  import { MAP_REL } from '@/labels/map'
6
6
  import { scanPhaseLabels } from '@/labels/phase'
7
+ import { scanTitleSpelling } from '@/labels/spelling'
7
8
  import { intro, logInfo, logStep, logWarn, outro, plural } from '@/ui'
8
9
 
9
10
  interface AuditOptions {
@@ -90,7 +91,7 @@ export function register(program: Command): void {
90
91
  labels
91
92
  .command('scan')
92
93
  .description(
93
- 'Fail a pull request whose title or body carries a phase label, a board identifier, or a session link',
94
+ 'Fail a pull request whose title or body carries a phase label, a board identifier, a session link, or a title with an unspelled word',
94
95
  )
95
96
  .helpOption('-h, --help', 'Show this help message')
96
97
  .option(
@@ -124,10 +125,18 @@ export function register(program: Command): void {
124
125
  'session and for no other reader, which no clone repairs, so it is read',
125
126
  'on a release pull request too, where the board identifier is not.',
126
127
  '',
128
+ "It also spell-checks the title alone against this repository's own",
129
+ 'cspell config, since release-please copies the title into',
130
+ 'CHANGELOG.md and nothing else spell-checks it first. The check shells',
131
+ "this repository's own resolved cspell binary and reports nothing when",
132
+ 'a target project carries none, rather than reaching the network or',
133
+ 'forcing a new dependency.',
134
+ '',
127
135
  'Exit codes:',
128
- ' 0 no phase label, no board identifier, and no session link found',
136
+ ' 0 none of the four found',
129
137
  ' 1 refused, with the reason on stderr or in the JSON record',
130
- ' 2 the title or body carries one of the three',
138
+ ' 2 the title or body carries a phase label, a board identifier, a',
139
+ ' session link, or a title word no dictionary holds',
131
140
  '',
132
141
  'Examples:',
133
142
  ' canon labels scan --event "$GITHUB_EVENT_PATH"',
@@ -339,6 +348,7 @@ async function runScan(opts: ScanOptions): Promise<number> {
339
348
  }
340
349
 
341
350
  const result = scanPhaseLabels(resolved)
351
+ const spelling = await scanTitleSpelling(resolved.title, process.cwd())
342
352
 
343
353
  logStep('Pull request')
344
354
  logInfo(
@@ -379,6 +389,34 @@ async function runScan(opts: ScanOptions): Promise<number> {
379
389
  for (const link of result.sessionLinks) logWarn(link)
380
390
  }
381
391
 
392
+ const spellingChecked = spelling.kind === 'checked'
393
+ const unspelledWords =
394
+ spelling.kind === 'checked' ? spelling.unknownWords : []
395
+
396
+ logStep(
397
+ spelling.kind === 'unavailable'
398
+ ? 'Spelling unavailable'
399
+ : unspelledWords.length === 0
400
+ ? 'Clean'
401
+ : 'Unspelled word found',
402
+ )
403
+ if (spelling.kind === 'unavailable' && spelling.reason === 'no-binary') {
404
+ logInfo(
405
+ `no node_modules/.bin/cspell resolved walking up from ${spelling.probedFrom}, so the title was not checked`,
406
+ )
407
+ } else if (spelling.kind === 'unavailable') {
408
+ logInfo(
409
+ `cspell did not exit clean or with issues found (${spelling.message}), so the title was not checked`,
410
+ )
411
+ } else if (unspelledWords.length === 0) {
412
+ logInfo('no word in the title is absent from every dictionary')
413
+ } else {
414
+ logWarn(
415
+ `${plural(unspelledWords.length, 'word')} in the title absent from every dictionary. Fix the spelling, or add jargon and project-specific terms to .cspell/project-terms.txt and dependency vocabulary to .cspell/tech-stack.txt.`,
416
+ )
417
+ for (const word of unspelledWords) logWarn(word)
418
+ }
419
+
382
420
  outro()
383
421
 
384
422
  if (emitJson) {
@@ -389,13 +427,16 @@ async function runScan(opts: ScanOptions): Promise<number> {
389
427
  semverTags: result.semverTags,
390
428
  boardReferences: result.boardReferences,
391
429
  sessionLinks: result.sessionLinks,
430
+ unspelledWords,
431
+ spellingChecked,
392
432
  })}\n`,
393
433
  )
394
434
  }
395
435
 
396
436
  return result.phaseLabels.length === 0 &&
397
437
  result.boardReferences.length === 0 &&
398
- result.sessionLinks.length === 0
438
+ result.sessionLinks.length === 0 &&
439
+ unspelledWords.length === 0
399
440
  ? 0
400
441
  : 2
401
442
  }
@@ -0,0 +1,120 @@
1
+ import { existsSync } from 'node:fs'
2
+ import { dirname, join, resolve } from 'node:path'
3
+ import { execa } from 'execa'
4
+
5
+ /**
6
+ * Why no unknown-word list was produced, which is never the same as a clean
7
+ * one.
8
+ *
9
+ * `no-binary` is the binary being absent anywhere from the root upward.
10
+ * `cspell` is a devDependency of this repository alone, per
11
+ * `075-dependencies.md`'s ban on importing a transitive-only package, so a
12
+ * target project that never adopted it gets no coverage from this check
13
+ * rather than a network fetch or a forced new dependency.
14
+ *
15
+ * `check-failed` is the binary running and returning neither of the two
16
+ * codes it defines, clean or issues found. `reject: false` is right, because
17
+ * a findings exit is the ordinary outcome for half of this check's runs, but
18
+ * discarding the code outright folded a crash, a bad config, or an
19
+ * unreadable dictionary into the same empty-stdout shape a clean title
20
+ * produces. Reading it the way `spawnAudit` in `src/audits/run.ts` reads its
21
+ * own child processes is what tells the two apart.
22
+ */
23
+ export type SpellingRefusal = 'no-binary' | 'check-failed'
24
+
25
+ export type SpellingScan =
26
+ | { readonly kind: 'checked'; readonly unknownWords: readonly string[] }
27
+ | {
28
+ readonly kind: 'unavailable'
29
+ readonly reason: 'no-binary'
30
+ readonly probedFrom: string
31
+ }
32
+ | {
33
+ readonly kind: 'unavailable'
34
+ readonly reason: 'check-failed'
35
+ readonly message: string
36
+ }
37
+
38
+ const CSPELL_EXIT_CLEAN = 0
39
+ const CSPELL_EXIT_ISSUES_FOUND = 1
40
+
41
+ /**
42
+ * Reads `cspell stdin --words-only --unique`'s stdout into a clean list.
43
+ *
44
+ * `--unique` already dedupes on cspell's side, so this exists for the same
45
+ * reason `parseAdvisories` exists beside `auditDependencies`: a pure function
46
+ * over a fixture string is a unit test that needs no binary on the machine
47
+ * running it, where the trailing blank line an empty scan's stdout carries
48
+ * is what actually needs the trim-and-filter below.
49
+ */
50
+ export function parseUnknownWords(stdout: string): string[] {
51
+ return stdout
52
+ .split(/\r?\n/)
53
+ .map((word) => word.trim())
54
+ .filter((word) => word.length > 0)
55
+ }
56
+
57
+ /**
58
+ * Walks from `root` up to the filesystem root looking for a resolved
59
+ * `cspell` binary, stopping at the first `node_modules/.bin/cspell` found.
60
+ *
61
+ * A caller running `canon labels scan` from a subdirectory of the checkout,
62
+ * rather than from its own root, is what a single `join(root, …)` check
63
+ * missed: `node_modules` sits at the checkout root, so the check found
64
+ * nothing, the title went unchecked, and the command exited 0 with no path
65
+ * naming what was probed or where it stopped looking.
66
+ */
67
+ function resolveCspellBinary(root: string): string | undefined {
68
+ let dir = resolve(root)
69
+
70
+ while (true) {
71
+ const candidate = join(dir, 'node_modules', '.bin', 'cspell')
72
+ if (existsSync(candidate)) return candidate
73
+
74
+ const parent = dirname(dir)
75
+ if (parent === dir) return undefined
76
+ dir = parent
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Shells this repository's own resolved `cspell` binary against a title over
82
+ * stdin, rather than a bare `execa('cspell', …)`.
83
+ *
84
+ * `bun src/cli.ts labels scan` is the exact invocation `phase-label-gate.yml`
85
+ * uses, and it runs outside `bun run`, so `node_modules/.bin` is not on
86
+ * `PATH` and a bare spawn throws `ENOENT`. Resolving the binary path directly
87
+ * under `root`, or an ancestor of it, is what a caller running from this
88
+ * checkout needs, and it is also what keeps the check from reaching a
89
+ * `bunx` fallback that could fetch `cspell` from the network on a machine
90
+ * that never asked for it.
91
+ */
92
+ export async function scanTitleSpelling(
93
+ title: string,
94
+ root: string,
95
+ ): Promise<SpellingScan> {
96
+ const binary = resolveCspellBinary(root)
97
+
98
+ if (binary === undefined) {
99
+ return { kind: 'unavailable', reason: 'no-binary', probedFrom: root }
100
+ }
101
+
102
+ const result = await execa(
103
+ binary,
104
+ ['stdin', '--words-only', '--unique', '--no-progress', '--no-summary'],
105
+ { cwd: root, input: title, reject: false },
106
+ )
107
+
108
+ if (
109
+ result.exitCode !== CSPELL_EXIT_CLEAN &&
110
+ result.exitCode !== CSPELL_EXIT_ISSUES_FOUND
111
+ ) {
112
+ return {
113
+ kind: 'unavailable',
114
+ reason: 'check-failed',
115
+ message: result.stderr.trim().split('\n').pop() || 'no output on stderr',
116
+ }
117
+ }
118
+
119
+ return { kind: 'checked', unknownWords: parseUnknownWords(result.stdout) }
120
+ }
@@ -38,17 +38,19 @@ External release identity used in git tags and release notes. Independent of pha
38
38
 
39
39
  ## Where each appears
40
40
 
41
- | Surface | Phase labels | Semver tags |
42
- | ------------------------- | ------------ | ----------------------------------- |
43
- | `.canon/tasks/` | yes | no |
44
- | Chat with the operator | yes | no |
45
- | PR titles | no | only when the PR cuts a release |
46
- | PR bodies | no | only when the PR cuts a release |
47
- | Review comments | no | only when referencing a release |
48
- | Issue titles and bodies | no | only when referencing a release |
49
- | Commit messages | no | only when the commit cuts a release |
50
- | Git tags | no | yes |
51
- | README and `CHANGELOG.md` | no | yes |
41
+ | Surface | Phase labels | Semver tags |
42
+ | ------------------------------------------------------------------- | ------------ | ----------------------------------- |
43
+ | `.canon/tasks/` | yes | no |
44
+ | Chat with the operator | yes | no |
45
+ | Tracked prose (context entries) | conditional | no |
46
+ | Shipped prose (`standards/`, `claude/skills/`, `governance/rules/`) | no | no |
47
+ | PR titles | no | only when the PR cuts a release |
48
+ | PR bodies | no | only when the PR cuts a release |
49
+ | Review comments | no | only when referencing a release |
50
+ | Issue titles and bodies | no | only when referencing a release |
51
+ | Commit messages | no | only when the commit cuts a release |
52
+ | Git tags | no | yes |
53
+ | README and `CHANGELOG.md` | no | yes |
52
54
 
53
55
  ## Rules
54
56
 
@@ -57,6 +59,9 @@ External release identity used in git tags and release notes. Independent of pha
57
59
  - Git tags use semver only. Phase labels never become tags.
58
60
  - A PR that cuts a release may reference its semver tag in the title or body. Phase labels still do not appear.
59
61
  - PR bodies, review comments, and issue text name the change itself, never the internal stream that scheduled it. Describe the work rather than the label it was planned under.
62
+ - A phase label in a context entry is permitted only when its substance is restated inline beside it. A bare label carrying nothing beside it is forbidden, since a reader without the board has nothing to resolve it against.
63
+ - A phase label carries no exception for a surface this repository authors and ships to a target it does not control: a standard, a skill body, and a governance rule are all forbidden outright, substance restated or not. The line is ownership rather than reachability. A shipped file is read by a project that never edits it, so the label names a board that reader can never reach. A context entry stays permitted above because it is this project's own tracked surface, never shipped.
64
+ - The shipped-surface rule governs a reference to a real row, never a token shown to illustrate the label's own format. A phase label naming no decision, written only to show the pattern's shape, is not a citation, which is what lets a standard defining the format still show what one looks like.
60
65
 
61
66
  ## Pre-publish check
62
67