CLI Specification
Complete signatures, flags, arguments, exit codes, and stdout/stderr conventions for the aburi command.
References:
config.md— config file resolution and CLI-flag overridesdiff-algorithm.md— how inputs are supplied toaburi diffmarkdown-projection.md— output Markdown conventionsextension-vocab.md— whataburi vocabqueries
1. Purpose
The single entry point to Aburi. Every capability is exposed through a subcommand.
Design principles:
- Easy to automate in CI: stable exit codes, JSON output available, non-interactive by default
- Friendly to humans: proper help, colored output, progress indication
- Follow Unix conventions: stdout/stderr separation, long/short options, env vars
2. Command Overview
aburi init # generate config file
aburi scan # generate IR
aburi diff <base>..<head> # compute diff
aburi explain <id-or-pattern> # show a single symbol/file
aburi vocab list|effects|extkinds|plugins|who-owns # query extension vocabulary
aburi --version / aburi -v # version
aburi --help / aburi -h # global help3. Common Options
Available on every command:
| Option | Short | Meaning |
|---|---|---|
--cwd <path> | — | Change the working directory (origin for config resolution) |
--config <path> | — | Explicitly specify the config file location |
--log-level <level> | — | debug / info / warn / error (default: info) |
--no-color | — | Disable colored output |
--help | -h | Show command-specific help |
Top-level only:
| Option | Short | Meaning |
|---|---|---|
--version | -v | Show version |
4. aburi init
Generates aburi.json from autodetect results.
4.1 Signature
aburi init [--output <path>] [--force] [--with-suggestions]
[--respect-gitignore|--no-respect-gitignore]4.2 Options
| Option | Meaning |
|---|---|
--output <path> | Output destination (default: ./aburi.json) |
--force | Overwrite an existing file |
--with-suggestions | Include enable candidates for plugins matching detected frameworks as JSONC comments |
--respect-gitignore / --no-respect-gitignore | Whether the language census honours .gitignore (default: honour). No config exists yet to override, so this is also the only way past a .gitignore that cannot be read |
4.3 Behavior
- Detect the workspace root (
component-detect.md§2.1) - Run each detector (§3)
- Convert results into Component candidates
- Generate JSON and write it to
--output, creating the directories that path names - Print a summary to stdout
4.4 When the File Already Exists
- Without
--force→ exit with an error (exit 2):Use --force or specify another path - With
--force→ overwrite; warning goes to stderr - When a directory stands at the path, neither applies: nothing overwrites one, so the answer names it and asks for a file path instead — the same sentence the write itself gives (§4.5). Offering
--forcehere would be advice whose only destination is that refusal
4.5 Exit Codes
| code | Meaning |
|---|---|
| 0 | Generation succeeded |
| 1 | Autodetect failure (permissions, etc.) |
| 2 | Existing file present without --force, or an --output that cannot hold a file — it names a directory, or a file stands on its parent path |
4.6 stdout Example
✓ Detected 3 components (pnpm workspaces)
✓ Detected 2 languages: ts, tsx
✓ Detected 1 framework: nestjs (in apps/billing)
✓ Wrote ./aburi.json
Next steps:
1. Install lang plugin: pnpm add -D @aburi/lang-typescript
2. Install framework plugin (optional): pnpm add -D @aburi/framework-nestjs
3. Install effect plugins (optional): pnpm add -D @aburi/effects-prisma
4. Enable them in aburi.json (uncomment the suggested entries)
5. Run: aburi scanWith --with-suggestions, install/enable lines for official plugins matching the detected frameworks are included in aburi.json as commented-out entries. Uncommenting them enables the plugins immediately:
{
"$schema": "https://aburi.kage1020.com/schema/aburi.config.v1.json",
"languages": ["lang-typescript"],
// Detected: nestjs. Install with: pnpm add -D @aburi/framework-nestjs
// "frameworks": ["framework-nestjs"],
"components": [/* ... */]
}5. aburi scan
Scans the workspace and generates the IR.
5.1 Signature
aburi scan [--output-dir <path>] [--format <json|md|both>] [--no-md|--no-json]
[--strict|--no-strict] [--discover]
[--quiet] [--compact]
[--concurrency <n>]
[--respect-gitignore|--no-respect-gitignore]
[--ignore <glob>]5.2 Options
| Option | Meaning |
|---|---|
--output-dir <path> | Output directory (default: config.output.dir, then out). Resolved against the working directory, as the config value is |
--format <json|md|both> | Output format (default: both) |
--no-md | Shortcut for --format json |
--no-json | Shortcut for --format md |
--strict / --no-strict | Override config.strict |
--discover | --no-strict + record undeclared vocab to out/aburi-vocab-discovered.json |
--quiet | Suppress progress output; stdout carries the final summary only |
--compact | Compact the JSON to a single line |
--concurrency <n> | Parser concurrency (default: CPU - 1) |
--respect-gitignore / --no-respect-gitignore | Override config.respectGitignore. A pair rather than the negative alone, so that a run which typed neither is distinguishable from one that asked for the default — an override the caller did not spell must leave the config alone |
--ignore <glob> | Append to config.ignore[] (repeatable) |
5.3 Behavior
- Resolve the config
- Load plugins and build the registry
- Walk the workspace, parsing each file in parallel
- Extraction pipeline: drop list → tag propagation → effect classification → fingerprint → Symbol finalization
- Write
<output-dir>/aburi.ir.json+<output-dir>/workspace.md+<output-dir>/components/*.md - Print a one-line final summary to stdout
5.4 Exit Codes
| code | Meaning |
|---|---|
| 0 | Extraction succeeded |
| 1 | Extraction error — a file the scan could not read. A source file that stopped being one by the time the scan reached it is skipped rather than fatal — a concurrent build can do that, and a rerun is the fix — but a permission, descriptor or IO failure still ends the run, because absorbing it would let the same commit produce a different Document on a different day. Two calls open files, discovery's stat and the read before extraction, and one predicate decides both: which of them a failure lands on is an accident of timing and must not change the outcome. A file the language plugin could read and refused to parse is not this: it is withdrawn and the code stays 0 (lang-plugin.md §7.1) — unless it took every file the scan found, or crossed minParsedFileRatio, which is a coverage gate rather than a read failure (§5.7) |
| 2 | Config error (schema violation, resolution failure, a --config path that names nothing). A config that exists and cannot be read is 1 — see §9 |
| 3 | Gate — the run finished and produced something the caller must not accept silently: a plugin load failure or manifest violation, a plugin exception that withdrew a file, undeclared vocab detected in strict mode, a scan whose coverage collapsed (§5.7), or a file the Document has no way to name (§5.8). Named by outcome rather than by cause because an empty scan caused by an ignore glob is not a plugin fault |
5.5 stdout Example
✓ Loaded 3 plugins (1 lang, 1 framework, 1 effects)
✓ Parsed 1234 files in 12.4s
✓ Extracted 542 kept · 87 dropped symbols
✓ Wrote out/aburi.ir.json + out/workspace.md + 3 component filesThe kept/dropped line is followed by the call-resolution census in the same format aburi diff uses (§6.6):
542 kept · 87 dropped · 1234 files
calls 1310 · resolved 1203 · unresolved 107 (external 30 · dynamic 60 · ambiguous 3 · no-match 14)With --quiet, only the final line:
542 kept · 87 dropped · 3 components5.6 stderr (Warnings)
⚠ Config /repo/apps/web/aburi.json sits below the workspace root /repo. …
⚠ 3 file(s) had recoverable parse errors.
⚠ 1 file(s) could not be parsed and were left out of the IR.
⚠ 5 file(s) contributed no Symbols: over-size=3, parse-failed=1, extraction-failed=1
⚠ over-size (3) — larger than maxFileSizeBytes. Raise the budget, or leave them out with ignore.
vendor/bundle.js: 2100000 > 1048576
vendor/legacy.js: 1400000 > 1048576
public/data.js: 1100000 > 1048576
⚠ parse-failed (1) — the language plugin refused the source. Deterministic: fix the file, or the plugin.
src/broken.ts: parse reported a non-recoverable error at 12:4 — unterminated string
⚠ extraction-failed (1) — a plugin threw while extracting. This is the reason the run does not exit clean.
src/route.ts: qualified name "{ GET, POST }" contains the non-identifier segment "{ GET, POST }"The two parse lines are counted apart rather than summed. The first counts files whose errors the plugin called recoverable; the second counts files the parse refused. A withdrawn file's errors appear on ScanResult.parseErrors all the same — they are the account of why it went — so summing the two would call them recoverable, which is the opposite of what the plugin said.
The split is by what the plugin said, not by what reached the IR: a file abandoned on its parseTimeoutMs budget is counted on the first line and is not in the document. That is deliberate. Its errors really are all recoverable — a refusal is decided before the first deadline reading — and lang-plugin.md §7.1.2 keeps them precisely so a slow parse of broken input does not send the reader to raise a budget that was never the problem.
The first line was previously the only account of an unparseable file's errors, so a withdrawn file's skip detail carries one of them: the refusal when there is one, otherwise the first recoverable error with its position.
The "contributed no Symbols" line is a census; under it each reason present gets a line of its own, saying what to do about it, and then its files with the detail @aburi/core recorded for each. The reasons want different responses — over-size points at maxFileSizeBytes, parse-timeout at parseTimeoutMs and a re-run, unreadable at a re-run alone — a tree that changed while the scan ran is the only thing that produces it, since every other read failure ends the run — unroutable at a bug in the plugin set or at a rename, parse-failed at the source, extraction-failed at the plugin — and one neutral line said none of it. The re-run / fix-something split is the one SkippedFile.reason draws in the IR schema.
Reasons appear in a fixed order — over-size, unreadable, unroutable, parse-failed, parse-timeout, extraction-failed, the order the IR schema's reason enum declares — in the census and in the groups alike, so the groups arrive in the order the census named them and neither depends on where in the workspace the losses happened to sit.
The listing is capped at ten files per reason, with a "…and N more" tail. Per reason rather than across the whole listing because one shared budget belongs to whichever reason lost the most files, and that is not the reason a reader most needs named: a hundred over-size files would push the one file a plugin threw on — the only reason that moves the exit code to 3 (§5.4) — inside the tail, leaving a non-zero status with nothing on screen to account for it. Every other reason leaves the code at 0.
extraction-failed is listed by that rule like any other reason rather than by one of its own. Its files were listed twice while it had its own clause: the message a plugin threw with is written to both skipped[].detail and extractionFailures[].message at a single site in the scan. ScanReport.extractionFailures is unchanged — it still carries the error's code, and it is still what decides the exit code and what the diff fault clause counts.
Where the lines come from is part of the contract. runScan writes them to a sink its caller supplies, so all three commands that scan report them, rather than one command's wrapper printing them while the other two discard the report. A caller that supplies no sink gets no incident report.
That is not the same as silence. The run's Logger is a separate channel — per file rather than per run, governed by ABURI_LOG_LEVEL (§11), and still writing to process.stderr whatever streams the caller injected. An embedded scan with no sink is quiet, not mute, and routing that channel to the caller is a known gap rather than something this contract covers. It is also why the files above are listed by the CLI itself rather than left to that channel: at ABURI_LOG_LEVEL=error the per-file lines are gone, and for over-size, unroutable and an unreadable raised during discovery there is no Logger line to lose — those three are decided before extraction and are not logged at all, so the report is their only account.
§5.8's paragraph sits directly under the coverage line and above this census — ahead of everything that is recoverable from the artifact, because it is the one section that is not.
A command that runs more than one scan labels them, after the glyph, with the scan the line came from:
⚠ base ref "main": 3 file(s) had recoverable parse errors.⚠ starts every line that stands on its own. The only lines without it are the indented per-file listing and its …and N more tail, which belong to the line above them and are attributed by it.
The warnings precede the stdout summary in a merged view (a terminal, 2>&1, an Actions log), where before the reporting moved they followed it. Deliberate: the last thing on screen is then the kept / dropped line and the artifact paths, which is the part a reader acts on.
5.7 Coverage
A scan that read almost none of the workspace exits 3. The shape it would otherwise produce is the dangerous one because it is a success: an IR with no Symbols diffs against another one as +0 -0 ~0, so every --fail-on gate downstream passes, and the run that lost the workspace is the one that looks healthiest.
parsedFiles === 0 gates unconditionally. Two lines, because the first move differs:
⚠ No file was discovered to scan. The IR is empty and will diff clean against any other empty IR. Check ignore and .gitignore, components[].roots, and whether a loaded language plugin claims any extension in this workspace.
⚠ 1200 file(s) discovered, 0 parsed — 1200 as parse-failed. The IR is empty and will diff clean against any other empty IR.Nothing discovered is a question about the config; nothing parsed is a question about whatever withdrew the files, so the second names the reason that took the most of them — ties broken by the reason enum's order, so the line is a function of the losses rather than of the walk. The line goes through the same sink as the rest of §5.6, above the census that is its evidence, so a two-scan diff labels it by side.
Anything above zero is the workspace's own call, through minParsedFileRatio (config.md). Absent by default: where the line sits between "lost some files" and "lost the workspace" depends on the repository, and a default would red a build for a judgement nobody made. Set it and the scan gates when parsedFiles / totalFiles falls below it — <, not <=, the same reading --fail-on's thresholds use.
The floor counts every skip reason. parse-timeout is the reason whose loss varies by machine, and so the one a floor is usually reached for, but it is not the only one that hides a blind spot: which reason produced the loss decides the fix, not whether coverage collapsed. Each one is named per file directly below (§5.6) either way.
keptSymbols plays no part. A file that parses cleanly and declares nothing is counted as parsed, which is correct — a repository of configuration and tests is not a failed scan — so a Symbol count says something about the code where parsedFiles says what this policy is about.
Neither gate withholds anything the run would otherwise have written, as neither does for a plugin exception: a reviewer gets whatever --format asked for and a non-zero code rather than the artifact and a green light. --format md writes no IR either way, so "the IR is written" is a claim about what gating does not change rather than about what a gated run always produces.
5.8 Files the Document cannot name
Two things put a file beyond what a Document can name, and both end the same way: the file is not skippable — a skip entry is a path plus a reason, and it is the path that is missing — and not countable either, because invariant #21 holds stats.skippedFiles's length to totalFiles - parsedFiles.
The first: a name a path cannot spell. A filename may contain a backslash on any POSIX filesystem, and a Document path may not: / is its only separator, so there is no way to write such a name down that a reader will not take for a directory boundary (ir-schema.md §14 invariant #10).
So it leaves totalFiles the way a file no plugin claims does, and the run says so itself:
⚠ 3 file(s) were left out of the IR and out of its counts, under 2 name(s) with no spelling here: "/" is the only separator a Document path has, so a name holding a backslash cannot be written down at all. Rename each one below. To leave one out with ignore instead, write its backslash twice — a glob pattern spends a single one as an escape, so the name as printed does not match itself.
odd\name.ts
src/v\1 — a directory, and the 2 file(s) under itIt exits 3. Every other loss leaves a trace in the artifact that a later reader can find the file by; this one leaves none, so the exit code and that paragraph are the whole record. A scan that dropped source and went green would be reporting a clean run over a workspace it did not describe.
One line per name that has to change, not per file, and no cap. A backslash in a directory name disqualifies every file beneath it and none of those filenames is at fault, which is the same rule the unroutable detail follows for : and # — so the line names the path up to and including the offending segment, and the file count under it. That also makes the listing the length of the work rather than of the damage, which is what lets it go uncapped: every other listing in §5.6 caps at ten because stats.skippedFiles[] holds the rest, and here nothing does.
The ignore spelling is not the printed one. Patterns reach picomatch, which spends a lone backslash as an escape:
| pattern | excludes src/v\1/util.ts |
|---|---|
src/v\1/** | no |
src/v\\1/** | yes |
src/v?1/** | yes |
The first row is the name exactly as the paragraph above prints it, which is why the advice says to double the character rather than leaving the reader to infer it from one failed attempt.
The check runs after the extension filter, so a notes\1.txt in a TypeScript workspace is filtered on the extension alone: it was never a candidate, and an incident that gates the exit code should not be raised about a file the scan was never going to read.
Renaming is the only fix that makes the file describable. ignore only stops the run mentioning it — no setting gives the character a spelling here, because it has none rather than a disallowed one.
aburi explain answers for one of these files without consulting the document, since no document could hold it: see §7.2.
The second cause: two names, one path. A path is normalized on the way into the Document (§1.2), and two names differing only in how they are composed normalize to the same string. The Document then has one path for two files, which is no name for either.
⚠ 2 file(s) were left out of the IR and out of its counts, on 1 path(s) that two names claim at once: the Document holds every string in Unicode NFC, and these names differ only in how they are composed, so normalizing them gives one path for more than one file. Rename one of each group. ignore matches the spelling on disk rather than the one below, so the pattern that works there is a wildcard.
src/café.ts — claimed by 2 file(s) on disk:
"src/café.ts" (U+0073 U+0072 U+0063 U+002F U+0063 U+0061 U+0066 U+0065 U+0301 U+002E U+0074 U+0073)
"src/café.ts" (U+0073 U+0072 U+0063 U+002F U+0063 U+0061 U+0066 U+00E9 U+002E U+0074 U+0073)Every claimant is withdrawn, not one kept. A rule granting the path to the NFC-spelled claimant is partial — two different decomposed spellings can normalize to one composed path with no NFC claimant among them — and it is the pair that is at fault rather than either file, the same reading a backslash in a directory name gets.
The codepoints are not decoration. The whole property of the pair is that the names are different bytes and identical glyphs, so the two lines would otherwise print the same.
The pair is decided over every candidate, not over the files that were read: one path landing in stats.skippedFiles[] and on symbols[].source.file is the contradiction aburi diff resolves as a deletion, and two skipped claimants break invariant #21 outright.
Renaming all but one of the group is the fix. ignore does something different in each direction, and the summary "use a wildcard" is false, so the paragraph states both. Measured against discovery's own options, on a composed and a decomposed spelling of one path:
ignore pattern | candidates left |
|---|---|
| none | both, withdrawn as a collision |
| the path printed above | the claimant not spelled that way, scanned normally |
| the other claimant's spelling | the composed one, scanned normally |
src/caf* | none |
So the printed path is not a pattern that fails: it is the one that resolves the collision and keeps a file, by excluding the claimant it names. It matches nothing only in the case two paragraphs up — where no claimant is spelled as the normalized path — and that is the case the "wildcard" advice is actually for.
aburi explain has no dedicated answer for this one, the way it does for a name a path cannot spell: that is decidable from the argument string, and a collision is a property of a group, so nothing in the argument says whether one exists. On the default path it rescans, so the section above is printed and the run exits 3 like any other scan that found one. Under --ir or --no-rescan there is no scan and the document holds no trace of the group, so the lookup answers No matches at exit 1 — which is the case to know about.
6. aburi diff
Compares two IRs.
6.1 Signature
aburi diff <base>..<head> # specify git refs
aburi diff --base <ir.json> --head <ir.json> # specify existing IRs directly6.2 Options
| Option | Meaning |
|---|---|
--base <path> | Base IR file path (instead of a ref) |
--head <path> | Head IR file path |
--output-dir <path> | Destination for diff.json / diff.md (default: config.output.dir, then out) |
--format <json|md|both> | Output format |
--filter <kinds> | Comma-separated restriction to change kinds (added,removed,changed,moved,moved+changed) |
--fail-on <kinds> | Exit 3 if even one change of the given kinds (status granularity) exists (for CI gates) |
--quiet | Limit stdout to a single final summary line |
6.3 Arguments
Ref forms:
main..HEAD— base=main, head=HEADv1.2.0..v1.3.0— tag comparisonabc123..def456— direct commit specification
Exactly one .. separates the two refs. The three-dot form main...HEAD — what a GitHub compare URL and git diff a...b spell — is rejected as a syntax violation (exit 2, §6.5) with a message naming the two-dot rewrite and pointing at git merge-base <base> <head> (placeholders, not the caller's own refs pasted into a runnable command: a ref name may legally contain $, backticks and ;). It is refused rather than accepted-as-two-dot because the two forms answer different questions, and silently answering the other one is worse than saying so. A merge-base form of its own is under consideration (see the roadmap).
If git is unavailable, pass existing IR files via the --base / --head pair.
6.4 Behavior
With refs:
- Pre-validation (§6.4.1)
- Create a temporary git worktree and check out the base ref
- Directory name: the worktree's own directory is named after the head workspace's directory (
<temp>/base/<head-workspace-dirname>), under abase/level of its own so the leaf is free to be any name the workspace has —base-outandhead-outincluded, which as siblings would be the run's own temporary scan outputs - Rationale: component autodetection falls back to the directory name for a Component rooted at the workspace root (
component-detect.md§4.1 — a Component underpackages/*takes its name from its own directory and never reaches this one). A fixed name would give the base side a different Component id from the head's, so a workspace whose Component is the root itself and which declares neither a package name nor explicitcomponents[]would report one Component added and one removed on every diff - Two leaves the rule cannot use: an empty one (only at the filesystem root) and
@(whichgit worktree addcannot spell under.git/worktrees/, failing withfatal: not a git repository). Both fall back tobase. That is safe because a substitution can only reinstate the defect by supplying a Component id differing from the head's, and both leaves kebab-case to nothing: where detection decides ids the scan refuses the workspace withinvalid-component-idand names the directory that did it, and wherecomponents[]declares them no directory name is read on either side
- Directory name: the worktree's own directory is named after the head workspace's directory (
- Run
aburi scanon the base and store the IR temporarily- Config used: apply the head-side
aburi.jsonto the base scan as well (the base is interpreted through the head's view; a stale config remaining in the base is ignored) - Rationale: using the config as of the base ref would make "config change = the entire IR changes", breaking the diff. Fixing the view to the head automatically resolves config differences
- A future
--base-config <path>may be provided to override this (planned — see the roadmap)
- Config used: apply the head-side
- Run
aburi scanon the head (the original cwd) - Compare the two IRs and compute the diff (
diff-algorithm.md) - Write
<output-dir>/diff.json+<output-dir>/diff.md - Print a one-line summary to stdout
- Clean up the worktree
With file inputs: skip steps 1-3 and start at step 5.
6.4.1 Git Pre-Validation (Ref Form)
Before creating the worktree, the following checks run in order; on failure, exit 1 with a concrete remediation message on stderr:
| Check | Failure message |
|---|---|
git rev-parse <base> succeeds | Base ref '<base>' not found. If this is a CI shallow clone, run: git fetch --deepen=50 origin <base> |
Repository is not shallow (git rev-parse --is-shallow-repository is false) | Repository is shallow. aburi diff requires base ref history. Run: git fetch --unshallow |
Sparse-checkout is disabled (git config core.sparseCheckout is false or unset) | Sparse-checkout detected. aburi diff requires full file tree. Disable with: git sparse-checkout disable |
git submodule status is empty (submodules are not yet supported) | Submodules detected: <list>. Submodule-aware diff is not yet supported. (warning; continue) |
| On Windows, trial-check whether the base ref contains symbolic links | Symbolic links in working tree may fail to materialize in worktree on Windows. (warning; continue) |
6.4.1.5 Plugin Dependency Resolution at the Base Ref
When scanning the base ref for aburi diff <base>..<head>, Aburi shares the head's node_modules (the worktree only materializes the base sources at a separate path; dependency resolution uses the original cwd's node_modules).
- Rationale: since §6.4 decided to "apply the head's
aburi.jsonto the base scan as well", the plugin set also comes from the head. Reinstalling from the base ref'spackage.jsonfor the base scan would explode build time, and--frozen-lockfiledoes not work on shallow clones - A relative plugin ref (
./plugins/x.mjs) in that config resolves against the head's workspace root, not the worktree, for the same reason: the ref belongs to the head's plugin set, and a commit that adds a plugin file would otherwise make the base scan fail to load a config the head reads fine. Everything else inside the config —ignore,components[].roots— keeps resolving against the scan's own workspace root, which for the base scan is the worktree, because those name sources and the base's sources are what the worktree holds - Known limitation: when the base ref's sources cannot be extracted with the head's plugins (e.g. the base uses syntax from an older framework version and the head's framework plugin only supports the newer one), parsing/extraction may fail
- This is a consequence of the "IR generator is pinned to the head environment" design
- On failure, the affected file is skipped with a warning log
- The request "apply the base ref's contemporaneous plugin set" is under consideration as
--base-plugins <path>for a future release (see the roadmap)
6.4.2 GitHub Actions Guidance
Required setup when using aburi diff in CI:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # full history (aburi diff fails on shallow clones)Or fetch-depth: 50 or more, at a depth that includes the base ref. The default of 1 cannot be used.
6.5 Exit Codes
| code | Meaning |
|---|---|
| 0 | Diff computed successfully (regardless of whether differences exist) |
| 1 | Computation error (invalid IR, git error) |
| 2 | Argument error (<base>..<head> syntax violation; one of --base/--head missing) |
| 3 | Changes matching --fail-on were detected (CI gate), or one of the two scans this command ran did not exit clean (§5.6) |
The second cause is about greenness, not about counts. A file a plugin threw on is recorded in stats.skippedFiles, so its Symbols already classify as unknown rather than as deletions and the diff overstates nothing. The exit code is what would be wrong: the same workspace makes aburi scan exit 3, and asking it for a diff instead must not turn it green.
A fault at the base ref gates as well. That is a policy rather than a side effect, and it has a cost — a broken base reddens every diff taken against it until the base moves — but a comparison with a broken half is not evidence about the half that worked.
It gates on the scan's exit code, not on a named incident, and the diagnostic wording is derived from what the scan actually reported so that a second reason arrives with the code right and the message still true. Two more have: a scan whose coverage collapsed (§5.7), and one that found a file the Document cannot name (§5.8). The second is named ahead of "discovered no file to read" and behind the other two faults, because an unnameable file leaves totalFiles — so a workspace whose whole candidate set is unnameable discovers nothing and that fault is this one's consequence, while leaving the denominator can only raise the parsed ratio and so cannot produce either of the others. A coverage collapse reddens a diff the same way a plugin exception does, and names itself — The base scan parsed none of the 1200 file(s) it found — rather than falling back to "did not exit clean". A plugin exception is named first when both apply, because a scan that threw on every file has the coverage fault as a consequence of it rather than as a second finding.
When a gate clause and a scan fault both apply the code is 3 either way, both messages are printed, and DiffReport.faultedScans names the sides so a programmatic caller does not have to read the warnings to tell the two apart.
6.6 stdout Example
Normal:
+5 -3 ~12 ↔2 ⤴1 (added · removed · changed · moved · moved+changed)
calls 1310 · resolved 1203 · unresolved 107 (external 30 · dynamic 60 · ambiguous 3 · no-match 14)
→ out/diff.mdThe second line is the head IR's stats.callResolution (call-resolution.md §8.1). It answers "is the Slice View below missing edges?" without a second command. Buckets with a count of zero are omitted; when nothing is unresolved the line reads calls N · resolved N · unresolved 0.
A head IR produced before the counter existed cannot be back-filled, so the line is omitted rather than printed as zeroes — zeroes would assert a clean call graph the run never observed. Omitting it silently would be its own failure, though: the reviewer would read the Slice View without knowing the signal that explains a suspicious singleton is absent. A one-line note therefore goes to stderr (§18.2 — stdout carries result data only), naming the cause and pointing at a re-scan of the head revision.
The unknown count is appended to the first line when there is one — +5 -3 ~12 ↔2 ⤴1 · ?2 unknown — and omitted otherwise, so the line a reviewer skims on every PR does not grow a permanent ?0. It qualifies the counts beside it: the added and removed totals are smaller than the truth by that much.
It counts Symbols only. summary.depsUnknown counts the same thing for dependencies[] (diff-algorithm.md §6.2.1) and stays out of this line for the reason the dependency counts already do: the glyph line reports the Symbol-level shape of a change, and depsAdded / depsRemoved are not on it either. depsAdded, depsRemoved and depsUnknown all appear in diff.json; diff.md lists the corresponding edges under Dependency changes and prints no counts at all.
A file skipped by both scans produces no unknown entry — there are no Symbols from it in either document, so the matcher has no leftover to classify. It is reported at document level instead: diff.json carries the path and each scan's reason in notCompared[], and diff.md lists it under ## 🚫 Not compared (diff-algorithm.md §6.3). The stderr line stays, deliberately shorter than the artifact — a count and a capped list of paths, no reasons — because it is the cover note for whoever is watching the command, and a terminal line that grows with the size of a workspace's blind spot stops being read.
An IR that dropped files but predates stats.skippedFiles reports the count without the list, and aburi diff cannot then tell a lost file from a deleted one — it classifies every leftover as added / removed, which is the pre-field behaviour, and warns on stderr for each side that is in that state. It does not guess: inferring the list from totalFiles > parsedFiles would attach the doubt to whichever Symbols happened to be missing.
Ref mode runs two scans, so both scans' incidents (§5.6) appear, labelled by side. The base is labelled by its ref; the head is labelled head (working tree) and never by the ref spec's head token, which §6.4 does not scan.
A file lost on both sides therefore produces each scan's account of it and then the diff's: each scan saying what it failed to read, and the diff saying the comparison never happened. That is not duplication — neither scan is in a position to know the other lost the same file — and how many lines it comes to depends on the reason. A refusal costs two per scan (the withdrawal and the skip summary), an over-size file one.
One line has no counterpart in the artifact at all. A file whose parse reported recoverable errors reached the IR rather than stats.skippedFiles, so nothing marks it and nothing about it becomes unknown — yet those errors may have cost it a declaration, leaving its Symbol set short and moving added / removed with no file having gone missing. aburi diff says so when either scan reported one, and only ref mode can: parseErrorCount is a property of the scan, not of the document it wrote, so --base / --head has nothing to read.
--base / --head is not silent about faults, though. stats.skippedFiles[].reason persists extraction-failed, so file mode can see that a plugin threw when a document was written even though it never watched it happen; it names those paths per side. It does not gate on them: the fault already had its exit code in the run that hit it, and failing here would red a job for someone else's incident, on documents the caller pinned deliberately. DiffReport.faultedScans is null in this mode rather than empty, because "ran no scan" is not the same answer as "ran two clean scans".
--quiet:
+5 -3 ~12 ↔2 ⤴16.7 Uses of --fail-on
Used in CI for gates such as "PRs containing changes of a specific status require approval before merge":
aburi diff main..HEAD --fail-on changed,removed
# exit 3 if even one symbol has status "changed" or "removed"unknown counts Symbols only — it reads symbols[], not dependencies[], and not notCompared[]. A gate written meaning "fail if this diff is incomplete" therefore catches neither the dependency side (diff-algorithm.md §6.2.1) nor a file both scans skipped (§6.3), and no token exists for either. A bare token for the second would be the wrong default in any case: a workspace with a permanent over-size bundle would trip it on every pull request, which is an argument for a threshold rather than a flag. Whether the gate vocabulary should grow either family is an open decision.
Accepted Values
| Kind | Values |
|---|---|
| Status granularity | added / removed / changed / moved / moved+changed / dropped-toggled / unknown |
| Delta granularity | api-changed / logic-changed / syntax-changed |
| Direction granularity | dropped-toggled:to-dropped / dropped-toggled:to-kept |
| Count threshold | <value>:><N> (e.g. dropped-toggled:>10 fires when the count exceeds 10) |
Multiple values may be given, comma-separated.
Uses of Direction-Specific fail-on
--fail-on dropped-toggled:to-kept detects "symbols previously treated as dropped became kept". Useful for deliberate review of drop-rule relaxation. The inverse, dropped-toggled:to-dropped, detects "symbols that were kept became dropped" (e.g. checking the blast radius of a DTO consolidation).
Uses of Count Thresholds
In cases where a drop-rule change legitimately fires dropped-toggled in bulk (e.g. changing all DTOs), a plain --fail-on dropped-toggled misfires. A threshold such as --fail-on dropped-toggled:>50 — "block only when there are more than 50" — is the practical choice.
Evaluation Rules
- Status granularity (
changed, etc.): fires if even one Symbol has the given status - Delta granularity (
api-changed, etc.): fires if even one Symbol has statuschangedormoved+changedwith the correspondingdelta.<axis>Changed: true
Examples:
aburi diff main..HEAD --fail-on api-changed
# exit 3 if even one symbol has delta.apiChanged: true
# changed symbols where only logic changed, and moved (status only), have no effect
aburi diff main..HEAD --fail-on api-changed,removed,dropped-toggled
# fires on API change OR removal OR drop-rule fluctuationThis allows fine-grained CI gates, making operational policies such as "only API changes require approval, logic changes are warnings" possible today.
7. aburi explain
Shows details of a single Symbol / file / pattern match.
7.1 Signature
aburi explain <id-or-pattern> [--output <path>] [--ir <path>] [--no-rescan] [--debug-resolution]7.2 Arguments
<id-or-pattern> is one of:
- Full Symbol id — the string contains
#and matches the<language>:<path>#<qname>form → direct lookup - File path — the string contains
/and either is an existing file or is a path the IR names instats.skippedFiles(§7.6) → show all Symbols in that file. A#in it does not disqualify it: the id form above is checked first and only claims the argument when it is one, and a file whose name holds an id separator is recorded instats.skippedFilesprecisely because no Symbol in it could be named - Partial-match pattern — anything not matching the above → collect candidates by case-sensitive substring match against each Symbol's qualified name (
Symbol.name)
A file path whose name holds a backslash is answered without consulting the document at all, with exit 3: no IR can name it (§5.8), so neither the skip list nor the file existing on disk says anything about it, and No matches would be a claim about the workspace when the absence is the format's. The same answer whether the run scanned or read an IR off disk, because it is decided from the argument.
7.2.1 Exact Definition of Partial Matching
- case-sensitive (
getUserandgetuserare distinct) - substring match on
Symbol.nameonly (= a partial match against the whole qualified name;Service.createhitsInvoiceService.createInvoice) - no glob support (patterns like
*Serviceare under consideration for a future release — see the roadmap) - If multiple candidates match, exit 2 + candidate list on stdout
7.3 Options
| Option | Meaning |
|---|---|
--output <path> | Write to a file (default: stdout). The directories the path names are created, as --output-dir creates its own; a path that cannot hold a file — it names a directory, or a file stands on its parent path — is exit 2 |
--ir <path> | Use an existing IR file. Resolved against the working directory, as every path-bearing flag is. Without it the command looks for <output-dir>/aburi.ir.json — config.output.dir when the config names one, otherwise out — starting at the working directory and walking up to the workspace root, taking the nearest — aburi scan writes under the directory it was run from, and a scan covers the whole workspace wherever it was started, so either place holds an answer about the same tree. Nothing found triggers a scan, or exits 2 under --no-rescan |
--no-rescan | Exit 2 when the lookup above finds no IR, instead of scanning. Staleness is not examined: an IR that is found is read whatever its age |
--debug-resolution | Append a ## Call resolution table: one row per call site with the resolved callee, or the call-resolution.md §8.1 bucket that explains the null, plus the competing candidates for ambiguous. Those buckets are per-run diagnostics that the IR deliberately does not persist, so the flag always rescans and is rejected (exit 2) alongside --no-rescan or --ir. It is a reporting flag, not a tuning knob — no IR or diff content changes |
7.4 Behavior
- Read the IR (invoke
aburi scaninternally if missing) - Resolve the argument:
- Full id → direct lookup
- File → all Symbols in that file
- Pattern → collect candidates by partial name match
- Generate the Markdown projection (
markdown-projection.md§7) - Emit to stdout (or
--output)
When step 1 rescans, that scan's incidents (§5.6) go to stderr, unlabelled — only one scan ran. Reading an existing IR reports nothing live: no scan happened here, and the incidents of the scan that wrote the file were reported when it did. What the document itself records about those incidents is a different matter, and step 2 reads it — see §7.6.
7.5 When Multiple Candidates Match
Multiple matches for "createInvoice":
1. ts:apps/billing/src/InvoiceService.ts#InvoiceService.createInvoice
2. ts:apps/billing/src/legacy/OldService.ts#OldService.createInvoice
3. ts:packages/test/src/factories.ts#createInvoice
Specify the full id to disambiguate.exit code: 2 (ambiguous).
7.6 When the Document Does Not Cover the Question
stats.skippedFiles (ir-schema.md §2) names every file the scan gave up on and why. A lookup that finds nothing is an assertion of absence, and that list can contradict it: the file that would have declared the Symbol was withdrawn, so the document does not know.
One principle decides every case. The answer is unknown (exit 3) when the document positively identifies the file the question named as one it never analysed; it stays "not found" (exit 1), qualified, when the doubt is diffuse.
| Arm | Names a file? | Miss becomes |
|---|---|---|
| Full id | yes — the <path> segment of the id | unknown when that path is in stats.skippedFiles; otherwise the diffuse answer below |
| File path | yes — the argument | unknown when that path is in stats.skippedFiles; otherwise the diffuse answer below |
| Pattern | no | always the diffuse answer below |
The diffuse answer is not found (exit 1) with a line counting the files the document says it never analysed, and it attaches to every miss the document could not tie to the file the question named — including a miss in the two naming arms on a file that was analysed after all.
$ aburi explain src/route.ts --ir out/aburi.ir.json
Cannot answer "src/route.ts": this IR never analysed src/route.ts (parse-failed), so it cannot say what that file declares.
EXIT=3Consequences of the principle, each of which is a case that would otherwise be argued separately:
- The file arm does not require the path to exist on disk. A path named in
stats.skippedFilesreaches the arm as well.--irand--no-rescanexist so a CI job can question a pinned artifact from a tree that need not hold the same files, and demanding the file locally would drop exactly the motivating case into the pattern arm. - The check runs on a miss only, so a hit is never qualified. A hit is the document speaking about a Symbol it holds. An
over-sizefile is skipped by every run of a workspace, so caveating hits would caveat every answer that workspace ever gives — and where a hit really is suspect because the scan broke, the §7.7 gate already covers it. This also settles the id whose<path>segment andsymbols[].source.filedisagree, which a re-export or a generated file produces: the Symbol is right there, and is answered. - The id arm asks the id grammar, not the
#. Dispatch is a silhouette; the file segment is only read out of a string that satisfies the whole Symbol-id grammar (ir-schema.md§3.1). A typo that happens to contain a skipped path names no file, and gets the pattern arm's diffuse line rather than a positive claim about coverage. - A document predating
stats.skippedFilescan only ever give the diffuse answer.totalFiles > parsedFileswith no list says how many files were lost and nothing about which, so it can never identify the file the question named, in any arm.aburi diffreports the same shape per side (§6.6).
The diffuse line is a count and a pointer at stats.skippedFiles, not a list: the question was about one Symbol, and answering it with an inventory of the run buries it.
7.7 Exit Codes
| code | Meaning |
|---|---|
| 0 | Success |
| 1 | The requested symbol was not found |
| 2 | Multiple candidates; disambiguation required, or an --output that cannot hold a file |
| 3 | The answer would not be safe: the scan this command ran did not exit clean (§5.6), or the document names the file the question asked about as one it never analysed (§7.6) |
Exit 3 outranks the other three, and the two routes to it are the same statement about different evidence. When the scan broke, every answer is suspect: a single hit may have had a competing candidate in the withdrawn file and should have been 2, and a not-found may be describing the withdrawal rather than the workspace. When the document is intact but says it never read the file in question, only that question is unanswerable — which is the case that matters most, because No matches is otherwise indistinguishable from "that Symbol does not exist". Reading an existing IR reaches this code by the second route only; no scan ran.
8. aburi vocab
Queries the registered extension vocabulary.
8.1 Subcommands
aburi vocab list # all vocab
aburi vocab effects # effect ids only
aburi vocab extkinds # extKinds only
aburi vocab plugins # plugin list
aburi vocab who-owns <id> # the plugin that owns this id8.2 Common Options
| Option | Meaning |
|---|---|
--json | Machine-readable JSON output instead of a table |
8.3 Exit Codes
| code | Meaning |
|---|---|
| 0 | Success |
| 1 | id not found (who-owns only) |
| 2 | Subcommand missing or invalid |
8.4 Output Examples
aburi vocab effects:
core / db.read — Database read operation
core / db.write — Database write operation
...
effects-nest / x-nest:lifecycle.on-module-init — NestJS OnModuleInit hook
effects-prisma / (prefix x-prisma) — Prisma plugin namespaceaburi vocab who-owns x-nest:lifecycle.on-module-init:
Plugin: effects-nest (v1.2.3)
Type: effects
Declaration: explicit (provides.effects[])
Description: NestJS OnModuleInit hook9. Exit Code Conventions (All Commands)
| code | Use |
|---|---|
| 0 | Complete success |
| 1 | Runtime error (IO, extraction, git) |
| 2 | Input error (CLI arguments / config / missing / ambiguous) |
| 3 | Plugin error / fail-on gate / strict violation |
128+N is for fatal signals (Aburi itself does not use it).
The line between 1 and 2 is who has to act, not which subsystem failed. 2 covers the whole of what the reader wrote: a config that does not parse, does not conform, or names the same component twice — and a --config path that names nothing, which is the "missing" in the table above and is a mistyped argument rather than an IO failure. 1 covers a config that is there and cannot be read: a permission, a mount, a directory of that name. No edit to aburi.json fixes any of those. And a failure Aburi's own invariants raised is 1 with the run named as a bug in Aburi, since 2 is a sentence about the reader's file and would send them looking for something that is not in it.
A plugin error means the same thing in every command that scans. scan, diff and explain all exit 3 when the scan they ran did not exit clean, whichever of them ran it.
Code 3 also covers the narrower statement "this answer would not be safe", which a command can reach without having scanned anything. aburi explain does when the document it read names the file the question asked about as one that scan never analysed (§7.6): the toolchain is fine and the document is intact, but the one question put to it is unanswerable, and reporting 1 would be an assertion of absence. What a command still does not do is inherit a status from a document it merely read — an IR that records losses irrelevant to the question is reported on and left at the code the answer itself earned.
10. stdout / stderr Conventions
| Stream | Use |
|---|---|
| stdout | Results (summary lines, JSON, Markdown body). Pipeable; the target of CI parsing |
| stderr | Progress, warnings, error messages, colored UI |
Example: aburi diff main..HEAD --quiet > result.txt 2> log.txt
The --json flag (aburi vocab only) dedicates stdout to machine-readable JSON.
11. Environment Variables
| Variable | Meaning |
|---|---|
ABURI_CONFIG | Config file path (equivalent to --config) |
ABURI_LOG_LEVEL | Equivalent to --log-level |
NO_COLOR | If set, disable coloring (standard convention) |
FORCE_COLOR | If set, force coloring on (standard convention) |
CI | If set, CI mode (§12) |
Precedence: CLI flags > environment variables > config file.
12. CI Mode
Automatic switches when the CI=true env is detected:
- Suppress progress animations (final summary only)
- Disable coloring (on if
FORCE_COLORis set) - Emit stack traces on errors (debug only)
Can also be enabled via an explicit --ci flag (for CI environments where the env is not set).
13. Config Resolution Order
1. --config CLI flag
2. ABURI_CONFIG env
3. <cwd>/aburi.jsonc
4. <cwd>/aburi.json
5. Repeat 3-4 recursively in parent directories, up to the **filesystem** root — not the
workspace root, so a config shared across several repositories is still honoured
6. autodetect (works with no config present)Passing --cwd changes the cwd and therefore the search origin.
aburi diff in ref mode runs this list once, against the invoking cwd, before it materialises the base worktree — and hands the single answer (an absolute path, or "nothing found, autodetect") to both scans. Re-running it from inside the worktree would resolve steps 1-5 against the base revision instead of the head — a relative ABURI_CONFIG relocates exactly like a relative --config, since both arrive as the same value — and would turn a head-side step 6 into a step 3-5 hit whenever the base ref still carries a config the head deleted. Either is what §6.4 step 3 forbids.
14. Concurrency
- Parser concurrency for
aburi scan: default =max(1, CPU_count - 1) - Override with
--concurrency <n> - The effective concurrency is capped at
min(specified, floor(availableMemoryMB / wasmHeapPerWorkerMB))(lang-plugin.md§8.1)- A guard against crashes from exceeding the WASM heap budget
- The per-worker budget's source of truth is
capabilities.wasmHeapPerWorkerMBin the plugin manifest (range: 16–4096 MiB; 256 MiB when undeclared) - When multiple lang plugins coexist in the same run, the maximum of the declared values is used
- On memory-constrained CI,
--concurrency 1is recommended (for debugging)
A future release will switch to Node worker_threads instead of a worker pool (see the roadmap). Architecture details — pool sizing, work partitioning, serialization boundary, and byte-identical merge — are specified in performance.md.
15. Planned Features
| Feature | Summary |
|---|---|
aburi watch | Watch file changes and regenerate the IR |
aburi doctor | Consistency check of config / plugins |
aburi serve | LSP-like local server (IDE integration) |
aburi vocab list --json extension | merged with discoverer output |
None of these are implemented yet; see the roadmap. The signatures are reserved to avoid future compatibility breaks.
16. Help Output
aburi --help:
aburi - Render meaningful code structure as IR for review
Usage:
aburi <command> [options]
Commands:
init Generate aburi.json from autodetect
scan Generate IR from current workspace
diff Compute diff between two IRs
explain Show single symbol details
vocab Show registered extension vocabulary
Common options:
--cwd <path> Set working directory
--config <path> Override config file location
--log-level <level> debug | info | warn | error
--no-color Disable colored output
-h, --help Show help for command
-v, --version Show version
Run "aburi <command> --help" for command-specific options.Each command's --help follows the same three-section structure: "Usage / Options / Examples".
17. Verifiable Properties
| ID | Input | Expectation |
|---|---|---|
| CL1 | aburi --version | One-line version string, exit 0 |
| CL2 | aburi --help | Help for all commands, exit 0 |
| CL3 | aburi nope | Unknown command, exit 2 |
| CL4 | aburi init with existing aburi.json | exit 2, error message on stderr |
| CL5 | aburi init --force | Overwrites existing file, exit 0, warning on stderr |
| CL6 | aburi scan (no config) | Runs via autodetect, exit 0 |
| CL7 | aburi scan --discover | Records undeclared vocab, exit 0 |
| CL8 | aburi scan strict + undeclared vocab | exit 3 |
| CL9 | aburi diff main..HEAD --fail-on changed with changes | exit 3 |
| CL10 | aburi diff with missing arguments | exit 2 |
| CL11 | aburi explain <ambiguous> | exit 2, candidate list on stdout |
| CL12 | aburi vocab who-owns <unknown> | exit 1 |
| CL13 | aburi vocab list --json | machine-readable JSON, exit 0 |
| CL14 | Piping stdout (`aburi scan --quiet | wc -l`) |
| CL15 | NO_COLOR=1 aburi scan | No coloring |
| CL16 | CI=true aburi scan | Progress animation suppressed |
| CL17 | --log-level debug aburi diff on error | Stack trace on stderr |
| CL18 | aburi --config ./custom.json scan | Uses the specified config |
| CL19 | aburi explain <name> where a plugin threw during the rescan | exit 3, incident lines on stderr |
| CL20 | aburi diff main..HEAD where a plugin threw at the base ref | exit 3 with no --fail-on clause, base-labelled lines on stderr |
| CL21 | aburi explain src/route.ts --ir <ir> where that IR names src/route.ts in stats.skippedFiles | exit 3, the file and its skip reason on stderr, nothing on stdout |
| CL22 | aburi explain <pattern> with no match, against an IR that skipped files | exit 1, No matches plus a line counting them |
| CL23 | aburi scan in a workspace holding a source file whose name contains a backslash | exit 3, the name to rename on stderr, and the file not counted in stats.totalFiles |
| CL24 | aburi explain 'src/weird\name.ts' where that file exists | exit 3, a line saying no IR can name it, nothing on stdout |
| CL25 | aburi init --output config/aburi.jsonc where config/ does not exist | Creates the directory, writes the file, exit 0 |
| CL26 | aburi explain <id> --output docs/alpha.md where docs/ does not exist | Creates the directory, writes the Markdown, exit 0 |
| CL27 | aburi init or aburi explain with an --output that names a directory, or whose parent path is an existing file | exit 2, the path and the remedy on stderr |
18. Design Decisions
18.1 Limiting Status-Class Exit Codes to Three
Only the four values 0 / 1 / 2 / 3. Respects Linux convention while reserving code 3 for the CI gate (--fail-on). Finer-grained exit codes increase the burden on consumers, so they are avoided.
18.2 Strict stdout / stderr Separation
To avoid confusion when CI uses 2>/dev/null or > result.txt, all progress and warnings go to stderr. Only result data (summary / JSON / Markdown) goes to stdout.
18.3 Why --fail-on Ships from the Start
A CI gate is the feature that delivers the most value at review adoption time. Once "automatically block PRs with dangerous changes" works, Aburi adoption accelerates sharply. Retrofitting it would require rewriting CI configurations, so it is provided from the start.
18.4 Why aburi diff Uses git worktree
Checking out the base ref would require stashing the current work, risking accidental loss of uncommitted changes. With git worktree, the base can be materialized at a separate path while the head's working directory is preserved.
18.5 Partial Matching in aburi explain
Typing the full id every time is burdensome. Returning candidates on a partial match and disambiguating when there are several is the practical UX. In the majority of cases without ID collisions there is 1 hit; ambiguity is an explicit error.
18.6 Why aburi vocab Is a Standalone Subcommand
Vocab is a query target independent of the IR. A standalone aburi vocab command has higher discoverability than a flag like aburi scan --show-vocab (it also appears on its own in the help output).
18.7 Why Environment Variables Substitute for CLI Flags
In CI / Docker / Makefiles, configuring via env is easier than threading flags through. Standard conventions (NO_COLOR / CI) are respected, and Aburi-specific envs (ABURI_*) are provided as well.
18.8 Why Planned Features Are Reserved
Reserving the names aburi watch / aburi doctor / aburi serve today prevents name collisions and compatibility breaks when they are added later. They are not implemented, but are documented.