@erclx/aitk 3.47.0 → 3.48.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/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/claude-address-review/SKILL.md +3 -0
- package/claude/skills/claude-diagram/SKILL.md +1 -1
- package/claude/skills/claude-pr-review/SKILL.md +3 -1
- package/claude/skills/git-followup/SKILL.md +1 -1
- package/claude/skills/git-issue/SKILL.md +2 -0
- package/claude/skills/git-pr/SKILL.md +3 -1
- package/claude/skills/git-split/SKILL.md +1 -1
- package/docs/agents/commands.md +57 -56
- package/docs/agents/index.md +1 -1
- package/docs/agents/superseded.md +35 -7
- package/package.json +1 -1
- package/src/commands/gov.ts +64 -9
- package/src/commands/labels.ts +195 -0
- package/src/gov/superseded.ts +223 -15
- package/src/labels/phase.ts +95 -0
- package/src/markdown/scan.ts +13 -2
|
@@ -134,6 +134,9 @@ session when one did. Nothing tests the reply for either rule, since the scan
|
|
|
134
134
|
below reads it for banned characters and phase labels alone, so both halves hold
|
|
135
135
|
while a run applies them.
|
|
136
136
|
|
|
137
|
+
The `pull_request` check the git-pr surface carries reads a pull request's own
|
|
138
|
+
title and body, not a reply comment, so the scan below stays the only gate here.
|
|
139
|
+
|
|
137
140
|
When step 5 rebased the branch, say so in the summary sentence and name which
|
|
138
141
|
files were resolved by hand and which the regen rebuilt. The next review is a
|
|
139
142
|
full pass rather than a delta, and the reader is owed the reason.
|
|
@@ -82,7 +82,7 @@ A reader deciding whether the entry still holds starts from the code paths the e
|
|
|
82
82
|
|
|
83
83
|
Quote node labels containing spaces or special characters with double quotes (`A["Web shell"]`). Avoid parentheses inside labels, they break some renderers. Use `<br/>` for line breaks inside labels.
|
|
84
84
|
|
|
85
|
-
Run the scan in `${CLAUDE_SKILL_DIR}/../../standards/publish.md` against the whole file, including inside mermaid `subgraph` labels and node text. The file lands in the repository rather than on a remote, so the scan's destination scoping leaves the character checks and not the phase-label one. A colon also works as a replacement inside a label. The pedagogical voice the diagram standard asks for is a yield on voice alone and buys no exemption from these bans.
|
|
85
|
+
Run the scan in `${CLAUDE_SKILL_DIR}/../../standards/publish.md` against the whole file, including inside mermaid `subgraph` labels and node text. The file lands in the repository rather than on a remote, so the scan's destination scoping leaves the character checks and not the phase-label one, and the `pull_request` check the git-pr surface carries reads only a pull request's own title and body, never a file the diff carries. A colon also works as a replacement inside a label. The pedagogical voice the diagram standard asks for is a yield on voice alone and buys no exemption from these bans.
|
|
86
86
|
|
|
87
87
|
Nothing enforces the ban inside the diagram. The standards-audit hook toggles on a fence and skips every line within it, so a label carrying an em dash passes silently while the same character in the explanation paragraph below is caught. Check the labels by reading them. This is the one place in the file where the author is the only gate.
|
|
88
88
|
|
|
@@ -29,7 +29,7 @@ reader scanning the thread finds the current verdict where the last one sat.
|
|
|
29
29
|
|
|
30
30
|
## Step 1: resolve the PR and read context
|
|
31
31
|
|
|
32
|
-
Resolve the PR: `gh pr view --json number,headRefName,headRefOid,title` for the current branch, or use a PR number the user names. The first seven characters of `headRefOid` are `<short-sha>`, which names the body file in Step 4.
|
|
32
|
+
Resolve the PR: `gh pr view --json number,headRefName,headRefOid,title,body` for the current branch, or use a PR number the user names. The first seven characters of `headRefOid` are `<short-sha>`, which names the body file in Step 4.
|
|
33
33
|
|
|
34
34
|
Read these in parallel from the project root, skipping any that do not exist:
|
|
35
35
|
|
|
@@ -160,6 +160,8 @@ X critical, Y should-fix, Z minor. Reviewed against project docs and the board.
|
|
|
160
160
|
|
|
161
161
|
A stale ticked box goes in a `**PR body**` block, in place of a `**`path/to/file.ext`**` block and ahead of every one of those, since it precedes the code the diff carries rather than sitting inside it.
|
|
162
162
|
|
|
163
|
+
Run `aitk labels scan --title "<title>" --body "<body>" --head <headRefName>` against the PR under review, since Step 1 already holds all three and this pass is the last human-shaped gate before merge. A hit lands in the same `**PR body**` block, `should-fix`, naming each token the scan returns. This reads the pull request being reviewed, distinct from the comment this pass is about to post, which the scan later in this step still covers.
|
|
164
|
+
|
|
163
165
|
A later pass carrying findings keeps that shape and changes only the summary line:
|
|
164
166
|
|
|
165
167
|
```markdown
|
|
@@ -36,7 +36,7 @@ A missing tracking ref is no longer a guard. An open pull request proves the bra
|
|
|
36
36
|
- A body a person edited by hand between rounds gets no special handling: judge it against the tree the same way regardless of who wrote it last, since a hand-edit the fix commit has made stale is the exact drift this sync exists to close.
|
|
37
37
|
7. Route on the invocation and the comment count for the reply alone.
|
|
38
38
|
- When invoked with `reply-owned`, skip this step: the caller posts its own reply.
|
|
39
|
-
- Otherwise, if the count is above zero, the followup addresses review feedback: post a one-line summary of the fix with `gh pr comment --body`, first running the scan in `${CLAUDE_SKILL_DIR}/../../standards/publish.md` against it, since the hook does not see an inline comment body.
|
|
39
|
+
- Otherwise, if the count is above zero, the followup addresses review feedback: post a one-line summary of the fix with `gh pr comment --body`, first running the scan in `${CLAUDE_SKILL_DIR}/../../standards/publish.md` against it, since the hook does not see an inline comment body. The `pull_request` check the git-pr surface carries triggers on a push or an open rather than a plain edit, so neither this comment nor the title and body step 6 synced reaches it.
|
|
40
40
|
- If it is zero, nothing further runs. The sync in step 6 already did this branch's job.
|
|
41
41
|
|
|
42
42
|
## After completion
|
|
@@ -41,6 +41,8 @@ After outputting the preview, execute the final command immediately. Claude Code
|
|
|
41
41
|
|
|
42
42
|
Before running the final command, run the scan in `${CLAUDE_SKILL_DIR}/../../standards/publish.md` against the title and body. The title and body go straight to the remote with nothing checking them on the way, so this scan is the only gate. It covers the phase-label check as well as the characters, since both go to a reader who has no task board. It applies on top of the banned phrases in `${CLAUDE_SKILL_DIR}/../../standards/issue.md`.
|
|
43
43
|
|
|
44
|
+
The `pull_request` check the git-pr surface carries reads a pull request's own title and body alone, so an issue reaches no check behind this scan either.
|
|
45
|
+
|
|
44
46
|
### Final command
|
|
45
47
|
|
|
46
48
|
Map a bug to `--label bug` and a task to `--label enhancement`.
|
|
@@ -70,7 +70,9 @@ Leave a box unchecked only for the human-only cases the reference defines, and n
|
|
|
70
70
|
|
|
71
71
|
### Pre-publish scan
|
|
72
72
|
|
|
73
|
-
Before running the final command, run the scan in `${CLAUDE_SKILL_DIR}/../../standards/publish.md` against the PR title and body. The title and body go straight to the remote with nothing checking them on the way, so
|
|
73
|
+
Before running the final command, run the scan in `${CLAUDE_SKILL_DIR}/../../standards/publish.md` against the PR title and body. The title and body go straight to the remote with nothing checking them on the way, so run the scan regardless of what backs it downstream. It covers the phase-label check as well as the characters, since both go to a reader who has no task board. It applies on top of the banned phrases in `${CLAUDE_SKILL_DIR}/../../standards/pr.md`.
|
|
74
|
+
|
|
75
|
+
A `pull_request` workflow job now backs the phase-label half for this repository, running `aitk labels scan` against the opened title and body. A project holding an older `aitk` carries no such job and reaches no check at all, so the scan above stays required rather than optional.
|
|
74
76
|
|
|
75
77
|
### Resolving the pull request
|
|
76
78
|
|
|
@@ -69,7 +69,7 @@ If Mode is Independent, append this line to the preview:
|
|
|
69
69
|
|
|
70
70
|
After outputting the preview, execute the final commands immediately. Claude Code's tool permission dialog is the confirmation gate. Do not wait for user input.
|
|
71
71
|
|
|
72
|
-
Before running them, run the scan in `${CLAUDE_SKILL_DIR}/../../standards/publish.md` against every title and body. Each body stages under `.claude/.tmp/` and leaves through `gh`, so the hook sees neither and this scan is the only gate.
|
|
72
|
+
Before running them, run the scan in `${CLAUDE_SKILL_DIR}/../../standards/publish.md` against every title and body. Each body stages under `.claude/.tmp/` and leaves through `gh`, so the hook sees neither and this scan is the only gate. Each resulting branch reaches the `pull_request` check the git-pr surface carries once its own pull request opens, but not before, so this scan is what catches a leak ahead of that.
|
|
73
73
|
|
|
74
74
|
### Final commands
|
|
75
75
|
|
package/docs/agents/commands.md
CHANGED
|
@@ -9,62 +9,63 @@ Full help: `aitk <command> --help`. Behavior notes for the install and sync verb
|
|
|
9
9
|
|
|
10
10
|
## Project-level
|
|
11
11
|
|
|
12
|
-
| Command | Purpose
|
|
13
|
-
| -------------------------- |
|
|
14
|
-
| `aitk init [path]` | Bootstrap a project with selected toolkit domains
|
|
15
|
-
| `aitk sync [path]` | Sync all installed domains in a target project
|
|
16
|
-
| `aitk sync --check` | Report toolkit drift and the installed version against the newest published (`--json`, `--exit-code`)
|
|
17
|
-
| `aitk sandbox [cat:cmd]` | Run sandbox scenarios (interactive or routed), toolkit-only like the tree it reads
|
|
18
|
-
| `aitk sandbox reset` | Reset sandbox to baseline
|
|
19
|
-
| `aitk sandbox clean` | Wipe the sandbox
|
|
20
|
-
| `aitk sandbox check` | Score a provisioned sandbox against a scenario expectation (`--json` for the verdict)
|
|
21
|
-
| `aitk sandbox coverage` | Report which scenarios declare expectations (`--json`, `--strict`, `--skills`)
|
|
22
|
-
| `aitk indexes regen` | Regenerate `index.md` files from sibling frontmatter
|
|
23
|
-
| `aitk docs [topic]` | Emit toolkit reference docs (`list`, or a topic by name)
|
|
24
|
-
| `aitk design render` | Render `.claude/DESIGN.md` tokens to HTML and CSS
|
|
25
|
-
| `aitk slides render` | Render a `.claude/SLIDES.md` source into a PowerPoint deck
|
|
26
|
-
| `aitk slides list` | List the available slide layouts (`--json` for the catalog)
|
|
27
|
-
| `aitk feedback` | Write toolkit feedback from stdin to `.claude/review/feedback/`, or open a GitHub issue with `--github`
|
|
28
|
-
| `aitk transcripts <url>` | Fetch a YouTube transcript with metadata frontmatter (needs `yt-dlp`)
|
|
29
|
-
| `aitk tasks archive` | Move a shipped task off the board, clear its ordering row, and regenerate the index
|
|
30
|
-
| `aitk tasks pull-request` | Record a pull request number on the task a branch closes, by stem or `--plan` (`--json`)
|
|
31
|
-
| `aitk tasks outcome` | Mark outcomes `[x]` on a task by position, repeating `--close` (`--json`)
|
|
32
|
-
| `aitk tasks validate` | Report board rows whose plan, task file, group, file set, or blocker does not hold (`--json`)
|
|
33
|
-
| `aitk intake list` | Report intake folder counts, or one folder's items, keeping what is unread with `--unread` (`--json`)
|
|
34
|
-
| `aitk intake answer` | Write selections into one cluster's answer slots, repeating `--set <item>=<answer>` (`--json`)
|
|
35
|
-
| `aitk teach list` | Report learning workspaces and the ordinal a new one takes, or what one workspace holds (`--json`)
|
|
36
|
-
| `aitk teach open` | Open a workspace at the next ordinal with its mission, resources, and glossary files (`--json`)
|
|
37
|
-
| `aitk teach resource` | Record sources and leads in a workspace, repeating `--read` or `--lead` as `<title>=<url>` (`--json`)
|
|
38
|
-
| `aitk teach glossary` | Add terms to a workspace glossary alphabetically, repeating `--term <term>=<definition>` (`--json`)
|
|
39
|
-
| `aitk records validate` | Report a session record or a standard against the standard governing it, per kind (`--json`)
|
|
40
|
-
| `aitk records migrate` | Rewrite the records a validate finding names a recoverable transform for (`--write`, `--json`)
|
|
41
|
-
| `aitk records size` | Report what each record folder holds and how much of it is recent, heaviest first (`--json`)
|
|
42
|
-
| `aitk records push` | Commit the nine backed record folders and push them to a private records remote (`--json`)
|
|
43
|
-
| `aitk records pull` | Fetch the records remote and write it back, refusing rather than discarding unpushed records (`--json`)
|
|
44
|
-
| `aitk sessions list` | Resolve live sessions to the worktree and branch each holds, filtered by `--branch` (`--json`)
|
|
45
|
-
| `aitk worktrees list` | Report which worktrees are reclaimable, keyed on the pull request having merged, with every refusal and the removal route named (`--json`)
|
|
46
|
-
| `aitk comments scan` | Measure comment density by language and comment kind, with a trend recomputed from git
|
|
47
|
-
| `aitk context audit` | Report required sections, length, cited paths, reference form, catalog tables, provenance, superseded-decision narration, and index drift
|
|
48
|
-
| `aitk markdown audit` | Fail any markdown path on a banned character, word, or spelling, and report the structural checkpoints
|
|
49
|
-
| `aitk claude skills audit` | Report both skill corpora against the mechanical rules in `standards/skill.md`
|
|
50
|
-
| `aitk standards audit` | Report the corpus against the `## Success criterion` gate, failing only on a standard new to the branch (`--json`, `--arrivals-only`)
|
|
51
|
-
| `aitk claude skills drift` | Name the shipped skill bodies rewritten between a given ref and `HEAD`, and the installed version against the newest published (`--json`)
|
|
52
|
-
| `aitk claude skills reach` | Report the bodies in either skill corpus citing a toolkit path no target project receives, exiting 2 on an unqualified one
|
|
53
|
-
| `aitk claude skills rank` | Score either skill corpus's descriptions against a case corpus by TF-IDF cosine similarity, reporting rank-one and top-three (`--cases <path>`)
|
|
54
|
-
| `aitk claude routing` | Report per `CLAUDE.md` section how many bullets name a path and how many of those a path-scoped rule already covers (`--json`)
|
|
55
|
-
| `aitk gov test-order` | Report where an implementation reached history ahead of the test covering it (`--json`)
|
|
56
|
-
| `aitk gov superseded` | Report where the tree still asserts a value a changed convention no longer produces, keyed on the value (`--json`)
|
|
57
|
-
| `aitk gov restated` | Report every instruction the always-loaded file or a rule shares with the seed, a shipped skill body, or another rule, classed and with its anchors named (`--json`)
|
|
58
|
-
| `aitk secrets scan` | Report credential-shaped values in the tree the package ships, keyed on issued values rather than on words (`--json`)
|
|
59
|
-
| `aitk deps audit` | Report published advisories against the resolved dependency set, refusing rather than reporting clean when the index is unreachable (`--json`)
|
|
60
|
-
| `aitk labels audit` | Report the labels a changed set earns from the pull request label map and the paths no row reaches (`--json`)
|
|
61
|
-
| `aitk
|
|
62
|
-
| `aitk
|
|
63
|
-
| `aitk audits
|
|
64
|
-
| `aitk
|
|
65
|
-
| `aitk
|
|
66
|
-
| `aitk
|
|
67
|
-
| `aitk
|
|
12
|
+
| Command | Purpose |
|
|
13
|
+
| -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
14
|
+
| `aitk init [path]` | Bootstrap a project with selected toolkit domains |
|
|
15
|
+
| `aitk sync [path]` | Sync all installed domains in a target project |
|
|
16
|
+
| `aitk sync --check` | Report toolkit drift and the installed version against the newest published (`--json`, `--exit-code`) |
|
|
17
|
+
| `aitk sandbox [cat:cmd]` | Run sandbox scenarios (interactive or routed), toolkit-only like the tree it reads |
|
|
18
|
+
| `aitk sandbox reset` | Reset sandbox to baseline |
|
|
19
|
+
| `aitk sandbox clean` | Wipe the sandbox |
|
|
20
|
+
| `aitk sandbox check` | Score a provisioned sandbox against a scenario expectation (`--json` for the verdict) |
|
|
21
|
+
| `aitk sandbox coverage` | Report which scenarios declare expectations (`--json`, `--strict`, `--skills`) |
|
|
22
|
+
| `aitk indexes regen` | Regenerate `index.md` files from sibling frontmatter |
|
|
23
|
+
| `aitk docs [topic]` | Emit toolkit reference docs (`list`, or a topic by name) |
|
|
24
|
+
| `aitk design render` | Render `.claude/DESIGN.md` tokens to HTML and CSS |
|
|
25
|
+
| `aitk slides render` | Render a `.claude/SLIDES.md` source into a PowerPoint deck |
|
|
26
|
+
| `aitk slides list` | List the available slide layouts (`--json` for the catalog) |
|
|
27
|
+
| `aitk feedback` | Write toolkit feedback from stdin to `.claude/review/feedback/`, or open a GitHub issue with `--github` |
|
|
28
|
+
| `aitk transcripts <url>` | Fetch a YouTube transcript with metadata frontmatter (needs `yt-dlp`) |
|
|
29
|
+
| `aitk tasks archive` | Move a shipped task off the board, clear its ordering row, and regenerate the index |
|
|
30
|
+
| `aitk tasks pull-request` | Record a pull request number on the task a branch closes, by stem or `--plan` (`--json`) |
|
|
31
|
+
| `aitk tasks outcome` | Mark outcomes `[x]` on a task by position, repeating `--close` (`--json`) |
|
|
32
|
+
| `aitk tasks validate` | Report board rows whose plan, task file, group, file set, or blocker does not hold (`--json`) |
|
|
33
|
+
| `aitk intake list` | Report intake folder counts, or one folder's items, keeping what is unread with `--unread` (`--json`) |
|
|
34
|
+
| `aitk intake answer` | Write selections into one cluster's answer slots, repeating `--set <item>=<answer>` (`--json`) |
|
|
35
|
+
| `aitk teach list` | Report learning workspaces and the ordinal a new one takes, or what one workspace holds (`--json`) |
|
|
36
|
+
| `aitk teach open` | Open a workspace at the next ordinal with its mission, resources, and glossary files (`--json`) |
|
|
37
|
+
| `aitk teach resource` | Record sources and leads in a workspace, repeating `--read` or `--lead` as `<title>=<url>` (`--json`) |
|
|
38
|
+
| `aitk teach glossary` | Add terms to a workspace glossary alphabetically, repeating `--term <term>=<definition>` (`--json`) |
|
|
39
|
+
| `aitk records validate` | Report a session record or a standard against the standard governing it, per kind (`--json`) |
|
|
40
|
+
| `aitk records migrate` | Rewrite the records a validate finding names a recoverable transform for (`--write`, `--json`) |
|
|
41
|
+
| `aitk records size` | Report what each record folder holds and how much of it is recent, heaviest first (`--json`) |
|
|
42
|
+
| `aitk records push` | Commit the nine backed record folders and push them to a private records remote (`--json`) |
|
|
43
|
+
| `aitk records pull` | Fetch the records remote and write it back, refusing rather than discarding unpushed records (`--json`) |
|
|
44
|
+
| `aitk sessions list` | Resolve live sessions to the worktree and branch each holds, filtered by `--branch` (`--json`) |
|
|
45
|
+
| `aitk worktrees list` | Report which worktrees are reclaimable, keyed on the pull request having merged, with every refusal and the removal route named (`--json`) |
|
|
46
|
+
| `aitk comments scan` | Measure comment density by language and comment kind, with a trend recomputed from git |
|
|
47
|
+
| `aitk context audit` | Report required sections, length, cited paths, reference form, catalog tables, provenance, superseded-decision narration, and index drift |
|
|
48
|
+
| `aitk markdown audit` | Fail any markdown path on a banned character, word, or spelling, and report the structural checkpoints |
|
|
49
|
+
| `aitk claude skills audit` | Report both skill corpora against the mechanical rules in `standards/skill.md` |
|
|
50
|
+
| `aitk standards audit` | Report the corpus against the `## Success criterion` gate, failing only on a standard new to the branch (`--json`, `--arrivals-only`) |
|
|
51
|
+
| `aitk claude skills drift` | Name the shipped skill bodies rewritten between a given ref and `HEAD`, and the installed version against the newest published (`--json`) |
|
|
52
|
+
| `aitk claude skills reach` | Report the bodies in either skill corpus citing a toolkit path no target project receives, exiting 2 on an unqualified one |
|
|
53
|
+
| `aitk claude skills rank` | Score either skill corpus's descriptions against a case corpus by TF-IDF cosine similarity, reporting rank-one and top-three (`--cases <path>`) |
|
|
54
|
+
| `aitk claude routing` | Report per `CLAUDE.md` section how many bullets name a path and how many of those a path-scoped rule already covers (`--json`) |
|
|
55
|
+
| `aitk gov test-order` | Report where an implementation reached history ahead of the test covering it (`--json`) |
|
|
56
|
+
| `aitk gov superseded` | Report where the tree still asserts a value a changed convention no longer produces, keyed on the value and on the family stem behind a templated citation (`--json`) |
|
|
57
|
+
| `aitk gov restated` | Report every instruction the always-loaded file or a rule shares with the seed, a shipped skill body, or another rule, classed and with its anchors named (`--json`) |
|
|
58
|
+
| `aitk secrets scan` | Report credential-shaped values in the tree the package ships, keyed on issued values rather than on words (`--json`) |
|
|
59
|
+
| `aitk deps audit` | Report published advisories against the resolved dependency set, refusing rather than reporting clean when the index is unreachable (`--json`) |
|
|
60
|
+
| `aitk labels audit` | Report the labels a changed set earns from the pull request label map and the paths no row reaches (`--json`) |
|
|
61
|
+
| `aitk labels scan` | Fail a pull request whose title or body carries a phase label, sorting a release pull request's tokens as semver rather than as a leak (`--event`, `--json`) |
|
|
62
|
+
| `aitk census [path]` | Report tracked file count, a breakdown by extension, and a line total that skips whatever reads as binary (`--json`) |
|
|
63
|
+
| `aitk audits run` | Run every audit as one set, report per check under one verdict, and compare each count to the recorded baseline (`--json`, `--record`) |
|
|
64
|
+
| `aitk audits list` | List every audit the set runs, with the corpus each reads and whether it gates (`--json`) |
|
|
65
|
+
| `aitk inventory [subject]` | Walk every route a project declares and group its elements by the property each computes, as a listing rather than a gate (`--json`) |
|
|
66
|
+
| `aitk capture [source]` | Render HTML capture sources to PNG, toolkit-only and absent from an installed package |
|
|
67
|
+
| `aitk serve [dir]` | Serve a directory on the loopback interface and print the link that opens it, running until interrupted (`--port`, `--entry`, `--json`) |
|
|
68
|
+
| `aitk upgrade` | Reinstall the CLI globally with the package manager the install path names (`--json`) |
|
|
68
69
|
|
|
69
70
|
`aitk serve` ships and drives no browser, which is what separates it from the two that do. A generated page loses its stylesheet and 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. 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.
|
|
70
71
|
|
package/docs/agents/index.md
CHANGED
|
@@ -35,7 +35,7 @@ CLI catalog and invocation rules for agents, split by command domain. Start with
|
|
|
35
35
|
- [Citation reach](skills-reach.md): Reporting the skill bodies that cite a path no target project receives, which corpus the verb reads, the ownership key that decides what counts, the one-word qualifier that marks a citation as decided, and why the verb reports instead of gating
|
|
36
36
|
- [Standard success criteria](standards-audit.md): Reading the corpus against the Success criterion gate, why the check scopes to arrival rather than the whole corpus, and the exit codes it sets
|
|
37
37
|
- [State-scoped risk](state-scoped-risk.md): Reading committed state rather than an arriving change, the shipped-tree corpus the secret scan reads, what it keys on and how a deliberate value is exempted, the advisory check and its network failure mode, and why one gates while the other reports
|
|
38
|
-
- [Superseded values](superseded.md): Reading where the tree still asserts a value a changed convention no longer produces, why the sweep keys on the value rather than the file, the exemption marker, the blind
|
|
38
|
+
- [Superseded values](superseded.md): Reading where the tree still asserts a value a changed convention no longer produces, why the sweep keys on the value rather than the file, the family stem behind a templated citation, the exemption marker, the blind spots it cannot reach, and why it reports rather than gates
|
|
39
39
|
- [Targets](targets.md): The projects this toolkit installed into, the record the install writes against the sweep that backs it, what bounds each answer, and the cross-target pull request read
|
|
40
40
|
- [Tasks](tasks.md): Selecting a shipped task by stem or pull request, recording a number and closing an outcome, the refusal reasons, the board and backlog checks validate runs, and why the board root defaults to the main worktree
|
|
41
41
|
- [Teach](teach.md): Listing learning workspaces and the ordinal a new one takes, opening one with its required files, recording sources and glossary terms, resolving what the next lesson needs before it is written, the refusal reasons, and why every write here runs through a verb
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Superseded values
|
|
3
|
-
description: Reading where the tree still asserts a value a changed convention no longer produces, why the sweep keys on the value rather than the file, the exemption marker, the blind
|
|
3
|
+
description: Reading where the tree still asserts a value a changed convention no longer produces, why the sweep keys on the value rather than the file, the family stem behind a templated citation, the exemption marker, the blind spots it cannot reach, and why it reports rather than gates
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Superseded values
|
|
@@ -30,11 +30,31 @@ The slug transform is what proved it. Dropping the type segment left four fixtur
|
|
|
30
30
|
|
|
31
31
|
The whole corpus git lists: tracked files plus untracked ones git does not ignore. The untracked half keeps a file added on this branch in scope rather than one push later.
|
|
32
32
|
|
|
33
|
-
Matching is a literal substring,
|
|
33
|
+
Matching is a literal substring, for the value and for the family stem below it alike, so either one carrying regular-expression characters means what it says. Every occurrence on a line reports under its own column, since a line carrying the value twice is two edits, and a column both kinds reach reports once as the literal one.
|
|
34
34
|
|
|
35
35
|
Binary files are skipped on a NUL byte rather than on an extension list, and so is any listed path that will not open. Both are counted. A run states the files it opened against the files git listed, because a count of what passed reads as a verdict on the repository unless it also says how much it left out.
|
|
36
36
|
|
|
37
|
-
Pass an empty replacement to retire a value outright, as in `aitk gov superseded feature-feat- ""`. Findings report the same way
|
|
37
|
+
Pass an empty replacement to retire a value outright, as in `aitk gov superseded feature-feat- ""`. Findings report the same way, none is annotated, since a line cannot carry a replacement that does not exist, and no templated form is read, since there is no second value for a stem to diverge from.
|
|
38
|
+
|
|
39
|
+
## The templated citation
|
|
40
|
+
|
|
41
|
+
A citation naming a family as a pattern carries neither value. `aitk-*` and `internal-<X>` are the two forms this corpus writes, and a literal comparison matches neither, so a rename running the verb once per name reports clean while the family citations stay stale. Two of those on `#1250` had inverted, stating the old prefix as a guard, which left a rule instructing the next session to flag the correct naming as a mismatch.
|
|
42
|
+
|
|
43
|
+
Beside the literal comparison the run matches a family stem, derived from the segment the two values actually differ on, with everything they share carried ahead of it. `toolkit-operator` to `aitk-operator` derives `toolkit`, and `aitk-cli` to `aitk-shell` derives `aitk-cli` rather than `aitk`, which is what keeps a rename of one folder off every sibling in its family.
|
|
44
|
+
|
|
45
|
+
Three forms report, decided by the character after the stem and its separator:
|
|
46
|
+
|
|
47
|
+
| Form | Shape | What it names |
|
|
48
|
+
| ------------- | ------------ | ---------------------------------- |
|
|
49
|
+
| `glob` | `<stem>-*` | the family written as a pattern |
|
|
50
|
+
| `placeholder` | `<stem>-<X>` | the family with a stand-in segment |
|
|
51
|
+
| `prefix` | `<stem>-` | the family written bare |
|
|
52
|
+
|
|
53
|
+
The stem has to start a name. A letter, a digit, an underscore, or a separator directly before it means the stem sits mid-name, which is what a temp-directory prefix such as `aitk-check-toolkit-` looks like, and a path separator or a backtick before it means a citation, which is what `claude/skills/toolkit-*` looks like.
|
|
54
|
+
|
|
55
|
+
A hit under any of the three counts as a finding and moves the exit code. A stale templated citation is as real as a literal one, and reporting it outside the count would recreate the silence being fixed one report shape further along.
|
|
56
|
+
|
|
57
|
+
Every hit in a markdown file carries the nearest heading above it. A line reads differently under the section holding it, and `Use the aitk-* prefix on an internal skill` is a prohibition under `## Must not` and an instruction anywhere else. A reviewer reading that line without its heading made exactly that misreading against this tree.
|
|
38
58
|
|
|
39
59
|
## The exemption marker
|
|
40
60
|
|
|
@@ -42,15 +62,23 @@ A declaration disagreeing with a convention for a stated reason carries `aitk-al
|
|
|
42
62
|
|
|
43
63
|
Only a marker naming a reason counts. A bare token is a line that meant to say something and did not, and honoring it would let a typo mute a finding. This is the `aitk-allow-secret` shape, and both read the same placement rule through one helper.
|
|
44
64
|
|
|
45
|
-
## The blind
|
|
65
|
+
## The blind spots
|
|
66
|
+
|
|
67
|
+
The sweep sees the value, its stem, and nothing else. A prose reference that went stale without carrying either is invisible here, which is not hypothetical: alongside the mechanical instances, one declaration had gone stale by citing the wrong standard for the transform, and it matches no string this sweep could have been given.
|
|
68
|
+
|
|
69
|
+
The second is a family written in a form the three shapes do not read: a bracket style other than `<>`, a glob with no separator ahead of it, or the family described in words. Adding a form is a change to the classifier rather than a name a caller can pass, since a list of names cannot reach this class by construction, which is the whole finding behind the stem.
|
|
70
|
+
|
|
71
|
+
A value sweep closes most of the first class and no part of either one. The help text and the frame name both on every run, so a clean report is not read as a clean tree.
|
|
72
|
+
|
|
73
|
+
## The false-positive rate
|
|
46
74
|
|
|
47
|
-
|
|
75
|
+
Over-reporting is the design here as it is for the literal half, and the corpus bounds it. Sweeping `toolkit-operator` to `aitk-operator` over this tree named one templated hit outside the change's own fixtures, a passage recording the retired `toolkit-` prefix as history, which reads correctly in place.
|
|
48
76
|
|
|
49
|
-
A
|
|
77
|
+
A version reporting a page of hits teaches the next rename to skip the verb, which is worse than the blind spot being fixed. Read a report against that rate rather than against zero.
|
|
50
78
|
|
|
51
79
|
## Exit codes
|
|
52
80
|
|
|
53
|
-
Exit codes are `0` when nothing asserts the superseded value, `1` for a refusal, and `2` for at least one finding. It refuses an empty superseded value, which would match every line rather than a convention, a superseded value equal to its replacement, which means no convention changed, and a tree git cannot list, since an empty list passes each of its zero files. An empty replacement is not among them, because retiring a value is an ordinary reason to run this.
|
|
81
|
+
Exit codes are `0` when nothing asserts the superseded value or its family, `1` for a refusal, and `2` for at least one finding of either kind. It refuses an empty superseded value, which would match every line rather than a convention, a superseded value equal to its replacement, which means no convention changed, and a tree git cannot list, since an empty list passes each of its zero files. An empty replacement is not among them, because retiring a value is an ordinary reason to run this.
|
|
54
82
|
|
|
55
83
|
Nothing wires this into `bun run check` or into a hook. A value sweep over-reports by construction, and gating a measure carrying a known false-positive class is what teaches contributors to route around the stage. `aitk gov test-order` is the sibling precedent.
|
|
56
84
|
|
package/package.json
CHANGED
package/src/commands/gov.ts
CHANGED
|
@@ -306,18 +306,31 @@ export function register(program: Command): void {
|
|
|
306
306
|
`\`${SUPERSEDED_MARKER}: <reason>\` marker on its own line or the one above.`,
|
|
307
307
|
'',
|
|
308
308
|
'Pass an empty replacement to retire a value outright. Findings report',
|
|
309
|
-
'the same way
|
|
310
|
-
'replacement that does not exist
|
|
309
|
+
'the same way, none is annotated, since a line cannot carry a',
|
|
310
|
+
'replacement that does not exist, and no templated form is read, since',
|
|
311
|
+
'there is no second value for a stem to diverge from.',
|
|
311
312
|
'',
|
|
312
|
-
'
|
|
313
|
+
'Templated forms, matched on the segment the two values differ on:',
|
|
314
|
+
' glob <stem>-* , the family written as a pattern',
|
|
315
|
+
' placeholder <stem>-<X> , the family written with a stand-in segment',
|
|
316
|
+
' prefix <stem>- , the family written bare',
|
|
317
|
+
'',
|
|
318
|
+
'A hit under one of them counts as a finding, since a stale templated',
|
|
319
|
+
'citation is as real as a literal one. Each carries the nearest heading',
|
|
320
|
+
'above it in a markdown file, because a line reading as a prohibition',
|
|
321
|
+
'under one section reads as an instruction under another.',
|
|
322
|
+
'',
|
|
323
|
+
'Blind spots:',
|
|
313
324
|
' a prose reference that went stale without carrying the value, such as',
|
|
314
325
|
' a declaration citing the wrong standard for the transform, matches',
|
|
315
326
|
' nothing here and is reached by reading rather than by this sweep',
|
|
327
|
+
' a family named in any other form, such as a bracket style other than',
|
|
328
|
+
' <>, a trailing glob with no separator, or a description in words',
|
|
316
329
|
'',
|
|
317
330
|
'Exit codes:',
|
|
318
|
-
' 0 nothing in the corpus asserts the superseded value',
|
|
331
|
+
' 0 nothing in the corpus asserts the superseded value or its family',
|
|
319
332
|
' 1 refused, with the reason on stderr or in the JSON record',
|
|
320
|
-
' 2 at least one declaration
|
|
333
|
+
' 2 at least one declaration asserts either',
|
|
321
334
|
'',
|
|
322
335
|
'Examples:',
|
|
323
336
|
' aitk gov superseded feature-feat- feature-',
|
|
@@ -520,7 +533,37 @@ function describeHit(hit: SupersededHit): string {
|
|
|
520
533
|
const note = hit.carriesReplacement
|
|
521
534
|
? ' (the replacement is on this line)'
|
|
522
535
|
: ''
|
|
523
|
-
|
|
536
|
+
const section = hit.heading === undefined ? '' : ` under ${hit.heading}`
|
|
537
|
+
return `[${hit.match}] ${hit.file}:${hit.line}:${hit.column}${section}${note}: ${hit.preview}`
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Named separately from the literal hits rather than counted in with them. A
|
|
542
|
+
* templated hit is a wider match on a shorter string, so a reader weighing one
|
|
543
|
+
* is weighing a different question, and the section is where the forms it still
|
|
544
|
+
* cannot read are stated.
|
|
545
|
+
*/
|
|
546
|
+
function reportTemplated(
|
|
547
|
+
report: Extract<SupersededReport, { kind: 'measured' }>,
|
|
548
|
+
): void {
|
|
549
|
+
const templated = report.findings.filter((hit) => hit.match !== 'literal')
|
|
550
|
+
|
|
551
|
+
logStep('Templated')
|
|
552
|
+
if (report.stems === undefined) {
|
|
553
|
+
logInfo(
|
|
554
|
+
'no stem derives from these two values, so no templated citation was read',
|
|
555
|
+
)
|
|
556
|
+
} else if (templated.length === 0) {
|
|
557
|
+
logInfo(
|
|
558
|
+
`nothing names the family as ${report.stems.superseded}-*, ${report.stems.superseded}-<X>, or ${report.stems.superseded}- bare`,
|
|
559
|
+
)
|
|
560
|
+
} else {
|
|
561
|
+
for (const hit of templated) logWarn(describeHit(hit))
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
logInfo(
|
|
565
|
+
'not read: a family named in any other form, such as a bracket style other than <>, a trailing glob with no separator, or a description in words',
|
|
566
|
+
)
|
|
524
567
|
}
|
|
525
568
|
|
|
526
569
|
function reportSuperseded(
|
|
@@ -531,14 +574,26 @@ function reportSuperseded(
|
|
|
531
574
|
|
|
532
575
|
logStep('Sweep')
|
|
533
576
|
logInfo(`${report.superseded} → ${report.replacement} in ${root}`)
|
|
577
|
+
logInfo(
|
|
578
|
+
report.stems === undefined
|
|
579
|
+
? 'no family stem, so the sweep is the literal value alone'
|
|
580
|
+
: `family stem ${report.stems.superseded} → ${report.stems.replacement}`,
|
|
581
|
+
)
|
|
534
582
|
|
|
535
|
-
|
|
536
|
-
|
|
583
|
+
const literal = report.findings.filter((hit) => hit.match === 'literal')
|
|
584
|
+
|
|
585
|
+
// Named for the kind rather than for a verdict. `Clean` over the literal half
|
|
586
|
+
// alone would read as a clean tree on a run whose templated half is the one
|
|
587
|
+
// carrying every finding.
|
|
588
|
+
logStep('Literal')
|
|
589
|
+
if (literal.length === 0) {
|
|
537
590
|
logInfo('nothing in the corpus asserts the superseded value')
|
|
538
591
|
} else {
|
|
539
|
-
for (const finding of
|
|
592
|
+
for (const finding of literal) logWarn(describeHit(finding))
|
|
540
593
|
}
|
|
541
594
|
|
|
595
|
+
reportTemplated(report)
|
|
596
|
+
|
|
542
597
|
// Named rather than counted. A muted line is a judgment someone recorded,
|
|
543
598
|
// and a reader weighing this report has to be able to reach the reason.
|
|
544
599
|
logStep('Exempt')
|
package/src/commands/labels.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
1
2
|
import { resolve } from 'node:path'
|
|
2
3
|
import type { Command } from 'commander'
|
|
3
4
|
import { type LabelAuditRefusal, auditLabels } from '@/labels/audit'
|
|
4
5
|
import { MAP_REL } from '@/labels/map'
|
|
6
|
+
import { scanPhaseLabels } from '@/labels/phase'
|
|
5
7
|
import { intro, logInfo, logStep, logWarn, outro, plural } from '@/ui'
|
|
6
8
|
|
|
7
9
|
interface AuditOptions {
|
|
@@ -10,6 +12,14 @@ interface AuditOptions {
|
|
|
10
12
|
readonly json?: boolean
|
|
11
13
|
}
|
|
12
14
|
|
|
15
|
+
interface ScanOptions {
|
|
16
|
+
readonly event?: string
|
|
17
|
+
readonly title?: string
|
|
18
|
+
readonly body?: string
|
|
19
|
+
readonly head?: string
|
|
20
|
+
readonly json?: boolean
|
|
21
|
+
}
|
|
22
|
+
|
|
13
23
|
/** What a reader does about each way the audit produced no reading. */
|
|
14
24
|
const REFUSALS: Record<LabelAuditRefusal, string> = {
|
|
15
25
|
// An answer rather than a fault. A project declaring no map is labelled
|
|
@@ -76,6 +86,46 @@ export function register(program: Command): void {
|
|
|
76
86
|
.action(async (paths: string[], opts: AuditOptions) => {
|
|
77
87
|
process.exitCode = await runAudit(paths, opts)
|
|
78
88
|
})
|
|
89
|
+
|
|
90
|
+
labels
|
|
91
|
+
.command('scan')
|
|
92
|
+
.description(
|
|
93
|
+
'Fail a pull request whose title or body carries a phase label',
|
|
94
|
+
)
|
|
95
|
+
.helpOption('-h, --help', 'Show this help message')
|
|
96
|
+
.option(
|
|
97
|
+
'--event <path>',
|
|
98
|
+
'GitHub pull_request event payload to read, such as $GITHUB_EVENT_PATH',
|
|
99
|
+
)
|
|
100
|
+
.option('--title <text>', 'Title to scan, overriding the event payload')
|
|
101
|
+
.option('--body <text>', 'Body to scan, overriding the event payload')
|
|
102
|
+
.option('--head <ref>', 'Head branch, overriding the event payload')
|
|
103
|
+
.option('--json', 'Add a machine-readable record on stdout')
|
|
104
|
+
.addHelpText(
|
|
105
|
+
'after',
|
|
106
|
+
[
|
|
107
|
+
'',
|
|
108
|
+
'Reads aitk standards versioning for the two namespaces and sorts every',
|
|
109
|
+
'version-shaped token this pull request carries into the one the pull',
|
|
110
|
+
'request is allowed to hold. A release-please pull request, read off its',
|
|
111
|
+
'own fixed head branch and title, may carry semver references. Every',
|
|
112
|
+
'other pull request may carry neither, so any token found there is a',
|
|
113
|
+
'leaked phase label.',
|
|
114
|
+
'',
|
|
115
|
+
'Exit codes:',
|
|
116
|
+
' 0 no phase label found',
|
|
117
|
+
' 1 refused, with the reason on stderr or in the JSON record',
|
|
118
|
+
' 2 the title or body carries a phase label',
|
|
119
|
+
'',
|
|
120
|
+
'Examples:',
|
|
121
|
+
' aitk labels scan --event "$GITHUB_EVENT_PATH"',
|
|
122
|
+
' aitk labels scan --title "feat: x" --body "planned under v68.5" --head feat/x',
|
|
123
|
+
'',
|
|
124
|
+
].join('\n'),
|
|
125
|
+
)
|
|
126
|
+
.action(async (opts: ScanOptions) => {
|
|
127
|
+
process.exitCode = await runScan(opts)
|
|
128
|
+
})
|
|
79
129
|
}
|
|
80
130
|
|
|
81
131
|
async function runAudit(paths: string[], opts: AuditOptions): Promise<number> {
|
|
@@ -164,3 +214,148 @@ async function runAudit(paths: string[], opts: AuditOptions): Promise<number> {
|
|
|
164
214
|
|
|
165
215
|
return coverage.uncovered.length === 0 ? 0 : 2
|
|
166
216
|
}
|
|
217
|
+
|
|
218
|
+
/** Why `runScan` had no title and body to hand `scanPhaseLabels`. */
|
|
219
|
+
type ScanInputRefusal = 'no-input' | 'unreadable-event' | 'not-a-pull-request'
|
|
220
|
+
|
|
221
|
+
type ResolvedScanInput =
|
|
222
|
+
| {
|
|
223
|
+
readonly kind: 'resolved'
|
|
224
|
+
readonly title: string
|
|
225
|
+
readonly body: string
|
|
226
|
+
readonly headRefName: string
|
|
227
|
+
}
|
|
228
|
+
| {
|
|
229
|
+
readonly kind: 'refused'
|
|
230
|
+
readonly reason: ScanInputRefusal
|
|
231
|
+
readonly message: string
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Reads a title, a body, and a head branch from explicit flags first and the
|
|
236
|
+
* named event payload second, so a caller testing the wiring by hand never
|
|
237
|
+
* needs a real GitHub event file on disk.
|
|
238
|
+
*/
|
|
239
|
+
function resolveScanInput(opts: ScanOptions): ResolvedScanInput {
|
|
240
|
+
let title = opts.title
|
|
241
|
+
let body = opts.body
|
|
242
|
+
let headRefName = opts.head
|
|
243
|
+
|
|
244
|
+
if (opts.event !== undefined) {
|
|
245
|
+
let raw: string
|
|
246
|
+
try {
|
|
247
|
+
raw = readFileSync(opts.event, 'utf8')
|
|
248
|
+
} catch {
|
|
249
|
+
return {
|
|
250
|
+
kind: 'refused',
|
|
251
|
+
reason: 'unreadable-event',
|
|
252
|
+
message: `${opts.event} could not be read, so no payload was there to scan.`,
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
let payload: unknown
|
|
257
|
+
try {
|
|
258
|
+
payload = JSON.parse(raw)
|
|
259
|
+
} catch {
|
|
260
|
+
return {
|
|
261
|
+
kind: 'refused',
|
|
262
|
+
reason: 'unreadable-event',
|
|
263
|
+
message: `${opts.event} is not valid JSON, so no payload was there to scan.`,
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const pullRequest =
|
|
268
|
+
typeof payload === 'object' && payload !== null
|
|
269
|
+
? (payload as Record<string, unknown>).pull_request
|
|
270
|
+
: undefined
|
|
271
|
+
|
|
272
|
+
if (typeof pullRequest !== 'object' || pullRequest === null) {
|
|
273
|
+
return {
|
|
274
|
+
kind: 'refused',
|
|
275
|
+
reason: 'not-a-pull-request',
|
|
276
|
+
message: `${opts.event} carries no pull_request, so no title or body exists to scan.`,
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const record = pullRequest as Record<string, unknown>
|
|
281
|
+
const head = record.head
|
|
282
|
+
title ??= typeof record.title === 'string' ? record.title : undefined
|
|
283
|
+
body ??= typeof record.body === 'string' ? record.body : undefined
|
|
284
|
+
headRefName ??=
|
|
285
|
+
typeof head === 'object' &&
|
|
286
|
+
head !== null &&
|
|
287
|
+
typeof (head as Record<string, unknown>).ref === 'string'
|
|
288
|
+
? ((head as Record<string, unknown>).ref as string)
|
|
289
|
+
: undefined
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (title === undefined) {
|
|
293
|
+
return {
|
|
294
|
+
kind: 'refused',
|
|
295
|
+
reason: 'no-input',
|
|
296
|
+
message:
|
|
297
|
+
'No --event, --title, or --body given, so there is nothing to scan.',
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return {
|
|
302
|
+
kind: 'resolved',
|
|
303
|
+
title,
|
|
304
|
+
body: body ?? '',
|
|
305
|
+
headRefName: headRefName ?? '',
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
async function runScan(opts: ScanOptions): Promise<number> {
|
|
310
|
+
const emitJson = opts.json ?? false
|
|
311
|
+
|
|
312
|
+
intro('aitk labels scan')
|
|
313
|
+
|
|
314
|
+
const resolved = resolveScanInput(opts)
|
|
315
|
+
|
|
316
|
+
if (resolved.kind === 'refused') {
|
|
317
|
+
logStep('Refused')
|
|
318
|
+
logWarn(resolved.message)
|
|
319
|
+
outro()
|
|
320
|
+
|
|
321
|
+
if (emitJson) {
|
|
322
|
+
process.stdout.write(
|
|
323
|
+
`${JSON.stringify({ reason: resolved.reason, message: resolved.message })}\n`,
|
|
324
|
+
)
|
|
325
|
+
}
|
|
326
|
+
return 1
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const result = scanPhaseLabels(resolved)
|
|
330
|
+
|
|
331
|
+
logStep('Pull request')
|
|
332
|
+
logInfo(
|
|
333
|
+
result.cutsRelease
|
|
334
|
+
? 'reads as a release-please pull request'
|
|
335
|
+
: 'reads as an ordinary pull request',
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
logStep(result.phaseLabels.length === 0 ? 'Clean' : 'Phase label found')
|
|
339
|
+
if (result.phaseLabels.length === 0) {
|
|
340
|
+
logInfo('no phase label in the title or body')
|
|
341
|
+
} else {
|
|
342
|
+
logWarn(
|
|
343
|
+
`${plural(result.phaseLabels.length, 'phase label')} in the title or body. Describe the change itself and drop the internal label before publishing.`,
|
|
344
|
+
)
|
|
345
|
+
for (const label of result.phaseLabels) logWarn(label)
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
outro()
|
|
349
|
+
|
|
350
|
+
if (emitJson) {
|
|
351
|
+
process.stdout.write(
|
|
352
|
+
`${JSON.stringify({
|
|
353
|
+
cutsRelease: result.cutsRelease,
|
|
354
|
+
phaseLabels: result.phaseLabels,
|
|
355
|
+
semverTags: result.semverTags,
|
|
356
|
+
})}\n`,
|
|
357
|
+
)
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return result.phaseLabels.length === 0 ? 0 : 2
|
|
361
|
+
}
|
package/src/gov/superseded.ts
CHANGED
|
@@ -15,11 +15,33 @@ import { listRepositoryFiles } from '@/git-files'
|
|
|
15
15
|
*/
|
|
16
16
|
export const SUPERSEDED_MARKER = 'aitk-allow-superseded'
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* What matched at one column.
|
|
20
|
+
*
|
|
21
|
+
* `literal` is the superseded value itself. The other three are the stem
|
|
22
|
+
* followed by a glob, by an angle-bracket placeholder, or by nothing that
|
|
23
|
+
* continues a name, which are the three forms this corpus writes a family in.
|
|
24
|
+
* A literal comparison reaches none of them, which is why a rename running the
|
|
25
|
+
* verb once per name reported clean over seven stale citations.
|
|
26
|
+
*/
|
|
27
|
+
export type SupersededMatch = 'literal' | 'glob' | 'placeholder' | 'prefix'
|
|
28
|
+
|
|
18
29
|
export interface SupersededHit {
|
|
19
30
|
readonly file: string
|
|
20
31
|
/** One-based, matching the `file:line` form a reader clicks. */
|
|
21
32
|
readonly line: number
|
|
22
33
|
readonly column: number
|
|
34
|
+
readonly match: SupersededMatch
|
|
35
|
+
/**
|
|
36
|
+
* The nearest heading above the hit in a markdown file, absent elsewhere and
|
|
37
|
+
* above the first heading.
|
|
38
|
+
*
|
|
39
|
+
* A line reads differently under the section holding it. `Use the aitk-*
|
|
40
|
+
* prefix on an internal skill` is a prohibition under `## Must not` and an
|
|
41
|
+
* instruction anywhere else, and a reviewer reading the line alone made
|
|
42
|
+
* exactly that misreading against this tree.
|
|
43
|
+
*/
|
|
44
|
+
readonly heading: string | undefined
|
|
23
45
|
/**
|
|
24
46
|
* Whether the replacement sits on the same line, outside the superseded
|
|
25
47
|
* occurrences themselves.
|
|
@@ -45,11 +67,22 @@ export interface SupersededOptions {
|
|
|
45
67
|
readonly replacement: string
|
|
46
68
|
}
|
|
47
69
|
|
|
70
|
+
/**
|
|
71
|
+
* The segment pair a templated citation is matched on, reported so a run states
|
|
72
|
+
* the net it cast rather than only what the net caught.
|
|
73
|
+
*/
|
|
74
|
+
export interface SupersededStems {
|
|
75
|
+
readonly superseded: string
|
|
76
|
+
readonly replacement: string
|
|
77
|
+
}
|
|
78
|
+
|
|
48
79
|
export type SupersededReport =
|
|
49
80
|
| {
|
|
50
81
|
readonly kind: 'measured'
|
|
51
82
|
readonly superseded: string
|
|
52
83
|
readonly replacement: string
|
|
84
|
+
/** Absent when the two values yield no bounded stem to match on. */
|
|
85
|
+
readonly stems: SupersededStems | undefined
|
|
53
86
|
/** Everything git listed, so the report states its own bound. */
|
|
54
87
|
readonly listed: number
|
|
55
88
|
/** Files opened, which is what the verdict actually covers. */
|
|
@@ -68,22 +101,181 @@ export type SupersededReport =
|
|
|
68
101
|
*/
|
|
69
102
|
const PREVIEW_LIMIT = 200
|
|
70
103
|
|
|
104
|
+
/** The separator a family name is built from across this corpus. */
|
|
105
|
+
const SEGMENT_SEPARATOR = '-'
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* A character that continues a name, so `aitk-` inside `aitk-cli` is read as
|
|
109
|
+
* one name rather than as the family prefix written bare.
|
|
110
|
+
*/
|
|
111
|
+
const NAME_CHARACTER = /[A-Za-z0-9]/
|
|
112
|
+
|
|
71
113
|
/**
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
114
|
+
* A character that puts the stem mid-name when it sits directly before it, so
|
|
115
|
+
* a stem is only read where a name starts.
|
|
116
|
+
*
|
|
117
|
+
* The separator is here and not in `NAME_CHARACTER` because it decides one side
|
|
118
|
+
* only. `aitk-check-toolkit-` is a temp-directory prefix and matched the stem
|
|
119
|
+
* `toolkit` on four fixtures before this, where `claude/skills/toolkit-*` is a
|
|
120
|
+
* path and has to keep matching.
|
|
121
|
+
*/
|
|
122
|
+
const SEGMENT_CONTINUATION = /[A-Za-z0-9_-]/
|
|
123
|
+
|
|
124
|
+
const HEADING = /^#{1,6}\s+\S/
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* A fence opening or closing a code block, tracked so a shell comment inside
|
|
128
|
+
* one is not read as the section a hit below it sits under. A `# Install` line
|
|
129
|
+
* in a bash block is the ordinary shape of that.
|
|
130
|
+
*/
|
|
131
|
+
const FENCE = /^\s*(?:```|~~~)/
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* The segment the two values differ on, carried with everything they share
|
|
135
|
+
* ahead of it.
|
|
136
|
+
*
|
|
137
|
+
* The shared prefix alone is what this exists against. `aitk-cli` and
|
|
138
|
+
* `aitk-feedback-file` share `aitk`, so a stem cut there matches every sibling
|
|
139
|
+
* and reports the whole family on a rename of one folder. Including the
|
|
140
|
+
* differing segment bounds the net to what actually changed, which leaves
|
|
141
|
+
* `aitk-cli` to `aitk-shell` matching neither sibling and `toolkit-operator` to
|
|
142
|
+
* `aitk-operator` matching the family prefix that did move.
|
|
143
|
+
*
|
|
144
|
+
* An empty replacement yields nothing. Retiring a value outright leaves no
|
|
145
|
+
* second value to diverge from, so every stem would run to the first segment
|
|
146
|
+
* and match the family the retirement never touched.
|
|
147
|
+
*/
|
|
148
|
+
export function deriveStems(
|
|
149
|
+
options: SupersededOptions,
|
|
150
|
+
): SupersededStems | undefined {
|
|
151
|
+
if (options.replacement === '') return undefined
|
|
152
|
+
|
|
153
|
+
const supersededSegments = options.superseded.split(SEGMENT_SEPARATOR)
|
|
154
|
+
const replacementSegments = options.replacement.split(SEGMENT_SEPARATOR)
|
|
155
|
+
|
|
156
|
+
let index = 0
|
|
157
|
+
while (
|
|
158
|
+
index < supersededSegments.length &&
|
|
159
|
+
index < replacementSegments.length &&
|
|
160
|
+
supersededSegments[index] === replacementSegments[index]
|
|
161
|
+
) {
|
|
162
|
+
index += 1
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const superseded = trimSeparators(
|
|
166
|
+
supersededSegments.slice(0, index + 1).join(SEGMENT_SEPARATOR),
|
|
167
|
+
)
|
|
168
|
+
const replacement = trimSeparators(
|
|
169
|
+
replacementSegments.slice(0, index + 1).join(SEGMENT_SEPARATOR),
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
if (superseded === '' || replacement === '') return undefined
|
|
173
|
+
if (superseded === replacement) return undefined
|
|
174
|
+
|
|
175
|
+
return { superseded, replacement }
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function trimSeparators(value: string): string {
|
|
179
|
+
let end = value.length
|
|
180
|
+
while (end > 0 && value[end - 1] === SEGMENT_SEPARATOR) end -= 1
|
|
181
|
+
return value.slice(0, end)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Which templated form sits at `column`, or nothing when the stem there
|
|
186
|
+
* continues into an ordinary name.
|
|
187
|
+
*
|
|
188
|
+
* The character after `<stem>-` decides all three, so one scan reads every
|
|
189
|
+
* form. A name character means a sibling spelled out, which the literal
|
|
190
|
+
* comparison already answers for or correctly ignores.
|
|
191
|
+
*/
|
|
192
|
+
function classifyStem(
|
|
193
|
+
line: string,
|
|
194
|
+
column: number,
|
|
195
|
+
stem: string,
|
|
196
|
+
): SupersededMatch | undefined {
|
|
197
|
+
const before = column === 0 ? '' : line[column - 1]
|
|
198
|
+
if (before !== '' && SEGMENT_CONTINUATION.test(before)) return undefined
|
|
199
|
+
|
|
200
|
+
const after = line[column + stem.length + 1] ?? ''
|
|
201
|
+
if (after === '*') return 'glob'
|
|
202
|
+
if (after === '<') return 'placeholder'
|
|
203
|
+
if (after !== '' && NAME_CHARACTER.test(after)) return undefined
|
|
204
|
+
return 'prefix'
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Whether the value replacing whatever matched appears on the line somewhere
|
|
209
|
+
* other than inside the matched occurrences, which is what makes the flag mean
|
|
210
|
+
* anything when one value contains the other.
|
|
75
211
|
*
|
|
76
212
|
* An empty replacement carries nothing, so it answers false rather than the
|
|
77
213
|
* true every line returns from a containment test against the empty string.
|
|
78
214
|
* Retiring a value outright is what passes one, and reporting every finding as
|
|
79
215
|
* carrying its replacement there says the opposite of what happened.
|
|
216
|
+
*
|
|
217
|
+
* A templated hit reads the stem pair instead, since the line repairing
|
|
218
|
+
* `toolkit-*` carries `aitk-*` and never the full name either value spells.
|
|
80
219
|
*/
|
|
81
|
-
function carriesReplacement(
|
|
82
|
-
|
|
83
|
-
|
|
220
|
+
function carriesReplacement(
|
|
221
|
+
line: string,
|
|
222
|
+
options: SupersededOptions,
|
|
223
|
+
stems: SupersededStems | undefined,
|
|
224
|
+
match: SupersededMatch,
|
|
225
|
+
): boolean {
|
|
226
|
+
if (match === 'literal') {
|
|
227
|
+
if (options.replacement === '') return false
|
|
228
|
+
return line.split(options.superseded).join('').includes(options.replacement)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (stems === undefined) return false
|
|
232
|
+
const matched = `${stems.superseded}${SEGMENT_SEPARATOR}`
|
|
233
|
+
return line
|
|
234
|
+
.split(matched)
|
|
235
|
+
.join('')
|
|
236
|
+
.includes(`${stems.replacement}${SEGMENT_SEPARATOR}`)
|
|
84
237
|
}
|
|
85
238
|
|
|
86
|
-
/**
|
|
239
|
+
/**
|
|
240
|
+
* Every column in one line carrying the superseded value or its family stem,
|
|
241
|
+
* ordered left to right.
|
|
242
|
+
*
|
|
243
|
+
* A stem occurrence sharing a column with a literal one is dropped. The stem is
|
|
244
|
+
* a prefix of the value it derives from whenever only the last segment moved,
|
|
245
|
+
* so the same text would otherwise report twice under two kinds.
|
|
246
|
+
*/
|
|
247
|
+
function matchLine(
|
|
248
|
+
line: string,
|
|
249
|
+
options: SupersededOptions,
|
|
250
|
+
stems: SupersededStems | undefined,
|
|
251
|
+
): { column: number; match: SupersededMatch }[] {
|
|
252
|
+
const matches: { column: number; match: SupersededMatch }[] = []
|
|
253
|
+
|
|
254
|
+
let column = line.indexOf(options.superseded)
|
|
255
|
+
while (column !== -1) {
|
|
256
|
+
matches.push({ column, match: 'literal' })
|
|
257
|
+
column = line.indexOf(options.superseded, column + 1)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (stems !== undefined) {
|
|
261
|
+
const pattern = `${stems.superseded}${SEGMENT_SEPARATOR}`
|
|
262
|
+
let at = line.indexOf(pattern)
|
|
263
|
+
while (at !== -1) {
|
|
264
|
+
const form = classifyStem(line, at, stems.superseded)
|
|
265
|
+
if (form !== undefined && !matches.some((hit) => hit.column === at)) {
|
|
266
|
+
matches.push({ column: at, match: form })
|
|
267
|
+
}
|
|
268
|
+
at = line.indexOf(pattern, at + 1)
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return matches.sort((first, second) => first.column - second.column)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Every occurrence of `superseded` or of its family stem in one file's text,
|
|
277
|
+
* exemptions separated.
|
|
278
|
+
*/
|
|
87
279
|
export function sweepText(
|
|
88
280
|
file: string,
|
|
89
281
|
text: string,
|
|
@@ -92,10 +284,19 @@ export function sweepText(
|
|
|
92
284
|
const lines = text.split('\n')
|
|
93
285
|
const findings: SupersededHit[] = []
|
|
94
286
|
const exempt: SupersededHit[] = []
|
|
287
|
+
const stems = deriveStems(options)
|
|
288
|
+
const sectioned = file.endsWith('.md')
|
|
289
|
+
let heading: string | undefined
|
|
290
|
+
let fenced = false
|
|
95
291
|
|
|
96
292
|
for (const [index, line] of lines.entries()) {
|
|
97
|
-
|
|
98
|
-
|
|
293
|
+
if (sectioned) {
|
|
294
|
+
if (FENCE.test(line)) fenced = !fenced
|
|
295
|
+
else if (!fenced && HEADING.test(line)) heading = line.trim()
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const matches = matchLine(line, options, stems)
|
|
299
|
+
if (matches.length === 0) continue
|
|
99
300
|
|
|
100
301
|
const muted = isMarked(lines, index, SUPERSEDED_MARKER)
|
|
101
302
|
const bucket = muted ? exempt : findings
|
|
@@ -104,17 +305,17 @@ export function sweepText(
|
|
|
104
305
|
trimmed.length > PREVIEW_LIMIT
|
|
105
306
|
? `${trimmed.slice(0, PREVIEW_LIMIT)}…`
|
|
106
307
|
: trimmed
|
|
107
|
-
const alsoReplacement = carriesReplacement(line, options)
|
|
108
308
|
|
|
109
|
-
|
|
309
|
+
for (const { column, match } of matches) {
|
|
110
310
|
bucket.push({
|
|
111
311
|
file,
|
|
112
312
|
line: index + 1,
|
|
113
313
|
column: column + 1,
|
|
114
|
-
|
|
314
|
+
match,
|
|
315
|
+
heading,
|
|
316
|
+
carriesReplacement: carriesReplacement(line, options, stems, match),
|
|
115
317
|
preview,
|
|
116
318
|
})
|
|
117
|
-
column = line.indexOf(options.superseded, column + 1)
|
|
118
319
|
}
|
|
119
320
|
}
|
|
120
321
|
|
|
@@ -135,9 +336,15 @@ export function sweepText(
|
|
|
135
336
|
* what was listed, opened, and skipped are what keep it from reading as a
|
|
136
337
|
* verdict over the whole tree.
|
|
137
338
|
*
|
|
339
|
+
* Beside the literal comparison it matches the family stem the two values
|
|
340
|
+
* differ on, so a citation writing the family as a pattern enters the report.
|
|
341
|
+
* The trade is false positives, bounded at one across a sixteen-folder rename,
|
|
342
|
+
* which is why this reports rather than gates.
|
|
343
|
+
*
|
|
138
344
|
* What it cannot see is a prose reference that went stale without carrying the
|
|
139
|
-
* value, such as a declaration citing the wrong standard for the transform
|
|
140
|
-
*
|
|
345
|
+
* value, such as a declaration citing the wrong standard for the transform, and
|
|
346
|
+
* a family written in a form neither the value nor the three stem shapes reach.
|
|
347
|
+
* A value sweep closes most of this class and no part of either one.
|
|
141
348
|
*/
|
|
142
349
|
export async function readSuperseded(
|
|
143
350
|
root: string,
|
|
@@ -198,6 +405,7 @@ export async function readSuperseded(
|
|
|
198
405
|
kind: 'measured',
|
|
199
406
|
superseded: options.superseded,
|
|
200
407
|
replacement: options.replacement,
|
|
408
|
+
stems: deriveStems(options),
|
|
201
409
|
listed: listed.length,
|
|
202
410
|
files,
|
|
203
411
|
skipped,
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { linesOutsideFences, maskCodeSpans } from '@/markdown/scan'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The two version namespaces `standards/versioning.md` keeps apart, and why a
|
|
5
|
+
* pull request cannot be sorted between them by pattern alone.
|
|
6
|
+
*
|
|
7
|
+
* A phase label (`v68.5`) and a semver reference (`v3.44`) both match
|
|
8
|
+
* `v<digits>(.<digits>){1,2}`, so a shape-only scan cannot tell them apart. The
|
|
9
|
+
* first measurement of this defect tried exactly that and returned 28 of 60
|
|
10
|
+
* correct. What separates the two is not the token but the pull request: a
|
|
11
|
+
* release-please pull request legitimately carries version numbers in its
|
|
12
|
+
* generated body, and every other pull request does not.
|
|
13
|
+
*/
|
|
14
|
+
export interface PhaseScanInput {
|
|
15
|
+
readonly title: string
|
|
16
|
+
readonly body: string
|
|
17
|
+
readonly headRefName: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface PhaseScanResult {
|
|
21
|
+
/** Whether this pull request is release-please's own, per its fixed shape. */
|
|
22
|
+
readonly cutsRelease: boolean
|
|
23
|
+
readonly phaseLabels: readonly string[]
|
|
24
|
+
readonly semverTags: readonly string[]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const VERSION_TOKEN = /\bv\d+(?:\.\d+){1,2}\b/g
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The head branch release-please opens every release pull request under.
|
|
31
|
+
*
|
|
32
|
+
* `release-please-config.json` names the package `aitk`, so the observed
|
|
33
|
+
* branch is `release-please--branches--main--components--aitk`. The prefix
|
|
34
|
+
* stops short of the component segment, since that segment moves with the
|
|
35
|
+
* package name and the branch segment does not.
|
|
36
|
+
*/
|
|
37
|
+
const RELEASE_HEAD_PREFIX = 'release-please--branches--main'
|
|
38
|
+
|
|
39
|
+
const RELEASE_TITLE_PREFIX = 'chore(main): release '
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Two fixed signals rather than one, because either alone is a string an
|
|
43
|
+
* author's own pull request could reproduce. A title starting with the
|
|
44
|
+
* release commit type is plain conventional-commit text, and a head branch is
|
|
45
|
+
* exactly what a PR renamed for review would want to imitate to slip a real
|
|
46
|
+
* phase label past this check. Together they name release-please's own act of
|
|
47
|
+
* opening the pull request, which nothing else on the remote can perform.
|
|
48
|
+
*/
|
|
49
|
+
function isReleasePullRequest(input: PhaseScanInput): boolean {
|
|
50
|
+
return (
|
|
51
|
+
input.headRefName.startsWith(RELEASE_HEAD_PREFIX) &&
|
|
52
|
+
input.title.startsWith(RELEASE_TITLE_PREFIX)
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Drops a fenced block outright and blanks a code span inside what remains,
|
|
58
|
+
* so a token quoted rather than written is read the way a reader reads it:
|
|
59
|
+
* shown, not asserted.
|
|
60
|
+
*
|
|
61
|
+
* `aitk markdown audit` excludes a fenced block and a code span from its own
|
|
62
|
+
* ban scan over the same kind of text, and this reuses that reading rather
|
|
63
|
+
* than inventing a second one. `#1208` is the corpus case that forced it: a
|
|
64
|
+
* backticked span quoting a test fixture's own version-shaped name, which the
|
|
65
|
+
* shape-only scan below cannot tell from a leak on its own.
|
|
66
|
+
*
|
|
67
|
+
* A link destination stays unmasked, unlike the ban scan's own reading. A
|
|
68
|
+
* release-please body's real semver reference sits inside the generated
|
|
69
|
+
* compare link's URL, and masking it would empty `semverTags` on the one
|
|
70
|
+
* pull request this check exists to pass, trading the corpus's one code-span
|
|
71
|
+
* leak for a hole in every release.
|
|
72
|
+
*/
|
|
73
|
+
function readable(text: string): string {
|
|
74
|
+
return linesOutsideFences(text).map(maskCodeSpans).join('\n')
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Reads a title and a body for version-shaped tokens and sorts every one
|
|
79
|
+
* found into the namespace this pull request is allowed to carry.
|
|
80
|
+
*
|
|
81
|
+
* The split runs on the pull request rather than on each token. A release
|
|
82
|
+
* pull request's tokens are read as the semver references its generated body
|
|
83
|
+
* legitimately carries, and every other pull request's tokens are read as
|
|
84
|
+
* leaked phase labels, which is what `standards/versioning.md` names the
|
|
85
|
+
* defect this exists to catch.
|
|
86
|
+
*/
|
|
87
|
+
export function scanPhaseLabels(input: PhaseScanInput): PhaseScanResult {
|
|
88
|
+
const text = readable(`${input.title}\n${input.body}`)
|
|
89
|
+
const tokens = [...new Set(text.match(VERSION_TOKEN) ?? [])]
|
|
90
|
+
const cutsRelease = isReleasePullRequest(input)
|
|
91
|
+
|
|
92
|
+
return cutsRelease
|
|
93
|
+
? { cutsRelease, phaseLabels: [], semverTags: tokens }
|
|
94
|
+
: { cutsRelease, phaseLabels: tokens, semverTags: [] }
|
|
95
|
+
}
|
package/src/markdown/scan.ts
CHANGED
|
@@ -150,6 +150,18 @@ function blank(match: string): string {
|
|
|
150
150
|
return ' '.repeat(match.length)
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
+
/**
|
|
154
|
+
* Blanks an inline code span alone, holding its width.
|
|
155
|
+
*
|
|
156
|
+
* Split from `maskDisplayed` for a reader who wants the narrower exclusion.
|
|
157
|
+
* A link destination often carries the one thing such a reader is after, such
|
|
158
|
+
* as a version-shaped token inside a generated compare link, where a code
|
|
159
|
+
* span is uniformly a quotation and never the claim itself.
|
|
160
|
+
*/
|
|
161
|
+
export function maskCodeSpans(text: string): string {
|
|
162
|
+
return text.replace(CODE_SPAN, blank)
|
|
163
|
+
}
|
|
164
|
+
|
|
153
165
|
/**
|
|
154
166
|
* Replaces displayed spans with spaces of equal width.
|
|
155
167
|
*
|
|
@@ -158,8 +170,7 @@ function blank(match: string): string {
|
|
|
158
170
|
* everything masked ahead of it on the line.
|
|
159
171
|
*/
|
|
160
172
|
export function maskDisplayed(text: string): string {
|
|
161
|
-
return text
|
|
162
|
-
.replace(CODE_SPAN, blank)
|
|
173
|
+
return maskCodeSpans(text)
|
|
163
174
|
.replace(LINK_DESTINATION, blank)
|
|
164
175
|
.replace(AUTOLINK, blank)
|
|
165
176
|
}
|