@erclx/canon 4.19.1 → 4.19.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.19.1",
4
+ "version": "4.19.2",
5
5
  "author": {
6
6
  "name": "Eric Le",
7
7
  "url": "https://github.com/erclx"
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@erclx/canon",
3
3
  "type": "module",
4
- "version": "4.19.1",
4
+ "version": "4.19.2",
5
5
  "description": "Infrastructure and quality tooling for developer workflows",
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -1,5 +1,6 @@
1
1
  import { linesOutsideFences, maskCodeSpans } from '@/markdown/scan'
2
- import { RECORD_ENTRIES, RECORD_ROOTS, spell } from '@/record-root'
2
+ import { RECORD_ENTRIES, RECORD_ROOTS } from '@/record-root'
3
+ import type { RecordRoot } from '@/record-root'
3
4
 
4
5
  /**
5
6
  * The two version namespaces `standards/versioning.md` keeps apart, and why a
@@ -70,13 +71,15 @@ function escapeLiteral(text: string): string {
70
71
  }
71
72
 
72
73
  /**
73
- * What is ignored under a root beyond the entries the record move relocated.
74
+ * What is ignored under the tracked root beyond the entries the record move
75
+ * relocated.
74
76
  *
75
77
  * `RECORD_ENTRIES` answers which folders that move carried across, and this
76
- * check asks which paths a reader on a remote cannot open. The two questions
77
- * differ by exactly one entry: the worktrees folder is ignored and stays out of
78
- * that list deliberately, since the harness creates a worktree there and
79
- * requires its target to sit there, so adding it upstream would tell the
78
+ * check asks which paths a reader on a remote cannot open under `.claude/`,
79
+ * the one root still read by entry name below. The two questions differ by
80
+ * exactly one entry: the worktrees folder is ignored and stays out of
81
+ * `RECORD_ENTRIES` deliberately, since the harness creates a worktree there
82
+ * and requires its target to sit there, so adding it upstream would tell the
80
83
  * migration to relocate a folder the harness pins.
81
84
  *
82
85
  * It is also the entry a worker announcement names most often, which is what
@@ -85,29 +88,61 @@ function escapeLiteral(text: string): string {
85
88
  const IGNORED_BEYOND_RECORDS: readonly string[] = ['worktrees']
86
89
 
87
90
  /**
88
- * A path a reader on a remote cannot open, which is a root plus one of the
89
- * entries that root ignores rather than the root alone.
91
+ * Which reading each record root takes: `whole` for a root one `.gitignore`
92
+ * line covers entirely, `entries` for a root kept narrow by name.
90
93
  *
91
- * `.claude/` is tracked and holds `rules`, `skills`, `hooks`, and `context`, so
92
- * a rule path resolves in any clone and is not a board reference. The scratch
93
- * folder goes through `spell` because it is the one entry whose name differs by
94
- * root. Reading the roots and the relocated entries from `src/record-root.ts`
95
- * is what makes a folder added there matched here without an edit, and the list
96
- * above is what covers the one thing that module deliberately does not carry.
94
+ * `.canon/` takes `whole`: the ignore file excludes the folder outright, and
95
+ * git does not descend into an excluded directory, so no entry list is ever
96
+ * wider than the root itself. `.claude/` takes `entries` because it is
97
+ * tracked. A `Record` over `RecordRoot` rather than a filtered list of the
98
+ * roots read one way, so a root added to `RECORD_ROOTS` fails to typecheck
99
+ * here until this map says which reading it takes, rather than falling
100
+ * through a filter into the entry-list branch unnoticed.
101
+ */
102
+ const ROOT_READING: Record<RecordRoot, 'whole' | 'entries'> = {
103
+ '.canon': 'whole',
104
+ '.claude': 'entries',
105
+ }
106
+
107
+ /**
108
+ * A path a reader on a remote cannot open.
109
+ *
110
+ * The two roots differ by why they are unreadable rather than by which one
111
+ * they are. A root reading `whole` matches on the root alone, since one
112
+ * ignore line covers everything beneath it and no entry list can ever be
113
+ * narrower than that. A root reading `entries` keeps the entry-list reading:
114
+ * `.claude/` is tracked and holds `rules`, `skills`, `hooks`, and `context`,
115
+ * so a rule path resolves in any clone and is not a board reference, and only
116
+ * the entries the record move relocated are unreadable there. Reading the
117
+ * roots and the relocated entries from `src/record-root.ts` is what makes a
118
+ * folder added there matched here without an edit, and the list above is what
119
+ * covers the one thing that module deliberately does not carry.
120
+ *
121
+ * The root-alone branch requires at least one tail character ahead of the
122
+ * shared tail capture, so a root written bare, such as the ignore line naming
123
+ * it as a concept, reports nothing: there is no path there for an author to
124
+ * remove. The entry-list branch instead bounds the entry name on the right
125
+ * with a lookahead, so `plans` does not also match the prefix of a longer
126
+ * word; that lookahead has to sit inside the entry branch rather than after
127
+ * the whole alternation; the character right after a bare root is an ordinary
128
+ * word character, and the same lookahead there would reject every root-alone
129
+ * match.
97
130
  *
98
- * The tail runs to the first whitespace or closing delimiter, so a report names
99
- * the whole path an author has to remove rather than the prefix that matched.
131
+ * The tail runs to the first whitespace or closing delimiter, so a report
132
+ * names the whole path an author has to remove rather than the prefix that
133
+ * matched.
100
134
  */
101
135
  const RECORD_PATH = new RegExp(
102
- `(?<![\\w./-])(?:${RECORD_ROOTS.map(
103
- (root) =>
104
- `${escapeLiteral(root)}/(?:${[
105
- ...RECORD_ENTRIES,
106
- ...IGNORED_BEYOND_RECORDS,
107
- ]
108
- .map((entry) => escapeLiteral(spell(root, entry)))
109
- .join('|')})`,
110
- ).join('|')})(?![\\w-])[^\\s\`)\\]]*`,
136
+ `(?<![\\w./-])(?:${RECORD_ROOTS.map((root) =>
137
+ ROOT_READING[root] === 'whole'
138
+ ? `${escapeLiteral(root)}/(?=[^\\s\`)\\]])`
139
+ : `${escapeLiteral(root)}/(?:${[
140
+ ...RECORD_ENTRIES,
141
+ ...IGNORED_BEYOND_RECORDS,
142
+ ]
143
+ .map((entry) => escapeLiteral(entry))
144
+ .join('|')})(?![\\w-])`,
145
+ ).join('|')})[^\\s\`)\\]]*`,
111
146
  'g',
112
147
  )
113
148
 
@@ -40,7 +40,7 @@ This check is one of the two the destination rule above scopes. The reader insid
40
40
 
41
41
  A phase label is one way text names the board, and a path under a record root is the other. Both resolve for a reader holding this checkout and neither resolves for anyone else, so this check is the second one the destination rule scopes.
42
42
 
43
- Two shapes get past a reader scanning for a bare label. A code span quoting a label is still the label, so read a span whose whole content is one as a hit and leave a longer token inside a span alone, which is a fixture name rather than a reference. The second shape is a path under a record root, gitignored and therefore absent from every clone, so `.canon/review/feedback/` names a folder the remote's reader cannot open. Under a tracked folder there is no hit, since `.claude/rules/core/005-behavior.md` resolves everywhere.
43
+ Two shapes get past a reader scanning for a bare label. A code span quoting a label is still the label, so read a span whose whole content is one as a hit and leave a longer token inside a span alone, which is a fixture name rather than a reference. The second shape is a path under a record root, gitignored and therefore absent from every clone, so `.canon/review/feedback/` names a folder the remote's reader cannot open. Under `.claude/`'s own tracked folders there is no hit, since `.claude/rules/core/005-behavior.md` resolves everywhere. `.canon/` carries no such carve-out: one ignore line covers the root whole, so every path beneath it is a hit regardless of which folder names it.
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