@erclx/canon 5.55.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 +8 -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 +2 -2
- package/claude/skills/deploy-app/SKILL.md +2 -0
- package/claude/skills/deprecation-migration/SKILL.md +7 -9
- package/claude/skills/document-health/references/axes.md +1 -1
- 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/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/markdown-audit.md +11 -4
- package/docs/agents/migrate.md +15 -13
- package/docs/agents/output-shape.md +2 -0
- package/docs/target/migrations.md +3 -3
- package/docs/workflow/operating-model.md +2 -0
- package/package.json +1 -1
- package/src/commands/markdown.ts +25 -29
- 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 +8 -0
- package/src/process/seeded-package.ts +21 -0
- package/src/serve/static.ts +2 -60
- 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/src/serve/live.ts +0 -113
|
@@ -26,24 +26,31 @@ The ban set and checkpoints ship in `src/markdown/bans.ts` and `src/markdown/str
|
|
|
26
26
|
| --------------- | ------------------------------------------------------------------------------------ | ----------------- |
|
|
27
27
|
| Bans | The two characters `markdown.md` bans, outside frontmatter, fences, spans, and links | Always |
|
|
28
28
|
| Links | A relative link resolving to nothing on disk | Always |
|
|
29
|
-
| Density | The six limits below, under `density`
|
|
29
|
+
| Density | The six limits and two table limits below, under `density` | On a gated prefix |
|
|
30
30
|
| Reference shape | A `kind: reference` page breaking its shape, under `referenceShape` | On a gated prefix |
|
|
31
|
-
| Tables | The
|
|
31
|
+
| Tables | The row count and wide token limits below, under `tables` | Never |
|
|
32
32
|
| Colons | A colon followed by more text in a prose line, under `colons` | Never |
|
|
33
33
|
| Heading case | A capital after the first word of a heading or `title`, under `headingCase` | Never |
|
|
34
34
|
|
|
35
35
|
A page marked `kind: reference` holds its opening, then `Output`, `Refusals`, and `See also` as H2s, each at most once and in that order. It takes no H3 and no table headed `Flag` or `Option`. An unmarked page is never measured.
|
|
36
36
|
|
|
37
|
-
Tables take the four limits in the `markdown-craft` skill's `tables.md`, on the density paths but never `CLAUDE.md`.
|
|
37
|
+
Tables take the four limits in the `markdown-craft` skill's `tables.md`, on the density paths but never `CLAUDE.md`.
|
|
38
|
+
|
|
39
|
+
- A cell past 120 visible characters reports as `table-cell` and a list after a table as `table-then-list`, both under `density`
|
|
40
|
+
- More than 8 body rows reports as `table-rows` and a code span with a run past 30 characters as `table-token`, both under `tables` and neither setting the exit code
|
|
38
41
|
|
|
39
42
|
| Density limit | Finding |
|
|
40
43
|
| ------------------- | ---------------------------------------------------------- |
|
|
41
44
|
| Paragraph length | A prose paragraph past 350 characters |
|
|
42
45
|
| Paragraphs in a row | More than 2 paragraphs with nothing else between them |
|
|
43
|
-
| Bullet length | A
|
|
46
|
+
| Bullet length | A bullet past 200 characters, a nested one on its own |
|
|
44
47
|
| Bullets in a row | More than 5 plain top-level bullets |
|
|
45
48
|
| Heading depth | Any heading past H3 |
|
|
46
49
|
| H3 opening | An H3 whose first block is neither a paragraph nor a table |
|
|
50
|
+
| Table cell | A table cell past 120 visible characters |
|
|
51
|
+
| List after table | A list as the block directly after a table |
|
|
52
|
+
|
|
53
|
+
Five notes fix how the limits count and where they apply.
|
|
47
54
|
|
|
48
55
|
- A paragraph under a list item counts toward that bullet, never as a paragraph
|
|
49
56
|
- A table, fence, list, heading, or bold section marker ends a run of paragraphs
|
package/docs/agents/migrate.md
CHANGED
|
@@ -8,21 +8,23 @@ kind: reference
|
|
|
8
8
|
|
|
9
9
|
`canon migrate <verb>` moves a project off a retired name or layout. Each verb reports its plan and writes nothing until `--write`, takes `--json`, and, except `rename`, takes `--root`. A second run rewrites nothing, which confirms the exclusions and markers fired.
|
|
10
10
|
|
|
11
|
-
| Verb | Moves
|
|
12
|
-
| ------------------ |
|
|
13
|
-
| `status` | Nothing. It lists every migration the project still needs, in run order.
|
|
14
|
-
| `rename` | The retired `aitk` name. `--scope target` rewrites toolkit-owned folders only.
|
|
15
|
-
| `skill-names` | Plugin skills off their `claude-` and `draft-` prefixes and other retired names, with every citation
|
|
16
|
-
| `records` | Session records from `.claude/` to `.canon/`, with every tracked citation
|
|
17
|
-
| `surface-roots` | `context
|
|
18
|
-
| `record-tree` | Citations inside `.canon/` folders a session still follows
|
|
19
|
-
| `rule-layout` | A flat installed-rule layout onto the `canon/`-wrapped one
|
|
20
|
-
| `scratch-evidence` | A folder a durable record cites out of `.tmp/`, with its citations
|
|
21
|
-
| `record-layout` | Review receipts, memory archives, feedback, renders, picks, and evidence into their current folders
|
|
22
|
-
| `session-defaults` | The seeded session defaults into an existing `.claude/settings.json
|
|
11
|
+
| Verb | Moves |
|
|
12
|
+
| ------------------ | --------------------------------------------------------------------------------------------------------------- |
|
|
13
|
+
| `status` | Nothing. It lists every migration the project still needs, in run order. |
|
|
14
|
+
| `rename` | The retired `aitk` name. `--scope target` rewrites toolkit-owned folders only. |
|
|
15
|
+
| `skill-names` | Plugin skills off their `claude-` and `draft-` prefixes and other retired names, with every citation |
|
|
16
|
+
| `records` | Session records from `.claude/` to `.canon/`, with every tracked citation |
|
|
17
|
+
| `surface-roots` | Records such as `context/` and `REQUIREMENTS.md`, and the stamp folder, from `.claude/` to `canon/` by `git mv` |
|
|
18
|
+
| `record-tree` | Citations inside the `.canon/` folders a session still follows, listed below the table |
|
|
19
|
+
| `rule-layout` | A flat installed-rule layout onto the `canon/`-wrapped one |
|
|
20
|
+
| `scratch-evidence` | A folder a durable record cites out of `.tmp/`, with its citations |
|
|
21
|
+
| `record-layout` | Review receipts, memory archives, feedback, renders, picks, and evidence into their current folders |
|
|
22
|
+
| `session-defaults` | The seeded session defaults into an existing `.claude/settings.json`, listed below the table |
|
|
23
|
+
|
|
24
|
+
`record-tree` follows citations in `diagrams`, `memory`, `plans`, `proposals`, `review`, `tasks`, and `teach`. `session-defaults` denies `Agent`, `Task`, and both merge commands, and turns auto memory off. The bullets below add detail per verb.
|
|
23
25
|
|
|
24
26
|
- `skill-names` takes no `--scope`, and skips the changelog, eval transcripts, and the pointer each renamed `draft-` skill leaves at its old name
|
|
25
|
-
- `surface-roots` leaves `rules/`, `skills/`, `hooks/`, and `settings.json`
|
|
27
|
+
- `surface-roots` moves `context/`, `wireframes/`, `ARCHITECTURE.md`, `REQUIREMENTS.md`, `DESIGN.md`, and the stamp folder, and leaves `rules/`, `skills/`, `hooks/`, and `settings.json` in place
|
|
26
28
|
- `rule-layout` classifies each flat file against the stamp as clean, edited, or unclaimed. Clean and edited files move untouched, and an unclaimed name stays.
|
|
27
29
|
- `record-layout` labels each evidence folder `(pick)` or `(evidence)` in the dry run
|
|
28
30
|
- `session-defaults` adds a key only when absent, merge entries included, and each older project runs it once. `--write` restores a removed `Agent` or `Task` and never removes a deny entry.
|
|
@@ -54,6 +54,8 @@ Discover names at runtime rather than hardcoding them.
|
|
|
54
54
|
| `canon docs list --json` | Consumer docs plus per-domain context |
|
|
55
55
|
| `canon audits list --json` | Audits the set runs, the corpus each reads, and whether it gates |
|
|
56
56
|
|
|
57
|
+
The table names the verbs, and three notes cover what some of them carry.
|
|
58
|
+
|
|
57
59
|
- `gov list` expands each stack to its rules and carries `unreferenced`, the rules no stack reaches, which install only through `--add`
|
|
58
60
|
- `claude skills list` reports shipped skills only. Each carries `family` (or `null`) and `requirement`, and a `false` requirement is a gap to close. `--names` and `--by-family` change the render.
|
|
59
61
|
- A skill with unparseable frontmatter returns an empty description rather than failing the listing
|
|
@@ -62,19 +62,19 @@ The seven `draft-` skills took verb-first names for what each makes. `canon migr
|
|
|
62
62
|
| `draft-ready` | `plan-ready` <!-- canon-keep-retired --> |
|
|
63
63
|
| `draft-screencast` | `script-screencast` <!-- canon-keep-retired --> |
|
|
64
64
|
|
|
65
|
+
Replace a citation of a skill with no successor by hand, or delete the sentence.
|
|
66
|
+
|
|
65
67
|
- `canvas` is now `canvas-board`. The sweep rewrites only `canon:canvas` and `skills/canvas`, so replace a bare prose mention by hand. <!-- canon-keep-retired -->
|
|
66
68
|
- `draft-ready` is rewritten only as `canon:draft-ready` and `skills/draft-ready`, since bare it also describes a pull request. Replace a bare mention of the skill by hand. <!-- canon-keep-retired -->
|
|
67
69
|
- `design-extract` and `sketch-design` are replaced by `canon:canvas-board`. <!-- canon-keep-retired -->
|
|
68
70
|
- `review-branch`, `seed-sync`, `restate-plainly`, `bash-cli-script`, `canon-operator`, and `canon-rollout` have no successor <!-- canon-keep-retired -->
|
|
69
71
|
- `create-standard`, `markdown-propose`, `memory-review`, `project-commands`, `repo-metadata`, and `ux-walkthrough` have no successor <!-- canon-keep-retired -->
|
|
70
72
|
|
|
71
|
-
|
|
73
|
+
The `memory` record kind left `canon records`, so `canon records validate memory` refuses as an unknown kind and `canon records migrate` is gone.
|
|
72
74
|
|
|
73
75
|
- `canon repo metadata`, `canon autoship`, and the `120-bash` rule are gone, and `canon gov sync` removes the rule <!-- canon-keep-retired -->
|
|
74
76
|
- `memory-capture`, the `603-memory` rule, and the `memory` standard are gone, so send a lesson to the context entry that owns the fact <!-- canon-keep-retired -->
|
|
75
77
|
|
|
76
|
-
The `memory` record kind left `canon records`, so `canon records validate memory` refuses as an unknown kind and `canon records migrate` is gone.
|
|
77
|
-
|
|
78
78
|
Two held files run, `.husky/post-merge` and `.claude/hooks/pr-create-log.sh`, and a stale copy calls a missing binary or names a removed skill. Resync what the toolkit owns before sweeping what the project wrote, and run the second line once per stack.
|
|
79
79
|
|
|
80
80
|
```bash
|
|
@@ -17,6 +17,8 @@ One warm session holds the cross-feature call and reviews. Cold worker sessions
|
|
|
17
17
|
| Planner | One session per row, warm or cold | Measure the row against the tree, write one plan | Enter a worktree, write the board |
|
|
18
18
|
| Worker | One cold worktree session per feature | Implement, self-check, open PR, answer the reviewer | Write the shared board, merge |
|
|
19
19
|
|
|
20
|
+
Each role keeps to its own writes, and five rules cover how the roles meet.
|
|
21
|
+
|
|
20
22
|
- The orchestrator loads `role-orchestrator`, a worker loads `role-worker` through `auto-ship` Step 0, and a planner and a reviewer load theirs from the launch that dispatches them
|
|
21
23
|
- The orchestrator decides which rows conflict, what merges first, and whether a row runs at all. Per-row planning can go cold.
|
|
22
24
|
- A dispatched planner writes only the plan, and reports what the task file got wrong rather than repairing it
|
package/package.json
CHANGED
package/src/commands/markdown.ts
CHANGED
|
@@ -141,9 +141,10 @@ export function register(program: Command): void {
|
|
|
141
141
|
'Depth weight is a judgment a reader settles, so it reports and never',
|
|
142
142
|
'fails a run.',
|
|
143
143
|
'',
|
|
144
|
-
'Density holds
|
|
145
|
-
'a row, a 200-character bullet, 5 plain bullets in a row, no H4,
|
|
146
|
-
'paragraph or table opening every H3
|
|
144
|
+
'Density holds eight limits: a 350-character paragraph, 2 paragraphs in',
|
|
145
|
+
'a row, a 200-character bullet, 5 plain bullets in a row, no H4, a',
|
|
146
|
+
'paragraph or table opening every H3, a 120-character table cell, and no',
|
|
147
|
+
'list as the block after a table. Only context, docs, skills, tooling',
|
|
147
148
|
'seeds and references, the wiki, README.md, and CONTRIBUTING.md take a',
|
|
148
149
|
'finding. CLAUDE.md and its seed take only the bullet run and the H3',
|
|
149
150
|
'opening, inside Key paths. In a canon checkout one gates under a',
|
|
@@ -154,10 +155,9 @@ export function register(program: Command): void {
|
|
|
154
155
|
'and none gates.',
|
|
155
156
|
'A config that does not parse refuses with its path named.',
|
|
156
157
|
'',
|
|
157
|
-
'Tables take
|
|
158
|
-
'
|
|
159
|
-
'
|
|
160
|
-
'fails a run.',
|
|
158
|
+
'Tables take two more limits on the same paths, CLAUDE.md aside: 8 body',
|
|
159
|
+
'rows, and a code span with a run past 30 characters. Each reports and',
|
|
160
|
+
'never fails a run.',
|
|
161
161
|
'',
|
|
162
162
|
'A page whose frontmatter carries kind: reference takes the reference',
|
|
163
163
|
'shape: no H2 beyond Output, Refusals, and See also, each at most once',
|
|
@@ -637,15 +637,19 @@ const DENSITY_LABELS: Record<DensityKind, string> = {
|
|
|
637
637
|
'bullet-run': 'bullets in a row',
|
|
638
638
|
'heading-depth': 'heading depth',
|
|
639
639
|
'h3-opening': 'H3 opening',
|
|
640
|
+
'table-cell': 'cell length',
|
|
641
|
+
'table-then-list': 'list after table',
|
|
640
642
|
}
|
|
641
643
|
|
|
642
644
|
function describeDensity(finding: GatedDensityFinding): string {
|
|
643
645
|
const measured =
|
|
644
646
|
finding.kind === 'h3-opening'
|
|
645
647
|
? 'opens with neither a paragraph nor a table'
|
|
646
|
-
: finding.kind === '
|
|
647
|
-
?
|
|
648
|
-
:
|
|
648
|
+
: finding.kind === 'table-then-list'
|
|
649
|
+
? 'follows the table with no lead sentence'
|
|
650
|
+
: finding.kind === 'heading-depth'
|
|
651
|
+
? `H${finding.value} past H${finding.limit}`
|
|
652
|
+
: `${finding.value} past ${finding.limit}`
|
|
649
653
|
const gate = finding.gating ? ' gating' : ''
|
|
650
654
|
return ` :${finding.line} ${DENSITY_LABELS[finding.kind]} ${measured}${gate}`
|
|
651
655
|
}
|
|
@@ -662,10 +666,10 @@ function reportDensity(
|
|
|
662
666
|
): void {
|
|
663
667
|
logStep('Density')
|
|
664
668
|
logInfo(
|
|
665
|
-
`Paragraphs past ${CHECKPOINTS.paragraph} characters, more than 2 paragraphs in a row, bullets past ${CHECKPOINTS.bullet} characters, more than 5 plain bullets in a row, a heading past H3,
|
|
669
|
+
`Paragraphs past ${CHECKPOINTS.paragraph} characters, more than 2 paragraphs in a row, bullets past ${CHECKPOINTS.bullet} characters, more than 5 plain bullets in a row, a heading past H3, an H3 opening with neither a paragraph nor a table, a table cell past ${TABLE_CELL} characters, and a list directly after a table.`,
|
|
666
670
|
)
|
|
667
671
|
logInfo(
|
|
668
|
-
'Measured on context entries, docs, skill bodies, references, and requirements, tooling seeds and stack references, the wiki, README.md, and CONTRIBUTING.md. CLAUDE.md and its seed take the bullet run and the H3 opening inside Key paths alone. Every other path takes no density finding.',
|
|
672
|
+
'Measured on context entries, docs, skill bodies, references, and requirements, tooling seeds and stack references, the wiki, README.md, and CONTRIBUTING.md. CLAUDE.md and its seed take the bullet run and the H3 opening inside Key paths alone, with no table finding. Every other path takes no density finding.',
|
|
669
673
|
)
|
|
670
674
|
logInfo(densityGatingLine(gated, source))
|
|
671
675
|
|
|
@@ -689,7 +693,7 @@ function reportDensity(
|
|
|
689
693
|
)
|
|
690
694
|
if (gating > 0) logWarn('A gating finding fails the run.')
|
|
691
695
|
logInfo(
|
|
692
|
-
'Split a long block at its seam, put a heading or a table between paragraphs, give a long list H3s,
|
|
696
|
+
'Split a long block at its seam, put a heading or a table between paragraphs, give a long list H3s, open each H3 with a sentence or a table, move a long cell into a bullet under the lead, and give a trailing list its own lead sentence.',
|
|
693
697
|
)
|
|
694
698
|
pipeOutput(
|
|
695
699
|
carrying
|
|
@@ -718,29 +722,23 @@ function densityGatingLine(
|
|
|
718
722
|
}
|
|
719
723
|
|
|
720
724
|
const TABLE_LABELS: Record<TableKind, string> = {
|
|
721
|
-
'table-cell': 'cell length',
|
|
722
725
|
'table-rows': 'rows',
|
|
723
|
-
'table-then-list': 'list after table',
|
|
724
726
|
'table-token': 'wide token',
|
|
725
727
|
}
|
|
726
728
|
|
|
727
729
|
function describeTable(finding: TableFinding): string {
|
|
728
|
-
|
|
729
|
-
finding.kind === 'table-then-list'
|
|
730
|
-
? 'follows the table with no lead sentence'
|
|
731
|
-
: `${finding.value} past ${finding.limit}`
|
|
732
|
-
return ` :${finding.line} ${TABLE_LABELS[finding.kind]} ${measured}`
|
|
730
|
+
return ` :${finding.line} ${TABLE_LABELS[finding.kind]} ${finding.value} past ${finding.limit}`
|
|
733
731
|
}
|
|
734
732
|
|
|
735
733
|
/**
|
|
736
|
-
* Lists
|
|
737
|
-
*
|
|
738
|
-
*
|
|
734
|
+
* Lists the table findings that only report, on the density scope's paths. The
|
|
735
|
+
* cell and the trailing list gate as density kinds, so `reportDensity` lists
|
|
736
|
+
* them.
|
|
739
737
|
*/
|
|
740
738
|
function reportTables(reports: readonly FileReport[]): void {
|
|
741
739
|
logStep('Tables')
|
|
742
740
|
logInfo(
|
|
743
|
-
`
|
|
741
|
+
`Tables past ${TABLE_ROWS} body rows and a code span in a cell holding a run past ${TABLE_TOKEN} characters.`,
|
|
744
742
|
)
|
|
745
743
|
logInfo(
|
|
746
744
|
'Measured on the paths density measures in full, so never on CLAUDE.md, outside fenced blocks. This never fails a run.',
|
|
@@ -760,7 +758,7 @@ function reportTables(reports: readonly FileReport[]): void {
|
|
|
760
758
|
`${plural(total, 'finding')} across ${plural(carrying.length, 'file')}`,
|
|
761
759
|
)
|
|
762
760
|
logInfo(
|
|
763
|
-
'
|
|
761
|
+
'Split a long table into groups each under a lead sentence, and move a wide token out of the table.',
|
|
764
762
|
)
|
|
765
763
|
pipeOutput(
|
|
766
764
|
carrying
|
|
@@ -917,11 +915,9 @@ function reportBullets(
|
|
|
917
915
|
): void {
|
|
918
916
|
logStep('Bullets')
|
|
919
917
|
logInfo(
|
|
920
|
-
'
|
|
921
|
-
)
|
|
922
|
-
logInfo(
|
|
923
|
-
'Nested items and fenced blocks are excluded. Weight is a judgment, never a defect.',
|
|
918
|
+
'Bullets measure characters, folding in continuation lines, and a nested bullet measures on its own.',
|
|
924
919
|
)
|
|
920
|
+
logInfo('Fenced blocks are excluded. Weight is a judgment, never a defect.')
|
|
925
921
|
|
|
926
922
|
const carrying = reports
|
|
927
923
|
.filter((report) => report.structure.heavyBullets.length > 0)
|
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
|
}
|