@erclx/canon 4.30.1 → 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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "canon",
3
3
  "description": "Automated governance, versioning, and discovery tools for Claude Code.",
4
- "version": "4.30.1",
4
+ "version": "4.30.2",
5
5
  "author": {
6
6
  "name": "Eric Le",
7
7
  "url": "https://github.com/erclx"
@@ -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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@erclx/canon",
3
3
  "type": "module",
4
- "version": "4.30.1",
4
+ "version": "4.30.2",
5
5
  "description": "Infrastructure and quality tooling for developer workflows",
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -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
  /**
@@ -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
- for (const match of line.matchAll(BACKTICKED)) {
258
- const span = match[1] ?? ''
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,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