@erclx/canon 4.72.1 → 4.74.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/plan-feature/SKILL.md +2 -2
- package/claude/skills/plan-groundwork/SKILL.md +6 -5
- package/claude/skills/plan-intake/SKILL.md +1 -1
- package/claude/skills/role-orchestrator/references/orchestrator-dispatch.md +1 -0
- package/claude/skills/role-orchestrator/references/orchestrator-parked.md +2 -1
- package/claude/skills/session-relay/REQUIREMENT.md +2 -0
- package/claude/skills/session-relay/SKILL.md +1 -0
- package/claude/skills/task-board/SKILL.md +37 -3
- package/docs/agents/commands.md +1 -0
- package/docs/agents/index.md +1 -1
- package/docs/agents/install-and-sync.md +10 -6
- package/docs/agents/records.md +29 -9
- package/docs/agents/tasks.md +2 -0
- package/docs/target-projects.md +5 -1
- package/governance/rules/core/091-channel.md +13 -0
- package/package.json +1 -1
- package/src/claude/skills-reach.ts +13 -3
- package/src/commands/context.ts +7 -4
- package/src/commands/design.ts +6 -1
- package/src/commands/records.ts +138 -0
- package/src/commands/transcripts.ts +20 -3
- package/src/context/architecture.ts +15 -8
- package/src/context/citations.ts +31 -6
- package/src/context/folders.ts +27 -8
- package/src/gate/measures.ts +17 -8
- package/src/init/plan.ts +8 -1
- package/src/init/steps.ts +17 -0
- package/src/intake/folder.ts +1 -1
- package/src/migrate/records.ts +11 -3
- package/src/records/backup.ts +110 -38
- package/src/records/ordinal.ts +243 -0
- package/src/records/validate.ts +28 -0
- package/src/surface-root.ts +101 -0
- package/src/sync/stamp.ts +20 -5
- package/src/tasks/answers.ts +10 -1
- package/src/transcripts/fetch.ts +31 -1
- package/standards/groundwork.md +1 -1
- package/standards/intake.md +1 -1
- package/standards/plan.md +1 -0
- package/tooling/base/configs/.husky/post-merge +27 -0
- package/tooling/claude/seeds/.claude/hooks/tasks-index.sh +5 -4
package/src/commands/records.ts
CHANGED
|
@@ -3,6 +3,13 @@ import { join } from 'node:path'
|
|
|
3
3
|
import type { Command } from 'commander'
|
|
4
4
|
import { BACKED_FOLDERS, pullRecords, pushRecords } from '@/records/backup'
|
|
5
5
|
import { migrateRecord } from '@/records/migrate'
|
|
6
|
+
import {
|
|
7
|
+
type ClaimOutcome,
|
|
8
|
+
claimOrdinal,
|
|
9
|
+
highestOrdinal,
|
|
10
|
+
isOrdinalKind,
|
|
11
|
+
ORDINAL_KINDS,
|
|
12
|
+
} from '@/records/ordinal'
|
|
6
13
|
import {
|
|
7
14
|
type FolderSize,
|
|
8
15
|
formatBytes,
|
|
@@ -40,6 +47,9 @@ const EXIT_FINDINGS = 2
|
|
|
40
47
|
/** Returned when a record carries a known transform and `--write` was not passed. */
|
|
41
48
|
const EXIT_MIGRATABLE = 2
|
|
42
49
|
|
|
50
|
+
/** Returned when `--claim` loses every retry to a collision. */
|
|
51
|
+
const EXIT_CONTENDED = 2
|
|
52
|
+
|
|
43
53
|
interface ValidateCommandOptions {
|
|
44
54
|
readonly json?: boolean
|
|
45
55
|
readonly root?: string
|
|
@@ -51,6 +61,10 @@ interface MigrateCommandOptions extends ValidateCommandOptions {
|
|
|
51
61
|
readonly write?: boolean
|
|
52
62
|
}
|
|
53
63
|
|
|
64
|
+
interface OrdinalCommandOptions extends ValidateCommandOptions {
|
|
65
|
+
readonly claim?: boolean
|
|
66
|
+
}
|
|
67
|
+
|
|
54
68
|
export function register(program: Command): void {
|
|
55
69
|
const records = program
|
|
56
70
|
.command('records')
|
|
@@ -140,6 +154,49 @@ export function register(program: Command): void {
|
|
|
140
154
|
process.exitCode = await runMigrate(kind, opts)
|
|
141
155
|
})
|
|
142
156
|
|
|
157
|
+
records
|
|
158
|
+
.command('ordinal')
|
|
159
|
+
.description(
|
|
160
|
+
'Report or claim the next ordinal shared by intake and groundwork folders',
|
|
161
|
+
)
|
|
162
|
+
.argument('<kind>', `Ordinal-bearing folder: ${ORDINAL_KINDS.join(', ')}`)
|
|
163
|
+
.argument('<slug>', 'The kebab-case slug the new folder will carry')
|
|
164
|
+
.helpOption('-h, --help', 'Show this help message')
|
|
165
|
+
.option('--json', 'Add a machine-readable record on stdout')
|
|
166
|
+
.option(
|
|
167
|
+
'--claim',
|
|
168
|
+
'Create the folder atomically instead of only reporting the ordinal',
|
|
169
|
+
)
|
|
170
|
+
.option('--root <path>', 'Project root, defaulting to the main worktree')
|
|
171
|
+
.addHelpText(
|
|
172
|
+
'after',
|
|
173
|
+
[
|
|
174
|
+
'',
|
|
175
|
+
'intake and groundwork folders share one ordinal sequence, so this reads',
|
|
176
|
+
'both .canon/intake/ and .canon/groundwork/ regardless of which kind was',
|
|
177
|
+
'asked for.',
|
|
178
|
+
'',
|
|
179
|
+
'Exit codes:',
|
|
180
|
+
' 0 reported the next ordinal, or --claim created the folder',
|
|
181
|
+
' 1 refused, with the reason on stderr or in the JSON record',
|
|
182
|
+
' 2 --claim lost every retry to a collision',
|
|
183
|
+
'',
|
|
184
|
+
'Without --claim this only reports, so two sessions reading at once can',
|
|
185
|
+
'still report the same number. --claim resolves that by creating the',
|
|
186
|
+
'folder as part of the same act, retrying past a losing race rather than',
|
|
187
|
+
'reporting one.',
|
|
188
|
+
'',
|
|
189
|
+
'Examples:',
|
|
190
|
+
' canon records ordinal intake my-topic',
|
|
191
|
+
' canon records ordinal groundwork my-topic --claim',
|
|
192
|
+
' canon records ordinal groundwork my-topic --claim --json',
|
|
193
|
+
'',
|
|
194
|
+
].join('\n'),
|
|
195
|
+
)
|
|
196
|
+
.action(async (kind: string, slug: string, opts: OrdinalCommandOptions) => {
|
|
197
|
+
process.exitCode = await runOrdinal(kind, slug, opts)
|
|
198
|
+
})
|
|
199
|
+
|
|
143
200
|
records
|
|
144
201
|
.command('size')
|
|
145
202
|
.description('Report what each record folder holds and how much is recent')
|
|
@@ -726,3 +783,84 @@ export function migrateExitCode(
|
|
|
726
783
|
if (!write) return EXIT_MIGRATABLE
|
|
727
784
|
return refused.length > 0 ? 1 : 0
|
|
728
785
|
}
|
|
786
|
+
|
|
787
|
+
async function runOrdinal(
|
|
788
|
+
kind: string,
|
|
789
|
+
slug: string,
|
|
790
|
+
opts: OrdinalCommandOptions,
|
|
791
|
+
): Promise<number> {
|
|
792
|
+
const emitJson = opts.json ?? false
|
|
793
|
+
|
|
794
|
+
if (!isOrdinalKind(kind)) {
|
|
795
|
+
return reportRefusal(
|
|
796
|
+
'canon records ordinal',
|
|
797
|
+
{
|
|
798
|
+
reason: 'unknown-kind',
|
|
799
|
+
message: `Not an ordinal-bearing kind: ${kind}. Expected one of: ${ORDINAL_KINDS.join(', ')}.`,
|
|
800
|
+
},
|
|
801
|
+
emitJson,
|
|
802
|
+
)
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
const root = opts.root ?? (await mainWorktreeRoot())
|
|
806
|
+
const claim = opts.claim ?? false
|
|
807
|
+
|
|
808
|
+
if (!claim) {
|
|
809
|
+
const next = String((await highestOrdinal(root)) + 1).padStart(2, '0')
|
|
810
|
+
|
|
811
|
+
if (emitJson) {
|
|
812
|
+
process.stdout.write(
|
|
813
|
+
`${JSON.stringify({ ok: true, root, kind, slug, ordinal: next, claimed: false })}\n`,
|
|
814
|
+
)
|
|
815
|
+
} else {
|
|
816
|
+
intro('canon records ordinal')
|
|
817
|
+
logStep('Next')
|
|
818
|
+
logInfo(`${next}-${slug} (report only, pass --claim to create it)`)
|
|
819
|
+
outro()
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
return 0
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
return reportOrdinal(root, await claimOrdinal(root, kind, slug), emitJson)
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
function reportOrdinal(
|
|
829
|
+
root: string,
|
|
830
|
+
outcome: ClaimOutcome,
|
|
831
|
+
emitJson: boolean,
|
|
832
|
+
): number {
|
|
833
|
+
if (!outcome.ok) {
|
|
834
|
+
if (emitJson) {
|
|
835
|
+
process.stderr.write(`${outcome.message}\n`)
|
|
836
|
+
process.stdout.write(
|
|
837
|
+
`${JSON.stringify({
|
|
838
|
+
ok: false,
|
|
839
|
+
reason: outcome.reason,
|
|
840
|
+
message: outcome.message,
|
|
841
|
+
lastOrdinal: outcome.lastOrdinal,
|
|
842
|
+
})}\n`,
|
|
843
|
+
)
|
|
844
|
+
return EXIT_CONTENDED
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
intro('canon records ordinal')
|
|
848
|
+
logStep('Refused')
|
|
849
|
+
logError(outcome.message)
|
|
850
|
+
outro()
|
|
851
|
+
return EXIT_CONTENDED
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
if (emitJson) {
|
|
855
|
+
process.stdout.write(
|
|
856
|
+
`${JSON.stringify({ root, ...outcome, claimed: true })}\n`,
|
|
857
|
+
)
|
|
858
|
+
} else {
|
|
859
|
+
intro('canon records ordinal')
|
|
860
|
+
logStep('Claimed')
|
|
861
|
+
logInfo(outcome.path)
|
|
862
|
+
outro()
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
return 0
|
|
866
|
+
}
|
|
@@ -1,24 +1,41 @@
|
|
|
1
1
|
import { resolve } from 'node:path'
|
|
2
2
|
import type { Command } from 'commander'
|
|
3
|
+
import { recordDir } from '@/record-root'
|
|
3
4
|
import { ensureYtDlp, fetchOne } from '@/transcripts/fetch'
|
|
4
5
|
import { palette } from '@/ui'
|
|
6
|
+
import { mainWorktreeRoot } from '@/worktree'
|
|
5
7
|
|
|
6
8
|
interface TranscriptOptions {
|
|
7
|
-
out
|
|
9
|
+
out?: string
|
|
8
10
|
keepTimestamps?: boolean
|
|
9
11
|
}
|
|
10
12
|
|
|
13
|
+
/**
|
|
14
|
+
* A caller-supplied `--out` resolves against the CWD, since naming a path
|
|
15
|
+
* explicitly opts out of the backed default. With none, the destination is
|
|
16
|
+
* the backed `transcripts` record folder at the main worktree root, which
|
|
17
|
+
* `canon records push` and `canon records pull` carry along with the rest.
|
|
18
|
+
*/
|
|
19
|
+
export async function resolveOutDir(opts: TranscriptOptions): Promise<string> {
|
|
20
|
+
return opts.out
|
|
21
|
+
? resolve(process.cwd(), opts.out)
|
|
22
|
+
: recordDir(await mainWorktreeRoot(), 'transcripts')
|
|
23
|
+
}
|
|
24
|
+
|
|
11
25
|
export function register(program: Command): void {
|
|
12
26
|
program
|
|
13
27
|
.command('transcripts <url>')
|
|
14
28
|
.description('Fetch a YouTube transcript with metadata frontmatter')
|
|
15
|
-
.option(
|
|
29
|
+
.option(
|
|
30
|
+
'-o, --out <path>',
|
|
31
|
+
'Output directory, defaulting to the backed transcripts record folder',
|
|
32
|
+
)
|
|
16
33
|
.option(
|
|
17
34
|
'--keep-timestamps',
|
|
18
35
|
'Prefix each line with [mm:ss] instead of prose',
|
|
19
36
|
)
|
|
20
37
|
.action(async (url: string, opts: TranscriptOptions) => {
|
|
21
|
-
const outDir =
|
|
38
|
+
const outDir = await resolveOutDir(opts)
|
|
22
39
|
const { GREEN, GREY, NC, RED, WHITE } = palette(process.stderr)
|
|
23
40
|
process.stderr.write(
|
|
24
41
|
`${GREY}┌${NC}\n${GREY}│${NC} ${WHITE}canon transcripts${NC}\n`,
|
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
import { access, readFile } from 'node:fs/promises'
|
|
2
|
-
import { join } from 'node:path'
|
|
2
|
+
import { join, relative } from 'node:path'
|
|
3
3
|
import { AUDITS } from '@/audits/catalog'
|
|
4
4
|
import { bodyLines } from '@/markdown/scan'
|
|
5
|
+
import { surfaceDir } from '@/surface-root'
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
|
-
* The record this measures, relative to
|
|
8
|
+
* The record this measures, relative to `root`.
|
|
8
9
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
10
|
+
* A function rather than a fixed constant, because the path now resolves at
|
|
11
|
+
* either surface root and a constant naming one of them would be believed to
|
|
12
|
+
* be true of a project that has moved. One fixed name rather than a folder
|
|
13
|
+
* walk otherwise, because the standard governing it names one document, and
|
|
14
|
+
* the length rule this measures is stated by whichever record sits there
|
|
15
|
+
* rather than by the standard or by this file.
|
|
12
16
|
*/
|
|
13
|
-
export
|
|
17
|
+
export function architectureRel(root: string): string {
|
|
18
|
+
return relative(root, surfaceDir(root, 'ARCHITECTURE.md'))
|
|
19
|
+
}
|
|
14
20
|
|
|
15
21
|
/**
|
|
16
22
|
* The line allowances a record states for itself, absent when it states none.
|
|
@@ -299,7 +305,8 @@ export function ceilingFor(allowances: Allowances, decisions: number): number {
|
|
|
299
305
|
export async function measureArchitecture(
|
|
300
306
|
root: string,
|
|
301
307
|
): Promise<ArchitectureReport | undefined> {
|
|
302
|
-
const
|
|
308
|
+
const rel = architectureRel(root)
|
|
309
|
+
const path = join(root, rel)
|
|
303
310
|
|
|
304
311
|
let source: string
|
|
305
312
|
try {
|
|
@@ -330,7 +337,7 @@ export async function measureArchitecture(
|
|
|
330
337
|
)
|
|
331
338
|
|
|
332
339
|
return {
|
|
333
|
-
rel
|
|
340
|
+
rel,
|
|
334
341
|
lines: source.replace(/\n$/, '').split('\n').length,
|
|
335
342
|
...(allowances !== undefined && {
|
|
336
343
|
allowances,
|
package/src/context/citations.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { readFile } from 'node:fs/promises'
|
|
|
3
3
|
import { resolve } from 'node:path'
|
|
4
4
|
import { listRepositoryFiles } from '@/git-files'
|
|
5
5
|
import { RECORD_ROOTS } from '@/record-root'
|
|
6
|
+
import { SURFACE_ROOTS } from '@/surface-root'
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Suppresses citation checking for the source line carrying it.
|
|
@@ -78,17 +79,41 @@ export function isFixture(rel: string): boolean {
|
|
|
78
79
|
}
|
|
79
80
|
|
|
80
81
|
/**
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
82
|
+
* The boundary a root's own leading character rules out.
|
|
83
|
+
*
|
|
84
|
+
* A dotted root cannot be a suffix of a path segment, since a segment boundary
|
|
85
|
+
* is a slash and a slash can never sit inside the dot itself, so a preceding
|
|
86
|
+
* slash is not a false match to guard against and the boundary admits it. That
|
|
87
|
+
* is what a relative link needs, since `../.claude/context/<entry>.md` carries
|
|
88
|
+
* a slash immediately before `.claude`. A bare root has no such protection: it
|
|
89
|
+
* is a suffix of a dotted root's own name and of any `/<root>/` path segment,
|
|
90
|
+
* so its boundary rejects a slash along with a name character or a dot.
|
|
91
|
+
*/
|
|
92
|
+
function rootBoundary(root: string): string {
|
|
93
|
+
return root.startsWith('.') ? '(?<![\\w.])' : '(?<![\\w./])'
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Every record and surface root is spelled, so a citation into a folder that
|
|
98
|
+
* has moved is still resolved. A pattern fixed at one root matches nothing
|
|
99
|
+
* after the move and reports nothing, which is a stale reference passing the
|
|
100
|
+
* check that exists to find it rather than a check that fails.
|
|
101
|
+
*
|
|
102
|
+
* Each root carries its own boundary rather than one shared ahead of the whole
|
|
103
|
+
* alternation, per `rootBoundary`. A shared boundary rejecting a slash blocked
|
|
104
|
+
* every dotted root from a relative link, which is the same failure this
|
|
105
|
+
* function's own boundary exists to prevent, aimed at the roots that never
|
|
106
|
+
* needed it.
|
|
85
107
|
*/
|
|
86
108
|
export function citationPattern(folders: readonly string[]): RegExp {
|
|
87
109
|
const names = folders.map((name) => escape(name))
|
|
88
|
-
const roots = RECORD_ROOTS
|
|
110
|
+
const roots = [...new Set([...RECORD_ROOTS, ...SURFACE_ROOTS])]
|
|
111
|
+
const alternatives = roots
|
|
112
|
+
.map((root) => `${rootBoundary(root)}${escape(root)}`)
|
|
113
|
+
.join('|')
|
|
89
114
|
|
|
90
115
|
return new RegExp(
|
|
91
|
-
`(?:${
|
|
116
|
+
`(?:${alternatives})/(?:${names.join('|')})/[A-Za-z0-9._/-]+\\.md`,
|
|
92
117
|
'g',
|
|
93
118
|
)
|
|
94
119
|
}
|
package/src/context/folders.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { existsSync } from 'node:fs'
|
|
|
2
2
|
import { dirname, relative, resolve } from 'node:path'
|
|
3
3
|
import { INDEX_FILE, listIndexes } from '@/indexes/walk'
|
|
4
4
|
import { RECORD_ROOTS } from '@/record-root'
|
|
5
|
+
import { SURFACE_ROOTS } from '@/surface-root'
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Folder names under a record root audited by default.
|
|
@@ -23,16 +24,34 @@ export const DEFAULT_FOLDERS: readonly string[] = [
|
|
|
23
24
|
]
|
|
24
25
|
|
|
25
26
|
/**
|
|
26
|
-
* The bases every folder in the default list is looked for under
|
|
27
|
-
* roots' own precedence order.
|
|
27
|
+
* The bases every folder in the default list is looked for under.
|
|
28
28
|
*
|
|
29
|
-
* `diagrams` is
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
29
|
+
* `diagrams` is a session record and resolves under `RECORD_ROOTS`, while
|
|
30
|
+
* `context` and `wireframes` are tracked and resolve under `SURFACE_ROOTS`, so
|
|
31
|
+
* the split this comment used to describe as hypothetical is real: two
|
|
32
|
+
* folders on this list read from two different root lists. The base array is
|
|
33
|
+
* their union rather than a per-name map, since a name resolving at the wrong
|
|
34
|
+
* root costs one extra `existsSync` and nothing else, where a map states the
|
|
35
|
+
* split a second time next to the one each resolver module already carries.
|
|
36
|
+
*
|
|
37
|
+
* `.claude` is pushed last because both lists name it, and each list's own
|
|
38
|
+
* precedence otherwise survives: `canon` still precedes `.claude` for a
|
|
39
|
+
* `SURFACE_ROOTS` name, and `.canon` still precedes `.claude` for a
|
|
40
|
+
* `RECORD_ROOTS` one. Order between `canon` and `.canon` is unobserved, since
|
|
41
|
+
* no name on this list resolves under both.
|
|
42
|
+
*
|
|
43
|
+
* Adding `canon` here means a target holding a root-level `canon/` folder of
|
|
44
|
+
* its own now resolves it as the toolkit's, since a project writing about a
|
|
45
|
+
* product called canon is a plausible name collision `canResolveAtRoot`'s own
|
|
46
|
+
* project-root gate does not cover. The audit only reports, so the cost is a
|
|
47
|
+
* wrong scope line rather than a wrong edit.
|
|
34
48
|
*/
|
|
35
|
-
const CLAUDE_BASES: readonly string[] =
|
|
49
|
+
const CLAUDE_BASES: readonly string[] = [
|
|
50
|
+
...new Set(
|
|
51
|
+
[...SURFACE_ROOTS, ...RECORD_ROOTS].filter((root) => root !== '.claude'),
|
|
52
|
+
),
|
|
53
|
+
'.claude',
|
|
54
|
+
]
|
|
36
55
|
|
|
37
56
|
/** The project root, reached only by a name the caller asked for. */
|
|
38
57
|
const ROOT_BASE = '.'
|
package/src/gate/measures.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto'
|
|
2
2
|
import { existsSync, readdirSync, readFileSync } from 'node:fs'
|
|
3
|
-
import { join } from 'node:path'
|
|
3
|
+
import { join, relative } from 'node:path'
|
|
4
4
|
import {
|
|
5
5
|
CLIENT_COMMAND_MARKER,
|
|
6
6
|
CLIENT_COMMANDS,
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
SHIPPED_CORPORA,
|
|
16
16
|
type ShippedReference,
|
|
17
17
|
} from '@/shipped/references'
|
|
18
|
+
import { surfaceDir } from '@/surface-root'
|
|
18
19
|
import {
|
|
19
20
|
README_PARAPHRASE_MARKER,
|
|
20
21
|
readmeCitationsIn,
|
|
@@ -122,11 +123,18 @@ export const SANDBOX_UNDECLARED_CEILING = 47
|
|
|
122
123
|
export const SANDBOX_ASSERTED_FLOOR = 26
|
|
123
124
|
|
|
124
125
|
/**
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
126
|
+
* Where the retained counts the audit stage compares each run against are
|
|
127
|
+
* read, relative to `root`.
|
|
128
|
+
*
|
|
129
|
+
* A function rather than a spelled constant, since the read now resolves at
|
|
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, and the write
|
|
132
|
+
* stays at the creation default for this batch, so the two can disagree for
|
|
133
|
+
* exactly the release window `src/surface-root.ts` documents.
|
|
128
134
|
*/
|
|
129
|
-
export
|
|
135
|
+
export function auditsBaselineRel(root: string): string {
|
|
136
|
+
return relative(root, surfaceDir(root, 'canon', 'baseline.json'))
|
|
137
|
+
}
|
|
130
138
|
|
|
131
139
|
export const CAPTURE_STAMP_FAILURE =
|
|
132
140
|
'A capture set disagrees with the stamp written when its image was captured. Run canon capture assets/captures --selector .window --out assets and commit each frame with its image and its stamp.'
|
|
@@ -958,16 +966,17 @@ export const auditSet: Measure = async (ctx) => {
|
|
|
958
966
|
),
|
|
959
967
|
)
|
|
960
968
|
}
|
|
969
|
+
const baselineRel = auditsBaselineRel(ctx.root)
|
|
961
970
|
emissions.push(
|
|
962
971
|
summary.grown > 0
|
|
963
972
|
? warn(
|
|
964
|
-
`${summary.grown} measure(s) grew against ${
|
|
973
|
+
`${summary.grown} measure(s) grew against ${baselineRel}. Run bun src/cli.ts audits run to see which, then fix them or re-record and say why.`,
|
|
965
974
|
)
|
|
966
|
-
: info(`No measure grew against ${
|
|
975
|
+
: info(`No measure grew against ${baselineRel}`),
|
|
967
976
|
)
|
|
968
977
|
if (typeof summary.shrunk === 'number' && summary.shrunk > 0) {
|
|
969
978
|
emissions.push(
|
|
970
|
-
info(`${summary.shrunk} measure(s) fell against ${
|
|
979
|
+
info(`${summary.shrunk} measure(s) fell against ${baselineRel}`),
|
|
971
980
|
)
|
|
972
981
|
}
|
|
973
982
|
|
package/src/init/plan.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const SKIPPABLE_DOMAINS = ['wiki', 'governance'] as const
|
|
1
|
+
export const SKIPPABLE_DOMAINS = ['wiki', 'governance', 'records'] as const
|
|
2
2
|
|
|
3
3
|
export type SkippableDomain = (typeof SKIPPABLE_DOMAINS)[number]
|
|
4
4
|
|
|
@@ -104,6 +104,13 @@ export function planInit(flags: InitFlags): InitPlan {
|
|
|
104
104
|
})
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
if (!flags.skip.skipped.has('records')) {
|
|
108
|
+
preview.push({
|
|
109
|
+
level: 'info',
|
|
110
|
+
text: 'records (one-time backup setup notice)',
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
|
|
107
114
|
const total = preview.filter((line) => line.level === 'info').length
|
|
108
115
|
|
|
109
116
|
return { preview, total }
|
package/src/init/steps.ts
CHANGED
|
@@ -4,6 +4,13 @@ import type { DomainStep } from '@/init/run'
|
|
|
4
4
|
/** Builds the child-process invocation for one domain. */
|
|
5
5
|
export type RunFactory = (args: readonly string[]) => () => Promise<boolean>
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* States the same fact as `docs/target-projects.md`'s backup section in its
|
|
9
|
+
* own words. Keep the two in step by hand; nothing compares them.
|
|
10
|
+
*/
|
|
11
|
+
const RECORDS_SETUP_NOTICE =
|
|
12
|
+
"No private repository to push to yet. Run 'canon records push' once one exists to print the one-time setup command, or --skip records to silence this."
|
|
13
|
+
|
|
7
14
|
/**
|
|
8
15
|
* Orders the domains an init installs. Base tooling seeds the files the later
|
|
9
16
|
* domains install alongside, so the sequence is part of the contract rather
|
|
@@ -56,6 +63,16 @@ export function buildSteps(
|
|
|
56
63
|
})
|
|
57
64
|
}
|
|
58
65
|
|
|
66
|
+
if (!flags.skip.skipped.has('records')) {
|
|
67
|
+
// `skip` here means there is nothing to run non-interactively, not that
|
|
68
|
+
// the caller opted out, unlike every other push of this kind above.
|
|
69
|
+
steps.push({
|
|
70
|
+
kind: 'skip',
|
|
71
|
+
label: 'Records backup',
|
|
72
|
+
notice: RECORDS_SETUP_NOTICE,
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
|
|
59
76
|
return steps
|
|
60
77
|
}
|
|
61
78
|
|
package/src/intake/folder.ts
CHANGED
|
@@ -138,7 +138,7 @@ function matchSlug(names: readonly string[], slug: string): SlugMatch {
|
|
|
138
138
|
return { kind: 'none' }
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
-
function extractOrdinal(name: string): string {
|
|
141
|
+
export function extractOrdinal(name: string): string {
|
|
142
142
|
return /^\d{2,}-/.exec(name)?.[0].slice(0, -1) ?? ''
|
|
143
143
|
}
|
|
144
144
|
|
package/src/migrate/records.ts
CHANGED
|
@@ -76,10 +76,14 @@ function escape(value: string): string {
|
|
|
76
76
|
* paths a session actually opened. Rewriting either makes it testify to
|
|
77
77
|
* something that never happened.
|
|
78
78
|
*
|
|
79
|
-
* This module and `src/record-root.ts` are the
|
|
79
|
+
* This module and `src/record-root.ts` are two of the sources that state the old
|
|
80
80
|
* root on purpose. Sweeping them turns every citation this expression is built
|
|
81
81
|
* from into its own replacement, leaving a rewriter that maps `.canon/` to
|
|
82
|
-
* `.canon/` and matches nothing.
|
|
82
|
+
* `.canon/` and matches nothing. `src/surface-root.ts` joins the list a release
|
|
83
|
+
* early, for the same reason: it spells `.claude` as data the moment it
|
|
84
|
+
* exists, and excluding it later would leave one release where a records
|
|
85
|
+
* migration in a target could rewrite the resolver that migration itself
|
|
86
|
+
* depends on.
|
|
83
87
|
*
|
|
84
88
|
* A test file is excluded because the fixtures that prove the old root still
|
|
85
89
|
* resolves have to keep building it. Rewriting one is worse than a failing
|
|
@@ -106,7 +110,11 @@ const EXCLUDED_PREFIXES: readonly string[] = [
|
|
|
106
110
|
'tooling/claude/seeds/.claude/hooks/',
|
|
107
111
|
]
|
|
108
112
|
|
|
109
|
-
const EXCLUDED_PATHS: readonly string[] = [
|
|
113
|
+
const EXCLUDED_PATHS: readonly string[] = [
|
|
114
|
+
'CHANGELOG.md',
|
|
115
|
+
'src/record-root.ts',
|
|
116
|
+
'src/surface-root.ts',
|
|
117
|
+
]
|
|
110
118
|
|
|
111
119
|
const EXCLUDED_SUFFIXES: readonly string[] = ['.test.ts']
|
|
112
120
|
|