New:Socket for Asana Is Now Available.Learn more
Get Started

@erclx/aitk

Package Overview
Dependencies
Maintainers
1
Versions
208
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@erclx/aitk - npm Package Compare versions

Comparing version
3.0.0
to
3.1.0
+57
docs/agents/skills-reach.md
---
title: Citation reach
description: Reporting the shipped skill bodies that cite a path no target project receives, 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
---
# Citation reach
`aitk claude skills reach [path]` reports every shipped skill body citing a path that exists in the toolkit and reaches no target project. It reads and reports. Repairing what it finds is separate work.
```bash
aitk claude skills reach
aitk claude skills reach --json
```
| Option | Behavior |
| -------- | ---------------------------------------------------------- |
| `--json` | Add a machine-readable record on stdout, keeping the frame |
## The defect it reads for
A plugin skill installs into a project and the toolkit's own tree is not there. A body naming `.claude/context/transcripts.md` resolves for a session running in this repository and sends every other reader to nothing, and no stage asked the question until this one. The shape is wider than one folder: a seed naming a standard with no route and a machine-readable field naming a toolkit-only path are the same claim, true here and false in a target.
## What counts as a citation
A backticked token carrying a separator and an extension, which is how every body spells a path it means a reader to open. Three forms are skipped by construction.
- A placeholder such as `.claude/context/<domain>.md`, which names a shape rather than a file
- A path resolved through `${CLAUDE_SKILL_DIR}`, which is self-contained wherever the plugin loads
- A sibling named relatively, such as `references/labels.md`, which matches no authoring root and travels with the body
The same sibling named from the repository root as `claude/skills/<name>/references/<file>.md` is reported rather than skipped. The plugin loads from a cache rather than from the project tree, so that spelling resolves for nobody and the report is correct.
A path the toolkit does not hold is dropped rather than reported. The measure asks whether a claim true here is false in a target, and a path true in neither is a different defect that `aitk context audit` already reports against its own corpus.
## The ownership key
A cited path counts when it sits under an authoring root no install channel delivers. Standards install nowhere and are read through the plugin corpus, snippets land under `.claude/snippets/`, governance rules under `.claude/rules/`, and the rest is this repository's own source, catalogs, and contract pages.
`src/`, `scripts/`, and bare `docs/` are deliberately outside the list. A body naming one of those is describing the reader's own tree, so listing them reports a correct citation on every run and buries the finding under the pass.
A path a seed installs is disowned twice, under its own name and under the folder spelling it takes once a project splits the entry. A domain that outgrows one file becomes `<domain>/`, which is still the entry the seed delivered, so reporting the split form would fail a project for growing.
## The qualifier
A correct citation and a defective one are the same string, and the sentence around it is the difference. A citation counts as decided when its line names the toolkit as the owner, matching the bodies that already spell it that way. The repair for a finding is to say whose copy the path is, never to delete the citation, since the paths name real documents a reader wants.
```markdown
Read `.claude/context/indexes.md` from the toolkit if context on the system is needed.
```
A line mentioning the toolkit for an unrelated reason exempts a citation on it. That is the accepted cost of a word over a notation every future body would have to learn.
## Exit codes
Exit codes are `0` when every citation names its owner, `1` for a refusal, and `2` when at least one is unqualified. The refusal is a tree carrying no `claude/skills/`, which ships no plugin body to measure, and it reports the reason rather than a clean count over nothing.
The verb reports rather than gates. A toolkit-scoped instruction is sometimes meant for a session in this repository, so failing a push on one would make the check something to route around. `aitk audits run` registers it with no gating exit for the same reason, and carries `unqualifiedCitations` as its retained count.
import { existsSync, readFileSync } from 'node:fs'
import { join } from 'node:path'
/**
* The tree that installs into a target. The internal skills under `.claude/`
* never leave this repository, so a citation there is read by a session that
* already has the file and cannot be a reach defect.
*/
const SHIPPED_SKILLS = join('claude', 'skills')
/**
* The authoring roots this repository owns and no install channel delivers.
*
* Every entry is a folder a target never holds under that spelling. Standards
* install nowhere and are reached through the plugin corpus, rules install
* under `.claude/rules/`, snippets under `.claude/snippets/`, and the rest are
* this repository's own source, docs, and catalogs.
*
* `src/`, `scripts/`, and bare `docs/` are deliberately absent. A body naming
* one of those is describing the reader's own tree, so listing them would
* report a correct citation on every run and bury the defect this measures.
* `docs/agents/` is the exception, being the CLI contract pages that exist
* here alone.
*/
const AUTHORING_ROOTS = [
'.claude/context/',
'claude/',
'docs/agents/',
'governance/',
'internal/',
'snippets/',
'standards/',
'tooling/',
'wiki/',
] as const
/**
* What marks a citation as deliberately naming this repository's own copy.
*
* The word rather than a notation, matching the three bodies that already
* spell it and the repair the plan settled on. A parser-visible syntax was the
* alternative and it invents a spelling for a handful of lines while leaving
* the shipped precedent unreadable.
*/
const QUALIFIER = /toolkit/i
/** A backticked token, which is how every body spells a path it cites. */
const TOKEN = /`([^`\s]+)`/g
/**
* A path a reader could open, which is the only kind worth measuring.
*
* Requires an extension and a separator, and admits no `<`, `$`, or `*`. A
* body writes `.claude/context/<domain>.md` to name a shape rather than a
* file, and `${CLAUDE_SKILL_DIR}/../../standards/markdown.md` to resolve
* against the plugin root, which is self-contained by construction.
*/
const CONCRETE = /^[.A-Za-z0-9_][A-Za-z0-9._/-]*\.[a-z]{1,4}$/
export interface Citation {
readonly file: string
/** One-based, matching the `file:line` form a reader clicks. */
readonly line: number
readonly path: string
readonly qualified: boolean
}
/** Why a scan produced no corpus, which is never the same as a clean one. */
export type ReachRefusal = 'no-skills'
export type ReachReport =
| {
readonly kind: 'measured'
/** Files opened, so a report can state what the verdict covers. */
readonly bodies: number
readonly qualified: readonly Citation[]
readonly unqualified: readonly Citation[]
}
| { readonly kind: 'refused'; readonly reason: ReachRefusal }
export function isQualified(line: string): boolean {
return QUALIFIER.test(line)
}
/**
* Every path a seed lands on in a target, spelled the way a body would cite it.
*
* Read off the seed tree rather than listed, so a seed added to any stack
* clears its own citations without this module being edited. Dotfiles are in
* scope because the whole seeded context corpus sits under `.claude/`.
*/
export function readReceivedPaths(root: string): Set<string> {
const toolingRoot = join(root, 'tooling')
if (!existsSync(toolingRoot)) return new Set()
const received = new Set<string>()
for (const path of new Bun.Glob('*/seeds/**/*').scanSync({
cwd: toolingRoot,
onlyFiles: true,
dot: true,
})) {
const posix = path.replaceAll('\\', '/')
received.add(posix.replace(/^[^/]+\/seeds\//, ''))
}
return received
}
/**
* Whether a cited path is this repository's own rather than the reader's.
*
* A seeded path is disowned twice over: under its own name, and under the
* folder spelling it takes once a target splits the entry. A domain that
* outgrows one file becomes `<domain>/`, which is still the entry the seed
* delivered, so reporting the split form would fail a target for growing.
*/
export function isToolkitOwned(path: string, received: Set<string>): boolean {
if (received.has(path)) return false
for (const seeded of received) {
const stem = seeded.replace(/\.md$/, '')
if (stem !== seeded && path.startsWith(`${stem}/`)) return false
}
return AUTHORING_ROOTS.some((prefix) => path.startsWith(prefix))
}
/**
* Every toolkit-owned path one shipped file cites, with the line's verdict.
*
* Existence is not checked here. A body may name a path this repository once
* held, and separating the shape test from the disk read is what lets the
* shape be tested without a tree on disk.
*/
export function citationsIn(
file: string,
text: string,
received: Set<string>,
): Citation[] {
const citations: Citation[] = []
for (const [index, line] of text.split('\n').entries()) {
const qualified = isQualified(line)
for (const match of line.matchAll(TOKEN)) {
const path = match[1]
if (!CONCRETE.test(path) || !path.includes('/')) continue
if (!isToolkitOwned(path, received)) continue
citations.push({ file, line: index + 1, path, qualified })
}
}
return citations
}
/**
* Reads every shipped body for a path its reader cannot open.
*
* A citation of a path this repository does not hold is dropped rather than
* reported. The measure asks whether a claim true here is false in a target,
* and a path true in neither is a different defect that `aitk context audit`
* already reports against its own corpus.
*/
export function scanReach(root: string): ReachReport {
const skillsRoot = join(root, SHIPPED_SKILLS)
if (!existsSync(skillsRoot)) return { kind: 'refused', reason: 'no-skills' }
const received = readReceivedPaths(root)
const files = [
...new Bun.Glob('**/*.md').scanSync({ cwd: skillsRoot, onlyFiles: true }),
].sort()
const qualified: Citation[] = []
const unqualified: Citation[] = []
for (const file of files) {
const posix = file.replaceAll('\\', '/')
const text = readFileSync(join(skillsRoot, file), 'utf8')
for (const citation of citationsIn(
`${SHIPPED_SKILLS.replaceAll('\\', '/')}/${posix}`,
text,
received,
)) {
if (!existsSync(join(root, citation.path))) continue
if (citation.qualified) qualified.push(citation)
else unqualified.push(citation)
}
}
return { kind: 'measured', bodies: files.length, qualified, unqualified }
}
+1
-1
{
"name": "aitk",
"description": "Automated governance, versioning, and discovery tools for Claude Code.",
"version": "3.0.0",
"version": "3.1.0",
"author": {

@@ -6,0 +6,0 @@ "name": "Eric Le",

@@ -38,3 +38,3 @@ ---

On the source path, also read the UI surfaces matched in Step 1 plus `docs/agents/output-shape.md` and `docs/index.md` for output shape or framing rules already documented.
On the source path, also read the UI surfaces matched in Step 1 plus any `docs/agents/output-shape.md` and `docs/index.md` the project itself carries, for output shape or framing rules already documented. Those two are the toolkit's own spelling, so a project keeping its framing rules elsewhere is read there instead.

@@ -41,0 +41,0 @@ On the greenfield path, also read `.claude/ARCHITECTURE.md` for platform, tech stack, and surface type. Do not scan `src/`, stylesheets, or UI modules. Step 1 already established they hold nothing.

@@ -46,4 +46,4 @@ ---

- Promoting an answered item onto the board, which is `claude-tasks` and runs after the answers land
- The item format, the answer contract, and retrieval, which `standards/intake.md` owns and this skill cites
- The item format, the answer contract, and retrieval, which the toolkit's `standards/intake.md` owns and this skill cites
- The comparable answer slots in groundwork and feature plans, which carry their own contracts and are a separate measurement
- Deciding when to fire. The skill is user-invoked through `disable-model-invocation`, so answering is the operator's call rather than a description match.

@@ -18,3 +18,3 @@ ---

`CLAUDE.md` states both branches of the rule, sending an ordinary judgment call to a pick with the tradeoff in one sentence and a preference-deciding call to the operator. The first branch has `snippets/decision-help.md` behind it and the second had no surface at all.
`CLAUDE.md` states both branches of the rule, sending an ordinary judgment call to a pick with the tradeoff in one sentence and a preference-deciding call to the operator. The first branch has the toolkit's `snippets/decision-help.md` behind it, which installs into a project as `.claude/snippets/decision-help.md`, and the second had no surface at all.

@@ -44,4 +44,4 @@ ## Must

- Making the ordinary judgment call, which is a pick plus a one-sentence tradeoff and needs no surface
- The chat-side pick with no repository behind it, which `snippets/decision-help.md` covers and reaches a different reader
- The chat-side pick with no repository behind it, which the toolkit's `snippets/decision-help.md` covers and reaches a different reader
- Writing the decision into a plan, task, or architecture record, which each owning standard governs and this skill only routes to
- Deciding when to fire. The skill is user-invoked through `disable-model-invocation`, so escalating is the operator's call rather than a description match.

@@ -114,3 +114,3 @@ ---

If `CLAUDE.md` exists but has no `## Indexes` section, offer to install the canonical convention block. The text below is the source of truth and is mirrored in `tooling/claude/seeds/CLAUDE.md`. Paste it verbatim. Do not rewrite, paraphrase, condense, or add punctuation.
If `CLAUDE.md` exists but has no `## Indexes` section, offer to install the canonical convention block. The text below is the source of truth and is mirrored in the toolkit's `tooling/claude/seeds/CLAUDE.md`. Paste it verbatim. Do not rewrite, paraphrase, condense, or add punctuation.

