@erclx/canon 5.38.0 → 5.39.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.
Files changed (149) hide show
  1. package/README.md +1 -1
  2. package/claude/.claude-plugin/plugin.json +1 -1
  3. package/claude/skills/api-design/SKILL.md +12 -9
  4. package/claude/skills/auto-ship/SKILL.md +43 -23
  5. package/claude/skills/backlog-triage/SKILL.md +62 -53
  6. package/claude/skills/backlog-triage/references/apply.md +69 -0
  7. package/claude/skills/build-in-slices/SKILL.md +8 -4
  8. package/claude/skills/canon-cli/SKILL.md +5 -1
  9. package/claude/skills/canon-feedback/SKILL.md +18 -8
  10. package/claude/skills/canon-feedback-triage/SKILL.md +10 -3
  11. package/claude/skills/canvas-board/SKILL.md +36 -17
  12. package/claude/skills/ci-workflow/SKILL.md +5 -4
  13. package/claude/skills/code-craft/SKILL.md +10 -6
  14. package/claude/skills/codebase-layout/SKILL.md +3 -5
  15. package/claude/skills/context-fold/REQUIREMENT.md +4 -4
  16. package/claude/skills/context-fold/SKILL.md +42 -23
  17. package/claude/skills/context-fold/references/context-refresh.md +4 -3
  18. package/claude/skills/context-fold/references/context.md +79 -64
  19. package/claude/skills/create-rule/SKILL.md +7 -3
  20. package/claude/skills/create-skill/SKILL.md +27 -7
  21. package/claude/skills/create-skill/references/skill-reference.md +41 -17
  22. package/claude/skills/decision-escalate/SKILL.md +8 -5
  23. package/claude/skills/deploy-app/SKILL.md +1 -1
  24. package/claude/skills/deprecation-migration/SKILL.md +12 -4
  25. package/claude/skills/design-taste/SKILL.md +17 -11
  26. package/claude/skills/docs-sync/REQUIREMENT.md +1 -0
  27. package/claude/skills/docs-sync/SKILL.md +17 -6
  28. package/claude/skills/docs-sync/references/docs.md +54 -21
  29. package/claude/skills/docs-sync/references/readme.md +32 -8
  30. package/claude/skills/document-health/SKILL.md +3 -4
  31. package/claude/skills/draft-and-pick/SKILL.md +73 -47
  32. package/claude/skills/draft-doc/SKILL.md +9 -4
  33. package/claude/skills/draft-doc/references/context.md +4 -4
  34. package/claude/skills/draft-doc/references/docs.md +1 -0
  35. package/claude/skills/draft-doc/references/wiki.md +3 -2
  36. package/claude/skills/draft-figure/SKILL.md +9 -7
  37. package/claude/skills/draft-identity/SKILL.md +24 -52
  38. package/claude/skills/draft-identity/references/card-route.md +46 -0
  39. package/claude/skills/draft-ready/SKILL.md +19 -7
  40. package/claude/skills/draft-screencast/SKILL.md +6 -112
  41. package/claude/skills/draft-screencast/references/beats-template.md +119 -0
  42. package/claude/skills/draft-slides/SKILL.md +7 -4
  43. package/claude/skills/git-branch/SKILL.md +3 -3
  44. package/claude/skills/git-commit/SKILL.md +5 -3
  45. package/claude/skills/git-followup/SKILL.md +16 -9
  46. package/claude/skills/git-issue/SKILL.md +6 -5
  47. package/claude/skills/git-pr/SKILL.md +56 -44
  48. package/claude/skills/git-ship/SKILL.md +43 -34
  49. package/claude/skills/git-ship/references/ci-watch.md +31 -0
  50. package/claude/skills/git-split/SKILL.md +28 -100
  51. package/claude/skills/git-split/references/independent.md +40 -0
  52. package/claude/skills/git-split/references/stacked.md +73 -0
  53. package/claude/skills/git-stage/SKILL.md +9 -7
  54. package/claude/skills/git-worktree/SKILL.md +27 -49
  55. package/claude/skills/git-worktree/references/cleanup.md +55 -0
  56. package/claude/skills/markdown-craft/SKILL.md +2 -2
  57. package/claude/skills/markdown-craft/references/markdown.md +82 -53
  58. package/claude/skills/plan-feature/SKILL.md +52 -46
  59. package/claude/skills/plan-feature/references/batches.md +46 -0
  60. package/claude/skills/plan-groundwork/SKILL.md +60 -74
  61. package/claude/skills/plan-groundwork/references/output.md +48 -0
  62. package/claude/skills/plan-intake/SKILL.md +28 -21
  63. package/claude/skills/plan-intake-answer/SKILL.md +14 -13
  64. package/claude/skills/read-frames/SKILL.md +2 -0
  65. package/claude/skills/record-screencast/SKILL.md +31 -11
  66. package/claude/skills/review-address/SKILL.md +76 -158
  67. package/claude/skills/review-address/references/rebase-only.md +34 -0
  68. package/claude/skills/review-craft/SKILL.md +27 -11
  69. package/claude/skills/review-pr/SKILL.md +91 -40
  70. package/claude/skills/review-ui/SKILL.md +62 -37
  71. package/claude/skills/review-ui/references/hosted-preview.md +30 -0
  72. package/claude/skills/role-orchestrator/SKILL.md +105 -74
  73. package/claude/skills/role-orchestrator/references/orchestrator-filing.md +26 -0
  74. package/claude/skills/role-planner/SKILL.md +66 -64
  75. package/claude/skills/role-reviewer/SKILL.md +57 -46
  76. package/claude/skills/role-worker/SKILL.md +68 -63
  77. package/claude/skills/search-craft/SKILL.md +7 -5
  78. package/claude/skills/session-compact/SKILL.md +16 -11
  79. package/claude/skills/session-map/SKILL.md +31 -10
  80. package/claude/skills/session-relay/SKILL.md +32 -65
  81. package/claude/skills/session-resume/SKILL.md +18 -13
  82. package/claude/skills/session-worktree/SKILL.md +70 -42
  83. package/claude/skills/systematic-debugging/SKILL.md +6 -1
  84. package/claude/skills/target-check/SKILL.md +13 -6
  85. package/claude/skills/target-setup/SKILL.md +39 -27
  86. package/claude/skills/task-board/SKILL.md +68 -60
  87. package/claude/skills/task-board/references/create-report.md +36 -0
  88. package/claude/skills/task-complete/SKILL.md +20 -18
  89. package/claude/skills/teach-workspace/SKILL.md +61 -33
  90. package/claude/skills/test-craft/SKILL.md +2 -1
  91. package/claude/skills/test-first/SKILL.md +15 -9
  92. package/claude/skills/ui-checklist/SKILL.md +18 -16
  93. package/claude/skills/ux-audit/SKILL.md +9 -10
  94. package/claude/skills/ux-measure/SKILL.md +22 -18
  95. package/claude/skills/video-craft/SKILL.md +15 -7
  96. package/claude/skills/write-human/SKILL.md +9 -1
  97. package/claude/skills/youtube-transcripts/SKILL.md +2 -1
  98. package/docs/agents/context-audit-checks.md +19 -1
  99. package/docs/agents/context-audit.md +2 -2
  100. package/docs/agents/driver.md +1 -1
  101. package/docs/agents/hooks.md +21 -1
  102. package/docs/agents/index.md +10 -14
  103. package/docs/agents/indexes.md +25 -29
  104. package/docs/agents/install-and-sync.md +124 -235
  105. package/docs/agents/markdown-audit-density.md +39 -0
  106. package/docs/agents/markdown-audit-weight.md +9 -7
  107. package/docs/agents/markdown-audit.md +18 -14
  108. package/docs/agents/migrate.md +41 -16
  109. package/docs/agents/output-shape.md +60 -45
  110. package/docs/agents/overview.md +36 -12
  111. package/docs/agents/routing.md +1 -1
  112. package/docs/agents/rule-citations.md +1 -1
  113. package/docs/agents/skills-reach.md +1 -1
  114. package/docs/agents/targets.md +25 -77
  115. package/docs/target/migrations.md +1 -1
  116. package/docs/target/projects.md +2 -2
  117. package/docs/workflow/ai-workflow.md +2 -2
  118. package/docs/workflow/parallel-features.md +2 -2
  119. package/docs/workflow/skill-map.md +1 -1
  120. package/docs/workflow/visual-design-workflow.md +1 -1
  121. package/package.json +1 -1
  122. package/src/commands/claude.ts +1 -1
  123. package/src/commands/context.ts +106 -7
  124. package/src/commands/hooks.ts +63 -0
  125. package/src/commands/markdown.ts +169 -32
  126. package/src/context/audit.ts +256 -5
  127. package/src/context/classify/prompts.ts +1 -1
  128. package/src/context/gate.ts +23 -7
  129. package/src/context/shape-gated.ts +28 -0
  130. package/src/design/document.ts +1 -1
  131. package/src/design/tokens.ts +1 -1
  132. package/src/gate/measures.ts +15 -190
  133. package/src/gate/stages.ts +7 -23
  134. package/src/hooks/reply-links.ts +110 -0
  135. package/src/markdown/density.ts +258 -0
  136. package/src/markdown/gate.ts +57 -18
  137. package/src/markdown/reference-shape.ts +103 -0
  138. package/src/markdown/scope.ts +174 -0
  139. package/src/markdown/structure.ts +9 -59
  140. package/src/shipped/references.ts +1 -1
  141. package/tooling/claude/seeds/.claude/hooks/reply-links.sh +72 -0
  142. package/tooling/claude/seeds/.claude/hooks/standards-audit.sh +22 -1
  143. package/tooling/claude/seeds/.claude/settings.json +4 -0
  144. package/docs/agents/commands.md +0 -154
  145. package/docs/agents/docs.md +0 -27
  146. package/docs/agents/drift-surfaces.md +0 -201
  147. package/docs/agents/gov-detect.md +0 -47
  148. package/docs/agents/scripting.md +0 -142
  149. package/src/markdown/growth.ts +0 -43
