@erclx/canon 5.54.0 → 5.56.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 +16 -0
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/api-design/SKILL.md +8 -10
- package/claude/skills/auto-ship/SKILL.md +4 -5
- package/claude/skills/auto-ship/references/failure-recovery.md +37 -17
- package/claude/skills/auto-ship/references/tier-failures.md +9 -7
- package/claude/skills/auto-ship/references/worktree-name.md +8 -5
- package/claude/skills/backlog-triage/SKILL.md +10 -10
- package/claude/skills/backlog-triage/references/apply.md +6 -6
- package/claude/skills/context-fold/references/classify.md +17 -4
- package/claude/skills/create-rule/references/rule.md +4 -0
- package/claude/skills/create-skill/SKILL.md +3 -3
- package/claude/skills/deploy-app/SKILL.md +2 -0
- package/claude/skills/deprecation-migration/SKILL.md +7 -9
- package/claude/skills/document-health/SKILL.md +6 -6
- package/claude/skills/document-health/references/axes.md +1 -1
- package/claude/skills/document-repair/SKILL.md +5 -5
- package/claude/skills/git-followup/SKILL.md +16 -5
- package/claude/skills/git-issue/references/issue.md +9 -11
- package/claude/skills/git-pr/references/publish.md +4 -5
- package/claude/skills/git-split/SKILL.md +11 -4
- package/claude/skills/markdown-craft/SKILL.md +1 -1
- package/claude/skills/markdown-craft/references/markdown.md +2 -2
- package/claude/skills/markdown-craft/references/tables.md +9 -7
- package/claude/skills/plan-feature/SKILL.md +2 -0
- package/claude/skills/plan-feature/references/plan.md +2 -0
- package/claude/skills/review-address/SKILL.md +9 -7
- package/claude/skills/review-craft/SKILL.md +7 -9
- package/claude/skills/review-craft/references/adopted.md +10 -8
- package/claude/skills/review-pr/SKILL.md +2 -0
- package/claude/skills/review-pr/references/key-changes.md +8 -6
- package/claude/skills/review-ui/references/driving.md +10 -8
- package/claude/skills/role-orchestrator/references/orchestrator-parked.md +6 -4
- package/claude/skills/role-orchestrator/references/orchestrator-refill.md +9 -3
- package/claude/skills/role-reviewer/SKILL.md +5 -5
- package/claude/skills/search-craft/SKILL.md +5 -7
- package/claude/skills/session-worktree/SKILL.md +2 -0
- package/claude/skills/session-worktree/references/main-root-writes.md +2 -0
- package/claude/skills/systematic-debugging/SKILL.md +9 -9
- package/claude/skills/systematic-debugging/references/adopted.md +1 -1
- package/claude/skills/target-check/SKILL.md +6 -8
- package/claude/skills/target-check/references/domains.md +1 -4
- package/claude/skills/target-setup/SKILL.md +8 -8
- package/claude/skills/target-update/SKILL.md +5 -5
- package/claude/skills/task-board/SKILL.md +2 -4
- package/claude/skills/task-board/references/tasks.md +5 -5
- package/claude/skills/task-complete/references/archive.md +5 -2
- package/claude/skills/test-craft/SKILL.md +2 -0
- package/claude/skills/test-craft/references/adopted.md +2 -0
- package/claude/skills/ui-checklist/SKILL.md +8 -9
- package/claude/skills/write-doc/SKILL.md +8 -8
- package/claude/skills/write-doc/references/readme.md +2 -0
- package/claude/skills/write-doc/references/wiki-kind.md +11 -4
- package/docs/agents/context-audit.md +16 -14
- package/docs/agents/install-and-sync.md +2 -2
- package/docs/agents/markdown-audit.md +11 -4
- package/docs/agents/migrate.md +15 -13
- package/docs/agents/output-shape.md +13 -10
- package/docs/target/migrations.md +3 -3
- package/docs/target/sync.md +1 -1
- package/docs/workflow/operating-model.md +2 -0
- package/package.json +1 -1
- package/scripts/core/install-check.sh +1 -1
- package/scripts/lib/ui.sh +12 -65
- package/src/audits/run.ts +1 -1
- package/src/claude/routing.ts +1 -1
- package/src/cli/run.ts +5 -6
- package/src/cli/ui.ts +87 -28
- package/src/cli.ts +6 -0
- package/src/commands/claude.ts +2 -3
- package/src/commands/design.ts +8 -13
- package/src/commands/gate.ts +5 -3
- package/src/commands/gov.ts +11 -21
- package/src/commands/labels.ts +1 -1
- package/src/commands/markdown.ts +25 -29
- package/src/commands/pr.ts +1 -1
- package/src/commands/sync.ts +1 -1
- package/src/commands/tooling.ts +5 -8
- package/src/commands/transcripts.ts +6 -11
- package/src/gate/sequencer.ts +1 -1
- package/src/gate/stages.ts +0 -10
- package/src/hooks/post-merge.ts +2 -5
- package/src/labels/density.ts +5 -46
- package/src/markdown/density.ts +41 -6
- package/src/markdown/scope.ts +6 -1
- package/src/markdown/structure.ts +18 -9
- package/src/markdown/tables.ts +32 -18
- package/src/process/harness.ts +11 -4
- package/src/process/seeded-package.ts +21 -0
- package/src/sandbox/equivalence.ts +0 -1
- package/src/sandbox/provision.ts +8 -13
- package/src/sandbox/scenario.ts +6 -12
- package/src/serve/static.ts +2 -60
- package/src/sync/engine.ts +3 -8
- package/src/sync/workflow.ts +3 -5
- package/src/tooling/verify-stack.ts +1 -1
- package/tooling/base/configs/.husky/post-merge +6 -10
- package/tooling/base/reference.md +7 -6
- package/tooling/claude/reference.md +7 -5
- package/tooling/claude/seeds/.claude/hooks/standards-audit.sh +7 -6
- package/scripts/core/check-color-source.sh +0 -51
- package/src/serve/live.ts +0 -113
|
@@ -2,7 +2,7 @@ import { resolve } from 'node:path'
|
|
|
2
2
|
import type { Command } from 'commander'
|
|
3
3
|
import { recordDir } from '@/roots/record'
|
|
4
4
|
import { ensureYtDlp, fetchOne } from '@/transcripts/fetch'
|
|
5
|
-
import {
|
|
5
|
+
import { intro, logError, logInfo, outro } from '@/cli/ui'
|
|
6
6
|
import { mainWorktreeRoot } from '@/git/worktree'
|
|
7
7
|
|
|
8
8
|
interface TranscriptOptions {
|
|
@@ -37,10 +37,7 @@ export function register(program: Command): void {
|
|
|
37
37
|
)
|
|
38
38
|
.action(async (url: string, opts: TranscriptOptions) => {
|
|
39
39
|
const outDir = await resolveOutDir(opts)
|
|
40
|
-
|
|
41
|
-
process.stderr.write(
|
|
42
|
-
`${GREY}┌${NC}\n${GREY}│${NC} ${WHITE}canon transcripts${NC}\n`,
|
|
43
|
-
)
|
|
40
|
+
intro('canon transcripts')
|
|
44
41
|
try {
|
|
45
42
|
ensureYtDlp()
|
|
46
43
|
const target = await fetchOne(url, {
|
|
@@ -48,14 +45,12 @@ export function register(program: Command): void {
|
|
|
48
45
|
keepTimestamps: opts.keepTimestamps ?? false,
|
|
49
46
|
})
|
|
50
47
|
process.stdout.write(`${target}\n`)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
)
|
|
48
|
+
logInfo(target)
|
|
49
|
+
outro()
|
|
54
50
|
} catch (error) {
|
|
55
51
|
const message = error instanceof Error ? error.message : String(error)
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
)
|
|
52
|
+
logError(message)
|
|
53
|
+
outro()
|
|
59
54
|
process.exitCode = 1
|
|
60
55
|
}
|
|
61
56
|
})
|
package/src/gate/sequencer.ts
CHANGED
package/src/gate/stages.ts
CHANGED
|
@@ -119,12 +119,6 @@ const TESTS_SCOPE = new RegExp(
|
|
|
119
119
|
].join('|'),
|
|
120
120
|
)
|
|
121
121
|
|
|
122
|
-
const script = (name: string, failure: string): Check => ({
|
|
123
|
-
kind: 'command',
|
|
124
|
-
argv: ['bash', `scripts/core/${name}`],
|
|
125
|
-
failure,
|
|
126
|
-
})
|
|
127
|
-
|
|
128
122
|
/**
|
|
129
123
|
* Every stage the gate runs, in the order it runs them.
|
|
130
124
|
*
|
|
@@ -530,10 +524,6 @@ export const STAGES: readonly Stage[] = [
|
|
|
530
524
|
argv: ['bun', 'run', 'check:shell'],
|
|
531
525
|
failure: 'Shell check failed',
|
|
532
526
|
},
|
|
533
|
-
script(
|
|
534
|
-
'check-color-source.sh',
|
|
535
|
-
'A color escape is defined outside scripts/lib/ui.sh.',
|
|
536
|
-
),
|
|
537
527
|
],
|
|
538
528
|
success: 'Shell check passed',
|
|
539
529
|
},
|
package/src/hooks/post-merge.ts
CHANGED
|
@@ -359,11 +359,8 @@ export async function runPostMerge(input: PostMergeInput): Promise<void> {
|
|
|
359
359
|
}
|
|
360
360
|
|
|
361
361
|
if (!isSkipped('CANON_SKIP_UPGRADE')) {
|
|
362
|
-
await step(
|
|
363
|
-
|
|
364
|
-
at([...STEP_VERBS.upgrade, '--json'], { CANON_NON_INTERACTIVE: '1' }),
|
|
365
|
-
(result, fields) =>
|
|
366
|
-
moveLines(result, fields, '⬆️ ', 'CLI not upgraded', []),
|
|
362
|
+
await step(input, at([...STEP_VERBS.upgrade, '--json']), (result, fields) =>
|
|
363
|
+
moveLines(result, fields, '⬆️ ', 'CLI not upgraded', []),
|
|
367
364
|
)
|
|
368
365
|
}
|
|
369
366
|
|
package/src/labels/density.ts
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
densityFindings,
|
|
5
|
-
} from '@/markdown/density'
|
|
6
|
-
import { type BodyLine, bodyLines, visibleText } from '@/markdown/scan'
|
|
7
|
-
import { CHECKPOINTS, LIST_ITEM } from '@/markdown/structure'
|
|
1
|
+
import { type DensityKind, densityFindings } from '@/markdown/density'
|
|
2
|
+
import { type BodyLine, bodyLines } from '@/markdown/scan'
|
|
3
|
+
import { LIST_ITEM } from '@/markdown/structure'
|
|
8
4
|
|
|
9
5
|
/**
|
|
10
6
|
* The four counted limits a pull request section carries. The heading limits
|
|
@@ -119,7 +115,6 @@ export interface PostDensityFinding extends Omit<BodyDensityFinding, 'kind'> {
|
|
|
119
115
|
}
|
|
120
116
|
|
|
121
117
|
const LENGTHS: readonly DensityKind[] = ['paragraph-length', 'bullet-length']
|
|
122
|
-
const NESTED_ITEM = /^(\s+)(?:[-*+]|\d+\.)\s+(.*)$/
|
|
123
118
|
const CHECKBOX = /^\s*[-*+]\s+\[[ xX]\]/
|
|
124
119
|
const FOOTER = /^🤖/
|
|
125
120
|
|
|
@@ -134,8 +129,8 @@ const FOOTER = /^🤖/
|
|
|
134
129
|
* paragraph run, and a checklist box under `What to look at` takes no length
|
|
135
130
|
* limit, since a driver needs its route, action, and result.
|
|
136
131
|
*
|
|
137
|
-
* A nested item is measured as a bullet of its own
|
|
138
|
-
* leaves out of its parent, so a finding's subject and its evidence never sum.
|
|
132
|
+
* A nested item is measured as a bullet of its own by `densityFindings()`, which
|
|
133
|
+
* leaves it out of its parent, so a finding's subject and its evidence never sum.
|
|
139
134
|
*/
|
|
140
135
|
export function postDensity(body: string): PostDensityFinding[] {
|
|
141
136
|
const lines = bodyLines(body).map((line) =>
|
|
@@ -153,7 +148,6 @@ export function postDensity(body: string): PostDensityFinding[] {
|
|
|
153
148
|
(chunk) => densityFindings(chunk),
|
|
154
149
|
),
|
|
155
150
|
...densityFindings(inside).filter(({ kind }) => LENGTHS.includes(kind)),
|
|
156
|
-
...nestedLengths(section.lines),
|
|
157
151
|
]
|
|
158
152
|
|
|
159
153
|
for (const finding of measured) {
|
|
@@ -214,41 +208,6 @@ function foldedApart(lines: readonly BodyLine[]): {
|
|
|
214
208
|
return { outside, inside }
|
|
215
209
|
}
|
|
216
210
|
|
|
217
|
-
/**
|
|
218
|
-
* A `bullet-length` finding for each nested item past the bullet limit, its
|
|
219
|
-
* length taken over the item and the lines indented under it that open no
|
|
220
|
-
* item of their own.
|
|
221
|
-
*/
|
|
222
|
-
function nestedLengths(lines: readonly BodyLine[]): DensityFinding[] {
|
|
223
|
-
const found: DensityFinding[] = []
|
|
224
|
-
|
|
225
|
-
for (const [index, line] of lines.entries()) {
|
|
226
|
-
const match = line.fenced ? null : NESTED_ITEM.exec(line.text)
|
|
227
|
-
if (!match) continue
|
|
228
|
-
|
|
229
|
-
const indent = match[1].length
|
|
230
|
-
const parts = [match[2].trim()]
|
|
231
|
-
for (const next of lines.slice(index + 1)) {
|
|
232
|
-
const lead = /^\s*/.exec(next.text)?.[0].length ?? 0
|
|
233
|
-
if (next.fenced || next.text.trim() === '' || lead <= indent) break
|
|
234
|
-
if (LIST_ITEM.test(next.text)) break
|
|
235
|
-
parts.push(next.text.trim())
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
const length = visibleText(parts.join(' ')).length
|
|
239
|
-
if (length > CHECKPOINTS.bullet) {
|
|
240
|
-
found.push({
|
|
241
|
-
kind: 'bullet-length',
|
|
242
|
-
line: line.number,
|
|
243
|
-
value: length,
|
|
244
|
-
limit: CHECKPOINTS.bullet,
|
|
245
|
-
})
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
return found
|
|
250
|
-
}
|
|
251
|
-
|
|
252
211
|
function sectionsOf(lines: readonly BodyLine[]): Section[] {
|
|
253
212
|
const sections: Section[] = []
|
|
254
213
|
let current: { name: string | null; lines: BodyLine[] } = {
|
package/src/markdown/density.ts
CHANGED
|
@@ -22,6 +22,8 @@ export type DensityKind =
|
|
|
22
22
|
| 'bullet-run'
|
|
23
23
|
| 'heading-depth'
|
|
24
24
|
| 'h3-opening'
|
|
25
|
+
| 'table-cell'
|
|
26
|
+
| 'table-then-list'
|
|
25
27
|
|
|
26
28
|
export interface DensityFinding {
|
|
27
29
|
readonly kind: DensityKind
|
|
@@ -52,14 +54,16 @@ type Block =
|
|
|
52
54
|
* here it belongs to the item: it counts toward the bullet's length and never
|
|
53
55
|
* toward a paragraph run, since a reader sees it inside the list.
|
|
54
56
|
*
|
|
55
|
-
* A nested item is
|
|
56
|
-
*
|
|
57
|
-
*
|
|
57
|
+
* A nested item is a block of its own, taking the lines indented past its
|
|
58
|
+
* marker, and its parent leaves it out along with everything indented under it.
|
|
59
|
+
* An indented fence inside an item joins neither and ends the nested item, and
|
|
60
|
+
* a column-zero one ends both.
|
|
58
61
|
*/
|
|
59
62
|
function blocksOf(lines: readonly BodyLine[]): Block[] {
|
|
60
63
|
const blocks: Block[] = []
|
|
61
64
|
let paragraph: { line: number; parts: string[] } | undefined
|
|
62
65
|
let item: { line: number; parts: string[]; nested: boolean } | undefined
|
|
66
|
+
let child: { line: number; parts: string[]; indent: number } | undefined
|
|
63
67
|
let inFence = false
|
|
64
68
|
let afterBlank = false
|
|
65
69
|
|
|
@@ -74,7 +78,19 @@ function blocksOf(lines: readonly BodyLine[]): Block[] {
|
|
|
74
78
|
paragraph = undefined
|
|
75
79
|
}
|
|
76
80
|
|
|
81
|
+
const closeChild = (): void => {
|
|
82
|
+
if (child) {
|
|
83
|
+
blocks.push({
|
|
84
|
+
kind: 'item',
|
|
85
|
+
line: child.line,
|
|
86
|
+
text: child.parts.join(' '),
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
child = undefined
|
|
90
|
+
}
|
|
91
|
+
|
|
77
92
|
const closeItem = (): void => {
|
|
93
|
+
closeChild()
|
|
78
94
|
if (item) {
|
|
79
95
|
blocks.push({ kind: 'item', line: item.line, text: item.parts.join(' ') })
|
|
80
96
|
}
|
|
@@ -95,7 +111,8 @@ function blocksOf(lines: readonly BodyLine[]): Block[] {
|
|
|
95
111
|
// The opening delimiter decides who owns the block. `bodyLines` marks a
|
|
96
112
|
// delimiter and the body alike, so the opener is the first fenced line.
|
|
97
113
|
if (!inFence) {
|
|
98
|
-
if (
|
|
114
|
+
if (item && indented) closeChild()
|
|
115
|
+
else {
|
|
99
116
|
closeAll()
|
|
100
117
|
blocks.push({ kind: 'other', line: line.number, table: false })
|
|
101
118
|
}
|
|
@@ -108,13 +125,27 @@ function blocksOf(lines: readonly BodyLine[]): Block[] {
|
|
|
108
125
|
|
|
109
126
|
if (text === '') {
|
|
110
127
|
closeParagraph()
|
|
128
|
+
closeChild()
|
|
111
129
|
afterBlank = true
|
|
112
130
|
continue
|
|
113
131
|
}
|
|
114
132
|
|
|
115
133
|
if (indented && item) {
|
|
116
|
-
|
|
117
|
-
|
|
134
|
+
const lead = leadOf(raw)
|
|
135
|
+
if (LIST_ITEM.test(raw)) {
|
|
136
|
+
closeChild()
|
|
137
|
+
item.nested = true
|
|
138
|
+
child = {
|
|
139
|
+
line: line.number,
|
|
140
|
+
parts: [text.replace(LIST_ITEM, '')],
|
|
141
|
+
indent: lead,
|
|
142
|
+
}
|
|
143
|
+
} else if (child && lead > child.indent) {
|
|
144
|
+
if (!TABLE_ROW.test(raw)) child.parts.push(text)
|
|
145
|
+
} else {
|
|
146
|
+
closeChild()
|
|
147
|
+
if (!item.nested && !TABLE_ROW.test(raw)) item.parts.push(text)
|
|
148
|
+
}
|
|
118
149
|
afterBlank = false
|
|
119
150
|
continue
|
|
120
151
|
}
|
|
@@ -163,6 +194,10 @@ function blocksOf(lines: readonly BodyLine[]): Block[] {
|
|
|
163
194
|
return blocks
|
|
164
195
|
}
|
|
165
196
|
|
|
197
|
+
function leadOf(raw: string): number {
|
|
198
|
+
return raw.length - raw.trimStart().length
|
|
199
|
+
}
|
|
200
|
+
|
|
166
201
|
/**
|
|
167
202
|
* Every breach of the six density limits, in line order.
|
|
168
203
|
*
|
package/src/markdown/scope.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
densityFindings,
|
|
5
5
|
} from '@/markdown/density'
|
|
6
6
|
import type { BodyLine } from '@/markdown/scan'
|
|
7
|
+
import { tableDensityFindings } from '@/markdown/tables'
|
|
7
8
|
|
|
8
9
|
/** The surfaces the retired bullet-stacks stage held, per `ALWAYS_GATED`. */
|
|
9
10
|
const BULLET_RUN_SCOPE = [
|
|
@@ -68,12 +69,16 @@ function keyPathsSection(lines: readonly BodyLine[]): readonly BodyLine[] {
|
|
|
68
69
|
* The density findings a path takes, being every finding on a density-scope
|
|
69
70
|
* file and the Key paths findings on a `CLAUDE.md`. The audit, the init probe,
|
|
70
71
|
* and `canon claude sync` all read this, so the three agree on what counts.
|
|
72
|
+
* The table cell and trailing-list findings join on a density-scope path only,
|
|
73
|
+
* since `densityFindings` stays what its direct callers grade.
|
|
71
74
|
*/
|
|
72
75
|
export function scopedDensityFindings(
|
|
73
76
|
rel: string,
|
|
74
77
|
lines: readonly BodyLine[],
|
|
75
78
|
): DensityFinding[] {
|
|
76
|
-
if (inDensityScope(rel))
|
|
79
|
+
if (inDensityScope(rel)) {
|
|
80
|
+
return [...densityFindings(lines), ...tableDensityFindings(lines)]
|
|
81
|
+
}
|
|
77
82
|
if (!KEY_PATHS_SCOPE.test(rel)) return []
|
|
78
83
|
return densityFindings(keyPathsSection(lines)).filter((finding) =>
|
|
79
84
|
KEY_PATHS_KINDS.includes(finding.kind),
|
|
@@ -415,10 +415,10 @@ export function longestRun(
|
|
|
415
415
|
}
|
|
416
416
|
|
|
417
417
|
/**
|
|
418
|
-
* Finds the
|
|
418
|
+
* Finds the bullets carrying more than a decision, nested ones included.
|
|
419
419
|
*
|
|
420
|
-
* A nested item is
|
|
421
|
-
* checkpoint asks what one bullet carries and a child carries its own. Lines
|
|
420
|
+
* A nested item is measured on its own and never folded into its parent, since
|
|
421
|
+
* the checkpoint asks what one bullet carries and a child carries its own. Lines
|
|
422
422
|
* continuing a bullet do fold in, so a heavy bullet cannot fall under the
|
|
423
423
|
* checkpoint by being wrapped across two source lines. A fenced line closes the
|
|
424
424
|
* open bullet, which keeps a bullet from absorbing the example below it.
|
|
@@ -436,6 +436,8 @@ export function heavyBullets(
|
|
|
436
436
|
interface FoldedBullet extends BulletFinding {
|
|
437
437
|
/** The bullet's source lines joined by the space a wrap would have carried. */
|
|
438
438
|
readonly text: string
|
|
439
|
+
/** The width of the indent before the marker, which a continuation must pass. */
|
|
440
|
+
readonly indent: number
|
|
439
441
|
}
|
|
440
442
|
|
|
441
443
|
/** The walk `heavyBullets` reports from. */
|
|
@@ -459,18 +461,18 @@ function foldBullets(
|
|
|
459
461
|
|
|
460
462
|
const item = line.text.match(LIST_ITEM)
|
|
461
463
|
const text = line.text.trim()
|
|
464
|
+
const lead = line.text.length - line.text.trimStart().length
|
|
462
465
|
|
|
463
466
|
// Structure is read off the raw line and only the weight is masked. A line
|
|
464
467
|
// carrying nothing but an autolink has no visible text at all, and reading
|
|
465
468
|
// its masked form as blank would close the bullet it continues.
|
|
466
469
|
if (item) {
|
|
467
470
|
close()
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
}
|
|
471
|
+
open = {
|
|
472
|
+
line: line.number,
|
|
473
|
+
characters: visibleText(text).length,
|
|
474
|
+
text,
|
|
475
|
+
indent: item[1].length,
|
|
474
476
|
}
|
|
475
477
|
continue
|
|
476
478
|
}
|
|
@@ -480,6 +482,13 @@ function foldBullets(
|
|
|
480
482
|
continue
|
|
481
483
|
}
|
|
482
484
|
|
|
485
|
+
// An indented line that does not pass the marker's indent belongs to a
|
|
486
|
+
// parent or to nothing, so it ends a nested item rather than extending it.
|
|
487
|
+
if (open && lead > 0 && lead <= open.indent) {
|
|
488
|
+
close()
|
|
489
|
+
continue
|
|
490
|
+
}
|
|
491
|
+
|
|
483
492
|
// The joining space a wrapped line would have carried, so folding two
|
|
484
493
|
// source lines measures what one unwrapped line would have.
|
|
485
494
|
if (open)
|
package/src/markdown/tables.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { DensityFinding } from '@/markdown/density'
|
|
1
2
|
import { type BodyLine, visibleText } from '@/markdown/scan'
|
|
2
3
|
import { LIST_ITEM, TABLE_ROW, TABLE_SEPARATOR } from '@/markdown/structure'
|
|
3
4
|
|
|
@@ -10,19 +11,14 @@ export const TABLE_ROWS = 8
|
|
|
10
11
|
/** The longest unbroken run a code span in a table cell may carry. */
|
|
11
12
|
export const TABLE_TOKEN = 30
|
|
12
13
|
|
|
13
|
-
export type TableKind =
|
|
14
|
-
| 'table-cell'
|
|
15
|
-
| 'table-rows'
|
|
16
|
-
| 'table-then-list'
|
|
17
|
-
| 'table-token'
|
|
14
|
+
export type TableKind = 'table-rows' | 'table-token'
|
|
18
15
|
|
|
19
16
|
export interface TableFinding {
|
|
20
17
|
readonly kind: TableKind
|
|
21
|
-
/** The row
|
|
18
|
+
/** The span's row, or the header for a row count. */
|
|
22
19
|
readonly line: number
|
|
23
|
-
|
|
24
|
-
readonly
|
|
25
|
-
readonly limit: number | null
|
|
20
|
+
readonly value: number
|
|
21
|
+
readonly limit: number
|
|
26
22
|
}
|
|
27
23
|
|
|
28
24
|
/**
|
|
@@ -55,16 +51,22 @@ function cellsOf(row: string): string[] {
|
|
|
55
51
|
}
|
|
56
52
|
|
|
57
53
|
/**
|
|
58
|
-
* Every breach of the four table limits, in line order
|
|
54
|
+
* Every breach of the four table limits, in line order, split by whether it
|
|
55
|
+
* gates.
|
|
59
56
|
*
|
|
60
57
|
* A table is a run of pipe rows whose second row is a delimiter, the shape a
|
|
61
58
|
* renderer draws. Pipe lines with no delimiter render as paragraph text, and a
|
|
62
59
|
* fenced line is an example, so neither is measured. The limits are a
|
|
63
60
|
* 120-character cell, 8 body rows, a list as the next block, and a code span
|
|
64
|
-
* holding a run past 30 characters.
|
|
61
|
+
* holding a run past 30 characters. The cell and the trailing list are density
|
|
62
|
+
* findings, and the row count and the wide token report and never gate.
|
|
65
63
|
*/
|
|
66
|
-
|
|
67
|
-
|
|
64
|
+
function measureTables(lines: readonly BodyLine[]): {
|
|
65
|
+
reported: TableFinding[]
|
|
66
|
+
density: DensityFinding[]
|
|
67
|
+
} {
|
|
68
|
+
const reported: TableFinding[] = []
|
|
69
|
+
const density: DensityFinding[] = []
|
|
68
70
|
let index = 0
|
|
69
71
|
|
|
70
72
|
while (index < lines.length) {
|
|
@@ -96,7 +98,7 @@ export function tableFindings(lines: readonly BodyLine[]): TableFinding[] {
|
|
|
96
98
|
for (const cell of cellsOf(row.text)) {
|
|
97
99
|
const length = visibleText(cell).length
|
|
98
100
|
if (length > TABLE_CELL) {
|
|
99
|
-
|
|
101
|
+
density.push({
|
|
100
102
|
kind: 'table-cell',
|
|
101
103
|
line: row.number,
|
|
102
104
|
value: length,
|
|
@@ -106,7 +108,7 @@ export function tableFindings(lines: readonly BodyLine[]): TableFinding[] {
|
|
|
106
108
|
for (const span of codeSpans(cell)) {
|
|
107
109
|
const run = unbrokenRun(span)
|
|
108
110
|
if (run > TABLE_TOKEN) {
|
|
109
|
-
|
|
111
|
+
reported.push({
|
|
110
112
|
kind: 'table-token',
|
|
111
113
|
line: row.number,
|
|
112
114
|
value: run,
|
|
@@ -119,7 +121,7 @@ export function tableFindings(lines: readonly BodyLine[]): TableFinding[] {
|
|
|
119
121
|
|
|
120
122
|
const bodyRows = tableRows.length - 1
|
|
121
123
|
if (bodyRows > TABLE_ROWS) {
|
|
122
|
-
|
|
124
|
+
reported.push({
|
|
123
125
|
kind: 'table-rows',
|
|
124
126
|
line: header.number,
|
|
125
127
|
value: bodyRows,
|
|
@@ -135,7 +137,7 @@ export function tableFindings(lines: readonly BodyLine[]): TableFinding[] {
|
|
|
135
137
|
!following.fenced &&
|
|
136
138
|
LIST_ITEM.test(following.text)
|
|
137
139
|
) {
|
|
138
|
-
|
|
140
|
+
density.push({
|
|
139
141
|
kind: 'table-then-list',
|
|
140
142
|
line: following.number,
|
|
141
143
|
value: null,
|
|
@@ -146,5 +148,17 @@ export function tableFindings(lines: readonly BodyLine[]): TableFinding[] {
|
|
|
146
148
|
index = end
|
|
147
149
|
}
|
|
148
150
|
|
|
149
|
-
return
|
|
151
|
+
return { reported, density }
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** The row count and wide token findings, which report and never gate. */
|
|
155
|
+
export function tableFindings(lines: readonly BodyLine[]): TableFinding[] {
|
|
156
|
+
return measureTables(lines).reported.sort((a, b) => a.line - b.line)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** The cell and trailing-list findings, which gate as density kinds do. */
|
|
160
|
+
export function tableDensityFindings(
|
|
161
|
+
lines: readonly BodyLine[],
|
|
162
|
+
): DensityFinding[] {
|
|
163
|
+
return measureTables(lines).density.sort((a, b) => a.line - b.line)
|
|
150
164
|
}
|
package/src/process/harness.ts
CHANGED
|
@@ -9,6 +9,13 @@ const CLI = join(PROJECT_ROOT, 'src/cli.ts')
|
|
|
9
9
|
/** No case has ever needed longer, and a blocked verb should fail fast. */
|
|
10
10
|
const DEFAULT_TIMEOUT_MS = 10_000
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* A spawn that reaches `bun add` fails at once against this unreachable
|
|
14
|
+
* address, so an unseeded install is a loud defect rather than a test that
|
|
15
|
+
* passes or fails with the network. `options.env` still wins.
|
|
16
|
+
*/
|
|
17
|
+
const DEAD_REGISTRY = 'http://127.0.0.1:9'
|
|
18
|
+
|
|
12
19
|
export interface ProcessRun {
|
|
13
20
|
readonly status: number | null
|
|
14
21
|
readonly stdout: string
|
|
@@ -90,8 +97,7 @@ export function snapshotStateDir(): string {
|
|
|
90
97
|
*
|
|
91
98
|
* A git hook exports `GIT_DIR`, which would resolve a fixture's git-aware
|
|
92
99
|
* reads against this checkout instead of the temporary directory a case
|
|
93
|
-
* builds, so every spawn drops the `GIT_` prefix
|
|
94
|
-
* flag every case needs to avoid a picker blocking on stdin.
|
|
100
|
+
* builds, so every spawn drops the `GIT_` prefix.
|
|
95
101
|
*
|
|
96
102
|
* `CANON_STATE_DIR` and `CANON_SANDBOX_DIR` get the same treatment as `GIT_DIR`,
|
|
97
103
|
* each pointed at a folder under the case's own `cwd` rather than dropped,
|
|
@@ -99,7 +105,8 @@ export function snapshotStateDir(): string {
|
|
|
99
105
|
* `HOME` to this machine's real `~/.local/state/canon`. `stateDir()` and
|
|
100
106
|
* `sandboxTree()` resolve the same three ways and share that parent, so both
|
|
101
107
|
* overrides move together. A case explicitly passing its own value through
|
|
102
|
-
* `options.env` still wins
|
|
108
|
+
* `options.env` still wins. The child's stdin is a pipe rather than a
|
|
109
|
+
* terminal, so every case runs headless.
|
|
103
110
|
*
|
|
104
111
|
* The `stateDir()` snapshot before and after the spawn is what actually
|
|
105
112
|
* catches an escape past that redirection, since a default can be wrong in a
|
|
@@ -125,9 +132,9 @@ export function runCli(
|
|
|
125
132
|
timeout: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
126
133
|
env: {
|
|
127
134
|
...inherited,
|
|
128
|
-
CANON_NON_INTERACTIVE: '1',
|
|
129
135
|
CANON_STATE_DIR: join(options.cwd, '.canon-state'),
|
|
130
136
|
CANON_SANDBOX_DIR: join(options.cwd, '.canon-state', 'sandbox'),
|
|
137
|
+
BUN_CONFIG_REGISTRY: DEAD_REGISTRY,
|
|
131
138
|
...options.env,
|
|
132
139
|
},
|
|
133
140
|
})
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { existsSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { PROJECT_ROOT } from '@/roots/project'
|
|
4
|
+
import { resolveChain } from '@/tooling/manifest'
|
|
5
|
+
import { collectDeps, parseSpec } from '@/tooling/package'
|
|
6
|
+
|
|
7
|
+
// A write run into a folder with no package.json would run `bun add -D` against
|
|
8
|
+
// the registry. A package.json already naming the stack's packages leaves
|
|
9
|
+
// nothing to install, so a test not about the manifest stays offline.
|
|
10
|
+
export const seedPackage = (dir: string, stack: string): void => {
|
|
11
|
+
const path = join(dir, 'package.json')
|
|
12
|
+
if (existsSync(path)) return
|
|
13
|
+
|
|
14
|
+
const devDependencies = Object.fromEntries(
|
|
15
|
+
collectDeps(resolveChain(PROJECT_ROOT, stack), {}).map((dep) => [
|
|
16
|
+
dep.name,
|
|
17
|
+
parseSpec(dep.spec).pin || '*',
|
|
18
|
+
]),
|
|
19
|
+
)
|
|
20
|
+
writeFileSync(path, JSON.stringify({ devDependencies }))
|
|
21
|
+
}
|
package/src/sandbox/provision.ts
CHANGED
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
logStep,
|
|
28
28
|
logWarn,
|
|
29
29
|
outro,
|
|
30
|
-
|
|
30
|
+
outroSuccess,
|
|
31
31
|
select,
|
|
32
32
|
} from '@/cli/ui'
|
|
33
33
|
|
|
@@ -99,9 +99,7 @@ function readGit(cwd: string, args: string[]): string | undefined {
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
function finish(message: string): void {
|
|
102
|
-
|
|
103
|
-
outro()
|
|
104
|
-
process.stderr.write(`\n${GREEN}${message}${NC}\n`)
|
|
102
|
+
outroSuccess(message)
|
|
105
103
|
}
|
|
106
104
|
|
|
107
105
|
function stageAnchorTree(
|
|
@@ -210,7 +208,7 @@ function install(
|
|
|
210
208
|
env: NodeJS.ProcessEnv,
|
|
211
209
|
): void {
|
|
212
210
|
const result = spawnSync('bun', [join(root, 'src', 'cli.ts'), ...args], {
|
|
213
|
-
env
|
|
211
|
+
env,
|
|
214
212
|
encoding: 'utf8',
|
|
215
213
|
stdio: ['ignore', 'ignore', 'pipe'],
|
|
216
214
|
})
|
|
@@ -432,10 +430,7 @@ async function provisionScenario(
|
|
|
432
430
|
baseEnv: NodeJS.ProcessEnv,
|
|
433
431
|
): Promise<void> {
|
|
434
432
|
const env: NodeJS.ProcessEnv = { ...baseEnv }
|
|
435
|
-
if (scenario.arm !== undefined)
|
|
436
|
-
env.SANDBOX_SCENARIO = scenario.arm
|
|
437
|
-
env.CANON_NON_INTERACTIVE = '1'
|
|
438
|
-
}
|
|
433
|
+
if (scenario.arm !== undefined) env.SANDBOX_SCENARIO = scenario.arm
|
|
439
434
|
|
|
440
435
|
const sandboxDir = join(root, 'sandbox')
|
|
441
436
|
const declared = declareScenario(root, scenario, env)
|
|
@@ -475,14 +470,14 @@ async function provisionScenario(
|
|
|
475
470
|
must(commitScenarioChanges(sandbox, isSkipAutoCommit, env))
|
|
476
471
|
tagBaseline(sandbox, env)
|
|
477
472
|
|
|
478
|
-
finish('
|
|
473
|
+
finish('Sandbox Ready')
|
|
479
474
|
}
|
|
480
475
|
|
|
481
476
|
function cleanSandbox(sandbox: string): void {
|
|
482
477
|
logStep('Removing sandbox')
|
|
483
478
|
rmSync(sandbox, { recursive: true, force: true })
|
|
484
479
|
logRemove(sandbox)
|
|
485
|
-
finish('
|
|
480
|
+
finish('Sandbox clean')
|
|
486
481
|
}
|
|
487
482
|
|
|
488
483
|
function isAtBaseline(sandbox: string): boolean {
|
|
@@ -520,7 +515,7 @@ async function resetSandbox(sandbox: string): Promise<void> {
|
|
|
520
515
|
|
|
521
516
|
if (isAtBaseline(sandbox)) {
|
|
522
517
|
logInfo('At baseline')
|
|
523
|
-
finish('
|
|
518
|
+
finish('Sandbox at baseline')
|
|
524
519
|
return
|
|
525
520
|
}
|
|
526
521
|
|
|
@@ -556,7 +551,7 @@ async function resetSandbox(sandbox: string): Promise<void> {
|
|
|
556
551
|
must(run('git', ['checkout-index', '-a', '-f'], quiet))
|
|
557
552
|
}
|
|
558
553
|
logInfo('Sandbox reset to baseline')
|
|
559
|
-
finish('
|
|
554
|
+
finish('Sandbox reset complete')
|
|
560
555
|
}
|
|
561
556
|
|
|
562
557
|
/**
|
package/src/sandbox/scenario.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
import { createRequire } from 'node:module'
|
|
14
14
|
import { dirname, join, relative } from 'node:path'
|
|
15
15
|
import {
|
|
16
|
+
isNonInteractive,
|
|
16
17
|
logError,
|
|
17
18
|
logInfo,
|
|
18
19
|
logStep,
|
|
@@ -580,8 +581,9 @@ export function createStageContext(options: ContextOptions): StageContext {
|
|
|
580
581
|
}
|
|
581
582
|
|
|
582
583
|
/**
|
|
583
|
-
* Picks the arm: a routed name first,
|
|
584
|
-
*
|
|
584
|
+
* Picks the arm: a routed name first, echoed on the frame, then a prompt. A
|
|
585
|
+
* headless caller naming no arm is refused rather than handed the first one,
|
|
586
|
+
* since a verdict on an arm nobody chose reads as a verdict on the scenario.
|
|
585
587
|
*/
|
|
586
588
|
async function routeArm(
|
|
587
589
|
names: readonly string[],
|
|
@@ -600,17 +602,9 @@ async function routeArm(
|
|
|
600
602
|
const routed = env.SANDBOX_SCENARIO ?? ''
|
|
601
603
|
if (routed !== '') return echo(routed)
|
|
602
604
|
|
|
603
|
-
|
|
604
|
-
// `is_non_interactive` read the shell it ran in.
|
|
605
|
-
const isHeadless =
|
|
606
|
-
env.CANON_NON_INTERACTIVE === '1' ||
|
|
607
|
-
// canon-keep-retired
|
|
608
|
-
env.AITK_NON_INTERACTIVE === '1'
|
|
609
|
-
if (isHeadless) return echo(names[0] ?? DEFAULT_ARM_NAME)
|
|
610
|
-
|
|
611
|
-
if (!process.stdin.isTTY) {
|
|
605
|
+
if (isNonInteractive()) {
|
|
612
606
|
logError(
|
|
613
|
-
`${prompt} requires a TTY. Pass
|
|
607
|
+
`${prompt} requires a TTY. Pass the arm as the scenario argument, one of: ${names.join(', ')}.`,
|
|
614
608
|
)
|
|
615
609
|
throw new ScenarioStop(1, 'exited')
|
|
616
610
|
}
|