@erclx/canon 5.32.0 → 5.33.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/auto-ship/SKILL.md +1 -1
- package/claude/skills/auto-ship/references/worktree-name.md +1 -1
- package/claude/skills/backlog-triage/REQUIREMENT.md +2 -2
- package/claude/skills/backlog-triage/SKILL.md +1 -1
- package/claude/skills/canon-cli/REQUIREMENT.md +16 -13
- package/claude/skills/canon-cli/SKILL.md +75 -32
- package/claude/skills/canon-feedback/SKILL.md +1 -1
- package/claude/skills/{canvas → canvas-board}/REQUIREMENT.md +2 -2
- package/claude/skills/{canvas → canvas-board}/SKILL.md +2 -2
- package/{standards → claude/skills/canvas-board/references}/design.md +11 -2
- package/claude/skills/context-fold/REQUIREMENT.md +25 -33
- package/claude/skills/context-fold/SKILL.md +25 -70
- package/claude/skills/context-fold/references/classify.md +3 -3
- package/claude/skills/context-fold/references/completion-report.md +4 -4
- package/claude/skills/context-fold/references/context-refresh.md +5 -5
- package/{standards → claude/skills/context-fold/references}/context.md +16 -5
- package/claude/skills/context-fold/references/unusable-baseline.md +3 -5
- package/claude/skills/create-rule/SKILL.md +2 -2
- package/{standards → claude/skills/create-rule/references}/rule.md +12 -5
- package/claude/skills/create-skill/SKILL.md +2 -2
- package/{standards → claude/skills/create-skill/references}/skill.md +17 -14
- package/claude/skills/design-taste/REQUIREMENT.md +3 -3
- package/claude/skills/design-taste/SKILL.md +1 -1
- package/claude/skills/docs-sync/REQUIREMENT.md +1 -1
- package/claude/skills/docs-sync/SKILL.md +3 -3
- package/{standards → claude/skills/docs-sync/references}/docs.md +11 -0
- package/{standards → claude/skills/docs-sync/references}/readme.md +9 -2
- package/claude/skills/draft-and-pick/SKILL.md +5 -5
- package/claude/skills/draft-doc/SKILL.md +6 -6
- package/claude/skills/draft-figure/REQUIREMENT.md +2 -2
- package/claude/skills/draft-figure/SKILL.md +2 -2
- package/claude/skills/draft-figure/references/hand-drawn.md +1 -1
- package/claude/skills/draft-figure/references/mermaid-path.md +1 -1
- package/claude/skills/draft-identity/REQUIREMENT.md +2 -2
- package/claude/skills/draft-identity/SKILL.md +2 -2
- package/claude/skills/draft-ready/SKILL.md +1 -1
- package/claude/skills/git-branch/SKILL.md +1 -1
- package/{standards → claude/skills/git-branch/references}/branch.md +1 -1
- package/claude/skills/git-commit/SKILL.md +2 -2
- package/claude/skills/git-followup/REQUIREMENT.md +1 -1
- package/claude/skills/git-followup/SKILL.md +1 -1
- package/claude/skills/git-issue/SKILL.md +1 -1
- package/claude/skills/git-pr/SKILL.md +13 -13
- package/claude/skills/git-pr/references/evidence.md +2 -2
- package/claude/skills/git-pr/references/final-command.md +38 -14
- package/claude/skills/git-pr/references/labels.md +5 -1
- package/{standards → claude/skills/git-pr/references}/pr.md +19 -9
- package/{standards → claude/skills/git-pr/references}/publish.md +4 -2
- package/claude/skills/git-pr/references/task-number.md +1 -1
- package/claude/skills/git-ship/SKILL.md +15 -14
- package/claude/skills/git-split/SKILL.md +3 -3
- package/claude/skills/git-stage/SKILL.md +1 -1
- package/claude/skills/plan-feature/REQUIREMENT.md +1 -1
- package/claude/skills/review-address/SKILL.md +1 -1
- package/claude/skills/review-address/references/post-review-findings.md +1 -1
- package/claude/skills/review-pr/REQUIREMENT.md +1 -1
- package/claude/skills/review-pr/SKILL.md +3 -3
- package/claude/skills/review-pr/references/body-variants.md +2 -2
- package/claude/skills/review-pr/references/key-changes.md +3 -3
- package/claude/skills/review-pr/references/post.md +4 -2
- package/claude/skills/review-ui/REQUIREMENT.md +1 -1
- package/claude/skills/review-ui/SKILL.md +1 -1
- package/claude/skills/review-ui/references/post.md +1 -1
- package/claude/skills/role-orchestrator/REQUIREMENT.md +3 -0
- package/claude/skills/role-orchestrator/SKILL.md +2 -2
- package/claude/skills/role-orchestrator/references/orchestrator-dispatch.md +2 -2
- package/claude/skills/role-orchestrator/references/orchestrator-handoff.md +8 -5
- package/claude/skills/role-orchestrator/references/orchestrator-launch.md +23 -1
- package/claude/skills/role-orchestrator/references/orchestrator-refill.md +2 -2
- package/claude/skills/role-orchestrator/references/orchestrator-resume.md +3 -0
- package/claude/skills/role-planner/REQUIREMENT.md +2 -1
- package/claude/skills/role-planner/SKILL.md +13 -2
- package/claude/skills/role-reviewer/SKILL.md +1 -1
- package/claude/skills/role-worker/REQUIREMENT.md +1 -0
- package/claude/skills/role-worker/SKILL.md +9 -1
- package/claude/skills/session-map/SKILL.md +1 -1
- package/claude/skills/session-resume/REQUIREMENT.md +2 -2
- package/claude/skills/session-worktree/SKILL.md +5 -5
- package/claude/skills/target-setup/references/detect.md +1 -1
- package/claude/skills/task-board/REQUIREMENT.md +7 -12
- package/claude/skills/task-board/SKILL.md +5 -17
- package/claude/skills/task-board/references/tasks.md +2 -2
- package/claude/skills/task-complete/REQUIREMENT.md +47 -0
- package/claude/skills/task-complete/SKILL.md +86 -0
- package/claude/skills/{task-board → task-complete}/references/archive.md +4 -4
- package/claude/skills/{context-fold → task-complete}/references/receipt-sweep.md +5 -5
- package/claude/skills/task-complete/references/unusable-baseline.md +19 -0
- package/claude/skills/teach-workspace/REQUIREMENT.md +6 -2
- package/claude/skills/teach-workspace/SKILL.md +8 -4
- package/claude/skills/teach-workspace/references/lesson-craft.md +1 -1
- package/claude/skills/{context-fold/references/promoted-pages.md → teach-workspace/references/promotion-fold.md} +3 -3
- package/claude/skills/teach-workspace/references/promotion.md +3 -3
- package/claude/skills/ui-checklist/SKILL.md +1 -1
- package/claude/skills/ux-audit/SKILL.md +1 -1
- package/claude/skills/ux-measure/SKILL.md +1 -1
- package/docs/agents/canvas.md +1 -1
- package/docs/agents/commands.md +3 -2
- package/docs/agents/context-audit-checks.md +4 -4
- package/docs/agents/index.md +3 -3
- package/docs/agents/indexes.md +1 -1
- package/docs/agents/install-and-sync.md +6 -11
- package/docs/agents/key-changes.md +5 -5
- package/docs/agents/label-coverage.md +11 -0
- package/docs/agents/migrate.md +1 -1
- package/docs/agents/records-push.md +2 -2
- package/docs/agents/records-stale.md +12 -47
- package/docs/agents/records-validate.md +3 -7
- package/docs/agents/records.md +5 -27
- package/docs/agents/rule-citations.md +1 -1
- package/docs/agents/sandbox.md +4 -4
- package/docs/agents/skills-audit.md +1 -1
- package/docs/agents/tasks-plans.md +2 -2
- package/docs/agents/tasks.md +2 -2
- package/docs/target/migrations.md +10 -2
- package/docs/target/projects.md +1 -1
- package/docs/target/sync.md +1 -1
- package/docs/workflow/operating-model.md +2 -2
- package/docs/workflow/parallel-features.md +7 -7
- package/docs/workflow/skill-map-on-request.md +9 -9
- package/docs/workflow/skill-map.md +12 -11
- package/docs/workflow/visual-design-workflow.md +2 -2
- package/governance/renames.toml +0 -11
- package/governance/rules/canon/context.md +13 -0
- package/governance/rules/claude/skill.md +17 -0
- package/governance/stacks/base.toml +2 -4
- package/package.json +4 -4
- package/scripts/core/install-check.sh +3 -3
- package/scripts/lib/ui.sh +0 -159
- package/src/audits/catalog.ts +0 -1
- package/src/claude/cases/misc.ts +1 -1
- package/src/claude/cases/setup.ts +4 -0
- package/src/claude/cases/workflow.ts +6 -1
- package/src/claude/seeds.ts +1 -1
- package/src/claude/skills-audit.ts +2 -2
- package/src/cli/help.ts +3 -1
- package/src/cli/ui.ts +2 -3
- package/src/commands/claude.ts +1 -1
- package/src/commands/gov.ts +2 -15
- package/src/commands/labels.ts +326 -7
- package/src/commands/migrate.ts +6 -1
- package/src/commands/pr.ts +1 -1
- package/src/commands/records.ts +13 -399
- package/src/commands/sandbox.ts +3 -6
- package/src/commands/tasks.ts +3 -3
- package/src/context/audit.ts +5 -5
- package/src/context/folders.ts +1 -1
- package/src/design/budget.ts +1 -1
- package/src/design/components.ts +1 -1
- package/src/design/document.ts +1 -1
- package/src/design/regen.ts +1 -1
- package/src/design/render.ts +1 -1
- package/src/gate/measures.ts +1 -1
- package/src/gate/sequencer.ts +56 -1
- package/src/gate/stages.ts +1 -7
- package/src/gov/install.ts +2 -3
- package/src/gov/stacks.ts +1 -1
- package/src/hooks/post-merge.ts +1 -1
- package/src/labels/apply.ts +39 -0
- package/src/labels/format.ts +3 -3
- package/src/labels/phase.ts +3 -3
- package/src/labels/sections.ts +43 -0
- package/src/migrate/skill-names.ts +20 -1
- package/src/pr/paths.ts +4 -4
- package/src/records/backup.ts +16 -6
- package/src/records/stale-canonical.ts +31 -1
- package/src/records/validate.ts +2 -188
- package/src/sandbox/census.ts +1 -1
- package/src/sandbox/coverage.ts +2 -6
- package/src/sandbox/equivalence.ts +5 -44
- package/src/sandbox/expect.ts +4 -4
- package/src/sandbox/provision.ts +13 -36
- package/src/sandbox/scenario.ts +26 -48
- package/src/sandbox/tree.ts +2 -6
- package/src/standards/read.ts +14 -1
- package/src/sync/git.ts +1 -1
- package/src/tasks/archive.ts +5 -5
- package/src/tasks/branch.ts +3 -3
- package/src/tasks/label.ts +1 -1
- package/src/tasks/record.ts +2 -2
- package/src/tasks/validate.ts +1 -1
- package/standards/index.md +0 -13
- package/standards/standard.md +1 -7
- package/tooling/base/reference.md +1 -1
- package/tooling/base/seeds/.github/pull_request_template.md +2 -2
- package/tooling/claude/reference.md +1 -1
- package/tooling/claude/seeds/.claude/hooks/pr-create-log.sh +9 -1
- package/tooling/claude/seeds/.claude/hooks/review-post-scan.sh +171 -0
- package/tooling/claude/seeds/.claude/settings.json +9 -0
- package/governance/rules/standards/context.md +0 -14
- package/governance/rules/standards/design.md +0 -12
- package/governance/rules/standards/readme.md +0 -12
- package/governance/rules/standards/rule.md +0 -13
- package/governance/rules/standards/skill.md +0 -21
- package/governance/rules/standards/standard.md +0 -13
- package/scripts/config.sh +0 -11
- package/scripts/lib/gov.sh +0 -14
- package/scripts/lib/sandbox-fixtures.sh +0 -191
- package/scripts/lib/sandbox-git.sh +0 -125
- package/scripts/lib/sandbox-path.sh +0 -50
- package/scripts/sandbox-hook.sh +0 -99
- package/src/gov/standard-rules.ts +0 -179
- package/src/records/migrate.ts +0 -78
- package/src/records/stale.ts +0 -243
- package/src/sandbox/hooks.ts +0 -128
- /package/claude/skills/{canvas → canvas-board}/references/canvas.md +0 -0
- /package/{standards → claude/skills/draft-figure/references}/figures.md +0 -0
- /package/{standards → claude/skills/git-branch/references}/slug.md +0 -0
- /package/{standards → claude/skills/git-commit/references}/commit.md +0 -0
- /package/{standards → claude/skills/git-commit/references}/versioning.md +0 -0
package/src/design/render.ts
CHANGED
|
@@ -105,7 +105,7 @@ interface Confidence {
|
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
/**
|
|
108
|
-
* The columns the confidence ratio reads, fixed by `
|
|
108
|
+
* The columns the confidence ratio reads, fixed by `claude/skills/canvas-board/references/design.md`. The
|
|
109
109
|
* first column of each table names its row, and `Multiplier` and `When used`
|
|
110
110
|
* restate what the row already carries, so none of them is something a source
|
|
111
111
|
* could anchor and none belongs in the denominator.
|
package/src/gate/measures.ts
CHANGED
|
@@ -317,7 +317,7 @@ export const designProse: Measure = async (ctx) => {
|
|
|
317
317
|
if (failures.length > 0) {
|
|
318
318
|
return {
|
|
319
319
|
emissions: [],
|
|
320
|
-
failure: `${failures.join(' ')} Cut to the budget in
|
|
320
|
+
failure: `${failures.join(' ')} Cut to the budget in claude/skills/canvas-board/references/design.md, moving the why to the context entry.`,
|
|
321
321
|
}
|
|
322
322
|
}
|
|
323
323
|
if (emissions.length === 0)
|
package/src/gate/sequencer.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
import { availableParallelism } from 'node:os'
|
|
1
3
|
import { join } from 'node:path'
|
|
2
4
|
import { execa } from 'execa'
|
|
3
5
|
import type {
|
|
@@ -44,6 +46,28 @@ export interface StageResult {
|
|
|
44
46
|
readonly queuedMs?: number
|
|
45
47
|
}
|
|
46
48
|
|
|
49
|
+
export interface LoadReading {
|
|
50
|
+
/** The 1-minute load average. */
|
|
51
|
+
readonly loadAverage: number
|
|
52
|
+
readonly cores: number
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Reads the 1-minute load average from `/proc/loadavg`. A platform without
|
|
57
|
+
* it, as macOS, has no reading to give, so this answers `undefined` rather
|
|
58
|
+
* than throwing into a stage that has already failed.
|
|
59
|
+
*/
|
|
60
|
+
export function readProcLoad(): LoadReading | undefined {
|
|
61
|
+
try {
|
|
62
|
+
const first = readFileSync('/proc/loadavg', 'utf8').split(' ')[0]
|
|
63
|
+
const loadAverage = Number.parseFloat(first)
|
|
64
|
+
if (!Number.isFinite(loadAverage)) return undefined
|
|
65
|
+
return { loadAverage, cores: availableParallelism() }
|
|
66
|
+
} catch {
|
|
67
|
+
return undefined
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
47
71
|
export interface GateContext extends MeasureContext {
|
|
48
72
|
/** Whether the format stage writes or checks. */
|
|
49
73
|
readonly write: boolean
|
|
@@ -58,6 +82,11 @@ export interface GateContext extends MeasureContext {
|
|
|
58
82
|
* wait and reads as a hung suite.
|
|
59
83
|
*/
|
|
60
84
|
readonly onQueue?: (label: string, holder: LockHolder) => void
|
|
85
|
+
/**
|
|
86
|
+
* Reads the machine load a failed locked stage ran under. Defaults to
|
|
87
|
+
* `/proc/loadavg`, and answers `undefined` where there is none.
|
|
88
|
+
*/
|
|
89
|
+
readonly readLoad?: () => LoadReading | undefined
|
|
61
90
|
}
|
|
62
91
|
|
|
63
92
|
export interface ChangedSet {
|
|
@@ -195,7 +224,11 @@ async function executeStage(
|
|
|
195
224
|
]
|
|
196
225
|
|
|
197
226
|
try {
|
|
198
|
-
const outcome =
|
|
227
|
+
const outcome = withLoadLine(
|
|
228
|
+
await runChecks(stage, ctx, queued),
|
|
229
|
+
stage,
|
|
230
|
+
ctx,
|
|
231
|
+
)
|
|
199
232
|
return held.waitedOn === undefined
|
|
200
233
|
? outcome
|
|
201
234
|
: { ...outcome, queuedMs: held.waitedMs }
|
|
@@ -204,6 +237,28 @@ async function executeStage(
|
|
|
204
237
|
}
|
|
205
238
|
}
|
|
206
239
|
|
|
240
|
+
/**
|
|
241
|
+
* The lock serializes suites and does nothing about the rest of the box, so a
|
|
242
|
+
* starved spawn test and a real failure read alike. The load beside the
|
|
243
|
+
* failure is what lets a report tell them apart.
|
|
244
|
+
*/
|
|
245
|
+
function withLoadLine(
|
|
246
|
+
outcome: StageOutcome,
|
|
247
|
+
stage: Stage,
|
|
248
|
+
ctx: GateContext,
|
|
249
|
+
): StageOutcome {
|
|
250
|
+
if (outcome.status !== 'failed') return outcome
|
|
251
|
+
|
|
252
|
+
const reading = (ctx.readLoad ?? readProcLoad)()
|
|
253
|
+
if (reading === undefined) return outcome
|
|
254
|
+
|
|
255
|
+
const text = `Load average ${reading.loadAverage} on ${reading.cores} cores when the ${stage.label} stage failed`
|
|
256
|
+
return {
|
|
257
|
+
...outcome,
|
|
258
|
+
emissions: [...outcome.emissions, { kind: 'info', text }],
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
207
262
|
async function runChecks(
|
|
208
263
|
stage: Stage,
|
|
209
264
|
ctx: GateContext,
|
package/src/gate/stages.ts
CHANGED
|
@@ -198,17 +198,11 @@ export const STAGES: readonly Stage[] = [
|
|
|
198
198
|
argv: ['gov', 'regen'],
|
|
199
199
|
failure: 'Consumed-copy regen failed',
|
|
200
200
|
},
|
|
201
|
-
{
|
|
202
|
-
kind: 'drift',
|
|
203
|
-
pathspec: 'governance/rules/standards',
|
|
204
|
-
failure:
|
|
205
|
-
'Standard-pointer rules drifted. Run bun run check and commit governance/rules/standards and .claude/rules.',
|
|
206
|
-
},
|
|
207
201
|
{
|
|
208
202
|
kind: 'drift',
|
|
209
203
|
pathspec: '.claude/rules',
|
|
210
204
|
failure:
|
|
211
|
-
'Consumed copies drifted. Run bun run check and commit
|
|
205
|
+
'Consumed copies drifted. Run bun run check and commit .claude/rules.',
|
|
212
206
|
},
|
|
213
207
|
],
|
|
214
208
|
success: 'Consumed copies clean',
|
package/src/gov/install.ts
CHANGED
|
@@ -66,9 +66,8 @@ export function installedRuleNames(
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
/**
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
* subdirectory and installs flat.
|
|
69
|
+
* A rule sitting directly under `governance/rules/` has no subdirectory and
|
|
70
|
+
* installs flat.
|
|
72
71
|
*/
|
|
73
72
|
export function ruleSubdir(src: string, rulesRoot: string): string {
|
|
74
73
|
const subdir = dirname(relative(rulesRoot, src))
|
package/src/gov/stacks.ts
CHANGED
|
@@ -79,7 +79,7 @@ export function loadGovStack(
|
|
|
79
79
|
* shape rather than two the caller has to tell apart.
|
|
80
80
|
*
|
|
81
81
|
* The directory wins over a rule file of the same name. They cannot collide
|
|
82
|
-
* while `
|
|
82
|
+
* while `claude/skills/create-rule/references/rule.md` requires a numeric prefix on a rule slug, since a
|
|
83
83
|
* band folder carries none.
|
|
84
84
|
*/
|
|
85
85
|
export function expandStackEntry(root: string, entry: string): string[] {
|
package/src/hooks/post-merge.ts
CHANGED
|
@@ -163,7 +163,7 @@ function archiveLines(
|
|
|
163
163
|
}
|
|
164
164
|
return (
|
|
165
165
|
`\n📋 Task not archived for #${number}: ${text(fields, 'message') ?? reason}\n` +
|
|
166
|
-
|
|
166
|
+
`Run /task-complete archive #${number} to resolve it by hand.\n\n`
|
|
167
167
|
)
|
|
168
168
|
}
|
|
169
169
|
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** What a label apply decided to do, before any write. */
|
|
2
|
+
export interface LabelDiff {
|
|
3
|
+
/** Earned labels the pull request lacks and the remote carries, so addable. */
|
|
4
|
+
readonly toAdd: readonly string[]
|
|
5
|
+
/** Every earned label the pull request lacks, whether or not it is addable. */
|
|
6
|
+
readonly owed: readonly string[]
|
|
7
|
+
/** Owed labels the remote does not carry, which are reported and never created. */
|
|
8
|
+
readonly missingOnRemote: readonly string[]
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Diffs what a branch earns against what its pull request already carries and
|
|
13
|
+
* what the remote can attach.
|
|
14
|
+
*
|
|
15
|
+
* The diff only adds. A label on the pull request that the map does not earn
|
|
16
|
+
* is a person's decision and never appears here, so nothing in the result can
|
|
17
|
+
* be turned into a removal. Order follows `earned`, which the map declares, so
|
|
18
|
+
* two runs over one branch produce one list.
|
|
19
|
+
*
|
|
20
|
+
* A label the remote lacks is separated from the addable set rather than
|
|
21
|
+
* dropped, because the REST add endpoint creates any label it is handed and
|
|
22
|
+
* that write to the repository settings is not the map's call.
|
|
23
|
+
*/
|
|
24
|
+
export function diffLabels(
|
|
25
|
+
earned: readonly string[],
|
|
26
|
+
present: readonly string[],
|
|
27
|
+
remote: readonly string[],
|
|
28
|
+
): LabelDiff {
|
|
29
|
+
const has = new Set(present)
|
|
30
|
+
const carried = new Set(remote)
|
|
31
|
+
|
|
32
|
+
const owed = earned.filter((label) => !has.has(label))
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
owed,
|
|
36
|
+
toAdd: owed.filter((label) => carried.has(label)),
|
|
37
|
+
missingOnRemote: owed.filter((label) => !carried.has(label)),
|
|
38
|
+
}
|
|
39
|
+
}
|
package/src/labels/format.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Which rule in `
|
|
2
|
+
* Which rule in `claude/skills/git-pr/references/pr.md`'s `## Title` section a title breaks.
|
|
3
3
|
*
|
|
4
4
|
* `structure` stands alone rather than beside the other three, because a
|
|
5
5
|
* title failing the shape match has no parsed `<type>`, `<scope>`, or
|
|
@@ -28,9 +28,9 @@ const LEADING_LETTERS = /^[A-Za-z]+/
|
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
30
|
* Grades a pull request title, or a commit subject sharing the same form,
|
|
31
|
-
* against `
|
|
31
|
+
* against `claude/skills/git-pr/references/pr.md`'s structure, casing, and length rules.
|
|
32
32
|
*
|
|
33
|
-
* Does not check `<type>` against `
|
|
33
|
+
* Does not check `<type>` against `claude/skills/git-commit/references/commit.md`'s fixed enum.
|
|
34
34
|
* `pr.md`'s own `## Title` section states format, casing, and length only,
|
|
35
35
|
* and names no type list of its own to check against.
|
|
36
36
|
*/
|
package/src/labels/phase.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { RECORD_ENTRIES, RECORD_ROOTS } from '@/roots/record'
|
|
|
3
3
|
import type { RecordRoot } from '@/roots/record'
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* The two version namespaces `
|
|
6
|
+
* The two version namespaces `claude/skills/git-commit/references/versioning.md` keeps apart, and why a
|
|
7
7
|
* pull request cannot be sorted between them by pattern alone.
|
|
8
8
|
*
|
|
9
9
|
* A phase label (`v68.5`) and a semver reference (`v3.44`) both match
|
|
@@ -186,7 +186,7 @@ const RELEASE_TITLE_PREFIX = 'chore(main): release '
|
|
|
186
186
|
* phase label past this check. Together they name release-please's own act of
|
|
187
187
|
* opening the pull request, which nothing else on the remote can perform.
|
|
188
188
|
*/
|
|
189
|
-
function isReleasePullRequest(input: PhaseScanInput): boolean {
|
|
189
|
+
export function isReleasePullRequest(input: PhaseScanInput): boolean {
|
|
190
190
|
return (
|
|
191
191
|
input.headRefName.startsWith(RELEASE_HEAD_PREFIX) &&
|
|
192
192
|
input.title.startsWith(RELEASE_TITLE_PREFIX)
|
|
@@ -265,7 +265,7 @@ function sessionLinks(text: string): string[] {
|
|
|
265
265
|
* The split runs on the pull request rather than on each token. A release
|
|
266
266
|
* pull request's tokens are read as the semver references its generated body
|
|
267
267
|
* legitimately carries, and every other pull request's tokens are read as
|
|
268
|
-
* leaked phase labels, which is what `
|
|
268
|
+
* leaked phase labels, which is what `claude/skills/git-commit/references/versioning.md` names the
|
|
269
269
|
* defect this exists to catch.
|
|
270
270
|
*
|
|
271
271
|
* A release pull request reports no board reference either, and the ground is
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { linesOutsideFences } from '@/markdown/scan'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The closed list of section headings a pull request body may carry, in the
|
|
5
|
+
* sentence case `claude/skills/git-pr/references/pr.md` states.
|
|
6
|
+
*/
|
|
7
|
+
const ALLOWED_HEADINGS: readonly string[] = [
|
|
8
|
+
'## Summary',
|
|
9
|
+
'## Key changes',
|
|
10
|
+
'## Technical context',
|
|
11
|
+
'## Testing',
|
|
12
|
+
'## For the reviewer',
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
const REQUIRED_HEADINGS: readonly string[] = ['## Summary', '## Key changes']
|
|
16
|
+
|
|
17
|
+
const H2 = /^##\s/
|
|
18
|
+
|
|
19
|
+
export interface SectionsCheck {
|
|
20
|
+
/** Level-two headings the closed list does not hold, as written. */
|
|
21
|
+
readonly outside: readonly string[]
|
|
22
|
+
/** Required headings the body never carries. */
|
|
23
|
+
readonly missing: readonly string[]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Grades the level-two headings of a pull request body against the closed list.
|
|
28
|
+
*
|
|
29
|
+
* The comparison is exact, so `## Key Changes` fails. `canon pr key-changes`
|
|
30
|
+
* reads its section case-insensitively, which keeps a body posted before the
|
|
31
|
+
* rename parsing while the scan holds new text to one spelling. Subheadings and
|
|
32
|
+
* fenced lines are not headings, so the list governs level two alone.
|
|
33
|
+
*/
|
|
34
|
+
export function checkSections(body: string): SectionsCheck {
|
|
35
|
+
const headings = linesOutsideFences(body)
|
|
36
|
+
.map((line) => line.trim())
|
|
37
|
+
.filter((line) => H2.test(line))
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
outside: headings.filter((heading) => !ALLOWED_HEADINGS.includes(heading)),
|
|
41
|
+
missing: REQUIRED_HEADINGS.filter((heading) => !headings.includes(heading)),
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -99,6 +99,25 @@ export const SKILL_NAME_MAP: Readonly<Record<string, string>> = {
|
|
|
99
99
|
'draft-diagram': 'draft-figure',
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
/**
|
|
103
|
+
* The skills that took a two-word name from a single word, which the map above
|
|
104
|
+
* may not carry as a bare key.
|
|
105
|
+
*
|
|
106
|
+
* `canvas` became `canvas-board`, and a bare `canvas` key would rewrite every
|
|
107
|
+
* unrelated use of the word: `canon canvas serve`, `src/canvas/`,
|
|
108
|
+
* `.canon/canvas/`, and the `internal-canvas` skill. So each row keys on a form
|
|
109
|
+
* only the skill spells, a plugin invocation or a skill folder path. The tail
|
|
110
|
+
* guard rejects a following `-`, so a rewritten `canon:canvas-board` is not
|
|
111
|
+
* matched again.
|
|
112
|
+
*
|
|
113
|
+
* What this leaves behind is a prose mention of the skill by its bare name,
|
|
114
|
+
* which has no token for a sweep to find and is fixed by hand.
|
|
115
|
+
*/
|
|
116
|
+
export const SKILL_CITATION_MAP: Readonly<Record<string, string>> = {
|
|
117
|
+
'canon:canvas': 'canon:canvas-board',
|
|
118
|
+
'skills/canvas': 'skills/canvas-board',
|
|
119
|
+
}
|
|
120
|
+
|
|
102
121
|
/**
|
|
103
122
|
* The skill rename.
|
|
104
123
|
*
|
|
@@ -132,7 +151,7 @@ export const SKILL_NAME_MAP: Readonly<Record<string, string>> = {
|
|
|
132
151
|
* listing every sweep reads never reaches them.
|
|
133
152
|
*/
|
|
134
153
|
export const SKILL_NAME_RULES: RenameRules = defineRenameRules({
|
|
135
|
-
replacements: SKILL_NAME_MAP,
|
|
154
|
+
replacements: { ...SKILL_NAME_MAP, ...SKILL_CITATION_MAP },
|
|
136
155
|
keepMarker: 'canon-keep-retired',
|
|
137
156
|
wholeToken: true,
|
|
138
157
|
excludedPrefixes: ['scripts/eval/result-'],
|
package/src/pr/paths.ts
CHANGED
|
@@ -3,12 +3,12 @@ import { findBacktickSpans } from '@/markdown/backticks'
|
|
|
3
3
|
/**
|
|
4
4
|
* The heading whose bullets state what a branch changed.
|
|
5
5
|
*
|
|
6
|
-
* Read alone, and never widened to a sibling section. `## Technical
|
|
6
|
+
* Read alone, and never widened to a sibling section. `## Technical context`
|
|
7
7
|
* legitimately names files a branch never touched, such as an install stamp
|
|
8
8
|
* inside a target, so a reader that took the whole body would manufacture a
|
|
9
9
|
* finding out of every argument the author made for the change.
|
|
10
10
|
*/
|
|
11
|
-
export const KEY_CHANGES = 'Key
|
|
11
|
+
export const KEY_CHANGES = 'Key changes'
|
|
12
12
|
|
|
13
13
|
/** What one bullet claimed, kept with its bullet so a finding can quote it. */
|
|
14
14
|
export interface PathClaim {
|
|
@@ -86,7 +86,7 @@ const BARE_LINE = /^:\d+(?:-\d+)?$/
|
|
|
86
86
|
* `canon markdown audit .claude/rules --json` carries a slash and names no file.
|
|
87
87
|
* Angle brackets answer a second, since `.canon/plans/feature-<slug>.md`
|
|
88
88
|
* describes a shape rather than naming a file. A glob and a caret describe a
|
|
89
|
-
* shape too, `^src/` being a grep pattern one body spelled in Key
|
|
89
|
+
* shape too, `^src/` being a grep pattern one body spelled in Key changes, and
|
|
90
90
|
* a leading anchor names something outside this repository.
|
|
91
91
|
*
|
|
92
92
|
* Deliberately not shared with `classifySpan` in `@/gov/citations`, which asks
|
|
@@ -307,7 +307,7 @@ function resolveSpan(span: string): ResolvedSpan | undefined {
|
|
|
307
307
|
}
|
|
308
308
|
|
|
309
309
|
/**
|
|
310
|
-
* Every path the `## Key
|
|
310
|
+
* Every path the `## Key changes` section claims a change to.
|
|
311
311
|
*
|
|
312
312
|
* `roots` names the entries the tree holds at its top level, which is what
|
|
313
313
|
* separates a whole path from one written partially. It is passed in rather
|
package/src/records/backup.ts
CHANGED
|
@@ -30,7 +30,7 @@ import { type BlockedPath, guardPayload } from '@/records/push-guard'
|
|
|
30
30
|
* rather than named here. That is what keeps this list at one line per surface
|
|
31
31
|
* as archives spread, which a sibling-per-archive layout could not.
|
|
32
32
|
*
|
|
33
|
-
* `RECORD_KINDS` in `validate.ts` overlaps this on
|
|
33
|
+
* `RECORD_KINDS` in `validate.ts` overlaps this on four names and carries one
|
|
34
34
|
* more that no backup reaches. The two lists differ on purpose: one is what a
|
|
35
35
|
* standard governs, this is what a disk loss would take, and `standards` is
|
|
36
36
|
* tracked so a backup would carry a second copy of committed files.
|
|
@@ -39,7 +39,6 @@ export const BACKED_FOLDERS = [
|
|
|
39
39
|
'diagrams',
|
|
40
40
|
'groundwork',
|
|
41
41
|
'intake',
|
|
42
|
-
'memory',
|
|
43
42
|
'plans',
|
|
44
43
|
'proposals',
|
|
45
44
|
'review',
|
|
@@ -51,6 +50,15 @@ export const BACKED_FOLDERS = [
|
|
|
51
50
|
'walkthroughs',
|
|
52
51
|
] as const
|
|
53
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Folders a retired kind left at a legacy `.claude` root that the allowlist
|
|
55
|
+
* must still read, so a half-finished move that strands one still trips
|
|
56
|
+
* `split-roots`. Absent from `BACKED_FOLDERS` because nothing writes them, and
|
|
57
|
+
* absent from `RETIRED_FOLDERS` because that list stages a deletion and this
|
|
58
|
+
* folder is an archive to keep.
|
|
59
|
+
*/
|
|
60
|
+
const LEGACY_RECORD_FOLDERS = ['memory'] as const
|
|
61
|
+
|
|
54
62
|
/**
|
|
55
63
|
* Names that have left `BACKED_FOLDERS` and whose removal still has to reach a
|
|
56
64
|
* records history once.
|
|
@@ -124,7 +132,9 @@ function isExcluded(name: string): boolean {
|
|
|
124
132
|
*/
|
|
125
133
|
function foldersAt(dir: string): string[] {
|
|
126
134
|
if (basename(dir) === '.claude') {
|
|
127
|
-
return BACKED_FOLDERS.filter((folder) =>
|
|
135
|
+
return [...BACKED_FOLDERS, ...LEGACY_RECORD_FOLDERS].filter((folder) =>
|
|
136
|
+
existsSync(join(dir, folder)),
|
|
137
|
+
)
|
|
128
138
|
}
|
|
129
139
|
|
|
130
140
|
const entries = existsSync(dir)
|
|
@@ -378,7 +388,7 @@ async function enclosingRemotes(
|
|
|
378
388
|
* Clears the four gates both verbs share and returns the records remote URL.
|
|
379
389
|
*
|
|
380
390
|
* The last two are the ones the payload depends on. This repository is public,
|
|
381
|
-
* so a records branch on any of its remotes serves the
|
|
391
|
+
* so a records branch on any of its remotes serves the plans and the
|
|
382
392
|
* groundwork trails to anyone who fetches all refs. Comparing the configured
|
|
383
393
|
* URL against every remote of the enclosing repository is what keeps a
|
|
384
394
|
* misconfigured `origin` from publishing them, and refusing when that list
|
|
@@ -416,7 +426,7 @@ async function resolveRemote(
|
|
|
416
426
|
if (!enclosing) {
|
|
417
427
|
return refuse(
|
|
418
428
|
'remote-unreadable',
|
|
419
|
-
`Cannot read the remotes of the project at ${root}, so the records origin cannot be checked against them. Records carry the
|
|
429
|
+
`Cannot read the remotes of the project at ${root}, so the records origin cannot be checked against them. Records carry the plans and the groundwork trails, and an unchecked origin risks publishing them.`,
|
|
420
430
|
)
|
|
421
431
|
}
|
|
422
432
|
|
|
@@ -424,7 +434,7 @@ async function resolveRemote(
|
|
|
424
434
|
if (enclosing.identities.includes(url)) {
|
|
425
435
|
return refuse(
|
|
426
436
|
'remote-shared',
|
|
427
|
-
`The records origin ${remote.text} is a remote of this project. Records carry the
|
|
437
|
+
`The records origin ${remote.text} is a remote of this project. Records carry the plans and the groundwork trails, so they need a repository of their own.`,
|
|
428
438
|
)
|
|
429
439
|
}
|
|
430
440
|
|
|
@@ -3,7 +3,6 @@ import { existsSync } from 'node:fs'
|
|
|
3
3
|
import { readFile } from 'node:fs/promises'
|
|
4
4
|
import { relative } from 'node:path'
|
|
5
5
|
import { gitEnv } from '@/git/env'
|
|
6
|
-
import { readReviewed } from '@/records/stale'
|
|
7
6
|
import { surfaceDir } from '@/roots/surface'
|
|
8
7
|
|
|
9
8
|
/** The record whose drift is identity rather than a stale path. */
|
|
@@ -22,6 +21,37 @@ export interface CanonicalDoc {
|
|
|
22
21
|
readonly latestRelease: string | null
|
|
23
22
|
}
|
|
24
23
|
|
|
24
|
+
const REVIEWED_FIELD = /^reviewed:[ \t]*(.*?)[ \t]*$/m
|
|
25
|
+
const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/
|
|
26
|
+
const RAW_FRONTMATTER = /^---\n([\s\S]*?)\n---/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Reads `reviewed` off the raw block rather than the parsed fields, for the
|
|
30
|
+
* reason `hasOpeningDate` in the validator gives: a YAML parser resolves a
|
|
31
|
+
* bare `YYYY-MM-DD` to a date on one schema and a string on another.
|
|
32
|
+
*
|
|
33
|
+
* A value that is not a calendar date reads as never reviewed and is carried
|
|
34
|
+
* back so the review can see it, rather than throwing over one hand edit.
|
|
35
|
+
*/
|
|
36
|
+
function readReviewed(
|
|
37
|
+
text: string,
|
|
38
|
+
): Pick<CanonicalDoc, 'reviewed' | 'invalidReviewed'> {
|
|
39
|
+
const block = RAW_FRONTMATTER.exec(text)?.[1]
|
|
40
|
+
const match = block ? REVIEWED_FIELD.exec(block) : null
|
|
41
|
+
if (!match) return { reviewed: null }
|
|
42
|
+
|
|
43
|
+
const value = match[1].replace(/^(['"])(.*)\1$/, '$2')
|
|
44
|
+
const parsed = Date.parse(`${value}T00:00:00Z`)
|
|
45
|
+
const isDate =
|
|
46
|
+
ISO_DATE.test(value) &&
|
|
47
|
+
Number.isFinite(parsed) &&
|
|
48
|
+
new Date(parsed).toISOString().slice(0, 10) === value
|
|
49
|
+
|
|
50
|
+
return isDate
|
|
51
|
+
? { reviewed: value }
|
|
52
|
+
: { reviewed: null, invalidReviewed: value }
|
|
53
|
+
}
|
|
54
|
+
|
|
25
55
|
export interface CanonicalReport {
|
|
26
56
|
readonly ok: true
|
|
27
57
|
readonly root: string
|