@erclx/canon 4.66.0 → 4.68.0
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-autoship → auto-ship}/REQUIREMENT.md +3 -3
- package/claude/skills/{claude-autoship → auto-ship}/SKILL.md +36 -36
- package/claude/skills/canon-cli/REQUIREMENT.md +2 -2
- package/claude/skills/canon-cli/SKILL.md +2 -2
- package/claude/skills/canon-feedback-triage/REQUIREMENT.md +1 -1
- package/claude/skills/canon-feedback-triage/SKILL.md +3 -3
- package/claude/skills/canon-operator/REQUIREMENT.md +1 -1
- package/claude/skills/canon-operator/SKILL.md +4 -4
- package/claude/skills/canon-rollout/REQUIREMENT.md +6 -6
- package/claude/skills/canon-rollout/SKILL.md +7 -7
- package/claude/skills/{claude-design-extract → design-extract}/REQUIREMENT.md +4 -4
- package/claude/skills/{claude-design-extract → design-extract}/SKILL.md +1 -1
- package/claude/skills/{claude-docs → docs-fold}/REQUIREMENT.md +3 -3
- package/claude/skills/{claude-docs → docs-fold}/SKILL.md +14 -14
- package/claude/skills/{claude-docs → docs-fold}/references/anchor-sweep.md +1 -1
- package/claude/skills/{claude-docs → docs-fold}/references/wireframe-sweep.md +1 -1
- package/claude/skills/docs-sync/REQUIREMENT.md +3 -3
- package/claude/skills/docs-sync/SKILL.md +1 -1
- package/claude/skills/draft-and-pick/REQUIREMENT.md +4 -4
- package/claude/skills/draft-and-pick/SKILL.md +6 -6
- package/claude/skills/draft-context/REQUIREMENT.md +2 -2
- package/claude/skills/draft-context/SKILL.md +3 -3
- package/claude/skills/{claude-diagram → draft-diagram}/REQUIREMENT.md +4 -4
- package/claude/skills/{claude-diagram → draft-diagram}/SKILL.md +3 -3
- package/claude/skills/draft-docs/REQUIREMENT.md +1 -1
- package/claude/skills/draft-wireframes/REQUIREMENT.md +1 -1
- package/claude/skills/draft-wireframes/SKILL.md +2 -2
- package/claude/skills/git-followup/REQUIREMENT.md +1 -1
- package/claude/skills/git-followup/SKILL.md +1 -1
- package/claude/skills/git-pr/SKILL.md +3 -3
- package/claude/skills/git-ship/REQUIREMENT.md +2 -2
- package/claude/skills/git-ship/SKILL.md +8 -8
- package/claude/skills/git-worktree/REQUIREMENT.md +2 -2
- package/claude/skills/git-worktree/SKILL.md +3 -3
- package/claude/skills/identity/REQUIREMENT.md +2 -2
- package/claude/skills/identity/SKILL.md +3 -3
- package/claude/skills/{claude-markdown-propose → markdown-propose}/REQUIREMENT.md +8 -8
- package/claude/skills/{claude-markdown-propose → markdown-propose}/SKILL.md +5 -5
- package/claude/skills/{claude-markdown-propose → markdown-propose}/references/format.md +1 -1
- package/claude/skills/{claude-memory-capture → memory-capture}/REQUIREMENT.md +5 -5
- package/claude/skills/{claude-memory-capture → memory-capture}/SKILL.md +13 -13
- package/claude/skills/{claude-memory-review → memory-review}/REQUIREMENT.md +4 -4
- package/claude/skills/{claude-memory-review → memory-review}/SKILL.md +10 -10
- package/claude/skills/migration-context/SKILL.md +1 -1
- package/claude/skills/migration-standards-drop/REQUIREMENT.md +1 -1
- package/claude/skills/migration-superseded/REQUIREMENT.md +1 -1
- package/claude/skills/{claude-feature → plan-feature}/REQUIREMENT.md +4 -4
- package/claude/skills/{claude-feature → plan-feature}/SKILL.md +4 -4
- package/claude/skills/{claude-groundwork → plan-groundwork}/REQUIREMENT.md +5 -5
- package/claude/skills/{claude-groundwork → plan-groundwork}/SKILL.md +8 -8
- package/claude/skills/{claude-intake → plan-intake}/REQUIREMENT.md +6 -6
- package/claude/skills/{claude-intake → plan-intake}/SKILL.md +10 -10
- package/claude/skills/{claude-intake-answer → plan-intake-answer}/REQUIREMENT.md +4 -4
- package/claude/skills/{claude-intake-answer → plan-intake-answer}/SKILL.md +5 -5
- package/claude/skills/{claude-address-review → review-address}/REQUIREMENT.md +5 -5
- package/claude/skills/{claude-address-review → review-address}/SKILL.md +6 -6
- package/claude/skills/{claude-address-review → review-address}/references/rebase-conflicts.md +1 -1
- package/claude/skills/{claude-review → review-branch}/REQUIREMENT.md +4 -4
- package/claude/skills/{claude-review → review-branch}/SKILL.md +4 -4
- package/claude/skills/{claude-pr-review → review-pr}/REQUIREMENT.md +5 -5
- package/claude/skills/{claude-pr-review → review-pr}/SKILL.md +11 -11
- package/claude/skills/{claude-orchestrate → role-orchestrator}/REQUIREMENT.md +5 -5
- package/claude/skills/{claude-orchestrate → role-orchestrator}/SKILL.md +20 -20
- package/claude/skills/{claude-orchestrate → role-orchestrator}/references/orchestrator-dispatch.md +30 -30
- package/claude/skills/{claude-orchestrate → role-orchestrator}/references/orchestrator-handoff.md +2 -2
- package/claude/skills/{claude-orchestrate → role-orchestrator}/references/orchestrator-parked.md +8 -8
- package/claude/skills/{claude-orchestrate → role-orchestrator}/references/orchestrator-poll.md +8 -8
- package/claude/skills/{claude-orchestrate → role-orchestrator}/references/orchestrator-resume.md +1 -1
- package/claude/skills/{claude-orchestrate → role-orchestrator}/references/orchestrator-sweep.md +1 -1
- package/claude/skills/{claude-orchestrate → role-orchestrator}/scripts/poll.sh +6 -6
- package/claude/skills/{claude-planner → role-planner}/REQUIREMENT.md +12 -12
- package/claude/skills/{claude-planner → role-planner}/SKILL.md +4 -4
- package/claude/skills/{claude-worker → role-worker}/REQUIREMENT.md +8 -8
- package/claude/skills/{claude-worker → role-worker}/SKILL.md +6 -6
- package/claude/skills/{claude-seed-sync → seed-sync}/REQUIREMENT.md +2 -2
- package/claude/skills/{claude-seed-sync → seed-sync}/SKILL.md +3 -3
- package/claude/skills/session-map/REQUIREMENT.md +2 -2
- package/claude/skills/session-map/SKILL.md +2 -2
- package/claude/skills/session-resume/REQUIREMENT.md +2 -2
- package/claude/skills/session-resume/SKILL.md +2 -2
- package/claude/skills/{claude-worktree → session-worktree}/REQUIREMENT.md +3 -3
- package/claude/skills/{claude-worktree → session-worktree}/SKILL.md +6 -6
- package/claude/skills/setup-plugins/references/plugin-catalog.md +1 -1
- package/claude/skills/{claude-standards-audit → standards-audit}/REQUIREMENT.md +2 -2
- package/claude/skills/{claude-standards-audit → standards-audit}/SKILL.md +2 -2
- package/claude/skills/systematic-debugging/REQUIREMENT.md +1 -1
- package/claude/skills/{claude-tasks → task-board}/REQUIREMENT.md +3 -3
- package/claude/skills/{claude-tasks → task-board}/SKILL.md +7 -7
- package/claude/skills/{claude-teach → teach-workspace}/REQUIREMENT.md +2 -2
- package/claude/skills/{claude-teach → teach-workspace}/SKILL.md +4 -4
- package/claude/skills/test-first/REQUIREMENT.md +2 -2
- package/claude/skills/{claude-ui-test → ui-test}/REQUIREMENT.md +3 -3
- package/claude/skills/{claude-ui-test → ui-test}/SKILL.md +3 -3
- package/claude/skills/{claude-ux-audit → ux-audit}/REQUIREMENT.md +6 -6
- package/claude/skills/{claude-ux-audit → ux-audit}/SKILL.md +5 -5
- package/claude/skills/{claude-ux-measure → ux-measure}/REQUIREMENT.md +5 -5
- package/claude/skills/{claude-ux-measure → ux-measure}/SKILL.md +5 -5
- package/docs/agents/commands.md +4 -1
- package/docs/agents/install-and-sync.md +1 -1
- package/docs/agents/key-changes.md +3 -3
- package/docs/agents/markdown-audit.md +1 -1
- package/docs/agents/restated.md +1 -1
- package/docs/agents/review-classification.md +1 -1
- package/docs/agents/sessions.md +1 -1
- package/docs/agents/state-scoped-risk.md +1 -1
- package/docs/agents/targets.md +1 -1
- package/docs/agents/tasks.md +4 -4
- package/docs/agents/teach.md +1 -1
- package/docs/target-projects.md +28 -9
- package/docs/workflow/ai-workflow.md +84 -84
- package/docs/workflow/operating-model.md +17 -17
- package/docs/workflow/visual-design-workflow.md +6 -6
- package/governance/rules/core/045-memory.md +1 -1
- package/governance/rules/core/085-worktrees.md +1 -1
- package/package.json +1 -1
- package/scripts/core/regen-agent-fixture.sh +1 -1
- package/scripts/core/regen-hero.sh +7 -7
- package/snippets/claude/decision-memo.md +1 -1
- package/src/autoship/paths.ts +1 -1
- package/src/claude/cases/all.ts +2 -2
- package/src/claude/cases/{claude-workflow.ts → workflow.ts} +33 -33
- package/src/commands/migrate.ts +90 -13
- package/src/commands/sync.ts +3 -3
- package/src/design/components.ts +1157 -4
- package/src/design/css.ts +44 -17
- package/src/design/tokens.ts +1 -1
- package/src/gov/restated.ts +2 -2
- package/src/markdown/structure.ts +1 -1
- package/src/migrate/plan.ts +13 -5
- package/src/migrate/rename.ts +210 -94
- package/src/migrate/skill-names.ts +89 -0
- package/src/pr/bijection.ts +1 -1
- package/src/pr/paths.ts +2 -2
- package/src/shipped/references.ts +1 -1
- package/src/sync/seeds-report.ts +1 -1
- package/src/targets/pulls.ts +2 -2
- package/src/tasks/answers.ts +1 -1
- package/src/tasks/archive.ts +4 -4
- package/src/tasks/record.ts +2 -2
- package/src/tasks/validate.ts +1 -1
- package/src/teach/fonts.ts +28 -0
- package/src/teach/nav.ts +11 -1
- package/src/teach/workspace.ts +4 -1
- package/standards/groundwork.md +1 -1
- package/standards/snippets.md +1 -1
- package/standards/tasks.md +3 -3
- package/standards/teach.md +2 -2
- package/tooling/base/configs/.husky/post-merge +3 -3
- package/tooling/claude/reference.md +5 -5
- package/tooling/claude/seeds/.claude/hooks/pr-create-log.sh +2 -2
- /package/claude/skills/{claude-memory-review → memory-review}/references/receipt-format.md +0 -0
- /package/claude/skills/{claude-orchestrate → role-orchestrator}/scripts/watch.sh +0 -0
- /package/claude/skills/{claude-teach → teach-workspace}/references/lesson-craft.md +0 -0
- /package/claude/skills/{claude-teach → teach-workspace}/references/pedagogy.md +0 -0
- /package/claude/skills/{claude-teach → teach-workspace}/references/promotion.md +0 -0
package/src/design/css.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import type { Component } from '@/design/components'
|
|
1
2
|
import { COMPONENTS } from '@/design/components'
|
|
3
|
+
import type { FontFace } from '@/design/fonts'
|
|
2
4
|
import { FONT_FACES } from '@/design/fonts'
|
|
3
5
|
import type { DesignTokens } from '@/design/tokens'
|
|
4
6
|
import { TOKENS } from '@/design/tokens'
|
|
@@ -113,39 +115,52 @@ ${pairs}
|
|
|
113
115
|
}`
|
|
114
116
|
}
|
|
115
117
|
|
|
116
|
-
function componentBlock(): string {
|
|
117
|
-
return
|
|
118
|
-
(
|
|
118
|
+
function componentBlock(components: readonly Component[]): string {
|
|
119
|
+
return components
|
|
120
|
+
.map(
|
|
121
|
+
(component) => `/* ${component.name}
|
|
119
122
|
${component.note} */
|
|
120
123
|
|
|
121
124
|
${component.rules}`,
|
|
122
|
-
|
|
125
|
+
)
|
|
126
|
+
.join('\n\n')
|
|
123
127
|
}
|
|
124
128
|
|
|
125
129
|
/**
|
|
126
|
-
* `@font-face` rules carrying
|
|
127
|
-
*
|
|
128
|
-
*
|
|
130
|
+
* `@font-face` rules carrying a font list as base64, so a stylesheet renders
|
|
131
|
+
* in the same typeface everywhere regardless of what the reader's machine has
|
|
132
|
+
* installed. Defaults to the mono stack's primary family; teach passes its own
|
|
133
|
+
* three faces instead of widening this default for every consumer.
|
|
129
134
|
*/
|
|
130
|
-
function fontFaceBlock(): string {
|
|
131
|
-
return
|
|
132
|
-
(
|
|
135
|
+
function fontFaceBlock(faces: readonly FontFace[]): string {
|
|
136
|
+
return faces
|
|
137
|
+
.map(
|
|
138
|
+
(face) => `@font-face {
|
|
133
139
|
font-family: '${face.family}';
|
|
134
140
|
font-weight: ${face.weight};
|
|
135
141
|
font-style: normal;
|
|
136
142
|
font-display: swap;
|
|
137
143
|
src: url(data:font/woff2;base64,${face.base64}) format('woff2');
|
|
138
144
|
}`,
|
|
139
|
-
|
|
145
|
+
)
|
|
146
|
+
.join('\n\n')
|
|
140
147
|
}
|
|
141
148
|
|
|
142
149
|
export interface CssOptions {
|
|
143
150
|
/** Prepended as a comment, naming what wrote the file and from where. */
|
|
144
151
|
readonly banner?: string
|
|
145
|
-
/**
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
152
|
+
/**
|
|
153
|
+
* Component rules ride along by default; a token-only consumer opts out
|
|
154
|
+
* with `false`. Pass an explicit list, such as teach's own chrome set, to
|
|
155
|
+
* emit those instead of the generic default.
|
|
156
|
+
*/
|
|
157
|
+
readonly components?: boolean | readonly Component[]
|
|
158
|
+
/**
|
|
159
|
+
* Off by default. `true` embeds the mono stack's faces as base64
|
|
160
|
+
* `@font-face` rules. Pass an explicit list, such as teach's three faces,
|
|
161
|
+
* to embed those instead.
|
|
162
|
+
*/
|
|
163
|
+
readonly embedFonts?: boolean | readonly FontFace[]
|
|
149
164
|
}
|
|
150
165
|
|
|
151
166
|
export function buildDesignCss(
|
|
@@ -155,10 +170,22 @@ export function buildDesignCss(
|
|
|
155
170
|
const banner =
|
|
156
171
|
options.banner === undefined ? '' : `/* ${options.banner} */\n\n`
|
|
157
172
|
const root = [':root {', ...tokenProperties(tokens), '}'].join('\n')
|
|
158
|
-
const
|
|
173
|
+
const faces =
|
|
174
|
+
options.embedFonts === true
|
|
175
|
+
? FONT_FACES
|
|
176
|
+
: Array.isArray(options.embedFonts)
|
|
177
|
+
? options.embedFonts
|
|
178
|
+
: undefined
|
|
179
|
+
const parts = faces ? [fontFaceBlock(faces), root] : [root]
|
|
159
180
|
parts.push(lightBlock(tokens))
|
|
160
181
|
|
|
161
|
-
|
|
182
|
+
const components =
|
|
183
|
+
options.components === false
|
|
184
|
+
? undefined
|
|
185
|
+
: Array.isArray(options.components)
|
|
186
|
+
? options.components
|
|
187
|
+
: COMPONENTS
|
|
188
|
+
if (components) parts.push(componentBlock(components))
|
|
162
189
|
|
|
163
190
|
return `${banner}${parts.join('\n\n')}\n`
|
|
164
191
|
}
|
package/src/design/tokens.ts
CHANGED
|
@@ -354,7 +354,7 @@ export const TOKENS: DesignTokens = {
|
|
|
354
354
|
'Motion is not used. No transition, animation, or keyframe declaration appears on any rendered surface, and the capture pipeline screenshots a static frame.',
|
|
355
355
|
|
|
356
356
|
iconography:
|
|
357
|
-
"No icon library is installed. `assets/brand/mark.svg` is the one authored icon, embedded inline in the hero topbar, and the surfaces otherwise draw literal glyph characters: `│ ├ ✓ ! ✗ + - ◆ ◇ ❯` for the terminal framing. The same mark also ships as a favicon on every rendered surface, as three independently-maintained copies that track different accents by design rather than by drift: `regen-hero.sh` derives one from the live SVG colored with whatever `--color-accent` (`#e0724b`) the fetched token CSS carries, `src/design/render.ts` carries the path data as a hardcoded literal colored via `colorValue('light-accent')` (`#a4471c`), since a data URI has no CSS context and that page renders on light chrome, and `
|
|
357
|
+
"No icon library is installed. `assets/brand/mark.svg` is the one authored icon, embedded inline in the hero topbar, and the surfaces otherwise draw literal glyph characters: `│ ├ ✓ ! ✗ + - ◆ ◇ ❯` for the terminal framing. The same mark also ships as a favicon on every rendered surface, as three independently-maintained copies that track different accents by design rather than by drift: `regen-hero.sh` derives one from the live SVG colored with whatever `--color-accent` (`#e0724b`) the fetched token CSS carries, `src/design/render.ts` carries the path data as a hardcoded literal colored via `colorValue('light-accent')` (`#a4471c`), since a data URI has no CSS context and that page renders on light chrome, and `teach-workspace`'s `SKILL.md` names one in prose colored `rgb(224,114,75)`, the same value as the dark accent written as decimal rather than hex to clear the shipped-references gate's commit-sha check. Unifying the three or repairing the one that looks drifted would break the fit each was chosen for.",
|
|
358
358
|
}
|
|
359
359
|
|
|
360
360
|
/** A role's value, or `undefined` where the record declares no such role. */
|
package/src/gov/restated.ts
CHANGED
|
@@ -24,7 +24,7 @@ export const RULES_REL = join('governance', 'rules')
|
|
|
24
24
|
/**
|
|
25
25
|
* Path pairs whose duplication is deliberate and already recorded.
|
|
26
26
|
*
|
|
27
|
-
* The seed is authored from the always-loaded file and `
|
|
27
|
+
* The seed is authored from the always-loaded file and `seed-sync`
|
|
28
28
|
* exists to reconcile the two, so a bullet appearing in both is the design
|
|
29
29
|
* rather than a defect. Excluding by pair rather than by content is what the
|
|
30
30
|
* plan settled on: the duplication is a location fact this repository already
|
|
@@ -574,7 +574,7 @@ function authorityFor(
|
|
|
574
574
|
if (candidate.kind === 'seed') {
|
|
575
575
|
return {
|
|
576
576
|
authority: 'claude-md',
|
|
577
|
-
reason: `${INSTRUCTIONS_REL} is authored first and the seed carries it to a target, so an edit starts there and reaches the seed through
|
|
577
|
+
reason: `${INSTRUCTIONS_REL} is authored first and the seed carries it to a target, so an edit starts there and reaches the seed through seed-sync`,
|
|
578
578
|
}
|
|
579
579
|
}
|
|
580
580
|
|
|
@@ -26,7 +26,7 @@ const HEADING = /^#{1,6}\s/
|
|
|
26
26
|
* run and a false one shortens every run around it until the measure stops
|
|
27
27
|
* reporting, which is the dearer of the two.
|
|
28
28
|
*
|
|
29
|
-
* A colon ends most markers and not all of them. `
|
|
29
|
+
* A colon ends most markers and not all of them. `review-pr` writes
|
|
30
30
|
* three colon-less ones into every body it posts and a bold path heading for
|
|
31
31
|
* each file it reviews, so requiring the colon held a real seam out. Two
|
|
32
32
|
* signals stand in where the colon is absent, because the shape a marker has to
|
package/src/migrate/plan.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
isExcludedPath,
|
|
3
3
|
renamePath,
|
|
4
|
+
type RenameRules,
|
|
4
5
|
renameText,
|
|
5
6
|
scanText,
|
|
6
7
|
} from '@/migrate/rename'
|
|
@@ -59,20 +60,27 @@ export function isToolkitOwned(path: string): boolean {
|
|
|
59
60
|
* reported or applied. A file whose content and path both stay put is dropped
|
|
60
61
|
* rather than carried as a no-op entry, which keeps the reported count equal
|
|
61
62
|
* to the number of files the sweep actually changes.
|
|
63
|
+
*
|
|
64
|
+
* The rules arrive as an argument rather than being read from a module, since
|
|
65
|
+
* this planner serves every rename the engine compiles and the four calls
|
|
66
|
+
* below have no other way to say which one they mean.
|
|
62
67
|
*/
|
|
63
|
-
export function planRename(
|
|
68
|
+
export function planRename(
|
|
69
|
+
sources: readonly RenameSource[],
|
|
70
|
+
rules: RenameRules,
|
|
71
|
+
): RenamePlan {
|
|
64
72
|
const entries: RenameEntry[] = []
|
|
65
73
|
const excluded: string[] = []
|
|
66
74
|
|
|
67
75
|
for (const source of sources) {
|
|
68
|
-
if (isExcludedPath(source.path)) {
|
|
76
|
+
if (isExcludedPath(source.path, rules)) {
|
|
69
77
|
excluded.push(source.path)
|
|
70
78
|
continue
|
|
71
79
|
}
|
|
72
80
|
|
|
73
|
-
const movesTo = renamePath(source.path)
|
|
74
|
-
const rewritten = renameText(source.text)
|
|
75
|
-
const counts = scanText(source.text)
|
|
81
|
+
const movesTo = renamePath(source.path, rules)
|
|
82
|
+
const rewritten = renameText(source.text, rules)
|
|
83
|
+
const counts = scanText(source.text, rules)
|
|
76
84
|
const moved = movesTo !== source.path
|
|
77
85
|
const changed = rewritten !== source.text
|
|
78
86
|
|
package/src/migrate/rename.ts
CHANGED
|
@@ -1,98 +1,154 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The token rewrite behind
|
|
2
|
+
* The token rewrite behind a mechanical rename.
|
|
3
3
|
*
|
|
4
4
|
* The sweep is mechanical and its danger is entirely in what it must not
|
|
5
5
|
* touch, so the scanner is one pass with the protected forms tried first
|
|
6
6
|
* rather than a chain of replacements. A chain reprocesses its own output,
|
|
7
7
|
* which is how a protected form that contains the token gets rewritten by a
|
|
8
8
|
* later rule that cannot see it was already decided.
|
|
9
|
+
*
|
|
10
|
+
* The token map, the protected forms, and the exclusion set are a parameter
|
|
11
|
+
* rather than module state, so one engine serves more than one rename. Two
|
|
12
|
+
* sweeps sharing a module constant would have to agree on a single map, and
|
|
13
|
+
* the second rename this repository needed shares nothing with the first
|
|
14
|
+
* except the scanning discipline above.
|
|
9
15
|
*/
|
|
10
16
|
|
|
11
17
|
/**
|
|
12
|
-
*
|
|
13
|
-
* ahead of the token itself so they pass through untouched.
|
|
18
|
+
* An article whose agreement the rename breaks.
|
|
14
19
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
20
|
+
* Stated as a pattern and a replacement rather than a function so a preset is
|
|
21
|
+
* data a test can read back. It runs against the already-rewritten line, which
|
|
22
|
+
* is what keeps it clear of the protected forms: a form the scanner passed
|
|
23
|
+
* through still spells the old token afterward, so it never matches here.
|
|
19
24
|
*/
|
|
20
|
-
|
|
25
|
+
export interface ArticleFixup {
|
|
26
|
+
readonly pattern: RegExp
|
|
27
|
+
readonly replacement: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** One rename's rules, as an author states them. */
|
|
31
|
+
export interface RenameRuleSpec {
|
|
32
|
+
/** Every spelling of the token, and what each becomes. */
|
|
33
|
+
readonly replacements: Readonly<Record<string, string>>
|
|
34
|
+
/** Marks a line that names a retired spelling on purpose. */
|
|
35
|
+
readonly keepMarker: string
|
|
36
|
+
/** Forms carrying a token that name something the rename leaves alone. */
|
|
37
|
+
readonly protectedForms?: readonly string[]
|
|
38
|
+
/** Files whose content is left alone entirely. */
|
|
39
|
+
readonly excludedPaths?: readonly string[]
|
|
40
|
+
/** Path prefixes whose files are left alone entirely. */
|
|
41
|
+
readonly excludedPrefixes?: readonly string[]
|
|
42
|
+
readonly articleFixups?: readonly ArticleFixup[]
|
|
43
|
+
/**
|
|
44
|
+
* Whether a token has to end where the word ends.
|
|
45
|
+
*
|
|
46
|
+
* A rename whose tokens are whole names wants this, and one whose tokens are
|
|
47
|
+
* word stems cannot have it. `aitk` is a stem that legitimately carries a
|
|
48
|
+
* suffix, as in `aitk-allow-superseded`, so requiring a boundary there would
|
|
49
|
+
* leave every hyphenated form behind. A skill name is not a stem, and the
|
|
50
|
+
* spelling below is the pre-rename one on purpose (canon-keep-retired):
|
|
51
|
+
* `claude-worktree` inside `claude-worktrees` is a different subject, the
|
|
52
|
+
* wiki page about the harness feature, which the rename must not move.
|
|
53
|
+
*/
|
|
54
|
+
readonly wholeToken?: boolean
|
|
55
|
+
}
|
|
21
56
|
|
|
22
57
|
/**
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
58
|
+
* A spec with its scanner compiled and its alternatives ordered.
|
|
59
|
+
*
|
|
60
|
+
* `tokenOrder` is reported rather than kept private because the ordering is a
|
|
61
|
+
* correctness property a caller has to be able to assert. A token containing a
|
|
62
|
+
* shorter token has to be tried first, and reading that off the rewritten
|
|
63
|
+
* string only works when the two happen to share a destination, which is
|
|
64
|
+
* correctness by accident rather than by rule.
|
|
27
65
|
*/
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
66
|
+
export interface RenameRules {
|
|
67
|
+
readonly replacements: Readonly<Record<string, string>>
|
|
68
|
+
readonly keepMarker: string
|
|
69
|
+
readonly protectedForms: readonly string[]
|
|
70
|
+
readonly tokenOrder: readonly string[]
|
|
71
|
+
readonly excludedPaths: readonly string[]
|
|
72
|
+
readonly excludedPrefixes: readonly string[]
|
|
73
|
+
readonly articleFixups: readonly ArticleFixup[]
|
|
74
|
+
readonly scan: RegExp
|
|
32
75
|
}
|
|
33
76
|
|
|
34
77
|
/**
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
78
|
+
* A branch that can never take, standing in for an empty protected list.
|
|
79
|
+
*
|
|
80
|
+
* The scanner reads a protected match off capture group 1, so a preset that
|
|
81
|
+
* protects nothing still has to emit that group or every later group shifts by
|
|
82
|
+
* one. An empty alternation would match the empty string at every position
|
|
83
|
+
* instead, which reports a protected hit on every character.
|
|
38
84
|
*/
|
|
39
|
-
const
|
|
40
|
-
`(${PROTECTED.join('|')})|(${Object.keys(REPLACEMENT).join('|')})`,
|
|
41
|
-
'g',
|
|
42
|
-
)
|
|
85
|
+
const NEVER_MATCHES = '(?!)'
|
|
43
86
|
|
|
44
87
|
/**
|
|
45
|
-
*
|
|
88
|
+
* What may not follow a token when a preset asks for whole tokens.
|
|
46
89
|
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
* into its own replacement and leaves a rewriter that maps `canon` to `canon`
|
|
55
|
-
* and matches nothing. Its tests name both spellings on purpose for the same
|
|
56
|
-
* reason, and the command's help text documents the old name a caller is
|
|
57
|
-
* migrating off. Whatever these four files should say after the rename is
|
|
58
|
-
* written by hand, because the sweep cannot be the thing that decides it.
|
|
90
|
+
* A plain word boundary rejects a following letter and accepts a following
|
|
91
|
+
* hyphen, since `\b` reads a hyphen as the end of a word. That leaves
|
|
92
|
+
* `plan-intake` matching inside `plan-intake-answer`, with the ordering of
|
|
93
|
+
* the alternation the only thing standing between them. Naming the characters
|
|
94
|
+
* that continue an identifier holds on both, so the ordering and the boundary
|
|
95
|
+
* each cover what the other could miss, and a slash, a dot, or a backtick
|
|
96
|
+
* still ends a token.
|
|
59
97
|
*/
|
|
98
|
+
const TOKEN_TAIL = '(?![A-Za-z0-9_-])'
|
|
99
|
+
|
|
100
|
+
function escapeForPattern(value: string): string {
|
|
101
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
102
|
+
}
|
|
103
|
+
|
|
60
104
|
/**
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
105
|
+
* Longest first, so a token is never consumed by a shorter token it contains.
|
|
106
|
+
*
|
|
107
|
+
* The sort is stable, so alternatives of equal length keep the order the
|
|
108
|
+
* author wrote them in and a preset whose tokens are all one length compiles
|
|
109
|
+
* to the alternation it already had.
|
|
66
110
|
*/
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
]
|
|
111
|
+
function longestFirst(values: readonly string[]): readonly string[] {
|
|
112
|
+
return [...values].sort((left, right) => right.length - left.length)
|
|
113
|
+
}
|
|
71
114
|
|
|
72
115
|
/**
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
* the current-variable case beside it and keep passing, reporting coverage for
|
|
77
|
-
* a fallback nothing exercises any more.
|
|
116
|
+
* One alternation so the engine decides each position once. Group 1 is a
|
|
117
|
+
* protected form and group 2 is a token to rewrite, and the protected branch
|
|
118
|
+
* sits first because a regex alternation is ordered.
|
|
78
119
|
*/
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
120
|
+
function compileScan(
|
|
121
|
+
protectedForms: readonly string[],
|
|
122
|
+
tokenOrder: readonly string[],
|
|
123
|
+
wholeToken: boolean,
|
|
124
|
+
): RegExp {
|
|
125
|
+
const guarded =
|
|
126
|
+
protectedForms.length > 0
|
|
127
|
+
? protectedForms.map(escapeForPattern).join('|')
|
|
128
|
+
: NEVER_MATCHES
|
|
87
129
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
130
|
+
const tail = wholeToken ? TOKEN_TAIL : ''
|
|
131
|
+
|
|
132
|
+
return new RegExp(
|
|
133
|
+
`(${guarded})|(${tokenOrder.map(escapeForPattern).join('|')})${tail}`,
|
|
134
|
+
'g',
|
|
135
|
+
)
|
|
91
136
|
}
|
|
92
137
|
|
|
93
|
-
export function
|
|
94
|
-
|
|
95
|
-
|
|
138
|
+
export function defineRenameRules(spec: RenameRuleSpec): RenameRules {
|
|
139
|
+
const protectedForms = longestFirst(spec.protectedForms ?? [])
|
|
140
|
+
const tokenOrder = longestFirst(Object.keys(spec.replacements))
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
replacements: spec.replacements,
|
|
144
|
+
keepMarker: spec.keepMarker,
|
|
145
|
+
protectedForms,
|
|
146
|
+
tokenOrder,
|
|
147
|
+
excludedPaths: spec.excludedPaths ?? [],
|
|
148
|
+
excludedPrefixes: spec.excludedPrefixes ?? [],
|
|
149
|
+
articleFixups: spec.articleFixups ?? [],
|
|
150
|
+
scan: compileScan(protectedForms, tokenOrder, spec.wholeToken === true),
|
|
151
|
+
}
|
|
96
152
|
}
|
|
97
153
|
|
|
98
154
|
/**
|
|
@@ -100,54 +156,114 @@ export function isExcludedPath(path: string): boolean {
|
|
|
100
156
|
* agrees with the replacement.
|
|
101
157
|
*
|
|
102
158
|
* The old name opens on a vowel sound and the new one does not, so every
|
|
103
|
-
* `an aitk` in the corpus reads wrong the moment the token moves.
|
|
104
|
-
* against the already-rewritten text rather than the source, which is what
|
|
105
|
-
* keeps it clear of the protected forms: `an aitk-sandbox` still says
|
|
106
|
-
* `aitk-sandbox` afterward, so it never matches here.
|
|
159
|
+
* `an aitk` in the corpus reads wrong the moment the token moves.
|
|
107
160
|
*
|
|
108
161
|
* The tail rejects a following letter rather than asking for a word boundary,
|
|
109
162
|
* which is what separates `an canonical` from `an CANON_STATE_DIR`. A boundary
|
|
110
163
|
* treats the underscore as part of the word and declines the environment
|
|
111
164
|
* variable, where the whole identifier is the token continuing.
|
|
112
165
|
*/
|
|
113
|
-
const
|
|
166
|
+
const AITK_ARTICLE = /\b([Aa])n(\s+`?)(canon|CANON|Canon)(?![A-Za-z])/g
|
|
114
167
|
|
|
115
168
|
/**
|
|
116
|
-
*
|
|
169
|
+
* The `aitk` to `canon` rename.
|
|
117
170
|
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
171
|
+
* `aitk-sandbox` is a separate repository that is not being renamed. It has to
|
|
172
|
+
* win against the bare token, and it also has to win against the owner-scoped
|
|
173
|
+
* spelling, since `erclx/aitk-sandbox` would otherwise rewrite to
|
|
174
|
+
* `erclx/canon-sandbox` and name a repository that does not exist.
|
|
175
|
+
*
|
|
176
|
+
* Case is carried in the map rather than derived, because the uppercase form
|
|
177
|
+
* is an environment variable prefix and the title-case form is a heading word,
|
|
178
|
+
* and a derived transform would have to guess which convention it was looking
|
|
179
|
+
* at.
|
|
180
|
+
*
|
|
181
|
+
* The changelog is release history. Its entries record what shipped under the
|
|
182
|
+
* old name, so rewriting them falsifies the record, and the pull request links
|
|
183
|
+
* it carries keep resolving because GitHub redirects a renamed repository's
|
|
184
|
+
* old URLs. An eval result is a transcript on the same argument, recording the
|
|
185
|
+
* commands a session actually ran under whatever name was current then.
|
|
186
|
+
*
|
|
187
|
+
* The sweep's own source is the other excluded member, and it is not a
|
|
188
|
+
* preference. This module states the `aitk` map as literal keys, so rewriting
|
|
189
|
+
* it turns every key into its own replacement and leaves a rewriter that maps
|
|
190
|
+
* `canon` to `canon` and matches nothing.
|
|
191
|
+
*
|
|
192
|
+
* The four test files below exist to prove the retired spellings still
|
|
193
|
+
* resolve, so both names appear in each on purpose. Rewriting one is worse
|
|
194
|
+
* than a broken test: the retired-variable case would collapse into a copy of
|
|
195
|
+
* the current-variable case beside it and keep passing, reporting coverage for
|
|
196
|
+
* a fallback nothing exercises any more.
|
|
124
197
|
*/
|
|
125
|
-
const
|
|
198
|
+
export const AITK_RULES: RenameRules = defineRenameRules({
|
|
199
|
+
replacements: {
|
|
200
|
+
aitk: 'canon',
|
|
201
|
+
AITK: 'CANON',
|
|
202
|
+
Aitk: 'Canon',
|
|
203
|
+
},
|
|
204
|
+
keepMarker: 'canon-keep-retired',
|
|
205
|
+
protectedForms: ['aitk-sandbox'],
|
|
206
|
+
excludedPrefixes: ['src/migrate/', 'scripts/eval/result-'],
|
|
207
|
+
excludedPaths: [
|
|
208
|
+
'CHANGELOG.md',
|
|
209
|
+
'src/commands/migrate.ts',
|
|
210
|
+
'src/sync/stamp.test.ts',
|
|
211
|
+
'src/targets/registry.test.ts',
|
|
212
|
+
'src/targets/sweep.test.ts',
|
|
213
|
+
'src/ui.test.ts',
|
|
214
|
+
],
|
|
215
|
+
articleFixups: [{ pattern: AITK_ARTICLE, replacement: '$1$2$3' }],
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
export interface ScanCount {
|
|
219
|
+
readonly renamed: number
|
|
220
|
+
readonly protectedCount: number
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function isExcludedPath(path: string, rules: RenameRules): boolean {
|
|
224
|
+
if (rules.excludedPaths.includes(path)) return true
|
|
225
|
+
return rules.excludedPrefixes.some((prefix) => path.startsWith(prefix))
|
|
226
|
+
}
|
|
126
227
|
|
|
127
228
|
/** Rewrites every unprotected spelling of the token. */
|
|
128
|
-
export function renameText(text: string): string {
|
|
229
|
+
export function renameText(text: string, rules: RenameRules): string {
|
|
129
230
|
const lines = text.split('\n')
|
|
130
231
|
const rewritten = lines.map((line, index) =>
|
|
131
|
-
isKept(lines, index) ? line : renameLine(line),
|
|
232
|
+
isKept(lines, index, rules) ? line : renameLine(line, rules),
|
|
132
233
|
)
|
|
133
234
|
|
|
134
235
|
return rewritten.join('\n')
|
|
135
236
|
}
|
|
136
237
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
238
|
+
/**
|
|
239
|
+
* Whether a line names a retired spelling on purpose.
|
|
240
|
+
*
|
|
241
|
+
* A fallback path, a retired environment variable, and a dictionary entry
|
|
242
|
+
* covering the record corpora all have to keep saying the old name, and a
|
|
243
|
+
* second run over an already-renamed tree would otherwise strip exactly the
|
|
244
|
+
* compatibility a rename shipped. The marker sits on the line itself or on the
|
|
245
|
+
* one above it, which is the same placement `canon-allow-superseded` already
|
|
246
|
+
* uses in this repository.
|
|
247
|
+
*/
|
|
248
|
+
function isKept(
|
|
249
|
+
lines: readonly string[],
|
|
250
|
+
index: number,
|
|
251
|
+
rules: RenameRules,
|
|
252
|
+
): boolean {
|
|
253
|
+
if (lines[index]?.includes(rules.keepMarker)) return true
|
|
254
|
+
return index > 0 && (lines[index - 1]?.includes(rules.keepMarker) ?? false)
|
|
140
255
|
}
|
|
141
256
|
|
|
142
|
-
function renameLine(line: string): string {
|
|
143
|
-
const replaced = line.replace(
|
|
144
|
-
|
|
257
|
+
function renameLine(line: string, rules: RenameRules): string {
|
|
258
|
+
const replaced = line.replace(
|
|
259
|
+
rules.scan,
|
|
260
|
+
(match, guarded: string | undefined) =>
|
|
261
|
+
guarded === undefined ? rules.replacements[match] : guarded,
|
|
145
262
|
)
|
|
146
263
|
|
|
147
|
-
return
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
`${article}${gap}${token}`,
|
|
264
|
+
return rules.articleFixups.reduce(
|
|
265
|
+
(text, fixup) => text.replace(fixup.pattern, fixup.replacement),
|
|
266
|
+
replaced,
|
|
151
267
|
)
|
|
152
268
|
}
|
|
153
269
|
|
|
@@ -156,15 +272,15 @@ function renameLine(line: string): string {
|
|
|
156
272
|
* from a diff so a run can say how much it protected, which is the number a
|
|
157
273
|
* reader needs to trust that the exclusions fired at all.
|
|
158
274
|
*/
|
|
159
|
-
export function scanText(text: string): ScanCount {
|
|
275
|
+
export function scanText(text: string, rules: RenameRules): ScanCount {
|
|
160
276
|
let renamed = 0
|
|
161
277
|
let protectedCount = 0
|
|
162
278
|
const lines = text.split('\n')
|
|
163
279
|
|
|
164
280
|
for (const [index, line] of lines.entries()) {
|
|
165
|
-
if (isKept(lines, index)) continue
|
|
281
|
+
if (isKept(lines, index, rules)) continue
|
|
166
282
|
|
|
167
|
-
for (const [, guarded] of line.matchAll(
|
|
283
|
+
for (const [, guarded] of line.matchAll(rules.scan)) {
|
|
168
284
|
if (guarded === undefined) renamed += 1
|
|
169
285
|
else protectedCount += 1
|
|
170
286
|
}
|
|
@@ -178,6 +294,6 @@ export function scanText(text: string): ScanCount {
|
|
|
178
294
|
* a protected form appearing in a path is protected there too and the two
|
|
179
295
|
* halves of the sweep cannot disagree about what the token means.
|
|
180
296
|
*/
|
|
181
|
-
export function renamePath(path: string): string {
|
|
182
|
-
return renameText(path)
|
|
297
|
+
export function renamePath(path: string, rules: RenameRules): string {
|
|
298
|
+
return renameText(path, rules)
|
|
183
299
|
}
|