@erclx/canon 4.79.0 → 4.80.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/canon-rollout/SKILL.md +1 -1
- package/claude/skills/git-pr/SKILL.md +3 -3
- package/claude/skills/git-pr/references/labels.md +1 -1
- package/docs/agents/audits.md +2 -2
- package/docs/agents/commands.md +2 -2
- package/docs/agents/install-and-sync.md +3 -3
- package/docs/agents/label-coverage.md +1 -1
- package/docs/target-projects.md +1 -1
- package/package.json +1 -1
- package/src/audits/baseline.ts +17 -5
- package/src/commands/sync.ts +1 -1
- package/src/gate/measures.ts +4 -4
- package/src/labels/map.ts +19 -5
- package/src/legacy-path.ts +15 -0
- package/src/migrate/plan.ts +1 -0
- package/src/migrate/surface-roots.ts +5 -6
- package/src/surface-root.ts +7 -14
- package/src/sync/stamp.ts +22 -17
- package/src/targets/sweep.ts +6 -2
|
@@ -36,7 +36,7 @@ Every target in every wave takes the same branch and the same title, written fro
|
|
|
36
36
|
- Title: `chore(agents): <what changed in that target>`.
|
|
37
37
|
- Body: the sections `${CLAUDE_SKILL_DIR}/../../standards/pr.md` fixes, describing what that target received.
|
|
38
38
|
- Body, one line: name this toolkit as the source of the changed files and where to change the content upstream. A reader in that repository months later has no other route to the fact that these files are toolkit-managed and that a local edit is lost at the next wave.
|
|
39
|
-
- Body, no version number anywhere in it. `${CLAUDE_SKILL_DIR}/../../standards/versioning.md` permits a semver tag only where the pull request cuts a release, and
|
|
39
|
+
- Body, no version number anywhere in it. `${CLAUDE_SKILL_DIR}/../../standards/versioning.md` permits a semver tag only where the pull request cuts a release, and `canon/config/config.json` in the target already records `syncedAt` and a sha256 per synced file. A version in the body is a second copy of a fact the target holds canonically, and it is the copy that goes stale while the pull request sits open.
|
|
40
40
|
|
|
41
41
|
The title withholds the source and the body supplies it, because the two have different audiences. `agents` says what changed without saying where it came from, which is what a target's own history wants, and the source line is for a reader inside that repository rather than for whoever merges.
|
|
42
42
|
|
|
@@ -11,7 +11,7 @@ Read these files in parallel:
|
|
|
11
11
|
|
|
12
12
|
- `${CLAUDE_SKILL_DIR}/../../standards/branch.md`: branch format, valid types, and constraints
|
|
13
13
|
- `${CLAUDE_SKILL_DIR}/../../standards/pr.md`: structure, rules, and banned phrases
|
|
14
|
-
- `${CLAUDE_SKILL_DIR}/references/labels.md`: label map format, matching, and the missing-label warning. Skip when the project has no `.claude/canon/pr-labels.toml`.
|
|
14
|
+
- `${CLAUDE_SKILL_DIR}/references/labels.md`: label map format, matching, and the missing-label warning. Skip when the project has no `canon/config/pr-labels.toml` and no `.claude/canon/pr-labels.toml`.
|
|
15
15
|
- `${CLAUDE_SKILL_DIR}/../../standards/markdown.md`: banned words, punctuation, and formatting for all generated text
|
|
16
16
|
- The `write-human` skill: voice, rhythm, and sentence construction for all generated text
|
|
17
17
|
- `${CLAUDE_SKILL_DIR}/../../standards/versioning.md`: phase label vs semver discipline
|
|
@@ -119,7 +119,7 @@ A `reason` of `no-map` is the answer that the project declared no map, which ear
|
|
|
119
119
|
|
|
120
120
|
Every other `reason` is a map or a range the verb could not read, which is `unreadable-map`, `no-domains`, `no-base`, and `unreadable-changes`, plus `bad-base` for a ref this skill resolved wrongly. Take the fallback below and warn beside the result line, naming the reason. A map with a typo in it still has rows a prefix match can reach, and reading the refusal as an absence would open the pull request with no labels and nothing said, which is the surface merging bare that the verb exists to name.
|
|
121
121
|
|
|
122
|
-
The fallback is reading `.claude/canon/pr-labels.toml` and matching it against the name-only diff per `${CLAUDE_SKILL_DIR}/references/labels.md`. It also covers no record coming back at all, which is an installed `canon` predating the verb, since a skill reaches a target the moment it merges while the CLI reaches one only when a release publishes. The fallback labels correctly and reports no uncovered path, which is the half only the verb carries.
|
|
122
|
+
The fallback is reading `canon/config/pr-labels.toml`, or `.claude/canon/pr-labels.toml` when the project has not moved, and matching it against the name-only diff per `${CLAUDE_SKILL_DIR}/references/labels.md`. It also covers no record coming back at all, which is an installed `canon` predating the verb, since a skill reaches a target the moment it merges while the CLI reaches one only when a release publishes. Naming both spellings matters exactly here: the binary old enough to need this fallback is the same binary that may predate the move, so the project's map can still sit at the older path. The fallback labels correctly and reports no uncovered path, which is the half only the verb carries.
|
|
123
123
|
|
|
124
124
|
Leave `pr_labels` empty when no map resolves or no prefix matches, which skips the labelling command rather than running it against nothing.
|
|
125
125
|
|
|
@@ -223,7 +223,7 @@ Respond with one line, using the `url` the final command printed:
|
|
|
223
223
|
|
|
224
224
|
Add a line for each `uncovered` path the labels step reported, naming the path and the map it belongs in:
|
|
225
225
|
|
|
226
|
-
`⚠️ No label covers <path>. Add a row to
|
|
226
|
+
`⚠️ No label covers <path>. Add a row to canon/config/pr-labels.toml or a [declined] entry.`
|
|
227
227
|
|
|
228
228
|
Add a further line only when the labelling command printed its warning, quoting the label `gh` refused:
|
|
229
229
|
|
|
@@ -17,7 +17,7 @@ Does not govern:
|
|
|
17
17
|
|
|
18
18
|
## Map format
|
|
19
19
|
|
|
20
|
-
The map lives at
|
|
20
|
+
The map lives at `canon/config/pr-labels.toml` in the project root, or `.claude/canon/pr-labels.toml` for a project that has not moved. Each key under `[domains]` is a label name and its value is the list of path prefixes that earn it.
|
|
21
21
|
|
|
22
22
|
```toml
|
|
23
23
|
[domains]
|
package/docs/agents/audits.md
CHANGED
|
@@ -19,7 +19,7 @@ canon audits list --json
|
|
|
19
19
|
| ----------------- | ---------------------------------------------------------------------------------------------------------- |
|
|
20
20
|
| `--json` | Add a machine-readable record on stdout, keeping the frame |
|
|
21
21
|
| `--root <path>` | Measure this tree instead of the current worktree |
|
|
22
|
-
| `--record` | Write this run's tracked counts to
|
|
22
|
+
| `--record` | Write this run's tracked counts to `canon/config/baseline.json` |
|
|
23
23
|
| `--corpus <name>` | Limit the run to one corpus (`tracked`, `per-machine`, `upstream`), repeatable, defaulting to every corpus |
|
|
24
24
|
|
|
25
25
|
## What it runs
|
|
@@ -75,7 +75,7 @@ Every run states how many corpora it measured against how many it skipped, inclu
|
|
|
75
75
|
|
|
76
76
|
## The retained baseline
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
`canon/config/baseline.json` holds the counts from the last run recorded with `--record`, alongside the day it was taken and the commit it was read at, falling back to `.claude/canon/baseline.json` for a project that has not moved. Each later run reports which counts moved and by how much.
|
|
79
79
|
|
|
80
80
|
The file is committed. A per-machine record cannot answer the question this half exists for, which is whether a number grew since anyone last looked, and a fresh checkout has to inherit that answer rather than start over. The cost is that a branch moving a count either fixes it or re-records it and says why, which is the shape the consumed-copy assertion already carries.
|
|
81
81
|
|
package/docs/agents/commands.md
CHANGED
|
@@ -56,7 +56,7 @@ Full help: `canon <command> --help`. Behavior notes for the install and sync ver
|
|
|
56
56
|
| `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`) |
|
|
57
57
|
| `canon migrate skill-names` | Move the prefixed skill folders onto their two-word names and rewrite every citation, reporting the plan without `--write` (`--root`, `--json`) |
|
|
58
58
|
| `canon migrate records` | Move the gitignored session records to `.canon/` and repoint every tracked citation, reporting the plan without `--write` (`--root`, `--json`) |
|
|
59
|
-
| `canon migrate surface-roots` | Move the tracked surfaces (`context/`, `wireframes/`,
|
|
59
|
+
| `canon migrate surface-roots` | Move the tracked surfaces (`context/`, `wireframes/`, the three loose documents, and the install stamp folder) from `.claude/` to `canon/` with `git mv` and repoint every tracked citation, reporting the plan without `--write` (`--root`, `--json`) |
|
|
60
60
|
| `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`) |
|
|
61
61
|
| `canon migrate rule-layout` | Move a target's installed rules from the flat `.claude/rules/<subdir>/` layout to `.claude/rules/canon/<subdir>/`, reporting the plan without `--write` (`--root`, `--json`) |
|
|
62
62
|
| `canon migrate scratch-evidence` | Move a folder under `.tmp/` that a durable record cites as evidence to `.canon/review/evidence/`, repointing every citation live or archived, reporting the plan without `--write` (`--root`, `--json`) |
|
|
@@ -167,7 +167,7 @@ Common patterns:
|
|
|
167
167
|
|
|
168
168
|
A record folder already present at the destination is a refusal rather than a merge, and a line carrying `canon-keep-record-root`, or the nearest non-blank line below it, keeps the old spelling for prose that dates a decision. The records themselves are never swept: everything under `.canon/` and every `.claude/` record folder is passed over and reported as a count on its own line, which is what keeps the run that follows the ignore collapse touching the same files as one before it. Running it twice rewrites nothing, which is the check that the exclusions, the markers, and that skip all fired.
|
|
169
169
|
|
|
170
|
-
`migrate surface-roots` moves the tracked surfaces from `.claude/` to `canon/`: `context/`, `wireframes/`, `ARCHITECTURE.md`, `REQUIREMENTS.md`,
|
|
170
|
+
`migrate surface-roots` moves the tracked surfaces from `.claude/` to `canon/`: `context/`, `wireframes/`, `ARCHITECTURE.md`, `REQUIREMENTS.md`, `DESIGN.md`, and the install stamp folder, respelled `canon/config/` since a project under `canon/` reserves the bare `canon` name for the CLI's own install. Every path is tracked, so each file moves with `git mv` and its history follows it, and every tracked citation of one is rewritten in the same run. `rules/`, `skills/`, `hooks/`, and `settings.json` stay where Claude Code reads them. It reports until `--write` is passed, and a destination already present under `canon/` refuses the whole plan rather than splitting a surface across both roots.
|
|
171
171
|
|
|
172
172
|
A line carrying `canon-keep-surface-root`, or the nearest non-blank line above it, keeps the old spelling. A rule's frontmatter `paths:` glob, a citation shaped like a path into another repository, and one inside a dated paragraph are reported by file and line and left alone. Running it twice rewrites nothing, which is the idempotence check. A project that never runs it keeps resolving each surface under `.claude/`, since every reader takes whichever root holds the entry.
|
|
173
173
|
|
|
@@ -158,9 +158,9 @@ Use `--json` for the machine-readable report and `--exit-code` to fail a CI job.
|
|
|
158
158
|
and missing files are both excluded from that exit code: a project-authored
|
|
159
159
|
rule never converges, and a sync that added a missing one silently changes
|
|
160
160
|
what the project is governed by, which stays a separate command an operator
|
|
161
|
-
chooses to run. Attribution reads `canon/config/config.json` first
|
|
162
|
-
back to `.claude/canon/config.json
|
|
163
|
-
|
|
161
|
+
chooses to run. Attribution reads `canon/config/config.json` first, which every
|
|
162
|
+
install and sync now writes, and falls back to `.claude/canon/config.json` for
|
|
163
|
+
a target that has not moved.
|
|
164
164
|
|
|
165
165
|
A target installed before stamping shipped has no such file, and neither does
|
|
166
166
|
that fallback do anything to migrate it. A target stamped before the file
|
|
@@ -28,7 +28,7 @@ Under `--json` the record holds stdout alone and the frame still renders on stde
|
|
|
28
28
|
|
|
29
29
|
## The map it reads
|
|
30
30
|
|
|
31
|
-
The map sits at `.claude/canon/pr-labels.toml` and carries two tables. `[domains]` keys a label name to the path prefixes that earn it, and `[declined]` keys a reason to the prefixes that earn no label on purpose.
|
|
31
|
+
The map sits at `canon/config/pr-labels.toml`, falling back to `.claude/canon/pr-labels.toml` for a project that has not moved, and carries two tables. `[domains]` keys a label name to the path prefixes that earn it, and `[declined]` keys a reason to the prefixes that earn no label on purpose.
|
|
32
32
|
|
|
33
33
|
```toml
|
|
34
34
|
[domains]
|
package/docs/target-projects.md
CHANGED
|
@@ -208,7 +208,7 @@ The report opens by naming the binary running it. The installed version reads ag
|
|
|
208
208
|
|
|
209
209
|
A `stale` file still matches what the toolkit installed, so the update is mechanical. A `customized` file carries local edits, so taking the upstream version is a decision and `canon:seed-sync` is the tool for it. A `stranded` file sits where an older toolkit installed it and the toolkit has since moved, which is a relocation the report names but no command runs.
|
|
210
210
|
|
|
211
|
-
That attribution comes from `canon/config/config.json
|
|
211
|
+
That attribution comes from `canon/config/config.json`, which every install and sync now writes, falling back to `.claude/canon/config.json` for a target stamped before that move. A target stamped before that path shipped is read from the retired `.claude/canon.json` instead, reported rather than migrated. Governance records a hash per installed file, plus the stack `canon gov install` was given, and tooling records the stack chain it resolved instead of any file hash, since its install runs no per-file walk to attribute.
|
|
212
212
|
|
|
213
213
|
Each domain holds its own toolkit commit, so syncing governance today does not move the revision tooling measures against, and each domain reports the upstream commits touching its own source path. Running any sync stamps that domain, and the report names the ones still unstamped.
|
|
214
214
|
|
package/package.json
CHANGED
package/src/audits/baseline.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { mkdir, readFile, writeFile } from 'node:fs/promises'
|
|
2
|
-
import { dirname, join } from 'node:path'
|
|
2
|
+
import { dirname, join, relative } from 'node:path'
|
|
3
3
|
import type { AuditResult } from '@/audits/catalog'
|
|
4
|
+
import { resolveExisting } from '@/legacy-path'
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Where the retained counts live, relative to the project root.
|
|
@@ -15,7 +16,14 @@ import type { AuditResult } from '@/audits/catalog'
|
|
|
15
16
|
* that installs the CLI. A baseline in the package would hand a target this
|
|
16
17
|
* repository's counts to measure its own tree against.
|
|
17
18
|
*/
|
|
18
|
-
export const BASELINE_REL = join('
|
|
19
|
+
export const BASELINE_REL = join('canon', 'config', 'baseline.json')
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Where the baseline wrote before this move, `.claude/canon/baseline.json`.
|
|
23
|
+
* `readBaseline` falls back to it so a target that has not moved still reads
|
|
24
|
+
* its recorded floor, and the write no longer lands there.
|
|
25
|
+
*/
|
|
26
|
+
const LEGACY_BASELINE_REL = join('.claude', 'canon', 'baseline.json')
|
|
19
27
|
|
|
20
28
|
export interface Baseline {
|
|
21
29
|
/** The day the record was taken, as `YYYY-MM-DD`. */
|
|
@@ -165,7 +173,11 @@ function isBaseline(value: unknown): value is Baseline {
|
|
|
165
173
|
export async function readBaseline(
|
|
166
174
|
root: string,
|
|
167
175
|
): Promise<Baseline | undefined> {
|
|
168
|
-
const path =
|
|
176
|
+
const path = resolveExisting([
|
|
177
|
+
join(root, BASELINE_REL),
|
|
178
|
+
join(root, LEGACY_BASELINE_REL),
|
|
179
|
+
])
|
|
180
|
+
const rel = relative(root, path)
|
|
169
181
|
|
|
170
182
|
let raw: string
|
|
171
183
|
try {
|
|
@@ -178,12 +190,12 @@ export async function readBaseline(
|
|
|
178
190
|
try {
|
|
179
191
|
parsed = JSON.parse(raw)
|
|
180
192
|
} catch {
|
|
181
|
-
throw new Error(`${
|
|
193
|
+
throw new Error(`${rel} does not parse as JSON. Fix or delete it.`)
|
|
182
194
|
}
|
|
183
195
|
|
|
184
196
|
if (!isBaseline(parsed)) {
|
|
185
197
|
throw new Error(
|
|
186
|
-
`${
|
|
198
|
+
`${rel} carries no recordedAt, commit, and checks. Fix or delete it.`,
|
|
187
199
|
)
|
|
188
200
|
}
|
|
189
201
|
|
package/src/commands/sync.ts
CHANGED
|
@@ -101,7 +101,7 @@ function renderCheck(report: CheckReport): void {
|
|
|
101
101
|
|
|
102
102
|
if (report.stampAtLegacyPath) {
|
|
103
103
|
logWarn(
|
|
104
|
-
'Stamp found at a retired path. Move it to
|
|
104
|
+
'Stamp found at a retired path. Move it to canon/config/config.json.',
|
|
105
105
|
)
|
|
106
106
|
}
|
|
107
107
|
|
package/src/gate/measures.ts
CHANGED
|
@@ -126,11 +126,11 @@ export const SANDBOX_ASSERTED_FLOOR = 26
|
|
|
126
126
|
* Where the retained counts the audit stage compares each run against are
|
|
127
127
|
* read, relative to `root`.
|
|
128
128
|
*
|
|
129
|
-
* A function rather than a spelled constant, since the read
|
|
129
|
+
* A function rather than a spelled constant, since the read resolves at
|
|
130
130
|
* either surface root and a constant naming one of them would be believed of
|
|
131
|
-
* a project that has moved. `canon audits run` owns writing it,
|
|
132
|
-
*
|
|
133
|
-
*
|
|
131
|
+
* a project that has moved. `canon audits run` owns writing it, at
|
|
132
|
+
* `canon/config/baseline.json` now that the write has flipped, falling back to
|
|
133
|
+
* `.claude/canon/baseline.json` for a target that has not moved.
|
|
134
134
|
*/
|
|
135
135
|
export function auditsBaselineRel(root: string): string {
|
|
136
136
|
return relative(root, surfaceDir(root, 'canon', 'baseline.json'))
|
package/src/labels/map.ts
CHANGED
|
@@ -1,14 +1,23 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs'
|
|
2
2
|
import { join } from 'node:path'
|
|
3
|
+
import { resolveExisting } from '@/legacy-path'
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
|
-
* Where a project declares its pull request label map
|
|
6
|
+
* Where a project declares its pull request label map.
|
|
6
7
|
*
|
|
7
8
|
* The file has already moved once, and that relocation rewrote every mention
|
|
8
|
-
* across four surfaces.
|
|
9
|
-
*
|
|
9
|
+
* across four surfaces. `LEGACY_MAP_REL` below is the one other place code
|
|
10
|
+
* spells it, kept only as a read fallback, so the next move is still one edit
|
|
11
|
+
* to the write path rather than a sweep.
|
|
10
12
|
*/
|
|
11
|
-
export const MAP_REL = join('
|
|
13
|
+
export const MAP_REL = join('canon', 'config', 'pr-labels.toml')
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Where the map lived before this move, `.claude/canon/pr-labels.toml`.
|
|
17
|
+
* `readLabelMap` falls back to it so a project that has not moved still reads
|
|
18
|
+
* its declared map.
|
|
19
|
+
*/
|
|
20
|
+
const LEGACY_MAP_REL = join('.claude', 'canon', 'pr-labels.toml')
|
|
12
21
|
|
|
13
22
|
/** A label name and the path prefixes that earn it, in the map's own order. */
|
|
14
23
|
export interface DomainRow {
|
|
@@ -90,9 +99,14 @@ export function parseLabelMap(source: string): LabelMap {
|
|
|
90
99
|
|
|
91
100
|
/** Reads the map a project declares at `root`, or says why it could not. */
|
|
92
101
|
export function readLabelMap(root: string): LabelMap {
|
|
102
|
+
const path = resolveExisting([
|
|
103
|
+
join(root, MAP_REL),
|
|
104
|
+
join(root, LEGACY_MAP_REL),
|
|
105
|
+
])
|
|
106
|
+
|
|
93
107
|
let source: string
|
|
94
108
|
try {
|
|
95
|
-
source = readFileSync(
|
|
109
|
+
source = readFileSync(path, 'utf8')
|
|
96
110
|
} catch {
|
|
97
111
|
return { kind: 'refused', reason: 'no-map' }
|
|
98
112
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Where a fallback chain resolves for reading: the first path that exists,
|
|
5
|
+
* current first, or the first candidate when none exists yet, which is
|
|
6
|
+
* always the write target.
|
|
7
|
+
*
|
|
8
|
+
* `src/audits/baseline.ts`, `src/labels/map.ts`, and `src/sync/stamp.ts` each
|
|
9
|
+
* trail a file that moved, and each used to reason about its own chain
|
|
10
|
+
* separately. The tuple type keeps a caller from passing an empty list, since
|
|
11
|
+
* every chain here has a write target to fall back to.
|
|
12
|
+
*/
|
|
13
|
+
export function resolveExisting(paths: readonly [string, ...string[]]): string {
|
|
14
|
+
return paths.find((path) => existsSync(path)) ?? paths[0]
|
|
15
|
+
}
|
package/src/migrate/plan.ts
CHANGED
|
@@ -20,13 +20,12 @@ const TO_ROOT: SurfaceRoot = 'canon'
|
|
|
20
20
|
/**
|
|
21
21
|
* Every entry this move relocates, at the name `.claude/` gives it.
|
|
22
22
|
*
|
|
23
|
-
* Read from `SURFACE_ENTRIES` rather than restated
|
|
24
|
-
*
|
|
25
|
-
*
|
|
23
|
+
* Read from `SURFACE_ENTRIES` rather than restated. `canon` names the install
|
|
24
|
+
* stamp folder and is the one entry whose name differs by root, which
|
|
25
|
+
* `sourcePrefix` and `destinationPrefix` already resolve through `spell`
|
|
26
|
+
* rather than restating the variant here.
|
|
26
27
|
*/
|
|
27
|
-
export const MOVED_ENTRIES: readonly string[] = SURFACE_ENTRIES
|
|
28
|
-
(entry) => entry !== 'canon',
|
|
29
|
-
)
|
|
28
|
+
export const MOVED_ENTRIES: readonly string[] = SURFACE_ENTRIES
|
|
30
29
|
|
|
31
30
|
function sourcePrefix(entry: string): string {
|
|
32
31
|
return join(FROM_ROOT, spell(FROM_ROOT, entry))
|
package/src/surface-root.ts
CHANGED
|
@@ -56,26 +56,19 @@ export function spell(root: SurfaceRoot, entry: string): string {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
/**
|
|
59
|
-
* The
|
|
59
|
+
* The root a surface resolves at: the first that carries it, and
|
|
60
|
+
* `CREATION_ROOT` when neither does.
|
|
60
61
|
*
|
|
61
|
-
* The stamp folder
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*/
|
|
66
|
-
function creationRootFor(entry: string): SurfaceRoot {
|
|
67
|
-
return entry === 'canon' ? '.claude' : CREATION_ROOT
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* The root a surface resolves at: the first that carries it, and the creation
|
|
72
|
-
* default when neither does.
|
|
62
|
+
* The stamp folder used to take a carve-out here, staying at `.claude/` while
|
|
63
|
+
* the install stamp and the audits baseline still wrote there by a fixed path
|
|
64
|
+
* of their own. Both moved to `canon/config/` by a fixed path of their own, so
|
|
65
|
+
* every entry now shares the one default.
|
|
73
66
|
*/
|
|
74
67
|
function rootOf(root: string, entry: string): SurfaceRoot {
|
|
75
68
|
return (
|
|
76
69
|
SURFACE_ROOTS.find((candidate) =>
|
|
77
70
|
existsSync(join(root, candidate, spell(candidate, entry))),
|
|
78
|
-
) ??
|
|
71
|
+
) ?? CREATION_ROOT
|
|
79
72
|
)
|
|
80
73
|
}
|
|
81
74
|
|
package/src/sync/stamp.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { existsSync, readFileSync } from 'node:fs'
|
|
|
3
3
|
import { mkdir, writeFile } from 'node:fs/promises'
|
|
4
4
|
import { dirname, join, sep } from 'node:path'
|
|
5
5
|
import { execa } from 'execa'
|
|
6
|
+
import { resolveExisting } from '@/legacy-path'
|
|
6
7
|
import { recordTarget } from '@/targets/registry'
|
|
7
8
|
|
|
8
9
|
/**
|
|
@@ -56,7 +57,21 @@ export interface Stamp {
|
|
|
56
57
|
readonly domains: Readonly<Partial<Record<StampDomain, DomainStamp>>>
|
|
57
58
|
}
|
|
58
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Where a stamp is written today, `canon/config/config.json`. Named
|
|
62
|
+
* `stampPath` rather than `surfaceStampPath` because the write side has only
|
|
63
|
+
* one current path, unlike the read side's fallback chain below.
|
|
64
|
+
*/
|
|
59
65
|
export function stampPath(target: string): string {
|
|
66
|
+
return join(target, 'canon', 'config', 'config.json')
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Where the stamp wrote before this move, `.claude/canon/config.json`. Kept
|
|
71
|
+
* for the same reason as the two spellings below: a target stamped under it
|
|
72
|
+
* still carries its config here, and `readStamp` falls back to it.
|
|
73
|
+
*/
|
|
74
|
+
export function claudeCanonStampPath(target: string): string {
|
|
60
75
|
return join(target, '.claude', 'canon', 'config.json')
|
|
61
76
|
}
|
|
62
77
|
|
|
@@ -84,18 +99,6 @@ export function retiredNameStampPath(target: string): string {
|
|
|
84
99
|
return join(target, '.claude', 'aitk', 'config.json')
|
|
85
100
|
}
|
|
86
101
|
|
|
87
|
-
/**
|
|
88
|
-
* The stamp path under the new surface root, `canon/config/config.json`.
|
|
89
|
-
*
|
|
90
|
-
* Read ahead of `stampPath`, extending the same mechanism rather than adding a
|
|
91
|
-
* new one: a target that has moved reads its config from the root it moved
|
|
92
|
-
* to, and one that has not falls through to the spellings below unchanged.
|
|
93
|
-
* The write destination does not move to it in this batch.
|
|
94
|
-
*/
|
|
95
|
-
function surfaceStampPath(target: string): string {
|
|
96
|
-
return join(target, 'canon', 'config', 'config.json')
|
|
97
|
-
}
|
|
98
|
-
|
|
99
102
|
/**
|
|
100
103
|
* Every spelling a stamp has been written under, current first. The order is
|
|
101
104
|
* the read order, so a target carrying more than one resolves to the newest.
|
|
@@ -105,10 +108,10 @@ function surfaceStampPath(target: string): string {
|
|
|
105
108
|
* second breaking change aimed at exactly the targets that were slowest to
|
|
106
109
|
* migrate the first time.
|
|
107
110
|
*/
|
|
108
|
-
export function stampPaths(target: string): readonly string[] {
|
|
111
|
+
export function stampPaths(target: string): readonly [string, ...string[]] {
|
|
109
112
|
return [
|
|
110
|
-
surfaceStampPath(target),
|
|
111
113
|
stampPath(target),
|
|
114
|
+
claudeCanonStampPath(target),
|
|
112
115
|
retiredNameStampPath(target),
|
|
113
116
|
legacyStampPath(target),
|
|
114
117
|
]
|
|
@@ -120,7 +123,10 @@ export function stampPaths(target: string): readonly string[] {
|
|
|
120
123
|
* there is nothing to migrate off of.
|
|
121
124
|
*/
|
|
122
125
|
export function isLegacyStamped(target: string): boolean {
|
|
123
|
-
if (
|
|
126
|
+
if (
|
|
127
|
+
existsSync(stampPath(target)) ||
|
|
128
|
+
existsSync(claudeCanonStampPath(target))
|
|
129
|
+
) {
|
|
124
130
|
return false
|
|
125
131
|
}
|
|
126
132
|
return (
|
|
@@ -158,8 +164,7 @@ export function toStampKey(rel: string): string {
|
|
|
158
164
|
* was read from.
|
|
159
165
|
*/
|
|
160
166
|
export function readStamp(target: string): Stamp | undefined {
|
|
161
|
-
|
|
162
|
-
return found === undefined ? undefined : readStampFile(found)
|
|
167
|
+
return readStampFile(resolveExisting(stampPaths(target)))
|
|
163
168
|
}
|
|
164
169
|
|
|
165
170
|
function readStampFile(path: string): Stamp | undefined {
|
package/src/targets/sweep.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { join, resolve } from 'node:path'
|
|
|
3
3
|
import { $ } from 'bun'
|
|
4
4
|
import { gitEnv } from '@/git-env'
|
|
5
5
|
import {
|
|
6
|
+
claudeCanonStampPath,
|
|
6
7
|
isLegacyStamped,
|
|
7
8
|
legacyStampPath,
|
|
8
9
|
retiredNameStampPath,
|
|
@@ -91,15 +92,18 @@ export interface SweepOptions {
|
|
|
91
92
|
}
|
|
92
93
|
|
|
93
94
|
/**
|
|
94
|
-
* Whether a folder carries an install stamp at the current path or
|
|
95
|
+
* Whether a folder carries an install stamp at the current path or any
|
|
95
96
|
* retired one. `aitk@3.57.0` still writes the folder form,
|
|
96
97
|
* `retiredNameStampPath`, as its current path, so a target a pre-rename
|
|
97
98
|
* binary syncs after this check drops the folder form would otherwise vanish
|
|
98
|
-
* from the walk with nothing saying so.
|
|
99
|
+
* from the walk with nothing saying so. `claudeCanonStampPath` is the spelling
|
|
100
|
+
* every target carried before the stamp moved to `canon/config/`, which a
|
|
101
|
+
* target this batch has not reached still writes to.
|
|
99
102
|
*/
|
|
100
103
|
function isStamped(path: string): boolean {
|
|
101
104
|
return (
|
|
102
105
|
Bun.file(stampPath(path)).size > 0 ||
|
|
106
|
+
Bun.file(claudeCanonStampPath(path)).size > 0 ||
|
|
103
107
|
Bun.file(retiredNameStampPath(path)).size > 0 ||
|
|
104
108
|
Bun.file(legacyStampPath(path)).size > 0
|
|
105
109
|
)
|