@erclx/canon 4.62.0 → 4.62.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.
@@ -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.62.0",
4
+ "version": "4.62.1",
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.62.0",
4
+ "version": "4.62.1",
5
5
  "description": "Infrastructure and quality tooling for developer workflows",
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -98,14 +98,18 @@ function citationPattern(folder: string): RegExp {
98
98
  )
99
99
  }
100
100
 
101
- /** One promoted folder's citation pattern, paired with its replacement. */
102
- const REWRITES: readonly {
101
+ interface Rewrite {
103
102
  readonly pattern: RegExp
104
103
  readonly folder: string
105
- }[] = PROMOTED_FOLDERS.map((folder) => ({
106
- pattern: citationPattern(folder),
107
- folder,
108
- }))
104
+ }
105
+
106
+ /** One rewrite per folder, paired with its citation pattern. */
107
+ function buildRewrites(folders: readonly string[]): readonly Rewrite[] {
108
+ return folders.map((folder) => ({
109
+ pattern: citationPattern(folder),
110
+ folder,
111
+ }))
112
+ }
109
113
 
110
114
  /**
111
115
  * Marks a line naming a promoted folder's old path on purpose, the same
@@ -125,40 +129,43 @@ function isKept(lines: readonly string[], index: number): boolean {
125
129
  return above >= 0 && (lines[above]?.includes(KEEP_MARKER) ?? false)
126
130
  }
127
131
 
128
- function rewriteLine(line: string): string {
129
- return REWRITES.reduce(
132
+ function rewriteLine(line: string, rewrites: readonly Rewrite[]): string {
133
+ return rewrites.reduce(
130
134
  (current, { pattern, folder }) =>
131
135
  current.replace(pattern, `.canon/review/evidence/${folder}`),
132
136
  line,
133
137
  )
134
138
  }
135
139
 
136
- /**
137
- * Rewrites every unmarked citation of a promoted folder into its destination
138
- * under `.canon/review/evidence/`, absolute regardless of how the source
139
- * citation was spelled. A file naming no promoted folder returns
140
- * byte-identical, and a marked line is returned unchanged.
141
- */
142
- export function rewriteScratchEvidence(text: string): string {
143
- const lines = text.split('\n')
144
- return lines
145
- .map((line, index) => (isKept(lines, index) ? line : rewriteLine(line)))
146
- .join('\n')
140
+ interface RewriteOutcome {
141
+ readonly text: string
142
+ readonly count: number
147
143
  }
148
144
 
149
- /** How many unmarked citations `rewriteScratchEvidence` would change. */
150
- export function countScratchEvidenceCitations(text: string): number {
145
+ /**
146
+ * Rewrites every unmarked citation of a folder `rewrites` covers into its
147
+ * destination under `.canon/review/evidence/`, absolute regardless of how the
148
+ * source citation was spelled, counting each as it goes. A file naming no
149
+ * such folder returns byte-identical with a count of zero, and a marked line
150
+ * is returned unchanged and uncounted.
151
+ */
152
+ function applyRewrites(
153
+ text: string,
154
+ rewrites: readonly Rewrite[],
155
+ ): RewriteOutcome {
151
156
  const lines = text.split('\n')
152
157
  let count = 0
153
158
 
154
- for (const [index, line] of lines.entries()) {
155
- if (isKept(lines, index)) continue
156
- for (const { pattern } of REWRITES) {
159
+ const rewritten = lines.map((line, index) => {
160
+ if (isKept(lines, index)) return line
161
+
162
+ for (const { pattern } of rewrites) {
157
163
  count += [...line.matchAll(pattern)].length
158
164
  }
159
- }
165
+ return rewriteLine(line, rewrites)
166
+ })
160
167
 
161
- return count
168
+ return { text: rewritten.join('\n'), count }
162
169
  }
163
170
 
164
171
  /** The files under every `BACKED_FOLDERS` entry, archives included. */
@@ -261,17 +268,17 @@ export function planScratchEvidence(
261
268
  sources: readonly ScratchEvidenceSource[],
262
269
  ): ScratchEvidencePlan {
263
270
  const { moves, collisions } = planFolderMoves(root)
271
+ const folders = PROMOTED_FOLDERS.filter(
272
+ (folder) => !collisions.includes(destinationPath(root, folder)),
273
+ )
274
+ const rewrites = buildRewrites(folders)
264
275
  const entries: CitationEntry[] = []
265
276
 
266
277
  for (const source of sources) {
267
- const rewritten = countScratchEvidenceCitations(source.text)
268
- if (rewritten === 0) continue
269
-
270
- entries.push({
271
- path: source.path,
272
- text: rewriteScratchEvidence(source.text),
273
- rewritten,
274
- })
278
+ const { text, count } = applyRewrites(source.text, rewrites)
279
+ if (count === 0) continue
280
+
281
+ entries.push({ path: source.path, text, rewritten: count })
275
282
  }
276
283
 
277
284
  return {
@@ -26,7 +26,7 @@ export default defineConfig([
26
26
  ...tseslint.configs.recommended,
27
27
  ...astro.configs.recommended,
28
28
  {
29
- files: ['**/*.{ts,tsx,js,jsx}'],
29
+ files: ['**/*.{ts,tsx,js,jsx,astro}'],
30
30
  plugins: {
31
31
  'simple-import-sort': simpleImportSort,
32
32
  'check-file': checkFile,
@@ -21,7 +21,7 @@ The astro stack covers Astro + TypeScript projects: content sites, marketing sit
21
21
  - `vitest.config.ts`: uses `getViteConfig` from `astro/config` (not `mergeConfig`). jsdom, globals, setup file, `passWithNoTests: true`, v8 coverage, `**/*.astro` in coverage excludes.
22
22
  - `playwright.config.ts`: all browsers, `webServer` runs `bun run build && bun run preview` on port `4321` plus `WORKTREE_PORT_OFFSET`, `reuseExistingServer: false`. Astro's dev/prod gap is wide (MDX, island hydration, asset optimization), so E2E always tests the built `dist/`. `DIST_PREBUILT` set in the environment drops the `build` half, running `bun run preview` alone against a `dist/` a prior CI job already produced.
23
23
  - `tsconfig.json`: extends `astro/tsconfigs/strict`, adds `skipLibCheck`, `vitest/globals` and `@testing-library/jest-dom` in types, `@/` paths.
24
- - `eslint.config.js`: overrides the web layer. Adds `eslint-plugin-astro` (`.astro` parser via `astro-eslint-parser`). React-hooks scoped to `.jsx`/`.tsx` only (`.astro` is not React). `src/pages/**` exempt from filename and folder naming conventions because Astro's file-based routing ties names to URL segments.
24
+ - `eslint.config.js`: overrides the web layer. Adds `eslint-plugin-astro` (`.astro` parser via `astro-eslint-parser`). React-hooks scoped to `.jsx`/`.tsx` only (`.astro` is not React). The shared block's `files` selector includes `.astro`, so `check-file/filename-naming-convention` reaches `.ts`, `.tsx`, and `.astro` under `KEBAB_CASE`, overriding Astro's own PascalCase component convention deliberately, on the ground that a component's name in markup comes from the import binding rather than the filename. `.js` and `.jsx` stay out of the rule's own pattern, matching `web` and `nextjs`. `check-file/folder-naming-convention`'s `src/**/!(__tests__|pages)` pattern reaches no folder under a real ESLint run, `.astro`-only or otherwise. See `.claude/context/tooling.md` for the measurement.
25
25
 
26
26
  ## Typecheck
27
27