@erclx/canon 4.28.0 → 4.29.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/claude-orchestrate/references/orchestrator-dispatch.md +4 -2
- package/claude/skills/claude-planner/REQUIREMENT.md +2 -2
- package/claude/skills/claude-planner/SKILL.md +2 -2
- package/docs/agents/commands.md +75 -75
- package/docs/agents/key-changes.md +11 -11
- package/docs/workflow/operating-model.md +1 -1
- package/package.json +1 -1
- package/src/commands/labels.ts +45 -4
- package/src/labels/spelling.ts +120 -0
- package/src/pr/paths.ts +88 -8
- package/standards/versioning.md +16 -11
|
@@ -249,8 +249,10 @@ over a plan nobody has written yet.
|
|
|
249
249
|
|
|
250
250
|
What a planning dispatch owes instead is the reverse reading, because the plan
|
|
251
251
|
it produces carries a constraint per track in flight and a row planned during a
|
|
252
|
-
wave is planned against a tree that wave is changing. `claude-planner`
|
|
253
|
-
|
|
252
|
+
wave is planned against a tree that wave is changing. `claude-planner` composes
|
|
253
|
+
the session roster with the pull request list for that read rather than reading
|
|
254
|
+
pull requests alone, which is why the brief carries no branch list for it: the
|
|
255
|
+
planner takes this reading itself either way.
|
|
254
256
|
|
|
255
257
|
One row per dispatch. A session reused across a batch pays the context load once
|
|
256
258
|
and ages its picture of the tree while it works, which is what puts the in-flight
|
|
@@ -50,7 +50,7 @@ four carried at least one, caught only because the planner ran
|
|
|
50
50
|
## Must
|
|
51
51
|
|
|
52
52
|
- Assert what a planning session is, what it may not write, and how long the role lasts, since `claude-feature` carries the procedure and no body carries the role
|
|
53
|
-
- State the in-flight read as a command
|
|
53
|
+
- State the in-flight read as a command composing the session roster with open pull requests, and say that a branch and a worktree are not evidence, since a count of either reported merged work as live
|
|
54
54
|
- Run that read once per task rather than once per batch, since a reused session ages its picture of the tree while it works
|
|
55
55
|
- Name every read a plan needed and a launch string did not carry, the task file's findings and the source files among them, since a count quoted from a row was wrong or stale in ten places across four plans
|
|
56
56
|
- Name the validate and audit calls the written plan passes through, since nothing else opens a plan and the audit caught ban hits in three of four trial plans
|
|
@@ -73,7 +73,7 @@ four carried at least one, caught only because the planner ran
|
|
|
73
73
|
## Guards
|
|
74
74
|
|
|
75
75
|
- Task file does not resolve from where the session stands: report it unreadable and name the main-root path, rather than reporting the row as absent
|
|
76
|
-
- A read of what is in flight returns nothing: report that no pull request is open rather than falling back to a branch or worktree count
|
|
76
|
+
- A read of what is in flight returns nothing: report that no pull request is open and no live session holds another branch, rather than falling back to a raw branch or worktree count
|
|
77
77
|
- Launch named no controlling session and an operator is present: ask which row to address rather than inferring
|
|
78
78
|
- Launch named no controlling session and nobody is present: infer from the sessions holding no feature branch, never from a name prefix, and say the addressee was inferred
|
|
79
79
|
|
|
@@ -39,8 +39,8 @@ Each item below is something a plan needed and a launch string did not carry.
|
|
|
39
39
|
|
|
40
40
|
## Read what is in flight rather than inferring it
|
|
41
41
|
|
|
42
|
-
- Run `gh pr list --json number,headRefName`, then `git diff --name-only main...<
|
|
43
|
-
- A branch
|
|
42
|
+
- Run `canon sessions list --json`, take the `branch` field of every session whose `repository` matches this one and whose branch is neither null nor the trunk branch, union that with every `headRefName` from `gh pr list --json number,headRefName`, dedupe by branch name, then diff each with `git diff --name-only main...<branch>` for its file set. That resolves locally whether or not a pull request exists, since every track in this repository shares one git directory across its worktrees.
|
|
43
|
+
- A bare branch or worktree with neither a live session nor a pull request behind it is still not evidence, since this repository squash-merges and leaves both behind. The roster read is already filtered to live sessions, so what changed is the second source composed with it rather than a raw count. It still misses a live session that reached its worktree through the direct-path fallback rather than `EnterWorktree`, since that session's registered branch never moves off the trunk.
|
|
44
44
|
- Run the read once per task rather than once per batch. A session planning several rows ages its picture of the tree while it works, and this read is what dates it.
|
|
45
45
|
- Name each in-flight set as a constraint, say which act it forbids, and stamp the block with the commit the tree was read at, per Constraints in the plan standard. A bare path list leaves the worker guessing.
|
|
46
46
|
|
package/docs/agents/commands.md
CHANGED
|
@@ -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,
|
|
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
|
|
|
@@ -53,17 +53,17 @@ The cut used to decide whether a span was read at all, and the paths past the co
|
|
|
53
53
|
|
|
54
54
|
A span anywhere in the bullet has to survive all of these:
|
|
55
55
|
|
|
56
|
-
| Rule
|
|
57
|
-
|
|
|
58
|
-
| No whitespace, `<`, `>`, `$`, `*`, `\|`, `?`, `^`
|
|
59
|
-
| No `://` and no leading `/`, `~`, `@`, `#`, `!`
|
|
60
|
-
| Contains `/`
|
|
61
|
-
| Not a dotted-decimal segment, or ends with `/`
|
|
62
|
-
| Not a single top-level folder
|
|
63
|
-
| A `file:line` span leads its bullet
|
|
64
|
-
| The region carries no no-change marker
|
|
65
|
-
|
|
66
|
-
The last three rows earn their place from the corpus rather than from a rule. Every body that spelled a lone top-level folder was naming where something lives, and a line citation that follows another claim in the same bullet points into the file being described
|
|
56
|
+
| Rule | What it keeps out |
|
|
57
|
+
| -------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
|
|
58
|
+
| No whitespace, `<`, `>`, `$`, `*`, `\|`, `?`, `^` | A backticked command, a placeholder, a glob, a pattern |
|
|
59
|
+
| No `://` and no leading `/`, `~`, `@`, `#`, `!` | A URL, an absolute path, a module alias |
|
|
60
|
+
| Contains `/` | A bare filename with no folder around it |
|
|
61
|
+
| Not a dotted-decimal segment, or ends with `/` | A dotted number such as an address |
|
|
62
|
+
| Not a single top-level folder | `src/`, which nobody claims to have rewritten whole |
|
|
63
|
+
| A `file:line` span leads its bullet, or the bullet carries no bare line-only companion | A citation into a file the bullet already named, such as `verify.sh:634` beside `:642` |
|
|
64
|
+
| The region carries no no-change marker | A bullet recording a file it deliberately left alone |
|
|
65
|
+
|
|
66
|
+
The last three rows earn their place from the corpus rather than from a rule. Every body that spelled a lone top-level folder was naming where something lives, and a line citation that follows another claim in the same bullet points into the file being described only when a bare line-only span sits beside it, such as `:642` following `verify.sh:634`. With no such companion the citation names a second, genuinely distinct file and stays a claim. A line citation leading its bullet stays a claim either way, which is how a body names the exact line it rewrote.
|
|
67
67
|
|
|
68
68
|
The no-change marker is the one rule the region cut cannot substitute for. A body writes "Leave `x` untouched, since the decision keeps it" to record a change it declined, and the path sits ahead of the first comma, so a stricter cut would not reach it and a looser one would find more. Since `keep` and `leave` each open a real claim often enough, the marker decides it rather than the leading verb. Three words carry the set: `untouched`, `unchanged`, and `as written`. `in place` was measured and dropped, because rewriting a file in place is an ordinary claim, and `no other line` was dropped because a correct bullet writes "as one insertion that touches no other line" about a change it did make. `alone` shipped in the set too, until review found every corpus occurrence sitting past the first comma, where the cut already excludes it, so the word voided no true claim there. Restrictive use is the more common one in this repository's own prose, and a comma-free bullet exposed it: "Move the threshold read into `src/gate/stages.ts` alone" asserts an edit, and the marker voided it while the word was still in the set. Over the 40-pull-request corpus the rule still voids no true claim.
|
|
69
69
|
|
|
@@ -77,7 +77,7 @@ start, and only a count of the defect's extent decides how big it is.
|
|
|
77
77
|
|
|
78
78
|
One feature travels this path end to end.
|
|
79
79
|
|
|
80
|
-
1. The next feature is planned with `claude-feature`, writing a plan to `.canon/plans/`. The orchestrator runs it warm when the row turns on a contract other features consume or a shared wiring seam, and dispatches a planner under `claude-planner` otherwise. A cold planner measures the row against the tree rather than trusting what the row claims, and it reads what is in flight
|
|
80
|
+
1. The next feature is planned with `claude-feature`, writing a plan to `.canon/plans/`. The orchestrator runs it warm when the row turns on a contract other features consume or a shared wiring seam, and dispatches a planner under `claude-planner` otherwise. A cold planner measures the row against the tree rather than trusting what the row claims, and it reads what is in flight by composing the live session roster with open pull requests. A bare branch or worktree is not evidence on its own, since this repository leaves both behind after a squash merge.
|
|
81
81
|
2. Orchestrator checks the plan waits on nobody, checks the branch is unclaimed, and checks the plan's file set is disjoint from every track in flight, then dispatches a background worker with `claude --bg` against the plan, naming the branch and the model on the launch rather than leaving the worker to derive either. No count caps how many run at once. The branch travels as the argument to the worker's own worktree call, which is the one place the name is read rather than inferred. It falls back to naming the invocation for a human to run through `claude-worktree` and `claude-autoship` when the plan still waits on an answer only the operator can give, the check refuses, the sets overlap, or a stated reason serializes the plan behind a track already in flight. Either way, the worker enters its own worktree, builds, self-checks, opens a PR, and stops at the PR boundary.
|
|
82
82
|
3. Orchestrator reviews the PR with `claude-pr-review` and posts findings to it.
|
|
83
83
|
4. Orchestrator tells the session holding that branch to run `claude-address-review` once the pass posted a finding at any severity, resolving the target then with `canon sessions list --branch` and reporting the invocation for the human when no live session holds it. The worker addresses the findings, rebases onto `origin/main` when a sibling landed first and left the branch unable to merge, then pushes a follow-up. A pass carrying only minor findings dispatches too, since the grade runs low often enough that a floor at should-fix loses fixes a worker would have made. `claude-pr-review` states that threshold and the heading follows it, so an open heading is itself the signal to send.
|
package/package.json
CHANGED
package/src/commands/labels.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
+
}
|
package/src/pr/paths.ts
CHANGED
|
@@ -54,9 +54,6 @@ export type KeyChangeRead =
|
|
|
54
54
|
/** The longest bullet a claim carries forward, matching the citation sweep. */
|
|
55
55
|
const PREVIEW_LIMIT = 200
|
|
56
56
|
|
|
57
|
-
/** A backticked span, the only carrier this corpus writes a path in. */
|
|
58
|
-
const BACKTICKED = /`([^`\n]+)`/g
|
|
59
|
-
|
|
60
57
|
/** A list item at any indent, in either bullet spelling or as an ordinal. */
|
|
61
58
|
const BULLET = /^\s*(?:[-*+]|\d+\.)\s+(.*)$/
|
|
62
59
|
|
|
@@ -70,6 +67,15 @@ const HEADING = /^(#{1,6})\s+(.+?)\s*$/
|
|
|
70
67
|
*/
|
|
71
68
|
const LINE_SUFFIX = /:\d+(?:-\d+)?$/
|
|
72
69
|
|
|
70
|
+
/**
|
|
71
|
+
* A span that carries a line suffix and nothing else, such as `:642` beside
|
|
72
|
+
* an earlier `verify.sh:634`. Its presence is what tells the citation guard
|
|
73
|
+
* below the bullet is describing lines inside one already-claimed file rather
|
|
74
|
+
* than naming a second file, since a genuinely distinct file never gets cited
|
|
75
|
+
* by a bare line number with nothing in front of the colon.
|
|
76
|
+
*/
|
|
77
|
+
const BARE_LINE = /^:\d+(?:-\d+)?$/
|
|
78
|
+
|
|
73
79
|
/**
|
|
74
80
|
* A character that puts the span outside a path this comparison resolves.
|
|
75
81
|
*
|
|
@@ -108,9 +114,73 @@ function isDottedNumber(span: string): boolean {
|
|
|
108
114
|
return /^\d+(?:\.\d+)+$/.test(segment)
|
|
109
115
|
}
|
|
110
116
|
|
|
117
|
+
/** One code span, paired by matching backtick-run length rather than count. */
|
|
118
|
+
interface BacktickSpan {
|
|
119
|
+
/** Index of the opening run's first backtick. */
|
|
120
|
+
readonly start: number
|
|
121
|
+
/** Index one past the closing run's last backtick. */
|
|
122
|
+
readonly end: number
|
|
123
|
+
/** The text between the two runs, delimiters excluded. */
|
|
124
|
+
readonly content: string
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Every code span in a bullet, pairing a run of backticks only with the next
|
|
129
|
+
* run of the same length.
|
|
130
|
+
*
|
|
131
|
+
* A single-backtick regex reads a doubled delimiter, such as
|
|
132
|
+
* ``` ``git status`` ```, as two unrelated single backticks: it opens on the
|
|
133
|
+
* second backtick of the pair, closes on the first backtick of the closing
|
|
134
|
+
* pair, and leaves one backtick before and after the span unconsumed. Every
|
|
135
|
+
* scan after that treats a stray leftover backtick as an opener, which
|
|
136
|
+
* absorbs the next real span's opening delimiter as its closer and drops the
|
|
137
|
+
* path inside past it. Pairing by run length rather than by single backtick
|
|
138
|
+
* keeps a doubled delimiter closed by a doubled delimiter, so nothing after
|
|
139
|
+
* it loses its pairing.
|
|
140
|
+
*
|
|
141
|
+
* An opening run with no same-length run after it is not a delimiter, per
|
|
142
|
+
* CommonMark, so it is left as literal text and the scan resumes at the next
|
|
143
|
+
* run rather than backtracking into the unmatched one.
|
|
144
|
+
*/
|
|
145
|
+
function findBacktickSpans(text: string): BacktickSpan[] {
|
|
146
|
+
const runs = [...text.matchAll(/`+/g)].map((match) => ({
|
|
147
|
+
start: match.index ?? 0,
|
|
148
|
+
length: match[0].length,
|
|
149
|
+
}))
|
|
150
|
+
|
|
151
|
+
const spans: BacktickSpan[] = []
|
|
152
|
+
let i = 0
|
|
153
|
+
while (i < runs.length) {
|
|
154
|
+
const open = runs[i]
|
|
155
|
+
const closeIndex = runs.findIndex(
|
|
156
|
+
(run, index) => index > i && run.length === open.length,
|
|
157
|
+
)
|
|
158
|
+
if (closeIndex === -1) {
|
|
159
|
+
i += 1
|
|
160
|
+
continue
|
|
161
|
+
}
|
|
162
|
+
const close = runs[closeIndex]
|
|
163
|
+
spans.push({
|
|
164
|
+
start: open.start,
|
|
165
|
+
end: close.start + close.length,
|
|
166
|
+
content: text.slice(open.start + open.length, close.start),
|
|
167
|
+
})
|
|
168
|
+
i = closeIndex + 1
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return spans
|
|
172
|
+
}
|
|
173
|
+
|
|
111
174
|
/** Blanks every backticked span so a cue search never fires inside one. */
|
|
112
175
|
function maskSpans(text: string): string {
|
|
113
|
-
|
|
176
|
+
let out = text
|
|
177
|
+
for (const span of findBacktickSpans(text)) {
|
|
178
|
+
out =
|
|
179
|
+
out.slice(0, span.start) +
|
|
180
|
+
' '.repeat(span.end - span.start) +
|
|
181
|
+
out.slice(span.end)
|
|
182
|
+
}
|
|
183
|
+
return out
|
|
114
184
|
}
|
|
115
185
|
|
|
116
186
|
/**
|
|
@@ -329,10 +399,20 @@ export function extractKeyChangePaths(
|
|
|
329
399
|
if (disclaimsChange(trimmed)) continue
|
|
330
400
|
|
|
331
401
|
const accusesBefore = claimRegion(trimmed).length
|
|
402
|
+
const spans = findBacktickSpans(trimmed)
|
|
403
|
+
// Whether a `file:line` span citing an already-claimed file is a second
|
|
404
|
+
// claim or a citation into the file just claimed turns on this: a bullet
|
|
405
|
+
// that also carries a bare `:642`-shaped span alongside it is describing
|
|
406
|
+
// two lines in one file, where a bullet with no such companion is naming
|
|
407
|
+
// a second, genuinely distinct file. Computed once, up front, so the
|
|
408
|
+
// guard below reads it rather than scanning again for every span.
|
|
409
|
+
const hasBareLineCompanion = spans.some((span) =>
|
|
410
|
+
BARE_LINE.test(span.content),
|
|
411
|
+
)
|
|
332
412
|
let claimed = false
|
|
333
413
|
|
|
334
|
-
for (const match of
|
|
335
|
-
const span = match
|
|
414
|
+
for (const match of spans) {
|
|
415
|
+
const span = match.content
|
|
336
416
|
const bare = span.replace(LINE_SUFFIX, '')
|
|
337
417
|
|
|
338
418
|
// A `file:line` span following another claim in the same bullet is a
|
|
@@ -342,7 +422,7 @@ export function extractKeyChangePaths(
|
|
|
342
422
|
// its bullet it is an ordinary claim, which is how a body names the exact
|
|
343
423
|
// line it rewrote.
|
|
344
424
|
const cited = bare !== span
|
|
345
|
-
if (cited && claimed) continue
|
|
425
|
+
if (cited && claimed && hasBareLineCompanion) continue
|
|
346
426
|
|
|
347
427
|
const resolved = resolveSpan(bare)
|
|
348
428
|
if (resolved === undefined) continue
|
|
@@ -357,7 +437,7 @@ export function extractKeyChangePaths(
|
|
|
357
437
|
path: resolved.path,
|
|
358
438
|
directory: resolved.directory,
|
|
359
439
|
anchored: roots.has(resolved.path.slice(0, resolved.path.indexOf('/'))),
|
|
360
|
-
leading:
|
|
440
|
+
leading: match.start < accusesBefore,
|
|
361
441
|
span,
|
|
362
442
|
bullet: index + 1,
|
|
363
443
|
preview,
|
package/standards/versioning.md
CHANGED
|
@@ -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
|
|
42
|
-
|
|
|
43
|
-
| `.canon/tasks/`
|
|
44
|
-
| Chat with the operator
|
|
45
|
-
|
|
|
46
|
-
|
|
|
47
|
-
|
|
|
48
|
-
|
|
|
49
|
-
|
|
|
50
|
-
|
|
|
51
|
-
|
|
|
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
|
|