@erclx/canon 4.23.0 → 4.24.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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "canon",
3
3
  "description": "Automated governance, versioning, and discovery tools for Claude Code.",
4
- "version": "4.23.0",
4
+ "version": "4.24.0",
5
5
  "author": {
6
6
  "name": "Eric Le",
7
7
  "url": "https://github.com/erclx"
@@ -94,6 +94,7 @@ The session also records nothing of what it learns. Both other callers of memory
94
94
  - Merge. Recommend merge or changes and leave the gate to the human.
95
95
  - Spawn a worker with the Agent tool, since an in-process subagent shares this session's context and cannot be steered or reached independently. A dispatched `claude --bg` process is not this: it is a separate session with its own worktree and its own PR, gated by the collision check and the file-set disjointness test.
96
96
  - Hand a worker anything but a plan, because scope lives there
97
+ - Name a count of the defect's extent or the mechanism behind it while filing a row, since this seat measures a defect from where it stands and the planner is the reader who commits to that scope. A blocker re-test, a collision check, and a file set decide whether a row can start rather than how big it is, and they stay.
97
98
  - Run a second orchestrator against the same board
98
99
  - Promote a task to fill the queue when nothing qualifies. A thin queue is a real answer.
99
100
  - Promote a parked row on how long it has waited, since age is not a measurement of the blocker
@@ -149,6 +149,14 @@ The tracked-file boundary collides with `CLAUDE.md`, which says to handle a smal
149
149
 
150
150
  Record a change identified while orchestrating against the task that owns it, fold one no task owns into the next task touching the same surface, and file a task only when no such task exists or is expected. Run `claude-review` when the boundary is crossed anyway, since a branch-diff pass is not independent and is the only check a self-authored change can get.
151
151
 
152
+ Filing a row states the defect and the surface it was seen on, and stops there. The extent and the cause belong to whoever plans it, so a row names neither a count of how far the defect reaches nor the mechanism behind it. Say in a plain sentence in the task file that the count was not taken, where the row's shape invites one, rather than in a marker of any prescribed form. The board's own cell has no room for it, since `${CLAUDE_SKILL_DIR}/../../standards/tasks.md` caps a `Waiting on` cell at two clauses.
153
+
154
+ A finding placed into another task's `## Findings` is filing under a second name and takes the same rule. An unverified cause costs more than an unverified count, since a count fails loudly at the first re-measurement while a mechanism nobody opened the module to check sends real work in the wrong direction.
155
+
156
+ What the measurement decides is the test, rather than whether one was taken. A measurement that decides whether a row can start stays in this seat, which covers the blocker re-test in `${CLAUDE_SKILL_DIR}/references/orchestrator-parked.md`, the collision check before a dispatch, and the file set refill step 6 lists against every track in flight. One that decides how big the row is goes to the planner.
157
+
158
+ Both kinds read the same tree, and only a sizing count commits a plan to a scope this seat cannot see the whole of. A row filed under `## Needs a plan` carries no `Touches` column either, so filing it unmeasured owes no file set and the disjointness machinery is untouched. A position claim in that group's cell is in scope as well: name the row it outranks or the class it belongs to, each a judgment about order, and never reach for a count of the defect's extent to justify the rank, which is the untested number this boundary keeps off the row wearing a ranking argument.
159
+
152
160
  ## Refilling the ready queue
153
161
 
154
162
  Keep enough planned, non-conflicting tasks available that a free worker never waits, and place the findings the last merge produced before promoting anything new. Run this after every merge, whenever the ready list thins, and whenever a wave is in flight with fewer unclaimed plans than there are workers building. `${CLAUDE_SKILL_DIR}/references/orchestrator-sweep.md` wraps this procedure for a batch of merges and adds the plan re-verification that a merge invalidates.
@@ -29,6 +29,8 @@ The blocker cell states what the row waits on, and each kind is tested different
29
29
  - Waiting on a plan: nothing external holds the row, so the pass writes the plan rather than testing anything. See The plan half below.
30
30
  - Waiting on an operator action, such as a run that happens from a shell: record it as untestable this pass and name what the operator has to do. A session cannot clear it, and re-measuring it every pass is waste.
31
31
 
32
+ Every measurement this pass takes is a blocker re-test on a row already filed, and it decides whether that row can start rather than how big it is. Sizing a row is the filing boundary `claude-orchestrate` states under `## Boundaries`, which leaves the extent and the cause to whoever plans the row.
33
+
32
34
  Write the result into the row. A re-test reported in chat is lost at the next compaction and the next pass measures the same thing again. Rewrite a blocker cell whose test no longer holds, move the row to the group its new state puts it in, and record the measurement in that task's `## Findings` with the date it was taken. `### Writing the board` in `claude-orchestrate` owns the method, and `canon tasks validate` runs once the board is rewritten and before the report.
33
35
 
34
36
  ## Two ways a re-test goes wrong
@@ -56,7 +56,7 @@ Full help: `canon <command> --help`. Behavior notes for the install and sync ver
56
56
  | `canon context audit` | Report required sections, length, cited paths, reference form, catalog tables, provenance, superseded-decision narration, and index drift |
57
57
  | `canon markdown audit` | Fail any markdown path on a banned character, word, or spelling, and report the structural checkpoints |
58
58
  | `canon claude skills audit` | Report both skill corpora against the mechanical rules in `standards/skill.md` |
59
- | `canon standards audit` | Report the corpus against the `## Success criterion` gate, failing only on a standard new to the branch (`--json`, `--arrivals-only`) |
59
+ | `canon standards audit` | Report the corpus against the success-criterion gate `standards/standard.md` states, failing only on a standard new to the branch (`--json`, `--arrivals-only`) |
60
60
  | `canon claude skills drift` | Name the shipped skill bodies rewritten between a given ref and `HEAD`, and the installed version against the newest published (`--json`) |
61
61
  | `canon claude skills reach` | Report the bodies in either skill corpus citing a toolkit path no target project receives, exiting 2 on an unqualified one |
62
62
  | `canon claude skills rank` | Score either skill corpus's descriptions against a case corpus by TF-IDF cosine similarity, reporting rank-one and top-three (`--cases <path>`) |
@@ -65,6 +65,14 @@ goes to the task that owns the surface, or folds into the next task touching it,
65
65
  because a session that writes a change cannot review it independently afterwards
66
66
  and no later session recovers that vantage.
67
67
 
68
+ Filing that row stops at the defect and the surface it was seen on. How far the
69
+ defect reaches and what causes it belong to whoever plans the row, so it arrives
70
+ carrying neither a count nor an asserted mechanism, and the seat that found it
71
+ says as much in a plain sentence rather than supplying either. What separates the
72
+ two is what a measurement decides: the blocker re-test, the collision check, and
73
+ the file set compared against every track in flight all decide whether a row can
74
+ start, and only a count of the defect's extent decides how big it is.
75
+
68
76
  ## The loop
69
77
 
70
78
  One feature travels this path end to end.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@erclx/canon",
3
3
  "type": "module",
4
- "version": "4.23.0",
4
+ "version": "4.24.0",
5
5
  "description": "Infrastructure and quality tooling for developer workflows",
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -62,7 +62,7 @@ export function register(program: Command): void {
62
62
  standards
63
63
  .command('audit')
64
64
  .description(
65
- 'Report the corpus against the `## Success criterion` gate in standards/standard.md',
65
+ 'Report the corpus against the success-criterion gate standards/standard.md states',
66
66
  )
67
67
  .argument('[path]', 'Project root, defaulting to the current directory')
68
68
  .helpOption('-h, --help', 'Show this help message')
@@ -78,7 +78,7 @@ export function register(program: Command): void {
78
78
  'Exit codes:',
79
79
  ' 0 the audit completed with every arriving standard carrying the section',
80
80
  ' 1 refused, with the reason on stderr',
81
- ' 2 a standard new to this branch carries no ## Success criterion section',
81
+ ' 2 a standard new to this branch carries no success-criterion section',
82
82
  '',
83
83
  'A standard already in the corpus without the section is a known gap',
84
84
  'standards/standard.md names, not a violation, so only an arrival fails.',
@@ -204,8 +204,8 @@ function reportArrivalGate(
204
204
  intro('canon standards audit')
205
205
  logError(
206
206
  missing.length === 1
207
- ? '1 standard new to this branch carries no ## Success criterion section'
208
- : `${missing.length} standards new to this branch carry no ## Success criterion section`,
207
+ ? '1 standard new to this branch carries no success-criterion section'
208
+ : `${missing.length} standards new to this branch carry no success-criterion section`,
209
209
  )
210
210
  pipeOutput(missing.join('\n'))
211
211
  outro()
@@ -217,7 +217,7 @@ function reportCorpus(
217
217
  logStep('Corpus')
218
218
  logInfo(`${plural(audit.standards.length, 'standard')} at standards/`)
219
219
  logInfo(
220
- `${plural(audit.withCriterion.length, 'standard')} carrying ## Success criterion`,
220
+ `${plural(audit.withCriterion.length, 'standard')} carrying a success criterion`,
221
221
  )
222
222
 
223
223
  logStep('Known gaps')
@@ -241,7 +241,7 @@ function reportCorpus(
241
241
  }
242
242
 
243
243
  logError(
244
- `${plural(audit.arrivalsWithoutCriterion.length, 'standard')} arrived carrying no ## Success criterion section`,
244
+ `${plural(audit.arrivalsWithoutCriterion.length, 'standard')} arrived carrying no success-criterion section`,
245
245
  )
246
246
  pipeOutput(audit.arrivalsWithoutCriterion.join('\n'))
247
247
  }
@@ -435,7 +435,7 @@ export const standardCriteria: Measure = async (ctx) => {
435
435
  emissions: [output(run.all)],
436
436
  failure:
437
437
  run.exitCode === 2
438
- ? 'A standard new to this branch carries no ## Success criterion section. Run bun src/cli.ts standards audit.'
438
+ ? 'A standard new to this branch carries no success-criterion section. Run bun src/cli.ts standards audit.'
439
439
  : 'canon standards audit could not read which standards arrived on this branch. Run bun src/cli.ts standards audit --json to see why.',
440
440
  }
441
441
  }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Preferred first, matching `src/tasks/trunk.ts`. A local `main` trailing the
3
+ * remote pulls other people's merged commits into the range, so a check
4
+ * reading it decides against files the branch never touched.
5
+ */
6
+ export const TRUNK_REFS = ['origin/main', 'main'] as const
7
+
8
+ /**
9
+ * The refs to try, in order, for the far side of a branch range: a named ref
10
+ * alone, or the trunk list when none was named.
11
+ */
12
+ export function baseCandidates(ref: string | undefined): readonly string[] {
13
+ return ref !== undefined ? [ref] : TRUNK_REFS
14
+ }
15
+
16
+ /** Whether a `merge-base` result names a commit rather than an empty read. */
17
+ export function isMergeBase(value: string | undefined): value is string {
18
+ return value !== undefined && value !== ''
19
+ }
package/src/git-files.ts CHANGED
@@ -1,13 +1,7 @@
1
1
  import { $ } from 'bun'
2
+ import { baseCandidates, isMergeBase } from '@/git-base'
2
3
  import { gitEnv } from '@/git-env'
3
4
 
4
- /**
5
- * Preferred first, matching `src/gov/test-order.ts` and `src/tasks/trunk.ts`. A
6
- * local `main` trailing the remote pulls other people's merged commits into the
7
- * range, so a check reading it decides against files the branch never touched.
8
- */
9
- const TRUNK_REFS = ['origin/main', 'main'] as const
10
-
11
5
  /** Runs git under `root` with the resolution variables a hook exports stripped. */
12
6
  async function git(
13
7
  root: string,
@@ -37,14 +31,9 @@ export async function resolveBaseRef(
37
31
  root: string,
38
32
  ref?: string,
39
33
  ): Promise<string | undefined> {
40
- if (ref !== undefined) {
41
- const merged = await git(root, ['merge-base', 'HEAD', ref])
42
- return merged === undefined || merged === '' ? undefined : merged
43
- }
44
-
45
- for (const trunk of TRUNK_REFS) {
46
- const merged = await git(root, ['merge-base', 'HEAD', trunk])
47
- if (merged !== undefined && merged !== '') return merged
34
+ for (const candidate of baseCandidates(ref)) {
35
+ const merged = await git(root, ['merge-base', 'HEAD', candidate])
36
+ if (isMergeBase(merged)) return merged
48
37
  }
49
38
 
50
39
  return undefined
@@ -1,13 +1,7 @@
1
1
  import { execaSync } from 'execa'
2
+ import { baseCandidates, isMergeBase } from '@/git-base'
2
3
  import { gitEnv } from '@/git-env'
3
4
 
4
- /**
5
- * Preferred first, matching `src/tasks/trunk.ts`. A clone with no remote still
6
- * answers off its local trunk, and a local `main` trailing the remote widens
7
- * the range rather than narrowing it, which over-reports instead of hiding.
8
- */
9
- const TRUNK_REFS = ['origin/main', 'main'] as const
10
-
11
5
  /**
12
6
  * The extensions this check can pair. A test sits beside its subject under one
13
7
  * name throughout this corpus, so the weakest assumption available is also the
@@ -349,22 +343,17 @@ function resolveBase(
349
343
  ref: string | undefined,
350
344
  head: string,
351
345
  ): string | { kind: 'unreadable'; reason: TestOrderRefusal; message: string } {
352
- if (ref !== undefined) {
353
- const merged = git(root, ['merge-base', head, ref])
354
- if (merged === undefined || merged === '') {
355
- return {
356
- kind: 'unreadable',
357
- reason: 'bad-base',
358
- message: `Ref ${ref} shares no history with HEAD in ${root}. Pass a ref this branch was taken from.`,
359
- }
360
- }
361
- return merged
346
+ for (const candidate of baseCandidates(ref)) {
347
+ const merged = git(root, ['merge-base', head, candidate])
348
+ if (isMergeBase(merged)) return merged
362
349
  }
363
350
 
364
- for (const trunk of TRUNK_REFS) {
365
- if (revParse(root, trunk) === undefined) continue
366
- const merged = git(root, ['merge-base', head, trunk])
367
- if (merged !== undefined && merged !== '') return merged
351
+ if (ref !== undefined) {
352
+ return {
353
+ kind: 'unreadable',
354
+ reason: 'bad-base',
355
+ message: `Ref ${ref} shares no history with HEAD in ${root}. Pass a ref this branch was taken from.`,
356
+ }
368
357
  }
369
358
 
370
359
  const rootCommits = git(root, ['rev-list', '--max-parents=0', head])