@erclx/canon 5.60.0 → 5.62.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/CHANGELOG.md +23 -0
- package/README.md +1 -1
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/canon-cli/SKILL.md +2 -1
- package/claude/skills/design-identity/references/card-route.md +3 -3
- package/claude/skills/design-record/references/design.md +19 -0
- package/claude/skills/document-health/REQUIREMENT.md +1 -1
- package/claude/skills/document-health/SKILL.md +6 -5
- package/claude/skills/document-health/references/axes.md +3 -1
- package/claude/skills/review-ui/REQUIREMENT.md +1 -1
- package/claude/skills/review-ui/SKILL.md +3 -2
- package/claude/skills/review-ui/references/post.md +2 -2
- package/claude/skills/target-update/SKILL.md +9 -1
- package/claude/skills/ui-checklist/REQUIREMENT.md +1 -1
- package/claude/skills/ui-checklist/SKILL.md +12 -0
- package/claude/skills/ux-audit/SKILL.md +1 -1
- package/docs/agents/install-and-sync.md +45 -11
- package/docs/agents/records.md +1 -1
- package/docs/index.md +1 -1
- package/docs/target/migrations.md +7 -9
- package/docs/target/sync.md +20 -1
- package/docs/workflow/ai-workflow.md +1 -1
- package/docs/workflow/index.md +3 -3
- package/docs/workflow/parallel-features.md +1 -1
- package/docs/workflow/visual-design-workflow.md +58 -32
- package/governance/detect.toml +8 -0
- package/governance/ledger/ui/a11y.md +8 -6
- package/governance/ledger/ui/keyboard.md +20 -21
- package/governance/ledger/ui/pointer.md +24 -0
- package/governance/ledger/ui/surface-capture.md +12 -12
- package/governance/ledger/ui/ux-completeness.md +11 -23
- package/governance/ledger/ui/ux-controls.md +26 -0
- package/governance/rules/ui/a11y.md +2 -0
- package/governance/rules/ui/keyboard.md +4 -7
- package/governance/rules/ui/pointer.md +23 -0
- package/governance/rules/ui/surface-capture.md +5 -5
- package/governance/rules/ui/ux-completeness.md +2 -10
- package/governance/rules/ui/ux-controls.md +25 -0
- package/governance/splits.toml +15 -0
- package/package.json +2 -1
- package/src/commands/driver.ts +29 -3
- package/src/commands/migrate.ts +7 -0
- package/src/commands/records.ts +3 -2
- package/src/commands/sync.ts +3 -1
- package/src/commands/tooling.ts +329 -10
- package/src/design/audit.ts +25 -1
- package/src/design/budget.ts +2 -1
- package/src/design/document.ts +5 -1
- package/src/design/themes.ts +176 -0
- package/src/design/tokens.ts +3 -0
- package/src/driver/drive.ts +17 -1
- package/src/driver/probes/axe.ts +195 -0
- package/src/driver/steps.ts +20 -1
- package/src/gate/measures.ts +9 -3
- package/src/gov/adapter.ts +53 -4
- package/src/gov/splits.ts +36 -0
- package/src/markdown/structure.ts +4 -1
- package/src/migrate/records.ts +46 -7
- package/src/records/stale-canonical.ts +2 -2
- package/src/sync/check.ts +9 -1
- package/src/sync/stamp.ts +135 -11
- package/src/tooling/disposition.ts +167 -0
- package/src/tooling/inject.ts +17 -2
- package/src/tooling/scan.ts +86 -7
- package/src/tooling/stamp.ts +51 -7
package/src/tooling/scan.ts
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs'
|
|
2
2
|
import { join } from 'node:path'
|
|
3
3
|
import { resolveSurfacePath } from '@/roots/surface'
|
|
4
|
+
import {
|
|
5
|
+
type Disposition,
|
|
6
|
+
hashFile,
|
|
7
|
+
readStamp,
|
|
8
|
+
stampedDispositions,
|
|
9
|
+
stampedHashes,
|
|
10
|
+
} from '@/sync/stamp'
|
|
4
11
|
import { mergeSections } from '@/tooling/gitignore'
|
|
5
12
|
import {
|
|
6
13
|
ancestorsFirst,
|
|
@@ -19,10 +26,14 @@ import {
|
|
|
19
26
|
} from '@/tooling/package'
|
|
20
27
|
import { isSubfolderTarget, isWithheldInSubfolder } from '@/tooling/subfolder'
|
|
21
28
|
|
|
29
|
+
export type DriftCause = 'stale' | 'edited' | 'unattributed'
|
|
30
|
+
|
|
31
|
+
/** `cause` is set on a drifted config alone. */
|
|
22
32
|
export interface ConfigState {
|
|
23
33
|
readonly rel: string
|
|
24
34
|
readonly stack: string
|
|
25
35
|
readonly state: 'new' | 'drifted' | 'matching'
|
|
36
|
+
readonly cause?: DriftCause
|
|
26
37
|
}
|
|
27
38
|
|
|
28
39
|
export interface SeedState {
|
|
@@ -41,9 +52,24 @@ export interface WithheldState {
|
|
|
41
52
|
readonly present: boolean
|
|
42
53
|
}
|
|
43
54
|
|
|
55
|
+
/**
|
|
56
|
+
* A golden file the project declined or customized, which every write skips.
|
|
57
|
+
* `moved` is set on a customized config alone, true once the golden copy no
|
|
58
|
+
* longer hashes to the one the project took.
|
|
59
|
+
*/
|
|
60
|
+
export interface DispositionState {
|
|
61
|
+
readonly rel: string
|
|
62
|
+
readonly stack: string
|
|
63
|
+
readonly kind: 'config' | 'seed'
|
|
64
|
+
readonly disposition: Disposition
|
|
65
|
+
readonly present: boolean
|
|
66
|
+
readonly moved?: boolean
|
|
67
|
+
}
|
|
68
|
+
|
|
44
69
|
export interface ScanResult {
|
|
45
70
|
readonly configs: readonly ConfigState[]
|
|
46
71
|
readonly withheld: readonly WithheldState[]
|
|
72
|
+
readonly dispositions: readonly DispositionState[]
|
|
47
73
|
readonly seeds: readonly SeedState[]
|
|
48
74
|
readonly scripts: readonly ScriptState[]
|
|
49
75
|
readonly deps: readonly DepState[]
|
|
@@ -53,6 +79,17 @@ export interface ScanResult {
|
|
|
53
79
|
readonly totalChanges: number
|
|
54
80
|
}
|
|
55
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Why a drifted config differs, which decides whether a write may replace it.
|
|
84
|
+
* A copy still at the hash the last write recorded is `stale`, since canon
|
|
85
|
+
* moved and the project did not. A copy off it is `edited`, and a copy with no
|
|
86
|
+
* recorded hash is `unattributed`, since nothing says who changed it.
|
|
87
|
+
*/
|
|
88
|
+
function driftCause(recorded: string | undefined, dest: string): DriftCause {
|
|
89
|
+
if (recorded === undefined) return 'unattributed'
|
|
90
|
+
return hashFile(dest) === recorded ? 'stale' : 'edited'
|
|
91
|
+
}
|
|
92
|
+
|
|
56
93
|
function isIdentical(a: string, b: string): boolean {
|
|
57
94
|
if (!existsSync(b)) return false
|
|
58
95
|
return readFileSync(a).equals(readFileSync(b))
|
|
@@ -72,11 +109,19 @@ function isIdentical(a: string, b: string): boolean {
|
|
|
72
109
|
* GitHub reads only at the root moves to `withheld` and counts as no change.
|
|
73
110
|
* The scope is read here so every caller of `scan` agrees without passing a
|
|
74
111
|
* flag.
|
|
112
|
+
*
|
|
113
|
+
* A path the project declined or customized moves to `dispositions` ahead of
|
|
114
|
+
* the subfolder test, so it is listed once. The record is read here for the
|
|
115
|
+
* same reason as the scope.
|
|
75
116
|
*/
|
|
76
117
|
export function scan(chain: readonly Manifest[], target: string): ScanResult {
|
|
77
118
|
const isSubfolder = isSubfolderTarget(target)
|
|
119
|
+
const stamp = readStamp(target)
|
|
120
|
+
const held = stampedDispositions(stamp, 'tooling')
|
|
121
|
+
const recorded = stampedHashes(stamp, 'tooling')
|
|
78
122
|
const configs: ConfigState[] = []
|
|
79
123
|
const withheld: WithheldState[] = []
|
|
124
|
+
const dispositions: DispositionState[] = []
|
|
80
125
|
const seenConfigs = new Set<string>()
|
|
81
126
|
|
|
82
127
|
for (const manifest of chain) {
|
|
@@ -87,18 +132,38 @@ export function scan(chain: readonly Manifest[], target: string): ScanResult {
|
|
|
87
132
|
const source = join(manifest.configsDir, rel)
|
|
88
133
|
const dest = join(target, rel)
|
|
89
134
|
|
|
135
|
+
const entry = held[rel]
|
|
136
|
+
if (entry !== undefined) {
|
|
137
|
+
dispositions.push({
|
|
138
|
+
rel,
|
|
139
|
+
stack: manifest.name,
|
|
140
|
+
kind: 'config',
|
|
141
|
+
disposition: entry.disposition,
|
|
142
|
+
present: existsSync(dest),
|
|
143
|
+
...(entry.disposition === 'customized'
|
|
144
|
+
? { moved: hashFile(source) !== entry.base }
|
|
145
|
+
: {}),
|
|
146
|
+
})
|
|
147
|
+
continue
|
|
148
|
+
}
|
|
149
|
+
|
|
90
150
|
if (isSubfolder && isWithheldInSubfolder(rel)) {
|
|
91
151
|
withheld.push({ rel, stack: manifest.name, present: existsSync(dest) })
|
|
92
152
|
continue
|
|
93
153
|
}
|
|
94
154
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
155
|
+
if (!existsSync(dest)) {
|
|
156
|
+
configs.push({ rel, stack: manifest.name, state: 'new' })
|
|
157
|
+
} else if (isIdentical(source, dest)) {
|
|
158
|
+
configs.push({ rel, stack: manifest.name, state: 'matching' })
|
|
159
|
+
} else {
|
|
160
|
+
configs.push({
|
|
161
|
+
rel,
|
|
162
|
+
stack: manifest.name,
|
|
163
|
+
state: 'drifted',
|
|
164
|
+
cause: driftCause(recorded[rel], dest),
|
|
165
|
+
})
|
|
166
|
+
}
|
|
102
167
|
}
|
|
103
168
|
}
|
|
104
169
|
|
|
@@ -110,6 +175,18 @@ export function scan(chain: readonly Manifest[], target: string): ScanResult {
|
|
|
110
175
|
if (seenSeeds.has(rel)) continue
|
|
111
176
|
seenSeeds.add(rel)
|
|
112
177
|
|
|
178
|
+
const entry = held[rel]
|
|
179
|
+
if (entry !== undefined) {
|
|
180
|
+
dispositions.push({
|
|
181
|
+
rel,
|
|
182
|
+
stack: manifest.name,
|
|
183
|
+
kind: 'seed',
|
|
184
|
+
disposition: entry.disposition,
|
|
185
|
+
present: existsSync(resolveSurfacePath(target, rel)),
|
|
186
|
+
})
|
|
187
|
+
continue
|
|
188
|
+
}
|
|
189
|
+
|
|
113
190
|
if (isSubfolder && isWithheldInSubfolder(rel)) {
|
|
114
191
|
withheld.push({
|
|
115
192
|
rel,
|
|
@@ -144,6 +221,7 @@ export function scan(chain: readonly Manifest[], target: string): ScanResult {
|
|
|
144
221
|
const totalChanges =
|
|
145
222
|
(newManifest ? 1 : 0) +
|
|
146
223
|
configs.filter((entry) => entry.state !== 'matching').length +
|
|
224
|
+
dispositions.filter((entry) => entry.moved === true).length +
|
|
147
225
|
seeds.filter((entry) => entry.state === 'missing').length +
|
|
148
226
|
scripts.filter((entry) => entry.state !== 'matching').length +
|
|
149
227
|
deps.filter((entry) => entry.state === 'missing').length +
|
|
@@ -152,6 +230,7 @@ export function scan(chain: readonly Manifest[], target: string): ScanResult {
|
|
|
152
230
|
return {
|
|
153
231
|
configs,
|
|
154
232
|
withheld,
|
|
233
|
+
dispositions,
|
|
155
234
|
seeds,
|
|
156
235
|
scripts,
|
|
157
236
|
deps,
|
package/src/tooling/stamp.ts
CHANGED
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs'
|
|
2
2
|
import { join } from 'node:path'
|
|
3
|
-
import {
|
|
4
|
-
|
|
3
|
+
import {
|
|
4
|
+
type DomainHashes,
|
|
5
|
+
hashFile,
|
|
6
|
+
readStamp,
|
|
7
|
+
stampedChain,
|
|
8
|
+
stampedHashes,
|
|
9
|
+
writeChainStamp,
|
|
10
|
+
} from '@/sync/stamp'
|
|
11
|
+
import {
|
|
12
|
+
configFiles,
|
|
13
|
+
isWholeStack,
|
|
14
|
+
loadManifest,
|
|
15
|
+
type Manifest,
|
|
16
|
+
} from '@/tooling/manifest'
|
|
5
17
|
import { readPackage } from '@/tooling/package'
|
|
6
18
|
|
|
7
19
|
/**
|
|
@@ -32,15 +44,47 @@ export async function recordToolingChain(
|
|
|
32
44
|
): Promise<string[] | undefined> {
|
|
33
45
|
if (isWorkspaceRoot(target)) return undefined
|
|
34
46
|
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
47
|
+
const stamp = readStamp(target)
|
|
48
|
+
const merged = mergeAddOns(toolkitRoot, stampedChain(stamp, 'tooling'), chain)
|
|
49
|
+
await writeChainStamp(
|
|
50
|
+
target,
|
|
51
|
+
{ domain: 'tooling', toolkitRoot },
|
|
52
|
+
merged,
|
|
53
|
+
now,
|
|
54
|
+
matchedHashes(chain, target, stampedHashes(stamp, 'tooling')),
|
|
39
55
|
)
|
|
40
|
-
await writeChainStamp(target, { domain: 'tooling', toolkitRoot }, merged, now)
|
|
41
56
|
return merged
|
|
42
57
|
}
|
|
43
58
|
|
|
59
|
+
/**
|
|
60
|
+
* The golden hash of every config the target now matches, over the hashes an
|
|
61
|
+
* earlier run recorded. A config the target holds a different copy of keeps
|
|
62
|
+
* its previous hash, so a held edit still reads as edited on the next run
|
|
63
|
+
* rather than as a stale copy the write may replace.
|
|
64
|
+
*/
|
|
65
|
+
function matchedHashes(
|
|
66
|
+
chain: readonly Manifest[],
|
|
67
|
+
target: string,
|
|
68
|
+
previous: DomainHashes,
|
|
69
|
+
): DomainHashes {
|
|
70
|
+
const hashes: Record<string, string> = { ...previous }
|
|
71
|
+
const seen = new Set<string>()
|
|
72
|
+
|
|
73
|
+
for (const manifest of chain) {
|
|
74
|
+
for (const rel of configFiles(manifest)) {
|
|
75
|
+
if (seen.has(rel)) continue
|
|
76
|
+
seen.add(rel)
|
|
77
|
+
|
|
78
|
+
const dest = join(target, rel)
|
|
79
|
+
if (!existsSync(dest)) continue
|
|
80
|
+
const golden = hashFile(join(manifest.configsDir, rel))
|
|
81
|
+
if (hashFile(dest) === golden) hashes[rel] = golden
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return hashes
|
|
86
|
+
}
|
|
87
|
+
|
|
44
88
|
/**
|
|
45
89
|
* An add-on stands alone beside a core chain, so syncing one must not displace
|
|
46
90
|
* the chain it layers onto. Base extends nothing and still heads a core chain.
|