@erclx/canon 4.104.0 → 4.105.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.104.0",
4
+ "version": "4.105.0",
5
5
  "author": {
6
6
  "name": "Eric Le",
7
7
  "url": "https://github.com/erclx"
@@ -325,22 +325,24 @@ canon tasks validate
325
325
  canon tasks validate --json
326
326
  ```
327
327
 
328
- Seven checks run. Plan and Collisions reach one half each of the `## Run now` test the board standard states. Mapping and Grouping test the folder contract and hold for every group, and Shape holds for every group too, ahead of the four. Ordering reaches only the `## Needs a plan` rows, and Blockers reaches every row outside `## Run now`:
329
-
330
- | Check | What it reports |
331
- | ---------- | ----------------------------------------------------------------------------------------------------------------------- |
332
- | Shape | A row whose cell count disagrees with its table's header, or one stranded behind a table a blank line already closed |
333
- | Plan | A `## Run now` row whose Plan column carries no link, resolves to no file, or disagrees with the task's own line |
334
- | Mapping | A row or backlog line naming no task file |
335
- | Grouping | A task carrying a row in more than one readiness group, or on both surfaces |
336
- | Ordering | A `## Needs a plan` row whose stated position disagrees with where it actually sits, or which states no position at all |
337
- | Collisions | Two `## Run now` rows whose Touches columns name a path in common |
338
- | Blockers | A parked row whose blocker has stopped holding, whose cited task resolves nowhere, or whose cited task was declined |
328
+ Seven checks run. Collisions reaches one half of the `## Run now` test the board standard states, and Plan reaches every group, reading the Plan column under `## Run now` and the task file's own line under the other two. Mapping and Grouping test the folder contract and hold for every group, and Shape holds for every group too, ahead of the four. Ordering reaches only the `## Needs a plan` rows, and Blockers reaches every row outside `## Run now`:
329
+
330
+ | Check | What it reports |
331
+ | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
332
+ | Shape | A row whose cell count disagrees with its table's header, or one stranded behind a table a blank line already closed |
333
+ | Plan | A `## Run now` row whose Plan column carries no link, resolves to no file, or disagrees with the task's own line, a `## Needs a plan` row whose task cites a live plan, or an `## Up next` row whose task cites none |
334
+ | Mapping | A row or backlog line naming no task file |
335
+ | Grouping | A task carrying a row in more than one readiness group, or on both surfaces |
336
+ | Ordering | A `## Needs a plan` row whose stated position disagrees with where it actually sits, or which states no position at all |
337
+ | Collisions | Two `## Run now` rows whose Touches columns name a path in common |
338
+ | Blockers | A parked row whose blocker has stopped holding, whose cited task resolves nowhere, or whose cited task was declined |
339
339
 
340
340
  Shape runs before any other check reads a row, since a row failing it carries no dependable fields for the rest to check. A blank or prose line closes the table above it, so the walk treats the next pipe line as a fresh header candidate rather than as a continuation. That candidate counts as a header only when the line behind it is a separator carrying the same cell count, and one that fails is `row-untabled`, stranded behind a table that already closed. Cell count still has to match the header on every row that clears that test, and a row whose count disagrees is `row-misshapen`, the shape a dropped pipe or a merged column produces.
341
341
 
342
342
  The Plan check reads the row and the task file both, because the two are written by different hands and only the task's own `Plan:` line reaches the archive. A row carrying a plan whose task states none is `plan-uncited`, and a pair naming two different plans is `plan-mismatched`. Both sides resolve against the board and against the project root before they compare, so a row writing `../plans/x.md` and a task writing `.canon/plans/x.md` name one file rather than two.
343
343
 
344
+ The other two groups have no Plan column, so the same check reads the task file's own `Plan:` line against what the group name claims. A `## Needs a plan` row whose task cites a plan that sits live on disk is `plan-parked`, since the group says no such plan exists and the row is either misplaced or its plan is stale and belongs in the archive. An `## Up next` row whose task cites no live plan is `plan-absent`, since that group says one is written. A plan already archived, or a cited path with no file behind it, reads as not live on both sides. A row whose task file is gone is left to `task-unresolved`. An `## Up next` row with nothing left to build, waiting on an act only a person can take, reports `plan-absent` once its plan archives while the task stays live. That report is expected, since the check reads the task file and not the `Waiting on` prose, so a row in that state moves to `## Needs a plan` or closes.
345
+
344
346
  Mapping spans two surfaces, because a task sits on `priority.md` when it would plausibly be planned soon and on `backlog.md` otherwise. A row or a backlog line naming no task file is `task-unresolved`, and a file both surfaces name is `row-duplicated` for the reason a task in two groups is: it claims two things about itself and only one can hold. A task file neither surface names is `unplaced` rather than a finding, since that is the normal state between a session filing it and a live orchestrator placing it on the board. One check across both is what lets a task move between them without the move reading as a dropped file.
345
347
 
346
348
  A backlog line is a bullet carrying a link to a sibling task, since the backlog is a flat unordered list rather than a table. A bullet holding prose is skipped rather than reported, which keeps the file's own intro out of the findings, and the task that bullet meant to name still lands in the `unplaced` array below rather than the findings. A project carrying no `backlog.md` reads as an empty backlog rather than a refusal, which leaves the one-to-one mapping this check ran before the second surface existed.
@@ -55,3 +55,8 @@ paths:
55
55
  - Alt text for functional images (icons, buttons) should describe the action, not the graphic.
56
56
  - Give the visual boundary of a control and every graphic that carries meaning at least 3:1 contrast against adjacent colors (WCAG Non-text Contrast, 1.4.11).
57
57
  - Never make color the only carrier of a state, error, or distinction. Pair it with text, an icon, or a pattern (WCAG Use of Color, 1.4.1).
58
+
59
+ ## Reflow
60
+
61
+ - Let content reflow at a viewport 320 CSS pixels wide with no loss of content or function and no two-dimensional scrolling. Only content whose meaning depends on both axes, such as a data table, a map, or a diagram, may scroll sideways (WCAG Reflow, 1.4.10, Level AA).
62
+ - Treat 320 as the criterion's own test condition rather than a device width. It is equivalent to a 1280 pixel window at 400 percent zoom, so a layout that breaks there breaks for a person who zooms.
@@ -25,7 +25,13 @@ paths:
25
25
 
26
26
  ## What a capture covers
27
27
 
28
- - Capture the full page at the viewport its case declares. Do not capture a component in isolation.
28
+ - Capture the full page. Do not capture a component in isolation.
29
+ - Declare the widths a layout case captures. Always include 320, the reflow floor from the reflow criterion in `410-a11y`, then add each breakpoint bucket the project writes rules for, read from the `## Layout` section of `DESIGN.md`.
30
+ - Fall back to the 320 floor alone when the project has no `DESIGN.md` or its record carries no `## Layout` section. Do not impose another project's breakpoints.
31
+ - Apply the declaration to a case that covers layout only. A case driving a menu open or an answer chosen tests a state, so it takes one width rather than the full set.
32
+ - Read a capture at 320 as proof that a frame was taken there, not that the page reflows correctly. The criterion is stated in `410-a11y` and nothing here enforces it.
33
+ - Write the capture into a folder named `evidence`. The image is collected for the pull request comment when any segment of its path is literally `evidence` and its filename carries an image extension, so the folder name is fixed and its position in the tree is the project's own.
34
+ - Several `evidence/` folders in one project is the ordinary shape rather than a problem to consolidate. One per surface is how a project with more than one rendered surface ends up.
29
35
  - Drive a state a screenshot cannot reach. An initial render reports nothing about a menu that opens, an answer that is chosen, or a rail that tracks scrolling.
30
36
  - Add a case to the capture record when adding a surface.
31
37
  - Remove a surface's case in the change that removes the surface.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@erclx/canon",
3
3
  "type": "module",
4
- "version": "4.104.0",
4
+ "version": "4.105.0",
5
5
  "description": "Infrastructure and quality tooling for developer workflows",
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -218,6 +218,7 @@ export function register(program: Command): void {
218
218
  '',
219
219
  'Checks:',
220
220
  ' every Run now row points at a plan file that resolves',
221
+ ' no Needs a plan row has a live plan, and every Up next row has one',
221
222
  ' every task file carries a board row or a backlog line, never both',
222
223
  ' no task carries more than one row',
223
224
  ' no two Run now rows touch the same file',
@@ -134,6 +134,8 @@ export function renderDesignDocument(tokens: DesignTokens = TOKENS): string {
134
134
  ),
135
135
  ),
136
136
  '',
137
+ section('Layout', tokens.layout),
138
+ '',
137
139
  section('Motion', tokens.motion),
138
140
  '',
139
141
  section('Iconography', tokens.iconography),
@@ -70,6 +70,7 @@ export interface DesignTokens {
70
70
  readonly spacingNote: string
71
71
  readonly borders: readonly BorderToken[]
72
72
  readonly bordersNote: string
73
+ readonly layout: string
73
74
  readonly motion: string
74
75
  readonly iconography: string
75
76
  readonly preamble: string
@@ -371,6 +372,9 @@ export const TOKENS: DesignTokens = {
371
372
  },
372
373
  ],
373
374
 
375
+ layout:
376
+ 'Capture widths are 320, 768, 1280 and 1536 pixels, declared once in `tooling/web/configs/e2e/screenshot.ts`, with 320 and 1280 committed as evidence. 320 is the reflow floor and the rest are the breakpoint buckets. The landing page under `web/` sets its own `max-width` media queries between 430 and 1100 pixels per component rather than from a shared scale.',
377
+
374
378
  motion:
375
379
  'Motion is not used. No transition, animation, or keyframe declaration appears on any rendered surface, and the capture pipeline screenshots a static frame.',
376
380
 
@@ -8,6 +8,7 @@ import {
8
8
  readOutcomes,
9
9
  readPlanTarget,
10
10
  readPullRequest,
11
+ resolveLivePlan,
11
12
  tasksDir,
12
13
  } from '@/tasks/archive'
13
14
  import { gitTrunkReader, type TrunkReader } from '@/tasks/trunk'
@@ -38,6 +39,8 @@ export const FINDING_KINDS = [
38
39
  'plan-unresolved',
39
40
  'plan-uncited',
40
41
  'plan-mismatched',
42
+ 'plan-parked',
43
+ 'plan-absent',
41
44
  'task-unresolved',
42
45
  'row-duplicated',
43
46
  'row-misshapen',
@@ -747,6 +750,70 @@ async function planDisagreement(
747
750
  }
748
751
  }
749
752
 
753
+ /**
754
+ * Tests the claim a group name makes about a plan. `## Needs a plan` says none
755
+ * is live and `## Up next` says one is written, and neither group has a `Plan`
756
+ * column, so the task file's own `Plan:` line is the only place either claim
757
+ * can be read against.
758
+ *
759
+ * A live plan is one the archive's resolution accepts and the disk holds, so a
760
+ * plan already archived reads as not live and a cited path with no file behind
761
+ * it reads the same way. A row whose task file is gone is left to
762
+ * `task-unresolved`, which owns that report.
763
+ */
764
+ async function checkGroupClaims(
765
+ rows: readonly BoardRow[],
766
+ dir: string,
767
+ root: string,
768
+ ): Promise<Finding[]> {
769
+ const parked = rows.filter(
770
+ (row) => row.group === 'Needs a plan' || row.group === 'Up next',
771
+ )
772
+
773
+ const found = await Promise.all(
774
+ parked.map(async (row) => groupClaimFinding(row, dir, root)),
775
+ )
776
+
777
+ return found.filter((finding): finding is Finding => finding !== undefined)
778
+ }
779
+
780
+ async function groupClaimFinding(
781
+ row: BoardRow,
782
+ dir: string,
783
+ root: string,
784
+ ): Promise<Finding | undefined> {
785
+ if (!row.stem) return undefined
786
+
787
+ const file = join(dir, `${row.stem}.md`)
788
+ if (!existsSync(file)) return undefined
789
+
790
+ const target = readPlanTarget(await readFile(file, 'utf8'))
791
+ const live = target ? resolveLivePlan(target, dir, root) : undefined
792
+ const hasLivePlan = live !== undefined && existsSync(live)
793
+
794
+ if (row.group === 'Needs a plan' && hasLivePlan) {
795
+ return {
796
+ kind: 'plan-parked',
797
+ group: row.group,
798
+ subject: row.stem,
799
+ message: `sits under ${row.group} and its task cites ${target}, a live plan. A row with a plan belongs under Up next or Run now, or the plan is stale and belongs in the archive.`,
800
+ }
801
+ }
802
+
803
+ if (row.group === 'Up next' && !hasLivePlan) {
804
+ return {
805
+ kind: 'plan-absent',
806
+ group: row.group,
807
+ subject: row.stem,
808
+ message: target
809
+ ? `sits under ${row.group}, which claims a written plan, and its task cites ${target}, which is not a live plan.`
810
+ : `sits under ${row.group}, which claims a written plan, and its task carries no Plan: line.`,
811
+ }
812
+ }
813
+
814
+ return undefined
815
+ }
816
+
750
817
  /**
751
818
  * Where a plan pointer lands, resolved against the board and against the
752
819
  * project root the way the archive resolves the same line. Neither base
@@ -1186,6 +1253,7 @@ export async function validateBoard(
1186
1253
  ...checkMapping(rows, backlog, dir),
1187
1254
  ...checkPlans(rows, dir, root),
1188
1255
  ...(await checkPlanAgreement(rows, dir, root)),
1256
+ ...(await checkGroupClaims(rows, dir, root)),
1189
1257
  ...checkCollisions(rows),
1190
1258
  ...checkOrdering(rows),
1191
1259
  ...parked.findings,
@@ -51,7 +51,7 @@ Use `## Personality`, `## Color`, `## Typography`, `## Spacing`, `## Borders`, `
51
51
 
52
52
  Add either optional section when the project has content for it:
53
53
 
54
- - `## Layout`: page width, breakpoints, and grid rules, as current constraints
54
+ - `## Layout`: page width, breakpoints, and grid rules, as current constraints. The surface-capture rule reads the breakpoints from here, so a project with none declared here is captured at the 320 reflow floor alone.
55
55
  - `## Mark`: the logo's construction rules and the files that carry it
56
56
 
57
57
  ## Template