@@ -1,154 +0,0 @@
1
- ---
2
- title: Command catalog
3
- description: Every top-level command with its purpose, each domain's subcommands and the page describing them, the browser commands, and the version skew report
4
- ---
5
-
6
- # Command catalog
7
-
8
- Bare `canon --help` lists every top-level command under five headings, Project, Domains, Author and render, Session and ship, and Repo reports, with descriptions cut at 80 columns. A pipe gets the same text without the frame glyphs. Behavior notes for the install and sync verbs live in `install-and-sync.md`.
9
-
10
- `canon <command> --help` lists subcommands, and `canon <command> <subcommand> --help` a leaf's flags, exit codes, and examples. `canon <command> --help --all` prints both. An unknown subcommand exits 1 under a group taking no operand.
11
-
12
- ## Project-level
13
-
14
- One row per top-level command, in the order `canon --help` prints them. A domain's subcommands are in `## Domain commands` below.
15
-
16
- | Command | Purpose |
17
- | --------------------------- | ------------------------------------------------------------------------------------------ |
18
- | `canon init [path]` | Bootstrap a project with selected toolkit domains |
19
- | `canon sync [path]` | Sync every installed domain, or report drift and version skew with `--check` |
20
- | `canon upgrade` | Reinstall the CLI globally with the package manager the install path names |
21
- | `canon migrate <verb>` | Move a project off a retired name or layout, reporting the plan until `--write` |
22
- | `canon targets <verb>` | Report the projects this toolkit installed into |
23
- | `canon gov <verb>` | Install and sync governance rules, and audit the tree against them |
24
- | `canon tooling <verb>` | Sync, diff, and verify tooling stacks |
25
- | `canon claude <verb>` | Install the Claude workflow and audit both skill corpora |
26
- | `canon wiki <verb>` | Scaffold the wiki pages |
27
- | `canon indexes <verb>` | Regenerate `index.md` files from sibling frontmatter, or flatten them |
28
- | `canon docs [topic]` | Emit toolkit reference docs (`list`, or a topic by name) |
29
- | `canon design <verb>` | Render or regenerate `canon/DESIGN.md` tokens, emit their CSS, install the base stylesheet |
30
- | `canon slides <verb>` | Render a deck folder of HTML slides, or list the decks |
31
- | `canon capture [source]` | Render HTML or a URL to PNG and prove each declared font resolved |
32
- | `canon serve [dir]` | Serve a directory on loopback and print the link that opens it |
33
- | `canon demo <verb>` | Compile and record a running app |
34
- | `canon inventory [subject]` | Group every route's elements by one computed property, never gating |
35
- | `canon drive <url> <run>` | Walk a page through named interactions and measure each state |
36
- | `canon transcripts <url>` | Fetch a YouTube transcript with metadata frontmatter (needs `yt-dlp`) |
37
- | `canon teach <verb>` | Open and author learning workspaces |
38
- | `canon canvas <verb>` | Serve a local canvas of pages and HTML frames, arrange, edit, and capture them |
39
- | `canon sandbox [cat:cmd]` | Run sandbox scenarios, toolkit-only like the tree it reads |
40
- | `canon tasks <verb>` | Read and write the task board, the plans it cites, and its archive |
41
- | `canon intake <verb>` | Report intake folders and write answers into them |
42
- | `canon records <verb>` | Validate, size, prune, and back up the session records |
43
- | `canon sessions <verb>` | Resolve live sessions to worktree and branch, and move one between machines |
44
- | `canon worktrees <verb>` | Report and remove worktrees whose pull request merged |
45
- | `canon hooks <verb>` | Run the steps a git hook drives |
46
- | `canon pr <verb>` | Read a pull request's diff, head, checks, and review, and post its evidence |
47
- | `canon feedback` | Open a GitHub issue from toolkit feedback on stdin, or print the block on failure |
48
- | `canon audits <verb>` | Run every audit as one set under one verdict against the recorded baseline |
49
- | `canon gate <verb>` | Run every stage that guards a branch here |
50
- | `canon secrets <verb>` | Report credential-shaped values in the tree the package ships |
51
- | `canon deps <verb>` | Report published advisories against the resolved dependency set |
52
- | `canon labels <verb>` | Read a changed set against the label map, and scan pull request text |
53
- | `canon comments <verb>` | Measure comment density by language and kind, with a trend from git |
54
- | `canon context <verb>` | Report context folder health, and classify changed or stale sections |
55
- | `canon markdown <verb>` | Fail markdown on a banned character or a dead link, and report its structure |
56
- | `canon census [path]` | Report tracked file count, extensions, and a line total skipping binaries |
57
- | `canon upstream <verb>` | Read Claude Code releases from a cursor, and list the toolkit's own mechanisms |
58
-
59
- `canon serve` drives no browser, which is what separates it from the four that do. All four ship now that `capture` does, so the line between them is the engine rather than the package. A generated page loses its script to an editor preview and to a `file://` open, so the link is the delivery rather than a convenience, and every generated surface here reaches a reader through one. A teach lesson's stylesheet is embedded rather than linked, so only its script still needs a server. It binds `127.0.0.1` and never a wildcard, because what it is pointed at is routinely a gitignored record tree. It sends `cache-control: no-store`, since a preview exists to be edited and reloaded and a cached stylesheet reads as a fix that did not work.
60
-
61
- A port already in use is the ordinary case rather than a refusal, so it walks forward to the next free one and reports which it took. That is why a caller reads `url` off the `--json` record instead of composing one from the port it asked for. Only contention is walked past. Any other bind failure refuses as `bind-failed` carrying the error's code, rather than being retried twenty times and reported as a range being full, which names a cause nothing checked.
62
-
63
- A request naming a directory is redirected to its trailing-slash form rather than answered in place. A browser resolves a relative asset against the last slash of the URL it is on, so answering `/lesson` directly leaves the page asking for `/course.css` instead of `/lesson/course.css`, and it renders unstyled through the server that exists to prevent exactly that.
64
-
65
- Containment is tested after symlinks are followed rather than on the path as written, and the test sits immediately before the read rather than beside the request that produced it. Resolving a request lexically clears a link pointing outside the served root, and a link out of the served root is the shape it guards against. Position is what makes the property hold: a directory request appends its index after the request path has been checked, so a check placed earlier leaves that index untested. `--index` answers a directory holding no `index.html` with a listing read per request, where the default stays a 404. Every entry the listing names takes the same containment test a request for it would, so a link out of the root is absent rather than greyed, and dotfiles are hidden. With no `--entry` and no root `index.html`, the printed link is `/` and `entryExists` reads true. An `--entry` that escapes the root refuses with `no-entry` before a port is taken, because `url` is the field a caller hands to a reader.
66
-
67
- `canon demo` is the second browser command and the one that ships, since its purpose is running in a target rather than regenerating what this repository commits. It needs a browser binary the package does not carry, installed once with `bunx playwright install chromium`.
68
-
69
- `canon inventory` is the third and takes the same answer for the same reason. It reads `inventory.toml` at the project root for its base URL, its routes, and the element query each subject runs over, so what it walks comes from the project rather than from the toolkit. It reports how many different answers a site gives for one property and never gates, because whether five focus rings across four routes is a defect is a judgment. A missing server and an unmatched query are both refusals rather than empty listings, since a listing with no rows reads as one consistent answer.
70
-
71
- ```toml
72
- base-url = "http://localhost:4173"
73
- routes = ["/", "/pricing", "/docs"]
74
-
75
- [subjects.focus]
76
- query = "button, a[href], input, select, textarea, [tabindex]"
77
- ```
78
-
79
- `canon drive` is the fourth and ships for the same reason the three before it do. What separates it from all three is the axis rather than the destination: `capture` and `inventory` each answer about a page as it loads, and every defect that exists only after a menu opens or the page scrolls is invisible to both. It takes a JSON run file naming the viewport, the probes, and the interaction sequence, since a route catalog is state a project holds and an interaction sequence is a script written for one question. Viewport heights are never defaulted, because the heights a defect hides at belong to the layout rather than to this command. It reports findings and never gates, since every probe it ships carries a class of false finding a throwaway version already produced. See `driver.md`.
80
-
81
- ## Domain commands
82
-
83
- Each domain exposes a consistent shape where applicable: `list`, `install`, `sync`, `create`. The page named beside a domain carries its flags, records, and refusals, and `canon <domain> --help --all` prints every subcommand's flags.
84
-
85
- - **`migrate`**, in `migrate.md`: `rename`, `skill-names`, `records`, `surface-roots`, `record-tree`, `rule-layout`, `scratch-evidence`, `record-layout`
86
- - **`targets`**, in `targets.md`: `list`, `pulls`
87
- - **`gov`**, in `install-and-sync.md`: `list`, `install`, `sync`, `build`, `regen`, `detect` (in `gov-detect.md`), `test-order`, `counts`, `superseded`, `citations`, `restated`
88
- - **`tooling`**, in `install-and-sync.md`: `list`, `sync`, `diff`, `reference`, `verify`, `inject`, `prune-gitignore`
89
- - **`claude`**, in `skills-audit.md`: `init`, `sync`, `setup`, `routing`, `plugin-update`, `seeds list`, `skills list`, `skills audit`, `skills drift`, `skills reach`, `skills rank`
90
- - **`wiki`**, in `scripting.md`: `init`
91
- - **`indexes`**, in `indexes.md`: `regen`, `list`
92
- - **`docs`**, in `docs.md`: `list`, `<topic>`
93
- - **`design`**, in `install-and-sync.md` for `install`, `sync`, and `fonts`, and below for the rest: `render`, `regen`, `css`, `install`, `sync`, `fonts`
94
- - **`slides`**, described below: `render`, `list`
95
- - **`demo`**, in `demo.md`: `compile`, `run`, `frames`
96
- - **`inventory`**, described above: `run`
97
- - **`teach`**, in `teach.md`: `list`, `up`, `open`, `resource`, `glossary`, `lesson`, `stylesheet`, `nav`, `render`
98
- - **`canvas`**, described below: `serve`, `list`, `page add`, `page rename`, `frame add`, `frame move`, `frame resize`, `fit`, `arrange`, `selection`, `edit`, `editing`, `capture`
99
- - **`sandbox`**, in `sandbox.md`: `run`, `equivalence`, `reset`, `clean`, `check`, `coverage`
100
- - **`tasks`**, in `tasks.md`: `next-label`, `archive`, `decline`, `plan-citations`, `plan-answers`, `plan-branch`, `plan-reach`, `plan-link`, `pull-request`, `outcome`, `list`, `validate`
101
- - **`intake`**, in `intake.md`: `list`, `answer`
102
- - **`records`**, in `records.md`: `validate`, `ordinal`, `size`, `stale`, `prune-tmp`, `push`, `pull`
103
- - **`sessions`**, in `sessions.md`: `list`, `export`, `import`
104
- - **`worktrees`**, in `worktrees.md`: `list`, `reclaim`, `repair-bare-flag` (hidden)
105
- - **`hooks`**, in `hooks.md`: `post-merge`
106
- - **`pr`**, in `pr-reads.md`: `key-changes`, `head`, `checks`, `review-state`, `evidence`, `preview`, `local`, `frames`, `lift` (in `pr-lift.md`), `tick` (in `pr-tick.md`)
107
- - **`upstream`**, in `upstream.md`: `fetch`, `catalog`, `due`, `advance`
108
- - **`audits`**, in `audits.md`: `run`, `list`
109
- - **`gate`**, in `gate.md`: `run`
110
- - **`secrets`**, in `state-scoped-risk.md`: `scan`
111
- - **`deps`**, in `state-scoped-risk.md`: `audit`
112
- - **`labels`**, in `label-coverage.md`: `audit`, `apply`, `scan`
113
- - **`comments`**, in `comments.md`: `scan`
114
- - **`context`**, in `context-audit.md`: `audit`, `classify diff`, `classify sweep`, `classifier show`, `classifier set`
115
- - **`markdown`**, in `markdown-audit.md`: `audit`
116
-
117
- Common patterns:
118
-
119
- - **`list --json`** → machine-readable catalog on stdout.
120
- - **`install <name> <path>`** → install a specific entry into a target project.
121
- - **`sync <path>`** → reapply all installed entries in a target project.
122
- - **`create [name]`** → scaffold a new authoring entry in this repo.
123
-
124
- Subcommands no other page describes:
125
-
126
- - **`claude plugin-update`** matches the installed marketplace plugin against `claude/.claude-plugin/plugin.json`'s own name and runs `claude plugin update` on it, reading the version back off `claude plugin list --json`, since the update call reports none of its own (`--json`).
127
- - **`claude skills rank`** scores either skill corpus's descriptions against a case corpus by TF-IDF cosine similarity, reporting rank-one and top-three (`--cases <path>`).
128
- - **`design regen`** rewrites this repository's `canon/DESIGN.md` from `src/design/tokens.ts` and `src/design/base.css` from `src/design/neutral.ts`. `design css` emits the tokens and components as CSS on stdout, with `--no-components` for properties alone, and `design render` declares each vendored face the typography table names under `--embed-fonts`.
129
- - **`slides list --json`** emits one row per deck under `.canon/slides/`, carrying `name`, `title`, `slides`, and `path`.
130
- - **`slides render [deck]`** takes a deck name or a folder, defaulting to the only deck, and lays out each `.html` file in filename order in Chromium at 1280 by 720 and rebuilds it as editable shapes. Each element drawn as a picture prints one `✗` line on stderr naming the slide, the selector, and the properties that forced it. A link left out, being a `#slide-N` past the deck's end or any target that is neither that nor an absolute URL, prints one too, and the exit stays 0.
131
- - **`slides.md`** describes a deck folder's `deck.json` and the faces it embeds, its per-slide `<body>` attributes, transitions and entrances, speaker notes, and charts.
132
- - **`canvas`**, with its verbs, content format, and write routes, is described in `canvas.md`.
133
- - **`labels apply <number>`** adds the labels this branch earns to its open pull request, refusing a pull request whose head is not the current branch or that is not open, and never creating a label the remote lacks or removing one a person added (`--check` writes nothing and exits 2 when labels are owed, `--base`, `--json`).
134
- - **`labels scan`** fails a pull request or a posted review whose title, body, or review comment carries a phase label, a label a code span quotes, a gitignored record path, a session link, a banned character, a body heading outside the closed section list or a missing `## Summary` or `## Key changes`, or a title breaking `claude/skills/git-pr/references/pr.md`'s format, casing, or length rule (`--event`, `--body-file`, `--json`).
135
-
136
- ## Version skew
137
-
138
- `canon sync --check` and `canon claude skills drift` are the two moments a
139
- target already stops to reconcile with the toolkit, so each reports the
140
- installed version against the newest published one. No other command performs
141
- the lookup, which keeps a registry round trip out of the catalog reads an agent
142
- runs in a loop.
143
-
144
- The report carries three states and never changes an exit code. `behind` names
145
- `canon upgrade` as the remedy, `current` says so, and `unknown` carries the
146
- reason the registry could not be reached. Branch on the `skew.state` field in
147
- the JSON record rather than on the exit, since an offline machine has to read as
148
- unmeasured rather than as a failing check.
149
-
150
- `canon upgrade --json` carries one further state, `pending`, that the skew
151
- report does not: the working directory's checkout holds a release npm does not
152
- serve yet, so there is nothing to install. The record names it in `checkout`
153
- beside `latest`, exits 0, and its `message` starts `CLI stays at`. Outside a
154
- canon checkout it never reads `pending`.
@@ -1,27 +0,0 @@
1
- ---
2
- title: Docs
3
- description: How canon docs resolves the toolkit's own reference surface from an install root, and how a split domain is named
4
- ---
5
-
6
- # Docs
7
-
8
- `canon docs` emits the toolkit's own reference docs so an agent in a target project can orient without the toolkit source checked out. The CLI resolves `docs/` and `canon/context/` from its install root, and which of the two it finds depends on how the CLI was installed. A registry install carries `docs/` alone, since `.claude/` is not published. A clone or a linked worktree carries both.
9
-
10
- - `canon docs list [--json]` lists the downstream catalog: the consumer-facing `docs/` surface plus per-domain narrative from `canon/context/` when that root is present. Toolkit-internal context entries (`ci`, `development`, `sandbox`) are dropped. From a registry install the context section is absent rather than empty.
11
- - `canon docs <topic>` prints one doc to stdout, resolved by exact name from `docs/` first, then `canon/context/`. Any doc the install carries is reachable by name, including the toolkit-internal topics the list omits.
12
-
13
- ## How a name resolves
14
-
15
- A domain too large for one file splits into `<domain>/` with a generated `index.md`, and both verbs name it by the folder. `canon docs <domain>` prints that index, which is the catalog routing to the sub-area files, and the listing describes it from the index's `subtitle` where a sibling file supplies `description`. A folder carrying no `index.md` is absent from both, since a catalog is what makes the sub-areas reachable, and so are the files inside it.
16
-
17
- A sub-area file resolves by its bare name too, so `canon docs operating-model` reaches `docs/workflow/operating-model.md`. Three spellings are tried in order, being a sibling file, a folder of that name, then a sub-area file one level down, and each spelling is tried across both roots before the next one runs. A sibling file therefore wins over a folder of the same name, and both win over a sub-area file. Reading the folder last is what keeps the sub-area spelling from changing any name that resolved before it existed.
18
-
19
- The sub-area spelling keeps the root order too. The first root whose folders carry the name answers, so `canon docs indexes` reaches `docs/agents/indexes.md` even though a context folder holds an entry of the same name. A bare name carried by two folders of that one root resolves to neither, which names the available topics and exits 1 rather than answering with whichever folder sorts first.
20
-
21
- `canon docs list` stops at the folder where `canon docs <topic>` goes on to name each file inside it. The listing is the downstream catalog rather than the index of what a name reaches, so a sub-area file appears there only where it declares a target-facing `category` of its own, and no sub-area file of the context root appears at all. A name that resolves and does not list is the divergence the toolkit-internal topics already carry.
22
-
23
- ## Output
24
-
25
- Data prints to stdout and the frame to stderr, so `canon docs <topic> > out.md` captures clean markdown. With no topic and no verb, `canon docs` runs `list`. An unknown topic names the available topics on stderr and exits 1.
26
-
27
- Only a `---` block opening on the first line counts as frontmatter, so a document body carrying horizontal rules emits whole.
@@ -1,201 +0,0 @@
1
- ---
2
- title: Drift surfaces
3
- description: The seven sections canon sync --check reports beside the per-domain scan, being seeds, superseded artifacts, unmigrated domains, surfaces at the old root, new rules and skills, and the reverse walk, with the managed-target gate and what counts toward the exit code
4
- ---
5
-
6
- # Drift surfaces
7
-
8
- `canon sync --check` reports seven sections outside the per-domain scan that
9
- `install-and-sync.md` describes, because each names something
10
- that walk cannot see: `seeds`, `superseded`, `unmigrated`, `unmovedSurfaces`,
11
- `newSkills`, `newRules`, and `reverse`. None of them produces a change, and no
12
- sync command applies any of them. The headings below group the first four
13
- together and give `newSkills` its description under `newRules`, which answers the
14
- same question about a different corpus.
15
-
16
- All seven report only against a toolkit-managed target, which is one carrying a
17
- `.claude/` directory, a `CLAUDE.md`, or a domain still at the root layout. The
18
- report says so through `managed` in the JSON and routes an unmanaged directory to
19
- `canon init`. Seeds are why the gate exists, since they enumerate from the toolkit
20
- source rather than from what a target installed, so an unmanaged directory would
21
- otherwise report every seed as `missing`.
22
-
23
- A root-layout domain counts as a marker on its own, because the detection fires
24
- only on root files the toolkit ships and a project in the old layout is one the
25
- toolkit installed. When `managed` is false every section comes back empty rather
26
- than the render alone going quiet, so a consumer reading `--json` never acts on a
27
- finding the rendered half withheld.
28
-
29
- ## Seeds, superseded artifacts, unmigrated domains, and old-root surfaces
30
-
31
- `seeds` classifies every seed the toolkit ships against the target's copy, as
32
- `matching`, `stale`, `drifted`, or `missing`. `missing` has no per-domain
33
- equivalent, since the domain walk lists what a target installed and cannot see a
34
- file that never arrived. There is no `customized` verdict here, because that one
35
- needs a stamp and seeds carry none, so a file history cannot attribute stays
36
- `drifted`. Merge the section by hand against the toolkit copy rather than
37
- replacing a file the project edits.
38
-
39
- A markdown seed installs rewritten rather than copied, since the `stub: true`
40
- marker the toolkit's own seed gate reads is stripped on the way in. The
41
- comparison above runs against what the install would write, so a marked seed a
42
- target never touched still reports `matching`. Every other seed copies byte for
43
- byte.
44
-
45
- `superseded` names a file a newer seed folder replaced, such as `.claude/TASKS.md`
46
- against the `.canon/tasks/` that now ships. The entry carries `replacedBy` and
47
- nothing else, and the file is never deleted, since the content belongs to the
48
- project and only its author can decide where it moves. The list derives from the
49
- seed tree rather than from a fixed set of filenames, so a folder added later is
50
- covered without a code change. Only an exact stem matches, which leaves a
51
- suffixed variant such as `TASKS-ARCHIVE.md` unreported.
52
-
53
- No skill splits the file. The destination's shape lives in the skill that owns
54
- it, so the split is the project's own to make.
55
-
56
- `unmigrated` names a domain sitting at the root layout an older toolkit installed
57
- to, with nothing at the path the current one reads. It carries `rootPath`,
58
- `installPath`, and a file count. Without it a project holding an older layout at its
59
- root reports zero entries for that domain and reads as clean, which is the most
60
- misleading state the report can produce.
61
-
62
- The field currently names no domain. Snippets was the one the toolkit ever
63
- installed at a project root, and it retired outright. A leftover `snippets/`
64
- folder reports under the reverse walk instead, where `docs/target/sync.md` says
65
- what to do with it.
66
-
67
- `unmovedSurfaces` names a tracked surface the target still holds at its `.claude/`
68
- spelling with nothing at the `canon/` one, such as `.claude/REQUIREMENTS.md` or
69
- `.claude/context/`. Each entry carries `rel` and `movesTo`, and the render names
70
- `canon migrate surface-roots`, the verb that moves them. Rules glob the `canon/`
71
- spelling alone, so a target on the old root stops loading the rules scoped to
72
- those files until it moves. <!-- canon-keep-surface-root -->
73
-
74
- ## Rules the target never received
75
-
76
- `newRules` names a rule the target could receive and its tree does not hold. A
77
- sync refreshes the files a target already holds and adds none, so without this
78
- section a project's rule set freezes at its install date while every file it
79
- does hold reports as current. That is the report's most confident wrong
80
- answer, since a clean result reads as a target holding everything the toolkit
81
- publishes.
82
-
83
- The list rides beside `newSkills`, which asks the same question about the plugin
84
- catalog. Both are names rather than paths, and neither queues a change, because
85
- the two remedies differ: a skill loads live from the plugin directory and needs
86
- nothing run, while a rule reaches a target only when someone runs
87
- `canon gov install`.
88
-
89
- Read a clean section as reporting rather than as delivering. Nothing here
90
- installs, nothing counts toward `--exit-code`, and a target can read the list and
91
- act on none of it. The value stops at an operator reading it, which is the same
92
- contract the skills list already sets.
93
-
94
- Since `canon gov install` records the stack it resolved, a target carrying that
95
- record answers this by comparing its current entitlement against its current
96
- tree, with no anchor and no git diff involved. That is what lets the section
97
- name a rule that shipped before the target's last sync: the anchor a sync
98
- advances plays no part in the read, where an anchor-bound diff can never see a
99
- rule on the far side of a window a later sync moved past it. The per-file
100
- `missing` state `canon gov sync` reports comes from the same comparison, so a
101
- rule the chain lists reaches both surfaces the same way.
102
-
103
- A target stamped before governance recorded a chain falls back to the older
104
- rule-folder-inference read below. The measurement there anchors on governance's own
105
- stamp rather than on the oldest anchor across domains, since rules are
106
- domain-scoped and a shared anchor would let another domain's sync move the
107
- revision rules are measured from. A target carrying no chain and no governance anchor
108
- reports nothing at all: it has no date to measure against, and diffing from the
109
- start of history would read the whole catalog as new.
110
-
111
- An anchor this toolkit cannot resolve reports nothing by the same route, and that
112
- one is not visible. A stamp naming a revision the running clone has never seen,
113
- which is what a registry install without history or a shallow clone produces,
114
- fails the read and yields an empty list rather than a stated absence. It looks
115
- identical to a target holding every rule the toolkit publishes. `newSkills`
116
- behaves the same way, and neither carries the `historyUnavailable` flag the
117
- per-domain scan uses to separate the two. Treat an empty section on a toolkit
118
- that is not a full clone as unmeasured rather than clean. This gap does not
119
- reach the chain-based read above, since it consults no anchor at all.
120
-
121
- In the fallback, entitlement is filtered, because a stack does not receive
122
- every rule. The base stack names ten rules, the language stacks add `code` and
123
- their own language and testing rules, and every other rule is opt-in, so an
124
- unfiltered list would tell a base consumer about rules it can never receive and
125
- train the reader to skip the section.
126
-
127
- The filter accepts a rule folder on either of two grounds. A folder holding a rule the
128
- base stack resolves to is entitled to every target, read through the stack file so
129
- a rule added to base later needs no code change. Every other rule folder is read off the folders the
130
- target already carries, which is the fallback's only evidence of what a target
131
- was entitled to before it recorded a chain. One rule folder can be reached by more than
132
- one stack, so the test over-reports inside a folder the target holds, which
133
- costs a line where under-reporting would cost the section its point.
134
-
135
- A rule the target already holds is dropped by name. That is what keeps a rule the
136
- toolkit moved between rule folders out of the list, since a rename reaches this
137
- read as an addition and only the name tells the two apart.
138
-
139
- This closes the dangling-citation case as a side effect rather than checking for
140
- one. The live instance is a rule citing a sibling authored after the target's
141
- install, and the section names the sibling as new rather than naming the citation
142
- as broken. That is enough for an operator to act on, and it is not a citation
143
- check.
144
-
145
- ## The reverse walk
146
-
147
- `reverse` is the one section built by walking the target rather than the
148
- catalog. Every other surface enumerates toolkit-owned keys and asks whether the
149
- target matches, so a folder the toolkit deleted appears in none of them. It
150
- carries `unclaimed` and `historyUnavailable`.
151
-
152
- `unclaimed` names a folder the target holds at a top-level path the toolkit once
153
- shipped and has since deleted. The candidate roots come from the toolkit's own
154
- history rather than from a list, so a root dropped later is covered without a
155
- code change. Scoping to those roots is what keeps the walk useful: walking the
156
- whole tree reports every project folder as unclaimed, which is true and says
157
- nothing.
158
-
159
- The managed gate above applies here too, and it is the one place it surprises.
160
- A directory holding a dropped folder and nothing else reports an empty `reverse`
161
- rather than the folder, because it carries none of the three markers. Read an
162
- empty section on an unmanaged target as a walk that never ran rather than as a
163
- clean result.
164
-
165
- Each entry carries `rel`, a file count, and an `attribution` of `dropped`,
166
- `project`, or `unattributed`. A dropped folder and one the project wrote are the
167
- same bytes at the same path, so the verdict is traced from history rather than
168
- guessed from the filesystem.
169
-
170
- Content matching a version the toolkit published reads as `dropped` and carries
171
- the `since` commit that published it. Names the toolkit shipped holding content
172
- it never published read as `unattributed`, which is a state in its own right
173
- rather than a soft yes. No overlap at all reads as `project`, and the render
174
- drops those while the JSON keeps them.
175
-
176
- Only files whose path the toolkit once held are hashed, so a project folder
177
- colliding on a retired name costs the walk no reads. The cost is that a file the
178
- toolkit shipped and the target renamed goes unmatched, the same limit the
179
- `unmigrated` count carries.
180
-
181
- ## What counts toward the gate
182
-
183
- `unmigrated` counts toward `--exit-code`, since running the relocation closes it.
184
- `superseded` and every seed state are excluded, for the reason `orphaned` already
185
- is: only the user can move content they wrote, so failing a job on it leaves the
186
- job red with no mechanical remedy.
187
-
188
- `unmovedSurfaces` is excluded although `canon migrate surface-roots` closes it.
189
- The move rewrites citations across the project, so it is a change the project
190
- schedules, and a job counting it would go red the day a target updated the CLI.
191
-
192
- `newRules` is excluded on a different ground, since a command does close it. What
193
- excludes it is that installing a rule changes what a project is governed by, so
194
- gating on the count would pressure a target into adopting rules nobody chose.
195
- `newSkills` is excluded because it needs no command at all.
196
-
197
- The whole `reverse` section is excluded on the same grounds, and more strongly.
198
- Every entry in it is a judgment about a file the project may own, and one of its
199
- three verdicts is a labelled unknown by design. The unmigrated detection shipped
200
- that exact false positive once, failing a push with no action that cleared it,
201
- so this section reports and gates nothing.
@@ -1,47 +0,0 @@
1
- ---
2
- title: Rule detection
3
- description: Proposing opt-in governance rules from a target's dependencies and paths, the evidence file the verb reads, the record it emits, and its exit codes
4
- ---
5
-
6
- # Rule detection
7
-
8
- `canon gov detect` proposes the opt-in governance rules a target's evidence points at, and writes nothing. A rule no stack names reaches a project only through this verb, which `canon:target-setup` calls before it previews an install.
9
-
10
- ```bash
11
- canon gov detect
12
- canon gov detect ../my-app --stack node --json
13
- ```
14
-
15
- | Option | Behavior |
16
- | ----------------- | ------------------------------------------------ |
17
- | `--stack <stack>` | Leave out every rule this stack already resolves |
18
- | `--json` | Emit the record on stdout instead of the frame |
19
-
20
- ## Evidence
21
-
22
- Each opt-in rule keeps a table in the toolkit's `governance/detect.toml`, keyed by rule basename. Its `deps` are package names, read from `package.json` (`dependencies` and `devDependencies`), `pyproject.toml`, `go.mod`, and `composer.json`. Its `files` are globs matched against the target's git-listed paths and against each folder on those paths, so `.github/workflows` matches a folder holding a workflow. A glob with no wildcard is also tested on disk, which is how `.canon/canvas` matches in a target that gitignores `.canon/`.
23
-
24
- Both reads stop at depth two, which reaches a service folder and a workspace package under `packages/<name>/`. Outside git the verb walks the directory to the same depth and skips `.git`, `node_modules`, `vendor`, and `.venv`. A manifest that does not parse contributes nothing.
25
-
26
- ## Record
27
-
28
- ```json
29
- {
30
- "target": "/abs/path/my-app",
31
- "stack": "node",
32
- "proposals": [
33
- {
34
- "rule": "react",
35
- "evidence": [{ "kind": "dep", "match": "react", "path": "package.json" }]
36
- }
37
- ]
38
- }
39
- ```
40
-
41
- `kind` is `dep` for a dependency, with `path` naming the manifest that declares it, or `file` for a glob, with `path` naming what it matched. `stack` is null when no `--stack` was passed.
42
-
43
- ## Exit codes
44
-
45
- The verb exits 0 on every readable target, including one with no evidence, which returns an empty `proposals` array. A target that does not exist and a `--stack` that does not resolve each exit 1 with the reason on stderr.
46
-
47
- Install what the operator kept with `canon gov install --add <rules> <target>`, which needs no stack.
@@ -1,142 +0,0 @@
1
- ---
2
- title: Scripting
3
- description: The runtime catalogs that replace hardcoded names, what each carries, and a headless invocation per domain
4
- ---
5
-
6
- # Scripting
7
-
8
- What a skill or script reads to discover names at runtime, and how each domain is invoked with no TTY.
9
-
10
- ## Runtime catalogs
11
-
12
- Use these to discover what's available instead of hardcoding names.
13
-
14
- | Command | Returns |
15
- | --------------------------------- | ---------------------------------------------------------------- |
16
- | `canon tooling list --json` | Stacks, extends chain, dep and script counts |
17
- | `canon gov list --json` | Governance stacks, rule sets, evidence, and unreferenced rules |
18
- | `canon gov detect --json` | Opt-in rules a target's dependencies and paths propose |
19
- | `canon claude seeds list --json` | Seed doc sources with content |
20
- | `canon claude skills list --json` | Plugin skills, descriptions, requirement flag, family vocabulary |
21
- | `canon docs list --json` | Consumer docs plus per-domain context |
22
- | `canon audits list --json` | Audits the set runs, the corpus each reads, and whether it gates |
23
-
24
- ### Catalog fields
25
-
26
- Every catalog serializes through `JSON.stringify`, so a name carrying a quote
27
- emits valid JSON. `canon tooling list` previously built
28
- its output with `printf` and no escaping.
29
-
30
- `canon claude seeds list` reads the same plan `canon claude init` applies, so the
31
- listing and the install cannot disagree. It now reports
32
- `canon/context/index.md`, which `init` has always installed and the listing
33
- never named, and it emits the project-level `CLAUDE.md` last rather than first.
34
-
35
- `canon gov list` reports each stack's rules already expanded. A stack entry names
36
- either a rule or a whole rule folder under `governance/rules/`, and the folder
37
- form reaches this listing as the rules it stands for rather than as the folder
38
- name. A consumer deduping `--add` extras against a stack therefore reads the same
39
- set the install writes, whichever form the stack file uses.
40
-
41
- It also carries `unreferenced`, the rules no stack reaches, on every invocation
42
- rather than behind a flag. These install only through `--add`, so the key answers
43
- what a stack leaves out without a caller resolving every stack itself. The key is
44
- present alongside `stacks` and `rules` and survives `--stacks` or `--rules`
45
- narrowing the other two.
46
-
47
- ### The skills catalog
48
-
49
- `canon claude skills list` reads `claude/skills/*/SKILL.md` and reports the folder
50
- name with the frontmatter description, sorted by name. Internal skills under
51
- `.claude/skills/` are excluded, since they never install into a target and a
52
- count spanning both overstates what ships. A skill whose frontmatter is missing
53
- or unparseable returns an empty description rather than failing the listing, so
54
- one malformed file cannot hide the rest of the catalog. `--names` emits skill
55
- names one per line.
56
-
57
- Each skill in `--json` carries `family`, the `metadata.family` its frontmatter
58
- declares, or `null` where it declares none. A top-level `families` array holds
59
- the vocabulary as `{ key, group }` entries in skill map order, where `group` is
60
- the map heading the family's rows sit under. Read a new skill's family from it
61
- rather than from the map. `--by-family` prints the human listing as one step per
62
- family, with a trailing step for any skill whose family is missing or unknown.
63
-
64
- Each entry also carries `requirement`, whether the folder holds a sibling
65
- `REQUIREMENT.md`. Every skill is meant to carry one, so a `false` is a gap to
66
- close rather than a recorded exemption, and the flag answers which skills are
67
- missing theirs without a caller listing the directory itself. `canon claude skills
68
- audit` gates that rule across both corpora, so read this flag for the state of
69
- the shipped catalog and the audit for whether a working tree conforms. Its
70
- surface is in `skills-audit.md`.
71
-
72
- ## Non-interactive examples
73
-
74
- ```bash
75
- # Report what a stack would change, writing nothing
76
- CANON_NON_INTERACTIVE=1 canon tooling diff astro /path/to/project
77
-
78
- # Sync a stack into a target project, overwriting its golden configs
79
- CANON_NON_INTERACTIVE=1 canon tooling sync astro /path/to/project --write
80
-
81
- # Install a governance stack (the stack argument is required headlessly)
82
- CANON_NON_INTERACTIVE=1 canon gov install node --add astro,shadcn /path/to/project
83
-
84
- # Update installed governance rules, dropping a retired .claude/GOV.md
85
- CANON_NON_INTERACTIVE=1 canon gov sync /path/to/project
86
-
87
- # Concatenate installed rules into a paste payload
88
- CANON_NON_INTERACTIVE=1 canon gov build /path/to/project
89
-
90
- # Sync a monorepo subtree, skipping the base layer the repo root already owns
91
- CANON_NON_INTERACTIVE=1 canon tooling sync vite-react /path/to/repo/frontend --skip base --write
92
-
93
- # Verify a stack end-to-end in a throwaway scaffold
94
- canon tooling verify vite-react
95
-
96
- # Apply one stack without scanning or prompting, for scripted provisioning
97
- canon tooling inject base /path/to/project
98
- canon tooling inject base /path/to/project --configs --seeds
99
-
100
- # Drop managed gitignore entries a manifest no longer declares
101
- # Prints the number removed on stdout, diagnostics on stderr
102
- canon tooling prune-gitignore base /path/to/project
103
-
104
- # Bootstrap a project. Any flag suppresses the confirmation prompt
105
- CANON_NON_INTERACTIVE=1 canon init --stack node --skip wiki /path/to/project
106
-
107
- # Run every domain sync. The git workflow is refused headlessly, so nothing is pushed
108
- CANON_NON_INTERACTIVE=1 canon sync /path/to/project
109
-
110
- # Scaffold .claude/wiki/ with a stub index. The target must already exist
111
- CANON_NON_INTERACTIVE=1 canon wiki init /path/to/project
112
-
113
- # Run a sandbox scenario non-interactively
114
- SANDBOX_SCENARIO=sync canon sandbox infra:tooling
115
-
116
- # Read every audit as one record. Exit 2 is a fact, 3 an audit that did not report
117
- canon audits run --json
118
-
119
- # Read committed state rather than an arriving change. Exit 2 carries findings
120
- canon secrets scan --json
121
- canon deps audit --json
122
- ```
123
-
124
- The two state-scoped verbs both exit 2 on findings and mean different things by
125
- it. A credential in the shipped tree is a fact and fails `canon audits run`, while
126
- a published advisory is a judgment that reports and moves no verdict. Neither
127
- reads a diff, so a consumer already running a review surface gets no overlap.
128
-
129
- Both refuse rather than report clean when they have no corpus, and the refusal
130
- reason is the field to branch on. A project publishing nothing refuses the secret
131
- scan with `no-manifest`, and one whose dependencies are not installed refuses the
132
- advisory check with `no-lockfile`. The aggregate reads those as an absent corpus
133
- rather than a broken run, so neither pins its verdict at `incomplete`.
134
-
135
- `canon tooling sync` is the one verb above whose flag is mandatory headlessly. It
136
- overwrites every golden config a stack ships, which reaches the CI workflow, the
137
- git hooks, the end-to-end harness, and the shell scripts under `scripts/`, so a
138
- headless run carrying neither `--check` nor `--write` reports what it would
139
- replace and exits 1 rather than applying it. Run `--check` first to read the
140
- list, then `--write` to apply it. `canon tooling sync --help` names both, and
141
- `canon tooling diff <stack> <target>` reports the per-stack path list resolved
142
- against a real target.
@@ -1,43 +0,0 @@
1
- import { bodyLines } from '@/markdown/scan'
2
- import {
3
- CHECKPOINTS,
4
- type Checkpoints,
5
- type LongBlock,
6
- longBlocks,
7
- } from '@/markdown/structure'
8
-
9
- /**
10
- * Reduces text to the words a reader meets, so a block reflowed, re-marked, or
11
- * moved under another heading still matches the copy it came from.
12
- *
13
- * List markers and emphasis marks are dropped because turning a bullet into a
14
- * labelled bullet or a paragraph changes the shape of a statement and none of
15
- * its words, which is the edit the sweep makes. A word changed is a text
16
- * changed, and that is what the growth stage exists to see.
17
- */
18
- export function normalizeProse(text: string): string {
19
- return text
20
- .replace(/^\s*(?:[-*+]|\d+\.)\s+/gm, '')
21
- .replace(/[*_]/g, '')
22
- .replace(/\s+/g, ' ')
23
- .trim()
24
- }
25
-
26
- /**
27
- * The paragraphs and top-level bullets of `source` past their checkpoints whose
28
- * text appears nowhere in `base`.
29
- *
30
- * Containment rather than line matching, so an unchanged long block passes
31
- * wherever it sits and an edited or new one fails. `base` is every document the
32
- * caller counts as the copy it may have come from, already normalized, since a
33
- * block moved into a sibling entry is as unchanged as one left in place.
34
- */
35
- export function grownBlocks(
36
- source: string,
37
- base: string,
38
- checkpoints: Checkpoints = CHECKPOINTS,
39
- ): LongBlock[] {
40
- return longBlocks(bodyLines(source), checkpoints).filter(
41
- (block) => !base.includes(normalizeProse(block.text)),
42
- )
43
- }