@@ -151,3 +151,5 @@ ```markdown

- `.claude/context/indexes.md`: system rationale, frontmatter contract, when to adopt
- `docs/agents/indexes.md`: `aitk indexes regen` flags, exit codes, JSON shape
Both pages sit in the toolkit and install nowhere, so a target reads them there rather than in its own tree.
- The toolkit's `.claude/context/indexes.md`: system rationale, frontmatter contract, when to adopt
- The toolkit's `docs/agents/indexes.md`: `aitk indexes regen` flags, exit codes, JSON shape

@@ -8,3 +8,3 @@ ---

Turn a pasted YouTube URL into a markdown file with YAML frontmatter and a cleaned prose body. The `aitk transcripts` command owns the fetch, VTT cleanup, and frontmatter. Do not reimplement that logic. See `.claude/context/transcripts.md` for the output format and field list.
Turn a pasted YouTube URL into a markdown file with YAML frontmatter and a cleaned prose body. The `aitk transcripts` command owns the fetch, VTT cleanup, and frontmatter. Do not reimplement that logic. The output format and field list live in the toolkit's `.claude/context/transcripts.md`, which a target does not receive.

@@ -11,0 +11,0 @@ ## Guards

@@ -25,7 +25,7 @@ ---

Fourteen verbs, listed by `aitk audits list`. Each runs once in its fullest form, and the aggregate reads that verb's own record rather than imposing a shared envelope on it. Every one of those records already has consumers naming its keys, so a common shape would be a breaking change bought for tidiness.
Fifteen verbs, listed by `aitk audits list`. Each runs once in its fullest form, and the aggregate reads that verb's own record rather than imposing a shared envelope on it. Every one of those records already has consumers naming its keys, so a common shape would be a breaking change bought for tidiness.
The verbs walk separate trees and share no state, so they run together. Measured on the authoring machine at twelve verbs, a run finished in 0.8 seconds of wall clock against 4.4 seconds of processor, which is under every other stage in `bun run check`. `aitk deps audit` is the one that changes that reading, since it reaches a network rather than a tree and its latency is the index's rather than this machine's.
Twelve of the fourteen read a tree on this disk. The two added by `state-scoped-risk.md` read committed state rather than an arriving change, which is the gap every review surface here leaves by construction.
Thirteen of the fifteen read a tree on this disk. The two added by `state-scoped-risk.md` read committed state rather than an arriving change, which is the gap every review surface here leaves by construction.

@@ -32,0 +32,0 @@ Each is invoked as the CLI the caller is running rather than as a global `aitk`. A globally installed binary resolves to the main checkout no matter which worktree is executing, so the aggregate would measure a tree the branch never touched and report a pass over it.

@@ -49,2 +49,3 @@ ---

| `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`) |
| `aitk claude skills reach` | Report the shipped bodies citing a toolkit path no target project receives, exiting 2 on an unqualified one |
| `aitk gov test-order` | Report where an implementation reached history ahead of the test covering it (`--json`) |

@@ -62,21 +63,21 @@ | `aitk secrets scan` | Report credential-shaped values in the tree the package ships, keyed on issued values rather than on words (`--json`) |

| Domain | Subcommands |
| ----------- | ------------------------------------------------------------------------------------------- |
| `tooling` | `list`, `sync`, `ref`, `create`, `verify`, `inject`, `prune-gitignore` |
| `snippets` | `list`, `install`, `sync`, `create` |
| `standards` | `list`, `<name>` |
| `gov` | `list`, `install`, `sync`, `build`, `regen`, `test-order` |
| `claude` | `init`, `sync`, `seeds list`, `skills list`, `skills audit`, `skills drift`, `setup [dest]` |
| `wiki` | `init` |
| `design` | `render` |
| `slides` | `render`, `list` |
| `tasks` | `archive`, `validate` |
| `intake` | `list`, `answer` |
| `teach` | `list`, `open`, `resource`, `glossary` |
| `comments` | `scan` |
| `context` | `audit` |
| `markdown` | `audit` |
| `secrets` | `scan` |
| `deps` | `audit` |
| `audits` | `run`, `list` |
| Domain | Subcommands |
| ----------- | ----------------------------------------------------------------------------------------------------------- |
| `tooling` | `list`, `sync`, `ref`, `create`, `verify`, `inject`, `prune-gitignore` |
| `snippets` | `list`, `install`, `sync`, `create` |
| `standards` | `list`, `<name>` |
| `gov` | `list`, `install`, `sync`, `build`, `regen`, `test-order` |
| `claude` | `init`, `sync`, `seeds list`, `skills list`, `skills audit`, `skills drift`, `skills reach`, `setup [dest]` |
| `wiki` | `init` |
| `design` | `render` |
| `slides` | `render`, `list` |
| `tasks` | `archive`, `validate` |
| `intake` | `list`, `answer` |
| `teach` | `list`, `open`, `resource`, `glossary` |
| `comments` | `scan` |
| `context` | `audit` |
| `markdown` | `audit` |
| `secrets` | `scan` |
| `deps` | `audit` |
| `audits` | `run`, `list` |

@@ -83,0 +84,0 @@ Common patterns:

@@ -29,2 +29,3 @@ ---

- [Skill audit](skills-audit.md): Measuring both skill corpora against standards/skill.md, the checks it reads, the requirement gate that is the only failing one, and the drift verb that names bodies rewritten since a ref
- [Citation reach](skills-reach.md): Reporting the shipped skill bodies that cite a path no target project receives, 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
- [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

@@ -31,0 +32,0 @@ - [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

{
"name": "@erclx/aitk",
"type": "module",
"version": "3.0.0",
"version": "3.1.0",
"description": "Infrastructure and quality tooling for developer workflows",

@@ -6,0 +6,0 @@ "license": "MIT",

@@ -0,1 +1,2 @@

import type { ReachRefusal } from '@/claude/skills-reach'
import type { AuditRefusal } from '@/deps/audit'

@@ -289,2 +290,17 @@ import type { ValidateRefusal as RecordRefusal } from '@/records/validate'

/**
* Reads the unqualified citations alone, leaving the qualified ones out.
*
* A qualified citation is a repair that already landed, so folding the two
* together would report a corpus getting worse every time one is fixed. The
* key is still read rather than assumed present, since a record carrying
* neither array is a shape that moved rather than a catalog with nothing in it.
*/
function reachCounts(record: unknown): Record<string, number> | undefined {
const root = asObject(record)
if (root === undefined || !Array.isArray(root.qualified)) return undefined
return allOf({ unqualifiedCitations: lengthOf(root.unqualified) })
}
function boardCounts(record: unknown): Record<string, number> | undefined {

@@ -408,2 +424,22 @@ const root = asObject(record)

{
id: 'skills-reach',
label: 'Shipped citation reach',
argv: ['claude', 'skills', 'reach', '--json'],
// Reports rather than gates, on the split this file already draws. A body
// naming a toolkit path is sometimes correct, since the instruction may be
// meant for a session in this repository, so the verdict is a judgment and
// a push failing on one teaches a contributor to route around the stage.
gatingExits: [],
corpus: 'tracked',
// The one reason this verb refuses for, and it is an absence rather than a
// break. A tracked corpus normally allows nothing, since a tree that ships
// to targets and cannot be found is a broken checkout, and this is the
// second exception on the same test the secret scan takes: no target holds
// `claude/skills/`, so without the allowance every project installing this
// CLI reports the verb unmeasured on every run and never changes, which is
// the permanent signal the per-machine allowance exists against.
absentReasons: ['no-skills'] satisfies ReachRefusal[],
counts: reachCounts,
},
{
id: 'tasks',

@@ -410,0 +446,0 @@ label: 'Task board',

@@ -27,2 +27,7 @@ import { existsSync } from 'node:fs'

import {
type ReachRefusal,
type ReachReport,
scanReach,
} from '@/claude/skills-reach'
import {
planSettings,

@@ -74,2 +79,6 @@ readSettings,

interface SkillsReachOptions {
readonly json?: boolean
}
const SEEDED_FILES: readonly string[] = [

@@ -165,4 +174,4 @@ 'ARCHITECTURE.md',

.command('skills')
.description('Plugin skill catalog (list, audit, drift)')
.argument('[subcommand]', "One of 'list', 'audit', or 'drift'")
.description('Plugin skill catalog (list, audit, drift, reach)')
.argument('[subcommand]', "One of 'list', 'audit', 'drift', or 'reach'")
.helpOption('-h, --help', 'Show this help message')

@@ -173,4 +182,4 @@ .action((subcommand: string | undefined) => {

subcommand === undefined
? "Missing subcommand. Use 'list', 'audit', or 'drift'."
: `Unknown subcommand: ${subcommand}. Use 'list', 'audit', or 'drift'.`,
? "Missing subcommand. Use 'list', 'audit', 'drift', or 'reach'."
: `Unknown subcommand: ${subcommand}. Use 'list', 'audit', 'drift', or 'reach'.`,
)

@@ -267,2 +276,38 @@ outro()

})
skills
.command('reach')
.description('Report shipped bodies citing a path no target receives')
.argument('[path]', 'Repository root, defaulting to the current directory')
.helpOption('-h, --help', 'Show this help message')
.option('--json', 'Add a machine-readable record on stdout')
.addHelpText(
'after',
[
'',
'Scope:',
' Every markdown file under claude/skills/, which is the tree that',
' installs into a target. A cited path counts when it sits under an',
' authoring root no install channel delivers and this repository',
' holds it. A path under src/, scripts/, or bare docs/ names the',
" reader's own tree and is not measured.",
'',
'Exit codes:',
' 0 every citation names the toolkit as the owner',
' 1 refused, with the reason on stderr',
' 2 at least one citation is unqualified',
'',
'Reports rather than gates. A toolkit-scoped instruction is sometimes',
'meant for a session in this repository, so the verdict is a reading',
'and the repair is to name the owner in the sentence.',
'',
'Examples:',
' aitk claude skills reach',
' aitk claude skills reach --json',
'',
].join('\n'),
)
.action((path: string | undefined, opts: SkillsReachOptions) => {
process.exitCode = runSkillsReach(path, opts)
})
}

@@ -575,3 +620,77 @@

/** What a reader does about the one way the corpus fails to build. */
const REACH_REFUSALS: Record<ReachRefusal, string> = {
'no-skills':
'No claude/skills/ here, so this tree ships no plugin body to measure.',
}
/**
* Measures the cwd rather than the toolkit root, matching the audit and drift
* verbs, so a linked worktree reads its own branch instead of `main`.
*/
function runSkillsReach(
path: string | undefined,
opts: SkillsReachOptions,
): number {
const root = resolve(path ?? process.cwd())
const report = scanReach(root)
if (report.kind === 'refused') {
frameError(REACH_REFUSALS[report.reason])
if (opts.json) {
process.stdout.write(
`${JSON.stringify({
root,
reason: report.reason,
message: REACH_REFUSALS[report.reason],
})}\n`,
)
}
return 1
}
intro('aitk claude skills reach')
reportReach(report)
outro()
if (opts.json) {
process.stdout.write(
`${JSON.stringify({
root,
bodies: report.bodies,
qualified: report.qualified,
unqualified: report.unqualified,
})}\n`,
)
}
return report.unqualified.length === 0 ? 0 : 2
}
/**
* States the corpus on every run, including the clean one. A count of what
* failed reads as a verdict on the catalog unless the run also says how many
* bodies it opened and how many citations it already accepted.
*/
function reportReach(report: Extract<ReachReport, { kind: 'measured' }>): void {
logStep('Corpus')
logInfo(
`${plural(report.bodies, 'shipped file')} read, ${plural(report.qualified.length, 'citation')} already naming the toolkit as owner`,
)
logStep('Unqualified citations')
if (report.unqualified.length === 0) {
logInfo('Every toolkit-owned path a shipped body cites names its owner.')
return
}
logWarn(plural(report.unqualified.length, 'citation'))
pipeOutput(
report.unqualified
.map((citation) => `${citation.file}:${citation.line} ${citation.path}`)
.join('\n'),
)
}
/**
* States the bound on every run, including the run that names nothing. A report

@@ -578,0 +697,0 @@ * listing only what moved reads as a verdict on what a session holds, and the