@erclx/canon 4.15.0 → 4.15.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/claude-autoship/SKILL.md +1 -1
- package/claude/skills/claude-docs/SKILL.md +2 -2
- package/claude/skills/claude-feature/SKILL.md +1 -1
- package/claude/skills/claude-markdown-propose/SKILL.md +1 -1
- package/claude/skills/claude-memory-capture/SKILL.md +1 -1
- package/claude/skills/claude-memory-review/SKILL.md +1 -1
- package/claude/skills/claude-orchestrate/SKILL.md +1 -1
- package/claude/skills/claude-orchestrate/references/orchestrator-dispatch.md +1 -1
- package/claude/skills/claude-orchestrate/references/orchestrator-parked.md +1 -1
- package/claude/skills/claude-pr-review/SKILL.md +1 -1
- package/claude/skills/claude-review/SKILL.md +1 -1
- package/claude/skills/claude-tasks/SKILL.md +1 -1
- package/claude/skills/claude-ui-test/SKILL.md +1 -1
- package/claude/skills/claude-ux-audit/SKILL.md +1 -1
- package/claude/skills/claude-ux-measure/SKILL.md +1 -1
- package/claude/skills/claude-worktree/SKILL.md +1 -1
- package/claude/skills/git-pr/SKILL.md +1 -1
- package/claude/skills/session-resume/SKILL.md +2 -2
- package/package.json +1 -1
- package/src/claude/skills-headings.ts +70 -0
|
@@ -10,7 +10,7 @@ Chain the post-plan pipeline in a single run. Every step has a stop condition. S
|
|
|
10
10
|
|
|
11
11
|
## Guards
|
|
12
12
|
|
|
13
|
-
- All `.canon/plans/` and `.canon/review/` reads resolve at the main worktree root, not the current worktree.
|
|
13
|
+
- All `.canon/plans/` and `.canon/review/` reads resolve at the main worktree root, not the current worktree. Resolve that root the way `claude-worktree` does.
|
|
14
14
|
- Derive `<slug>` per `${CLAUDE_SKILL_DIR}/../../standards/slug.md`. This skill takes the stop rather than the `latest` fallback, since it commits and opens a pull request. If empty, stop: `❌ Detached HEAD. Checkout the feature branch first.` Every later step keys its output on this slug, being the worktree, the review receipt, the branch, and the memory proposal, regardless of which plan Step 1 reads.
|
|
15
15
|
- Resolve `<plan>` in Step 1, ahead of any other read.
|
|
16
16
|
- If the working tree has uncommitted changes unrelated to the plan, stop: `❌ Uncommitted changes outside the plan. Commit or stash before autoshipping.`
|
|
@@ -49,7 +49,7 @@ Read these in parallel from the current worktree root (`pwd`), not the main work
|
|
|
49
49
|
- `.claude/DESIGN.md`
|
|
50
50
|
- `.claude/wireframes/index.md` and every `.claude/wireframes/<surface>.md`
|
|
51
51
|
|
|
52
|
-
Read the task board from the main worktree root instead,
|
|
52
|
+
Read the task board from the main worktree root instead, resolving that root the way `claude-worktree` does. It is gitignored scratch and never commits with the branch:
|
|
53
53
|
|
|
54
54
|
- `.canon/tasks/index.md` first, then the task files this session touched. That narrow read serves the marking step, which is the only step here that opens a task file.
|
|
55
55
|
|
|
@@ -210,7 +210,7 @@ Report a block left unfolded rather than dropping it:
|
|
|
210
210
|
|
|
211
211
|
## Step 9: sweep consumed receipts
|
|
212
212
|
|
|
213
|
-
Sweep the review and memory receipts this session consumed. Resolve all paths at the main worktree root, not the current worktree
|
|
213
|
+
Sweep the review and memory receipts this session consumed. Resolve all paths at the main worktree root, not the current worktree, the way `claude-worktree` does.
|
|
214
214
|
|
|
215
215
|
Every delete below is a shell operation, so send each as a plain single `Bash` command rather than joining two with `&&`, which is refused as compound from a linked worktree.
|
|
216
216
|
|
|
@@ -81,7 +81,7 @@ Omit empty sections. Do not print `None identified.` in chat.
|
|
|
81
81
|
|
|
82
82
|
### Full mode
|
|
83
83
|
|
|
84
|
-
Derive a 2-to-4-word kebab-case slug from the feature description. Write the full plan to `.canon/plans/feature-<slug>.md` at the main worktree root, not the current worktree.
|
|
84
|
+
Derive a 2-to-4-word kebab-case slug from the feature description. Write the full plan to `.canon/plans/feature-<slug>.md` at the main worktree root, not the current worktree. Resolve that root the way `claude-worktree` does. Create the directory if it does not exist.
|
|
85
85
|
|
|
86
86
|
From a linked worktree the file-editing tools refuse that path, so the plan goes out through `Bash`. Send the `mkdir -p` and the heredoc as two plain commands rather than joining them with `&&`, which is refused as compound.
|
|
87
87
|
|
|
@@ -25,7 +25,7 @@ Derive `<slug>` from the concern and the surface, kebab-case, naming the subject
|
|
|
25
25
|
- No matching folder, or the operator names a concern and a surface: **Propose**.
|
|
26
26
|
- A matching folder exists and the operator says apply, ship, or commit the answers: **Apply**.
|
|
27
27
|
|
|
28
|
-
All `.canon/proposals/` reads and writes resolve at the main worktree root, not the current worktree.
|
|
28
|
+
All `.canon/proposals/` reads and writes resolve at the main worktree root, not the current worktree. Resolve that root the way `claude-worktree` does.
|
|
29
29
|
|
|
30
30
|
## Write scope
|
|
31
31
|
|
|
@@ -13,7 +13,7 @@ The filename and its type prefix, the frontmatter, the body shape each type carr
|
|
|
13
13
|
|
|
14
14
|
## Guards
|
|
15
15
|
|
|
16
|
-
- All `.canon/memory/` reads and writes resolve at the main worktree root, not the current worktree.
|
|
16
|
+
- All `.canon/memory/` reads and writes resolve at the main worktree root, not the current worktree. Resolve that root the way `claude-worktree` does.
|
|
17
17
|
- From a linked worktree the file-editing tools refuse every path below, so each write in this skill goes out through `Bash` as a plain single command. A memory entry holds one fact and this session has read it, so an update rewrites the whole file with a heredoc rather than editing a line inside it.
|
|
18
18
|
- If `.canon/memory/` does not exist at the main worktree root, create it, along with an `index.md` carrying `title` and `subtitle` frontmatter. `canon claude init` seeds both, and a project predating that seed has neither. Regeneration errors without the index, so the first write into a bare folder would report a frontmatter failure against a file that is fine.
|
|
19
19
|
- If the session produced no user corrections, confirmations, or context disclosures worth persisting, stop: `✅ Nothing worth capturing.`
|
|
@@ -21,7 +21,7 @@ If the user re-pings the skill with no new phrase and a receipt exists, default
|
|
|
21
21
|
|
|
22
22
|
## Guards
|
|
23
23
|
|
|
24
|
-
- All `.canon/memory/` reads, edits, and archive moves resolve at the main worktree root, not the current worktree.
|
|
24
|
+
- All `.canon/memory/` reads, edits, and archive moves resolve at the main worktree root, not the current worktree. Resolve that root the way `claude-worktree` does.
|
|
25
25
|
- If no `.canon/memory/` directory exists at the main worktree root, stop: `❌ No .canon/memory/ directory found.`
|
|
26
26
|
- If `.canon/memory/` contains no `*.md` entries other than `index.md`, stop: `✅ No memory entries to review.`
|
|
27
27
|
- Cleanup is exempt from the two stops above. It works on receipts in `.canon/review/`, and a drained pen is the normal state once Apply has run, so a pen-shaped stop would strand the receipt it exists to delete.
|
|
@@ -23,7 +23,7 @@ session's review are different passes, and how a feature is sized.
|
|
|
23
23
|
|
|
24
24
|
## On invocation
|
|
25
25
|
|
|
26
|
-
Read the board in parallel, resolving the paths at the main worktree root
|
|
26
|
+
Read the board in parallel, resolving the paths at the main worktree root the way `claude-worktree` does:
|
|
27
27
|
|
|
28
28
|
- `.canon/tasks/priority.md`: execution order and what each task is waiting on
|
|
29
29
|
- `.canon/tasks/backlog.md`: what is not being scheduled, when the file exists
|
|
@@ -80,7 +80,7 @@ claude --bg --model <model> -n "worker-<project>-<slug>" "Run /canon:claude-work
|
|
|
80
80
|
|
|
81
81
|
The prefix reads `worker-` because that is the role it marks. It read `orchestrator-` until 2026-08-31, and no controlling session ever carried it, so a worker filtering the roster for that string found a sibling or itself on every row. Nothing matches the prefix programmatically, which is what kept the rename down to three strings.
|
|
82
82
|
|
|
83
|
-
`<project>` is the basename of the main worktree root, not of wherever the dispatcher happens to be running. Resolve the main root first, the way `claude-worktree` Step 1 does, since a bare `git rev-parse --show-toplevel` inside a linked worktree returns the worktree path rather than the project's.
|
|
83
|
+
`<project>` is the basename of the main worktree root, not of wherever the dispatcher happens to be running. Resolve the main root first, the way `claude-worktree` Step 1 does, since a bare `git rev-parse --show-toplevel` inside a linked worktree returns the worktree path rather than the project's.
|
|
84
84
|
|
|
85
85
|
`claude agents` lists every session on the machine with no path column and no per-project filter, so `<project>` in the name is the only thing left telling two fleets apart, and a session named off the worktree path instead would carry the branch folder rather than the project. Two projects each dispatching a bare `worker-page-driver` used to read as one row in that view.
|
|
86
86
|
|
|
@@ -15,7 +15,7 @@ The sweep's own question stays distinct from this one. It asks which parked row
|
|
|
15
15
|
|
|
16
16
|
## Scope
|
|
17
17
|
|
|
18
|
-
Every row under `## Up next` and `## Needs a plan` in `.canon/tasks/priority.md`. A `## Run now` row carries no blocker by definition, so the pass skips it. Resolve the board and each task file at the main worktree root,
|
|
18
|
+
Every row under `## Up next` and `## Needs a plan` in `.canon/tasks/priority.md`. A `## Run now` row carries no blocker by definition, so the pass skips it. Resolve the board and each task file at the main worktree root, the way `claude-worktree` does.
|
|
19
19
|
|
|
20
20
|
Take the rows in board order and finish one before opening the next. Clearing a row changes what the next row collides with, so a pass that measures every row first and writes afterwards writes against a board it has already invalidated.
|
|
21
21
|
|
|
@@ -36,7 +36,7 @@ Read these in parallel from the project root, skipping any that do not exist:
|
|
|
36
36
|
- `CLAUDE.md`: project type, conventions, and commands
|
|
37
37
|
- `.claude/REQUIREMENTS.md`: feature scope and non-goals
|
|
38
38
|
- `.claude/ARCHITECTURE.md`: technical design decisions
|
|
39
|
-
- `.canon/tasks/priority.md`: where this feature sits on the board and what each neighboring row waits on. Resolve this one at the main worktree root
|
|
39
|
+
- `.canon/tasks/priority.md`: where this feature sits on the board and what each neighboring row waits on. Resolve this one at the main worktree root the way `claude-worktree` does, since the board is gitignored and a linked worktree holds no copy of it
|
|
40
40
|
- `.canon/plans/feature-<slug>.md` for the branch, when present: the intent the PR should satisfy
|
|
41
41
|
|
|
42
42
|
Coding standards from `.claude/rules/` are auto-loaded by Claude Code.
|
|
@@ -120,7 +120,7 @@ If nothing is wrong, use: `✅ No findings.`
|
|
|
120
120
|
|
|
121
121
|
Derive `<slug>` per `${CLAUDE_SKILL_DIR}/../../standards/slug.md`. Fall back to `latest` on an empty result.
|
|
122
122
|
|
|
123
|
-
Write the full report directly to `.canon/review/branch/review-<slug>.md` at the main worktree root, not the current worktree.
|
|
123
|
+
Write the full report directly to `.canon/review/branch/review-<slug>.md` at the main worktree root, not the current worktree. Resolve that root the way `claude-worktree` does. Create the directory if it does not exist. Always overwrite.
|
|
124
124
|
|
|
125
125
|
From a linked worktree the file-editing tools refuse that path, so the report goes out through `Bash`. Send the `mkdir -p` and the heredoc as two plain commands rather than joining them with `&&`, which is refused as compound.
|
|
126
126
|
|
|
@@ -12,7 +12,7 @@ Read `${CLAUDE_SKILL_DIR}/../../standards/tasks.md` before writing any file. It
|
|
|
12
12
|
## Guards
|
|
13
13
|
|
|
14
14
|
- Resolve the board at the main worktree root, not `pwd`. Run `git worktree list --porcelain | grep -m 1 '^worktree ' | cut -d' ' -f2-`, falling back to `pwd` outside a git repo. Every read and write below resolves against that root. The board is gitignored scratch shared across worktrees, so a linked worktree writing to its own `pwd` creates a second board nothing else reads.
|
|
15
|
-
- From a linked worktree the file-editing tools refuse that root, so a new task file goes out through `Bash` as a plain single command carrying a heredoc. Archiving already runs through `canon tasks archive`, which resolves the root in-process. Marking an outcome shipped is `claude-docs` and runs through `canon tasks outcome`.
|
|
15
|
+
- From a linked worktree the file-editing tools refuse that root, so a new task file goes out through `Bash` as a plain single command carrying a heredoc. Archiving already runs through `canon tasks archive`, which resolves the root in-process. Marking an outcome shipped is `claude-docs` and runs through `canon tasks outcome`. Resolve that root the way `claude-worktree` does.
|
|
16
16
|
- If `.canon/tasks/` does not exist at that root, stop: `❌ No .canon/tasks/ board. Run canon claude init to set it up.`
|
|
17
17
|
- Route on the request rather than on a flag. Creating names work that does not exist yet, archiving names a task file already on the board. If the request fits neither, stop: `❌ Ambiguous. Say whether to create a task or archive one.`
|
|
18
18
|
- Never hand-edit `.canon/tasks/index.md`. A hook regenerates it from sibling frontmatter after a write. Do not run the regen command directly, except after a shell write from a linked worktree: the hook matches `Write|Edit|MultiEdit` and nothing fires on `Bash`, so that one case regenerates explicitly with `canon indexes regen --no-stage --root <main-root> <main-root>/.canon/tasks/index.md`.
|
|
@@ -59,7 +59,7 @@ If all changes are automatable, skip the manual checklist:
|
|
|
59
59
|
|
|
60
60
|
Derive `<slug>` per `${CLAUDE_SKILL_DIR}/../../standards/slug.md`. Fall back to `latest` on an empty result.
|
|
61
61
|
|
|
62
|
-
When a manual checklist is produced, write it directly to `.canon/review/ui-checklist-<slug>.md` at the main worktree root, not the current worktree.
|
|
62
|
+
When a manual checklist is produced, write it directly to `.canon/review/ui-checklist-<slug>.md` at the main worktree root, not the current worktree. Resolve that root the way `claude-worktree` does. Create the directory if it does not exist. Always overwrite.
|
|
63
63
|
|
|
64
64
|
From a linked worktree the file-editing tools refuse that path, so the checklist goes out through `Bash`. Send the `mkdir -p` and the heredoc as two plain commands rather than joining them with `&&`, which is refused as compound.
|
|
65
65
|
|
|
@@ -59,7 +59,7 @@ If nothing is wrong, use: `✅ No observations.`
|
|
|
59
59
|
|
|
60
60
|
Derive `<slug>` per `${CLAUDE_SKILL_DIR}/../../standards/slug.md`. Fall back to `latest` on an empty result.
|
|
61
61
|
|
|
62
|
-
Write the full report directly to `.canon/review/ux-audit-<slug>.md` at the main worktree root, not the current worktree.
|
|
62
|
+
Write the full report directly to `.canon/review/ux-audit-<slug>.md` at the main worktree root, not the current worktree. Resolve that root the way `claude-worktree` does. Create the directory if it does not exist. Always overwrite.
|
|
63
63
|
|
|
64
64
|
From a linked worktree the file-editing tools refuse that path, so the report goes out through `Bash`. Send the `mkdir -p` and the heredoc as two plain commands rather than joining them with `&&`, which is refused as compound.
|
|
65
65
|
|
|
@@ -106,7 +106,7 @@ Report the reading and stop there. A remedy for a poor verdict is a change with
|
|
|
106
106
|
|
|
107
107
|
Derive `<slug>` per `${CLAUDE_SKILL_DIR}/../../standards/slug.md`. Fall back to `latest` on an empty result.
|
|
108
108
|
|
|
109
|
-
Write the full reading directly to `.canon/review/ux-measure-<slug>.md` at the main worktree root, not the current worktree.
|
|
109
|
+
Write the full reading directly to `.canon/review/ux-measure-<slug>.md` at the main worktree root, not the current worktree. Resolve that root the way `claude-worktree` does. Create the directory if it does not exist. Always overwrite.
|
|
110
110
|
|
|
111
111
|
From a linked worktree the file-editing tools refuse that path, so the reading goes out through `Bash`. Send the `mkdir -p` and the heredoc as two plain commands rather than joining them with `&&`, which is refused as compound.
|
|
112
112
|
|
|
@@ -23,7 +23,7 @@ Run in parallel:
|
|
|
23
23
|
- `git branch --show-current 2>/dev/null || echo ""`
|
|
24
24
|
- `git config --get core.bare 2>/dev/null || echo false`
|
|
25
25
|
|
|
26
|
-
Plans always live at the main root, never inside a linked worktree.
|
|
26
|
+
Plans always live at the main root, never inside a linked worktree.
|
|
27
27
|
|
|
28
28
|
`EnterWorktree` writes `core.bare = true` into the shared config, and nothing restores it, so the repository can already be broken before this session arrives. Repair it before entering when the value is `true` and `<main-root>/.git` is a directory:
|
|
29
29
|
|
|
@@ -145,7 +145,7 @@ canon tasks pull-request <number> --plan feature-<slug> --json
|
|
|
145
145
|
|
|
146
146
|
The slug is a guess at which plan this branch carries rather than a fact about the task, which is why the verb re-checks it against the board and refuses instead of writing on a near miss. A branch whose slug names no plan file falls to the silent skip below, the same as one whose plan no task cites.
|
|
147
147
|
|
|
148
|
-
The verb resolves the board at the main worktree root in-process, adds `Pull request: #NNN` under the `Plan:`, `Groundwork:`, `Intake:`, or `Issue:` lines the task already carries, and corrects the number in place when the line exists. This is the route because the write is an edit inside an existing file, which the file-editing tools refuse from a linked worktree and which no shell stream editor may make.
|
|
148
|
+
The verb resolves the board at the main worktree root in-process, adds `Pull request: #NNN` under the `Plan:`, `Groundwork:`, `Intake:`, or `Issue:` lines the task already carries, and corrects the number in place when the line exists. This is the route because the write is an edit inside an existing file, which the file-editing tools refuse from a linked worktree and which no shell stream editor may make. That root is the one `claude-worktree` resolves on entry.
|
|
149
149
|
|
|
150
150
|
Skip this silently when the record is `ok: false` and `reason` is `no-board`, `no-match`, or `ambiguous`. Those are the three cases a guessed write would compound: no board, no task naming the plan, or more than one. One task, one pull request, and a wrong match archives the wrong task unattended once the branch merges. Report any other refusal rather than swallowing it.
|
|
151
151
|
|
|
@@ -7,7 +7,7 @@ description: Resumes a previous session by reading the handoff it left behind, t
|
|
|
7
7
|
|
|
8
8
|
## Step 1: read tracked work
|
|
9
9
|
|
|
10
|
-
Resolve `.canon/plans/`, `.canon/memory/`, and `.canon/tasks/` at the main worktree root
|
|
10
|
+
Resolve `.canon/plans/`, `.canon/memory/`, and `.canon/tasks/` at the main worktree root the way `claude-worktree` does.
|
|
11
11
|
|
|
12
12
|
Read these in parallel, skipping any that do not exist:
|
|
13
13
|
|
|
@@ -40,7 +40,7 @@ End with one line: `Start with: <first Up next item>` and note whether it has a
|
|
|
40
40
|
|
|
41
41
|
When the board is empty and a handoff was found, name what the handoff leaves open instead: `Start with: <the open thread the handoff names>`. A recommendation slot filled with nothing reads as a failed run, and a handoff on an empty board is the shape a session leaves when it was reasoning rather than shipping.
|
|
42
42
|
|
|
43
|
-
Do not offer to remove entries. A completed task is archived out of `.canon/tasks/` when work ships. The git log is the authoritative record of shipped work. Plan files are archived per the
|
|
43
|
+
Do not offer to remove entries. A completed task is archived out of `.canon/tasks/` when work ships. The git log is the authoritative record of shipped work. Plan files are archived per the lifecycle rule in `${CLAUDE_SKILL_DIR}/../../standards/plan.md`.
|
|
44
44
|
|
|
45
45
|
Memory is updated only when a recorded fact becomes wrong, never on resume. A domain fact reaches a session through `.claude/context/`, which `claude-memory-capture` routes to and the three-tier model loads on demand, so the memory folder read here is the residue no context entry owns.
|
|
46
46
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The citation shape that sends a reader to a named section of the consuming
|
|
3
|
+
* project's own `CLAUDE.md`.
|
|
4
|
+
*
|
|
5
|
+
* A shipped body reaches a target through the plugin cache, and no `CLAUDE.md`
|
|
6
|
+
* in this repository or in any known target carries the section these lines
|
|
7
|
+
* named. The pointer resolves to nothing, which a session answers by guessing
|
|
8
|
+
* the rule or dropping it, so the shape is banned rather than repaired one
|
|
9
|
+
* body at a time.
|
|
10
|
+
*
|
|
11
|
+
* It is a prose pattern rather than a resolution, because no check can read a
|
|
12
|
+
* target's own root file. A `See` or a `per`, then anything up to a period,
|
|
13
|
+
* then ` in ` and a backticked `CLAUDE.md`. The two verbs are what separate a
|
|
14
|
+
* citation from a mention: dropping them returns five extra lines that name
|
|
15
|
+
* the file without pointing into it, and the period is what stops the match at
|
|
16
|
+
* a sentence boundary so a body discussing the file two sentences earlier does
|
|
17
|
+
* not fail.
|
|
18
|
+
*
|
|
19
|
+
* The period is the only boundary, and excluding a backtick alongside it was
|
|
20
|
+
* the first shape. It reads as a tighter bound and is a hole, because
|
|
21
|
+
* `markdown.md` has a body backtick a named thing, so the natural spelling of
|
|
22
|
+
* the citation is `` See the `Parallel sessions` heading in `CLAUDE.md` `` and
|
|
23
|
+
* a class stopping at the first backtick never reaches the file. That heading
|
|
24
|
+
* is real in this repository's own root file. Both classes returned the same
|
|
25
|
+
* twenty lines over `claude/skills/` at the stamp, so the backtick exclusion
|
|
26
|
+
* was carrying nothing and hiding the spelling an author would reach for.
|
|
27
|
+
*
|
|
28
|
+
* Both verbs match in either case. The measured corpus spelled them `See` and
|
|
29
|
+
* `per` and the insensitive form returned the identical twenty hits with no
|
|
30
|
+
* false positive, so it costs nothing and catches a sentence-initial `Per`.
|
|
31
|
+
*
|
|
32
|
+
* A later body phrasing a legitimate reference this way fails the check and
|
|
33
|
+
* changes its phrasing. There is no exemption marker, since the point is that
|
|
34
|
+
* the target does not resolve for the reader who holds the body.
|
|
35
|
+
*/
|
|
36
|
+
const HEADING_CITATION = /\b(?:see|per)\b[^.]* in `CLAUDE\.md`/i
|
|
37
|
+
|
|
38
|
+
export interface HeadingCitation {
|
|
39
|
+
readonly file: string
|
|
40
|
+
/** One-based, matching the `file:line` form a reader clicks. */
|
|
41
|
+
readonly line: number
|
|
42
|
+
/** The whole line, so a report names what has to be rephrased. */
|
|
43
|
+
readonly text: string
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Every line in one body carrying the banned citation.
|
|
48
|
+
*
|
|
49
|
+
* The unit is the line rather than the match, since a report names a line for
|
|
50
|
+
* rephrasing and two hits on one line would arrive as identical entries.
|
|
51
|
+
*
|
|
52
|
+
* The corpus walk is deliberately absent. Keeping the shape separate from the
|
|
53
|
+
* tree read is what lets the pattern be tested against a string rather than
|
|
54
|
+
* against a fixture, the way `citationsIn` splits the same seam in
|
|
55
|
+
* `skills-reach.ts`.
|
|
56
|
+
*/
|
|
57
|
+
export function headingCitationsIn(
|
|
58
|
+
file: string,
|
|
59
|
+
text: string,
|
|
60
|
+
): HeadingCitation[] {
|
|
61
|
+
const citations: HeadingCitation[] = []
|
|
62
|
+
|
|
63
|
+
for (const [index, line] of text.split('\n').entries()) {
|
|
64
|
+
if (!HEADING_CITATION.test(line)) continue
|
|
65
|
+
|
|
66
|
+
citations.push({ file, line: index + 1, text: line })
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return citations
|
|
70
|
+
}
|