@erclx/canon 4.30.0 → 4.30.2
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-pr-review/SKILL.md +1 -1
- package/docs/agents/capture.md +2 -2
- package/docs/agents/commands.md +75 -75
- package/package.json +1 -1
- package/src/commands/capture.ts +5 -0
- package/src/commands/labels.ts +14 -98
- package/src/gov/citations.ts +5 -9
- package/src/labels/event.ts +152 -0
- package/src/markdown/backticks.ts +56 -0
- package/src/pr/paths.ts +2 -57
- package/standards/publish.md +1 -1
|
@@ -228,7 +228,7 @@ The `What is right` section is optional, capped at three bullets, and included o
|
|
|
228
228
|
|
|
229
229
|
Close the body with `🤖 Reviewed by Claude Code` on its own line so the review reads as an independent machine pass, not a human sign-off.
|
|
230
230
|
|
|
231
|
-
Before posting, run the scan in `${CLAUDE_SKILL_DIR}/../../standards/publish.md` against the body. The hook skips `.canon/tmp/`, so this scan is the only gate
|
|
231
|
+
Before posting, run the scan in `${CLAUDE_SKILL_DIR}/../../standards/publish.md` against the body. The hook skips `.canon/tmp/`, so this scan is the only gate ahead of the post. A finding phrased against an internal phase label is what the label half of the scan catches here. This repository reads the same text again once posted, on `phase-label-gate.yml`'s `pull_request_review` trigger, which is what closes the gap this scan leaves open for a review a person writes and posts by hand with no scripted step in front of it. That workflow is not seeded to any target this skill ships to, so a target's coverage stops at this pre-post scan, even though the `canon labels scan` verb behind it already reads a review payload once triggered.
|
|
232
232
|
|
|
233
233
|
Do not run the command below when `<prior-heading>` from Step 2 reads `## Review closed` and this pass carries nothing owed. That pass replaces the standing comment rather than adding one, under `### A close-out that repeats the standing one` at the end of this step. Posting first and reaching that section afterward leaves two close-outs both naming the new head, which is worse than the pair the guard exists against.
|
|
234
234
|
|
package/docs/agents/capture.md
CHANGED
|
@@ -5,7 +5,7 @@ description: Rendering HTML sources to PNG, what the command asserts about fonts
|
|
|
5
5
|
|
|
6
6
|
# Capture
|
|
7
7
|
|
|
8
|
-
`canon capture [source] --selector <sel>` renders HTML sources to PNG, which is how a generated documentation image is rebuilt from the markup it was generated out of. The source defaults to `assets/`, where a directory expands to every `.html` directly inside it, so adding a capture means dropping a file beside the first one and running the same command.
|
|
8
|
+
`canon capture [source] --selector <sel>` renders HTML sources to PNG, which is how a generated documentation image is rebuilt from the markup it was generated out of. The source defaults to `assets/`, where a directory expands to every `.html` directly inside it, so adding a capture means dropping a file beside the first one and running the same command. That default names this repository's own folder rather than a convention every target shares, and it stays because a missing one still refuses loud, naming the argument, rather than failing silently.
|
|
9
9
|
|
|
10
10
|
```bash
|
|
11
11
|
canon capture --selector .window
|
|
@@ -38,7 +38,7 @@ Each source renders at `deviceScaleFactor` 2 with a transparent background, and
|
|
|
38
38
|
|
|
39
39
|
What is asserted is the font. The command reads the first family the captured element declares and fails when the browser did not resolve it, because a fallback face rewraps the block and silently changes the output. Sources therefore name a real font rather than relying on `monospace`. A source that cannot render reports its own line and exits 1 without dropping the rest of the batch.
|
|
40
40
|
|
|
41
|
-
A run refuses before it reads anything else when `--selector` is absent, naming the flag and pointing at `--help`. Ordering it first is what keeps the message about the invocation: the source defaults to `assets`, so checking that first would answer `assets not found` from whatever directory the caller happened to be in and say nothing about the flag that was actually missing.
|
|
41
|
+
A run refuses before it reads anything else when `--selector` is absent, naming the flag and pointing at `--help`. Ordering it first is what keeps the message about the invocation: the source defaults to `assets`, so checking that first would answer `assets not found` from whatever directory the caller happened to be in and say nothing about the flag that was actually missing. That source default is a deliberate choice rather than one inherited from some general convention: a missing folder still refuses loud, naming the argument, once the flag check passes, where the selector's dropped default cropped the wrong region in silence.
|
|
42
42
|
|
|
43
43
|
The browser binary installs separately from the package. A first run does `bunx playwright install chromium` once, and a run that cannot launch one reports the engine's own remediation inside the frame and exits 1 rather than escaping as a stack trace.
|
|
44
44
|
|
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
|
|
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 or a posted review whose title, body, or review comment carries a phase label, a label a code span quotes, a gitignored record path, a session link, 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
package/src/commands/capture.ts
CHANGED
|
@@ -16,6 +16,11 @@ import {
|
|
|
16
16
|
pipeOutput,
|
|
17
17
|
} from '@/ui'
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Kept rather than removed, unlike the selector's default that came out for
|
|
21
|
+
* silently cropping the wrong region: a missing folder still refuses loud,
|
|
22
|
+
* naming the argument that failed to resolve.
|
|
23
|
+
*/
|
|
19
24
|
const DEFAULT_SOURCE = 'assets'
|
|
20
25
|
|
|
21
26
|
/**
|
package/src/commands/labels.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs'
|
|
2
1
|
import { resolve } from 'node:path'
|
|
3
2
|
import type { Command } from 'commander'
|
|
4
3
|
import { type LabelAuditRefusal, auditLabels } from '@/labels/audit'
|
|
4
|
+
import { resolveScanInput } from '@/labels/event'
|
|
5
5
|
import { MAP_REL } from '@/labels/map'
|
|
6
6
|
import { scanPhaseLabels } from '@/labels/phase'
|
|
7
7
|
import { scanTitleSpelling } from '@/labels/spelling'
|
|
@@ -91,12 +91,12 @@ export function register(program: Command): void {
|
|
|
91
91
|
labels
|
|
92
92
|
.command('scan')
|
|
93
93
|
.description(
|
|
94
|
-
'Fail a pull request whose title or
|
|
94
|
+
'Fail a pull request or a posted review whose title, body, or review comment carries a phase label, a board identifier, a session link, or a title with an unspelled word',
|
|
95
95
|
)
|
|
96
96
|
.helpOption('-h, --help', 'Show this help message')
|
|
97
97
|
.option(
|
|
98
98
|
'--event <path>',
|
|
99
|
-
'GitHub pull_request event payload to read, such as $GITHUB_EVENT_PATH',
|
|
99
|
+
'GitHub pull_request or pull_request_review event payload to read, such as $GITHUB_EVENT_PATH',
|
|
100
100
|
)
|
|
101
101
|
.option('--title <text>', 'Title to scan, overriding the event payload')
|
|
102
102
|
.option('--body <text>', 'Body to scan, overriding the event payload')
|
|
@@ -113,6 +113,11 @@ export function register(program: Command): void {
|
|
|
113
113
|
'other pull request may carry neither, so any token found there is a',
|
|
114
114
|
'leaked phase label.',
|
|
115
115
|
'',
|
|
116
|
+
'A posted review is read the same way, scanning `review.body` off a',
|
|
117
|
+
'`pull_request_review` payload rather than the title and body of the',
|
|
118
|
+
'pull request itself, which is text a person can otherwise publish',
|
|
119
|
+
'by writing straight into the review box instead of the description.',
|
|
120
|
+
'',
|
|
116
121
|
'It reports a board identifier beside that, being text naming the task',
|
|
117
122
|
'board rather than the change: a version token a code span quotes, and a',
|
|
118
123
|
'path under a record root, both of which a reader on the remote holds no',
|
|
@@ -236,97 +241,6 @@ async function runAudit(paths: string[], opts: AuditOptions): Promise<number> {
|
|
|
236
241
|
return coverage.uncovered.length === 0 ? 0 : 2
|
|
237
242
|
}
|
|
238
243
|
|
|
239
|
-
/** Why `runScan` had no title and body to hand `scanPhaseLabels`. */
|
|
240
|
-
type ScanInputRefusal = 'no-input' | 'unreadable-event' | 'not-a-pull-request'
|
|
241
|
-
|
|
242
|
-
type ResolvedScanInput =
|
|
243
|
-
| {
|
|
244
|
-
readonly kind: 'resolved'
|
|
245
|
-
readonly title: string
|
|
246
|
-
readonly body: string
|
|
247
|
-
readonly headRefName: string
|
|
248
|
-
}
|
|
249
|
-
| {
|
|
250
|
-
readonly kind: 'refused'
|
|
251
|
-
readonly reason: ScanInputRefusal
|
|
252
|
-
readonly message: string
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
/**
|
|
256
|
-
* Reads a title, a body, and a head branch from explicit flags first and the
|
|
257
|
-
* named event payload second, so a caller testing the wiring by hand never
|
|
258
|
-
* needs a real GitHub event file on disk.
|
|
259
|
-
*/
|
|
260
|
-
function resolveScanInput(opts: ScanOptions): ResolvedScanInput {
|
|
261
|
-
let title = opts.title
|
|
262
|
-
let body = opts.body
|
|
263
|
-
let headRefName = opts.head
|
|
264
|
-
|
|
265
|
-
if (opts.event !== undefined) {
|
|
266
|
-
let raw: string
|
|
267
|
-
try {
|
|
268
|
-
raw = readFileSync(opts.event, 'utf8')
|
|
269
|
-
} catch {
|
|
270
|
-
return {
|
|
271
|
-
kind: 'refused',
|
|
272
|
-
reason: 'unreadable-event',
|
|
273
|
-
message: `${opts.event} could not be read, so no payload was there to scan.`,
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
let payload: unknown
|
|
278
|
-
try {
|
|
279
|
-
payload = JSON.parse(raw)
|
|
280
|
-
} catch {
|
|
281
|
-
return {
|
|
282
|
-
kind: 'refused',
|
|
283
|
-
reason: 'unreadable-event',
|
|
284
|
-
message: `${opts.event} is not valid JSON, so no payload was there to scan.`,
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
const pullRequest =
|
|
289
|
-
typeof payload === 'object' && payload !== null
|
|
290
|
-
? (payload as Record<string, unknown>).pull_request
|
|
291
|
-
: undefined
|
|
292
|
-
|
|
293
|
-
if (typeof pullRequest !== 'object' || pullRequest === null) {
|
|
294
|
-
return {
|
|
295
|
-
kind: 'refused',
|
|
296
|
-
reason: 'not-a-pull-request',
|
|
297
|
-
message: `${opts.event} carries no pull_request, so no title or body exists to scan.`,
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
const record = pullRequest as Record<string, unknown>
|
|
302
|
-
const head = record.head
|
|
303
|
-
title ??= typeof record.title === 'string' ? record.title : undefined
|
|
304
|
-
body ??= typeof record.body === 'string' ? record.body : undefined
|
|
305
|
-
headRefName ??=
|
|
306
|
-
typeof head === 'object' &&
|
|
307
|
-
head !== null &&
|
|
308
|
-
typeof (head as Record<string, unknown>).ref === 'string'
|
|
309
|
-
? ((head as Record<string, unknown>).ref as string)
|
|
310
|
-
: undefined
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
if (title === undefined) {
|
|
314
|
-
return {
|
|
315
|
-
kind: 'refused',
|
|
316
|
-
reason: 'no-input',
|
|
317
|
-
message:
|
|
318
|
-
'No --event, --title, or --body given, so there is nothing to scan.',
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
return {
|
|
323
|
-
kind: 'resolved',
|
|
324
|
-
title,
|
|
325
|
-
body: body ?? '',
|
|
326
|
-
headRefName: headRefName ?? '',
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
|
|
330
244
|
async function runScan(opts: ScanOptions): Promise<number> {
|
|
331
245
|
const emitJson = opts.json ?? false
|
|
332
246
|
|
|
@@ -350,11 +264,13 @@ async function runScan(opts: ScanOptions): Promise<number> {
|
|
|
350
264
|
const result = scanPhaseLabels(resolved)
|
|
351
265
|
const spelling = await scanTitleSpelling(resolved.title, process.cwd())
|
|
352
266
|
|
|
353
|
-
logStep('Pull request')
|
|
267
|
+
logStep(resolved.source === 'review' ? 'Review comment' : 'Pull request')
|
|
354
268
|
logInfo(
|
|
355
|
-
|
|
356
|
-
? 'reads as a
|
|
357
|
-
:
|
|
269
|
+
resolved.source === 'review'
|
|
270
|
+
? 'reads as a posted review comment'
|
|
271
|
+
: result.cutsRelease
|
|
272
|
+
? 'reads as a release-please pull request'
|
|
273
|
+
: 'reads as an ordinary pull request',
|
|
358
274
|
)
|
|
359
275
|
|
|
360
276
|
logStep(result.phaseLabels.length === 0 ? 'Clean' : 'Phase label found')
|
package/src/gov/citations.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { isMarked } from '@/exempt-marker'
|
|
|
6
6
|
import { gitEnv } from '@/git-env'
|
|
7
7
|
import { listRuleFiles } from '@/gov/payload'
|
|
8
8
|
import { parseFrontmatter } from '@/indexes/frontmatter'
|
|
9
|
+
import { findBacktickSpans } from '@/markdown/backticks'
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* The inline token exempting one line from this sweep, shaped on the
|
|
@@ -114,13 +115,6 @@ const FENCE = /^\s*(?:```|~~~)/
|
|
|
114
115
|
|
|
115
116
|
const FRONTMATTER_DELIMITER = /^---\s*$/
|
|
116
117
|
|
|
117
|
-
/**
|
|
118
|
-
* A backticked span, which is the only carrier a rule writes a citation in. No
|
|
119
|
-
* rule in either corpus uses a markdown link, and matching running prose would
|
|
120
|
-
* report every sentence that happens to name a file.
|
|
121
|
-
*/
|
|
122
|
-
const BACKTICKED = /`([^`\n]+)`/g
|
|
123
|
-
|
|
124
118
|
/**
|
|
125
119
|
* The verb form, with the name captured. A leading letter or digit is required,
|
|
126
120
|
* which is what leaves `canon standards <name>` unmatched: that line teaches the
|
|
@@ -254,8 +248,10 @@ export function collectCitations(text: string): RawCitation[] {
|
|
|
254
248
|
})
|
|
255
249
|
}
|
|
256
250
|
|
|
257
|
-
|
|
258
|
-
|
|
251
|
+
// A backticked span is the only carrier a rule writes a citation in. No
|
|
252
|
+
// rule in either corpus uses a markdown link, and matching running prose
|
|
253
|
+
// would report every sentence that happens to name a file.
|
|
254
|
+
for (const { content: span } of findBacktickSpans(line)) {
|
|
259
255
|
const form = classifySpan(span)
|
|
260
256
|
if (form === undefined) continue
|
|
261
257
|
found.push({ line: index + 1, form, cited: span, preview, marked })
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
|
|
3
|
+
/** Why `resolveScanInput` had no title and body to hand `scanPhaseLabels`. */
|
|
4
|
+
export type ScanInputRefusal =
|
|
5
|
+
| 'no-input'
|
|
6
|
+
| 'unreadable-event'
|
|
7
|
+
| 'not-a-pull-request'
|
|
8
|
+
| 'unreadable-review'
|
|
9
|
+
|
|
10
|
+
export type ResolvedScanInput =
|
|
11
|
+
| {
|
|
12
|
+
readonly kind: 'resolved'
|
|
13
|
+
readonly title: string
|
|
14
|
+
readonly body: string
|
|
15
|
+
readonly headRefName: string
|
|
16
|
+
/** Which text was scanned, for the caller's own reporting. */
|
|
17
|
+
readonly source: 'pull-request' | 'review'
|
|
18
|
+
}
|
|
19
|
+
| {
|
|
20
|
+
readonly kind: 'refused'
|
|
21
|
+
readonly reason: ScanInputRefusal
|
|
22
|
+
readonly message: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface ScanInputOptions {
|
|
26
|
+
readonly event?: string
|
|
27
|
+
readonly title?: string
|
|
28
|
+
readonly body?: string
|
|
29
|
+
readonly head?: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Reads a title, a body, and a head branch from explicit flags first and the
|
|
34
|
+
* named event payload second, so a caller testing the wiring by hand never
|
|
35
|
+
* needs a real GitHub event file on disk.
|
|
36
|
+
*
|
|
37
|
+
* A `pull_request_review` payload carries no title of its own, and the text a
|
|
38
|
+
* reviewer wrote lives at `review.body` rather than at `pull_request.body`.
|
|
39
|
+
* `payload.review` is read ahead of `payload.pull_request` so a payload
|
|
40
|
+
* carrying both, which a real review event does, is never read off the
|
|
41
|
+
* pull request's own title and body once a review resolved.
|
|
42
|
+
*/
|
|
43
|
+
export function resolveScanInput(opts: ScanInputOptions): ResolvedScanInput {
|
|
44
|
+
let title = opts.title
|
|
45
|
+
let body = opts.body
|
|
46
|
+
let headRefName = opts.head
|
|
47
|
+
let source: 'pull-request' | 'review' = 'pull-request'
|
|
48
|
+
|
|
49
|
+
if (opts.event !== undefined) {
|
|
50
|
+
let raw: string
|
|
51
|
+
try {
|
|
52
|
+
raw = readFileSync(opts.event, 'utf8')
|
|
53
|
+
} catch {
|
|
54
|
+
return {
|
|
55
|
+
kind: 'refused',
|
|
56
|
+
reason: 'unreadable-event',
|
|
57
|
+
message: `${opts.event} could not be read, so no payload was there to scan.`,
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let payload: unknown
|
|
62
|
+
try {
|
|
63
|
+
payload = JSON.parse(raw)
|
|
64
|
+
} catch {
|
|
65
|
+
return {
|
|
66
|
+
kind: 'refused',
|
|
67
|
+
reason: 'unreadable-event',
|
|
68
|
+
message: `${opts.event} is not valid JSON, so no payload was there to scan.`,
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const envelope =
|
|
73
|
+
typeof payload === 'object' && payload !== null
|
|
74
|
+
? (payload as Record<string, unknown>)
|
|
75
|
+
: undefined
|
|
76
|
+
const review = envelope?.review
|
|
77
|
+
const pullRequest = envelope?.pull_request
|
|
78
|
+
|
|
79
|
+
if (typeof review === 'object' && review !== null) {
|
|
80
|
+
const reviewRecord = review as Record<string, unknown>
|
|
81
|
+
|
|
82
|
+
if (body === undefined) {
|
|
83
|
+
const rawBody = reviewRecord.body
|
|
84
|
+
if (rawBody === null || rawBody === undefined) {
|
|
85
|
+
body = ''
|
|
86
|
+
source = 'review'
|
|
87
|
+
} else if (typeof rawBody === 'string') {
|
|
88
|
+
body = rawBody
|
|
89
|
+
source = 'review'
|
|
90
|
+
} else {
|
|
91
|
+
return {
|
|
92
|
+
kind: 'refused',
|
|
93
|
+
reason: 'unreadable-review',
|
|
94
|
+
message: `${opts.event} carries a review.body that is neither a string nor null, so no text was there to scan.`,
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
title ??= ''
|
|
100
|
+
|
|
101
|
+
// Defense in depth for `isReleasePullRequest`, which already reads
|
|
102
|
+
// `cutsRelease` as false off the empty title above. A review carries no
|
|
103
|
+
// head ref of its own, so this reaches for the pull request's.
|
|
104
|
+
if (typeof pullRequest === 'object' && pullRequest !== null) {
|
|
105
|
+
const head = (pullRequest as Record<string, unknown>).head
|
|
106
|
+
headRefName ??=
|
|
107
|
+
typeof head === 'object' &&
|
|
108
|
+
head !== null &&
|
|
109
|
+
typeof (head as Record<string, unknown>).ref === 'string'
|
|
110
|
+
? ((head as Record<string, unknown>).ref as string)
|
|
111
|
+
: undefined
|
|
112
|
+
}
|
|
113
|
+
} else if (typeof pullRequest === 'object' && pullRequest !== null) {
|
|
114
|
+
const record = pullRequest as Record<string, unknown>
|
|
115
|
+
const head = record.head
|
|
116
|
+
title ??= typeof record.title === 'string' ? record.title : undefined
|
|
117
|
+
body ??= typeof record.body === 'string' ? record.body : undefined
|
|
118
|
+
headRefName ??=
|
|
119
|
+
typeof head === 'object' &&
|
|
120
|
+
head !== null &&
|
|
121
|
+
typeof (head as Record<string, unknown>).ref === 'string'
|
|
122
|
+
? ((head as Record<string, unknown>).ref as string)
|
|
123
|
+
: undefined
|
|
124
|
+
} else {
|
|
125
|
+
return {
|
|
126
|
+
kind: 'refused',
|
|
127
|
+
reason: 'not-a-pull-request',
|
|
128
|
+
message: `${opts.event} carries no pull_request or review, so no title or body exists to scan.`,
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Loosened from `title === undefined` alone so `--body` on its own, with no
|
|
134
|
+
// `--event` and no `--title`, exercises the same empty-title, review-style
|
|
135
|
+
// path a real review scans by hand.
|
|
136
|
+
if (title === undefined && body === undefined) {
|
|
137
|
+
return {
|
|
138
|
+
kind: 'refused',
|
|
139
|
+
reason: 'no-input',
|
|
140
|
+
message:
|
|
141
|
+
'No --event, --title, or --body given, so there is nothing to scan.',
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
kind: 'resolved',
|
|
147
|
+
title: title ?? '',
|
|
148
|
+
body: body ?? '',
|
|
149
|
+
headRefName: headRefName ?? '',
|
|
150
|
+
source,
|
|
151
|
+
}
|
|
152
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/** One code span, paired by matching backtick-run length rather than count. */
|
|
2
|
+
export interface BacktickSpan {
|
|
3
|
+
/** Index of the opening run's first backtick. */
|
|
4
|
+
readonly start: number
|
|
5
|
+
/** Index one past the closing run's last backtick. */
|
|
6
|
+
readonly end: number
|
|
7
|
+
/** The text between the two runs, delimiters excluded. */
|
|
8
|
+
readonly content: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Every code span in the text, pairing a run of backticks only with the next
|
|
13
|
+
* run of the same length.
|
|
14
|
+
*
|
|
15
|
+
* A single-backtick regex reads a doubled delimiter, such as
|
|
16
|
+
* ``` ``git status`` ```, as two unrelated single backticks: it opens on the
|
|
17
|
+
* second backtick of the pair, closes on the first backtick of the closing
|
|
18
|
+
* pair, and leaves one backtick before and after the span unconsumed. Every
|
|
19
|
+
* scan after that treats a stray leftover backtick as an opener, which
|
|
20
|
+
* absorbs the next real span's opening delimiter as its closer and drops the
|
|
21
|
+
* path inside past it. Pairing by run length rather than by single backtick
|
|
22
|
+
* keeps a doubled delimiter closed by a doubled delimiter, so nothing after
|
|
23
|
+
* it loses its pairing.
|
|
24
|
+
*
|
|
25
|
+
* An opening run with no same-length run after it is not a delimiter, per
|
|
26
|
+
* CommonMark, so it is left as literal text and the scan resumes at the next
|
|
27
|
+
* run rather than backtracking into the unmatched one.
|
|
28
|
+
*/
|
|
29
|
+
export function findBacktickSpans(text: string): BacktickSpan[] {
|
|
30
|
+
const runs = [...text.matchAll(/`+/g)].map((match) => ({
|
|
31
|
+
start: match.index ?? 0,
|
|
32
|
+
length: match[0].length,
|
|
33
|
+
}))
|
|
34
|
+
|
|
35
|
+
const spans: BacktickSpan[] = []
|
|
36
|
+
let i = 0
|
|
37
|
+
while (i < runs.length) {
|
|
38
|
+
const open = runs[i]
|
|
39
|
+
const closeIndex = runs.findIndex(
|
|
40
|
+
(run, index) => index > i && run.length === open.length,
|
|
41
|
+
)
|
|
42
|
+
if (closeIndex === -1) {
|
|
43
|
+
i += 1
|
|
44
|
+
continue
|
|
45
|
+
}
|
|
46
|
+
const close = runs[closeIndex]
|
|
47
|
+
spans.push({
|
|
48
|
+
start: open.start,
|
|
49
|
+
end: close.start + close.length,
|
|
50
|
+
content: text.slice(open.start + open.length, close.start),
|
|
51
|
+
})
|
|
52
|
+
i = closeIndex + 1
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return spans
|
|
56
|
+
}
|
package/src/pr/paths.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { findBacktickSpans } from '@/markdown/backticks'
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* The heading whose bullets state what a branch changed.
|
|
3
5
|
*
|
|
@@ -114,63 +116,6 @@ function isDottedNumber(span: string): boolean {
|
|
|
114
116
|
return /^\d+(?:\.\d+)+$/.test(segment)
|
|
115
117
|
}
|
|
116
118
|
|
|
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
|
-
|
|
174
119
|
/** Blanks every backticked span so a cue search never fires inside one. */
|
|
175
120
|
function maskSpans(text: string): string {
|
|
176
121
|
let out = text
|
package/standards/publish.md
CHANGED
|
@@ -44,7 +44,7 @@ Two shapes get past a reader scanning for a bare label. A code span quoting a la
|
|
|
44
44
|
|
|
45
45
|
Rewrite a hit to name what the reader can reach rather than deleting it. A row's subject stated plainly replaces its label, and what a record folder holds, said in a sentence, replaces its path.
|
|
46
46
|
|
|
47
|
-
`canon labels scan` runs this check and the phase-label one over a pull request title and body
|
|
47
|
+
`canon labels scan` runs this check and the phase-label one over a pull request title and body, and over a posted review's own text once one is submitted. Any other channel, a plain issue comment among them, is still the author's own scan.
|
|
48
48
|
|
|
49
49
|
## Session links
|
|
50
50
|
|