bwrb audit
Validate vault files against schema and report issues, with optional interactive repair.
Synopsis
Section titled “Synopsis”bwrb audit [options] [target]The target argument is auto-detected as type, path (contains /), or where expression.
Options
Section titled “Options”Targeting
Section titled “Targeting”| Option | Description |
|---|---|
-t, --type <type> | Filter by type path |
-p, --path <glob> | Filter by file path pattern |
-w, --where <expr> | Filter by frontmatter expression (repeatable) |
-b, --body <query> | Filter by Markdown body content; YAML frontmatter is excluded |
-a, --all | Target all files (explicit vault-wide selector) |
Issue Filtering
Section titled “Issue Filtering”| Option | Description |
|---|---|
--only <issue-type> | Only report specific issue type |
--ignore <issue-type> | Ignore specific issue type |
--strict | Treat unknown fields as errors instead of warnings |
--allow-field <fields> | Allow additional fields beyond schema (repeatable) |
--mention-fuzzy-threshold <n> | Max edit-distance cap (0–5) for the unlinked-mention fuzzy “did you mean?” tier (default: config.mention_fuzzy_threshold or 2; candidate length also caps the effective distance) |
--no-mention-fuzzy | Disable the unlinked-mention fuzzy “did you mean?” tier entirely |
--mention-link-once | With --fix --auto: link at most one unlinked-mention occurrence per note/target pair, overriding config.mention_link_once |
--no-mention-link-once | With --fix --auto: link every eligible unlinked-mention occurrence, overriding config.mention_link_once |
--check-schema-docs | Also report schema types/fields that have no description |
Repair
Section titled “Repair”| Option | Description |
|---|---|
--fix | Interactive repair mode (writes by default; requires explicit targeting) |
--auto | With --fix: automatically apply unambiguous fixes |
--dry-run | With --fix: preview fixes without writing |
--execute | With --fix --auto: apply auto-fixes for execute-gated issues (for example trailing-whitespace) |
--retention-action <kind> | Explicitly preview or execute configured archive, tombstone, or delete retention action; requires --fix --only retention-due |
Repair mode writes by default and requires explicit targeting (selectors or --all).
Use --dry-run to preview fixes without writing.
Retention remediation
Section titled “Retention remediation”Types can declare retention (see the schema reference). Audit reports a due record as a
retention-due warning. Text output names the available action kinds, while JSON includes
the full configured actions in issue metadata. It uses one local-day snapshot for the
entire run; the deadline is inclusive. A missing or invalid clock is a diagnostic, never
an invented deadline, so no actions are suggested until the clock is repaired.
Retention is never an automatic fix. Target it explicitly, select the issue, and name an
action; omission of --execute is a dry run:
bwrb audit --all --fix --only retention-due --retention-action archivebwrb audit --all --fix --only retention-due --retention-action archive --executeBefore writing, Bowerbird re-reads the note under its mutation lock and confirms it is still due. Archive updates wikilinks, tombstone applies only the schema-declared patch, and delete refuses live lineage, backlinks, or typed relations.
unlinked-mention auto-fixes normally link every eligible exact/alias occurrence. Set
config.mention_link_once: true or pass --mention-link-once to make
--fix --auto write at most one new wikilink per note/target pair. Existing
wikilinks to that target in the note, including display forms like
[[Target|display]], count as covered, so no new link is written for that target.
Detection and reporting remain exhaustive; only fix application is limited.
Delete semantics in repair mode:
bwrb audit --fix(interactive only) can offer an explicit[delete note]action for clearly unrecoverable type problems (currentlyorphan-fileandinvalid-type).- Delete is never the default choice.
bwrb audit --fix --autoandbwrb audit --fix --auto --executenever delete files.bwrb audit --fix --output jsonis invalid (interactive repair and JSON mode are intentionally separated).
Output
Section titled “Output”| Option | Description |
|---|---|
--output <format> | Output format: text, json |
Issue Types
Section titled “Issue Types”| Type | Description |
|---|---|
orphan-file | File in managed directory but no type field |
invalid-type | Type field value not recognized in schema |
missing-required | Required field is missing |
empty-string-required | Required field is empty/whitespace/empty list |
invalid-option | Field value not in allowed option values |
unknown-field | Field not defined in schema (warning by default) |
wrong-directory | File location doesn’t match its type’s output_dir (a subdirectory of output_dir counts as correct). An owned note is exempt: it correctly lives under its owner at <owner-dir>/<field>/, so it is checked against that location, not the owned type’s own output_dir. A genuinely-misplaced owned note is reported as owned-wrong-location (below) instead of this code, so it is never double-reported |
owned-wrong-location | An owned note — one its owner declares in an owned field — is not under its declaring owner’s <owner-dir>/<field>/ subtree (error; auto-fixable). This covers a note moved out into a pooled directory, a note physically placed under a different valid owner’s owned subtree, and a note under the same owner but the wrong owned-field subfolder (e.g. an owner with both tracks and demos, where a note declared in demos sits under tracks/): in every case --fix restores it under the declaring owner and field (computed from ownership), never stranding it in the owned type’s top-level output_dir. The issue carries the resolving (declaring) ownerPath |
owned-ambiguous-owner | A note whose basename is declared as owned by two or more distinct owners for an owned field whose child type matches the note’s own type (error; never auto-fixable). Ownership is genuinely ambiguous, so the note is not auto-restored under a guessed owner — --fix --auto --execute leaves it untouched and reports it for manual resolution (remove the duplicate owned declaration, or rename the note so each owner’s declaration is unique). A correctly-filed note of a different type that merely shares a basename with such a declaration is not flagged — the matching declarations don’t apply to it |
invalid-forked-from | Reserved forked-from provenance is not a UUID string (error; flag-only) |
missing-lineage-id | A note declares forked-from but lacks its own valid UUID id (error; flag-only) |
dangling-forked-from | forked-from references a UUID not present on any discovered note (warning; flag-only). Provenance is retained because the source may be restored later; this is also the deliberate result of deleting a fork parent with bwrb delete --force |
duplicate-note-id | Two or more notes use the same stable UUID id (error; flag-only) |
fork-cycle | Following immediate forked-from references forms a cycle (error; flag-only). Traversal is cycle-safe and always terminates |
format-violation | Field value doesn’t match expected format (wikilink, etc.) |
stale-reference | Wikilink points to non-existent file |
trailing-whitespace | Trailing spaces/tabs on raw frontmatter key: value lines (warning; auto-fixable) |
wrong-scalar-type | Scalar value has wrong type for schema |
illegal-aliases | An alias-role field violates the Obsidian aliases format (array of non-empty, unique strings): an empty/whitespace entry, a duplicate entry, or a non-string entry (error — matching what bwrb new/bwrb edit reject on write; empty/whitespace + duplicate cases are auto-fixable, a non-string entry is flag-only — see below) |
unlinked-mention | A known entity’s name or registered alias appears in body prose as plain text but is not wikilinked (warning; exact/alias matches auto-fixable, fuzzy/ambiguous matches flag-only — see below) |
frequent-unlinked-term | A proper-noun-ish term mentioned frequently across the vault that has no note yet (warning; advisory heuristic, never auto-fixable — see below) |
missing-body-section | A heading section declared in the type’s body_sections is missing from the note body, or present at the wrong heading level (warning; auto-fixable — --fix appends the canonical heading scaffold — see below) |
broken-body-wikilink | A well-formed [[wikilink]] in the note body whose target resolves to no note via the alias-aware, case-insensitive note index (warning; flag-only — offers a “did you mean?” hint but never auto-links — see below) |
malformed-body-wikilink | Wikilink bracket syntax in the body that is broken — an empty target ([[]]/[[ ]]) or an unclosed [[ (warning; flag-only) |
broken-body-file-link | A relative markdown file/image link in the body — [text](path.md) /  — whose target does not exist on disk (warning; flag-only) |
relative-date-cycle | A Relative Dates equal chain loops back on itself (warning; flag-only) |
relative-date-contradiction | Multiple equal constraints on a relative-date field resolve to different positions (warning; flag-only) |
relative-date-bound-violation | A resolved relative date violates an after or before bound (warning; flag-only) |
relative-date-invalid-ref | A relative-date anchor reference is missing or ambiguous (warning; flag-only) |
missing-successor | A recurring note satisfies its trigger (e.g. status = done) but its chain field (next) is empty — a successor was never spawned (e.g. completed outside bwrb). Warning; auto-fixable (--fix spawns it, identical to the fast path) |
invalid-recurrence | A recurrence rule is broken at the config level — a malformed trigger, a non-date offset base, or a template that doesn’t exist (error; never auto-fixable — a config error gets the same safety net as data) |
retention-due | A type’s explicit retention clock is due, or its clock value is invalid. Warning; never auto-fixed. Remediation requires --only retention-due, explicit targeting, --retention-action, and --execute after reviewing the dry run |
Note: built-in fields (id, name, and reserved provenance field forked-from) are always allowed and do not produce unknown-field issues. Ordinary bwrb new --json, bwrb edit, template input, schema defaults, and audit fixes cannot mutate reserved fields. Use bwrb new --fork for a new child or guarded bwrb lineage adopt for known derivation between two existing notes; all stored provenance receives the lineage checks above.
Invalid option values inside list fields are reported as invalid-option with listIndex metadata, not a separate issue code.
For a date field with multiple: true (a list of dates), each element is validated against the field’s granularity and an invalid element is reported as invalid-date-format with listIndex metadata identifying the offending value. An invalid date string element is reported for manual correction (not auto-fixed); scalar date values are auto-normalized.
A numeric date element (e.g. an unquoted 2026, which YAML parses as a number) is treated exactly like a numeric scalar date: it is owned by the date check, so it is reported once — as invalid-date-format when it isn’t a valid date for the field’s granularity, or as wrong-scalar-type (“should be quoted as a string”) when it is a valid date. It is never additionally reported as invalid-list-element. This matches scalar date handling and what bwrb new/bwrb edit accept on write.
A valid numeric date element is auto-fixable (#673): --fix quotes that one element in place (e.g. 2026 → "2026"), preserving array order and every other element — it does not collapse the array to a single scalar (the whole-field scalar fixer would). The fix is idempotent and survives the YAML round-trip: the quoted value is written as a string and re-read as a string, so a second --fix pass finds nothing (the value never re-flips to a bare number). An invalid numeric date element (e.g. 13 at day granularity) is not auto-quoted — it stays an invalid-date-format flag for manual correction, since quoting a genuinely invalid date would only hide the problem.
An empty or whitespace-only date value is treated as unset, not as an invalid date — the same convention every optional field follows (an empty optional field never produces a format/type issue). So an optional date field stored as "" produces no invalid-date-format issue, matching what bwrb new/bwrb edit accept on write. An empty required date is reported once as empty-string-required (consistent with every other required field), and an empty element inside a date list is reported once as invalid-list-element — never additionally as invalid-date-format.
Alias hygiene (illegal-aliases)
Section titled “Alias hygiene (illegal-aliases)”An alias-role field must hold an array of non-empty, unique strings (the Obsidian aliases format). The write path (bwrb new/bwrb edit) rejects a non-array value, an empty/whitespace entry, a duplicate entry, or a non-string entry as a hard error. Audit reports the exact same conditions at error severity, so a note hand-written with duplicate or blank aliases no longer slips through as a mere warning — write and audit agree.
--fix applies the safe, idempotent subset:
- Empty/whitespace entries are dropped.
- Duplicate entries are de-duplicated, preserving the first occurrence (so order is stable).
This reuses the same dedupe-style cleanup as duplicate-list-values, extended to also drop blanks, and converges on re-run (a second --fix changes nothing). It never merges distinct aliases, so no meaningful data is lost.
A non-string alias entry (e.g. a bare number) is flag-only: bwrb can’t infer the intended text, so it is surfaced for a manual fix rather than auto-cleaned. The alias entry is never dropped or coerced — it survives untouched. A non-array alias value is reported as wrong-scalar-type.
Note: illegal-aliases is the sole owner of alias-field list cleanup. An alias-role field therefore never additionally produces a generic duplicate-list-values warning or an invalid-list-element issue — both are suppressed so blanks and duplicates are detected and fixed exactly once, by illegal-aliases, in a single idempotent pass. (Running both removers on the same field used to destroy a distinct alias when blanks preceded a real entry; see #617.) Those generic checks still apply to all other list fields unchanged.
Examples
Section titled “Examples”Basic Auditing
Section titled “Basic Auditing”# Check all files (report only)bwrb audit
# Check only tasksbwrb audit --type objective/task
# Check specific directorybwrb audit --path "Ideas/**"
# Check files with specific statusbwrb audit --where "status=active"
# Check files containing TODObwrb audit --body "TODO"Issue Filtering
Section titled “Issue Filtering”# Only missing required fieldsbwrb audit --only missing-required
# Ignore unknown fieldsbwrb audit --ignore unknown-field
# Strict mode: unknown fields are errorsbwrb audit --strict
# Allow specific extra fieldsbwrb audit --allow-field custom --allow-field legacyRepair Mode
Section titled “Repair Mode”# Fix issues across the entire vault (explicit targeting required)bwrb audit --all --fix
# Preview fixes across the entire vaultbwrb audit --all --fix --dry-run --auto
# Interactive fix mode (writes by default; requires explicit targeting)bwrb audit --all --fix
# Interactive fix mode for a subsetbwrb audit --path "Ideas/**" --fix
# Preview fixes without writingbwrb audit --path "Ideas/**" --fix --dry-run
# Auto-apply unambiguous fixes (requires --execute)bwrb audit --path "Ideas/**" --fix --auto --execute
# Preview auto-fixesbwrb audit --path "Ideas/**" --fix --auto
# Auto-link only the first eligible unlinked mention per note/target pairbwrb audit --path "Ideas/**" --fix --auto --execute --mention-link-onceTrailing whitespace hygiene semantics
Section titled “Trailing whitespace hygiene semantics”trailing-whitespace is a raw-frontmatter hygiene warning with narrow scope:
- It inspects YAML frontmatter
key: valuelines only (single-line key/value entries). - Quoted values: whitespace inside the quotes is allowed; only whitespace after the closing quote is flagged.
- Block scalars (
|or>) are excluded; content lines inside the block are ignored. - It trims both spaces and tabs at line end.
- The fix is minimal-diff line trim only (no YAML reserialization).
Known non-matches:
- Whitespace before inline comments is not end-of-line whitespace (for example
status: raw # note) and is not flagged. - Nested
key:entries with no inline value are not treated as single-line key/value entries.
Auto-fix gating for this issue:
| Command | Result |
|---|---|
bwrb audit --fix | Interactive mode. If you confirm the fix, the line trim is written. |
bwrb audit --fix --auto | Preview only when trailing-whitespace is present (no writes). |
bwrb audit --fix --auto --execute | Applies trailing-whitespace auto-fixes to disk. |
If a run contains trailing-whitespace and --auto is used without --execute, treat that run as preview-only.
# Preview trailing-whitespace auto-fixes (no writes)bwrb audit --only trailing-whitespace --fix --auto
# Apply trailing-whitespace auto-fixesbwrb audit --only trailing-whitespace --fix --auto --executeExample on-disk behavior (minimal diff):
before: status: "raw"after: status: "raw"bwrb audit --fix refuses to run without a TTY when interactive fixes are needed (use --fix --auto for non-interactive previews, and add --execute for execute-gated auto-fixes like trailing-whitespace).
Unlinked-mention semantics (web integrity)
Section titled “Unlinked-mention semantics (web integrity)”unlinked-mention is the closed-world web-integrity check: bwrb knows every note (by name) and, via the alias-role field, every registered alias. It scans note bodies for any known name or alias that appears as plain text but is not wikilinked, so the vault stays a connected graph instead of islands.
It enforces a strict trust line — only matches bwrb can be certain about are auto-linked; everything uncertain becomes a visible review item that is never silently resolved:
| Tier | What it matches | Behavior |
|---|---|---|
| Exact / alias | The literal note name, or a registered alias, present as unlinked plain text and resolving to exactly one entity. Single-word note names must match canonical casing exactly, and common English single-word note names are skipped; aliases are explicit and stay case-insensitive. | Trusted → auto-fixable. --fix --auto --execute converts it to a wikilink (--fix --auto alone previews). |
| Fuzzy | A capitalized phrase that is a near (small Levenshtein distance) match to a known name, e.g. Steve Yeg ≈ Steve Yegge. Single capitalized common words are skipped. | Review item (“did you mean?”) — never auto-linked. Distance threshold is configurable. |
| Ambiguous | A surface that matches multiple distinct entities/aliases, e.g. Mercury | Never auto-resolved by --auto. Listed as a review item with all candidates; interactive --fix offers a pick-a-candidate prompt. |
Auto-fix output format:
- When the surface text equals the canonical note name, the fix uses a plain wikilink:
[[Steve Yegge]]. - When the surface differs from the canonical name (an alias, or different casing), the fix preserves the author’s text via the Obsidian display-alias form:
[[Entity|surface]].
# Exact name mentionbefore: I spoke with Steve Yegge today.after: I spoke with [[Steve Yegge]] today.
# Alias mention (display form preserves the surface)before: Notes from Stevey.after: Notes from [[Steve Yegge|Stevey]].False-positive guards (none of these are scanned or rewritten):
- Text already inside
[[wikilinks]], markdown links/images, fenced code blocks, inline code spans, and bare URLs. - A note never flags a mention of its own name or alias.
- Matching is word-boundary aware (
Adadoes not match insideAdafruitorCanada). Multi-word names and aliases are case-insensitive, but single-word note names are case-exact (Timecan match theTimenote;timecannot). Common English single-word note names such asTime,Push, orCurrentare not exact-matched at all, because sentence-initial capitalization makes them noisy. - Single-word note-name matches with capitalized canonical casing are skipped at positions where capitalization carries no signal: the start of the body, sentence starts after
.,!, or?, line starts, markdown list starts, headings, and blockquotes. Multi-word names, aliases, and lowercase canonical names are unaffected. - Corpus calibration catches vault-local common words that the static list cannot know. During an audit run, bwrb counts single-token word casing across the full vault snapshot’s prose (with code, links, URLs, and wikilinks masked for prose matching), and separately counts wikilink targets/display text as entity evidence. A single-word note name is dropped for that run when it appears in at least
mention_corpus_min_notesdistinct non-self notes and the non-canonical-case occurrence share is strictly greater thanmention_corpus_noncanonical_ratio. Dropped names are also removed from fuzzy suggestions andfrequent-unlinked-termnudges; declared aliases remain linkable.
Only frontmatter is exempt from scanning — this detection looks at body prose only. Surfaces shorter than three characters are ignored to avoid noise. The entity index is built once per run and each body is scanned in a single pass, so cost scales with body size rather than notes × entities. Scoped audits such as --path still build the mention index and corpus calibration from the full vault snapshot, then scan only the targeted files for issues.
# Report unlinked mentions onlybwrb audit --only unlinked-mention
# Auto-link trusted (exact/alias) mentions across the Notes directorybwrb audit --path "Notes/**" --fix --auto --executeFuzzy and ambiguous mentions are never modified by --fix --auto. Fuzzy matches are always flag-only (resolve them with interactive --fix, which skips them with the suggestion, or by editing manually). Ambiguous matches can be resolved in an interactive --fix session — see below.
Tuning the fuzzy tier (#622)
Section titled “Tuning the fuzzy tier (#622)”The fuzzy “did you mean?” tier is deliberately conservative. The configured threshold is a cap (default 2), and bwrb also scales the effective distance by candidate length: min(configured threshold, floor(candidate length / 4)). In practice, 4–7 character candidates allow distance 1, 8–11 allow distance 2, and short sentence-initial words like This or What do not get a loose edit budget.
- Per run (flag):
--mention-fuzzy-threshold <n>sets the max edit-distance cap, wherenis an integer in0–5. Raising it can surface looser matches only when candidate length also permits that distance; lowering it surfaces fewer.0(or--no-mention-fuzzy) disables the fuzzy tier entirely. Out-of-range or non-integer values produce a clear validation error. - Persistent (config): set
mention_fuzzy_thresholdin your vaultconfig(integer0–5). The flag, when present, overrides the config value for that run.
# Raise the fuzzy cap for one run (candidate length still applies)bwrb audit --only unlinked-mention --mention-fuzzy-threshold 3
# Turn off fuzzy suggestions entirelybwrb audit --only unlinked-mention --no-mention-fuzzy// .bwrb/schema.json — persist a higher cap for every run{ "config": { "mention_fuzzy_threshold": 3 }}The threshold affects only the fuzzy tier. Exact/alias auto-fix and ambiguous flagging are unchanged.
Tuning corpus calibration (#783)
Section titled “Tuning corpus calibration (#783)”Corpus calibration is enabled by default and is intentionally conservative:
mention_corpus_calibration:trueby default. Set tofalseto restore static-list-only single-word name behavior.mention_corpus_min_notes: default3. The word must appear in at least this many distinct non-self notes’ prose before damping can apply.mention_corpus_noncanonical_ratio: default0.5. The non-canonical-case share must be strictly greater than this value, so exactly0.5keeps the surface.
For capitalized names such as Builder, exact canonical-case occurrences count as proper-noun evidence. For lowercase canonical names, casing provides no proper-noun signal, so frequent lowercase prose counts as common usage. Aliases are exempt either way.
// .bwrb/schema.json — make corpus damping more conservative{ "config": { "mention_corpus_min_notes": 5, "mention_corpus_noncanonical_ratio": 0.75 }}Excluding mention targets
Section titled “Excluding mention targets”Use config exclusions when whole classes of notes should not become
unlinked-mention targets, such as imported books or contacts. Excluded notes
are still audited as source documents — their bodies can still flag mentions of
non-excluded entities. Exclusion only controls whether a note contributes its
own name and aliases to the target index.
mention_exclude_types: type names to exclude. Descendant types are excluded too, so excludingreferencealso excludes abooktype that extends it.mention_exclude_paths: vault-relative path globs to exclude with the same path matching used by--path, such asImports/**orBooks/.
Excluded names and aliases are also left out of fuzzy “did you mean?”
suggestions. Their known surfaces suppress frequent-unlinked-term nudges, so
bwrb will not suggest creating a new note for an excluded note that already
exists.
// .bwrb/schema.json — keep imported references out of mention targets{ "config": { "mention_exclude_types": ["book", "contact"], "mention_exclude_paths": ["Imports/**"] }}Resolving ambiguous mentions interactively (#622)
Section titled “Resolving ambiguous mentions interactively (#622)”When a surface matches more than one entity (for example Mercury resolving to both a [[Mercury]] note and a person aliased Mercury), bwrb never guesses. In an interactive --fix session (a real TTY), an ambiguous mention now prompts you to pick a candidate (or skip / quit):
Notes/Daily.md ⚠ Ambiguous unlinked mention on line 1: 'Mercury' could link to 2 entities Resolve ambiguous mention 'Mercury' to: 1. Freddie 2. Mercury 3. [skip] 4. [quit]Choosing a candidate rewrites the plain-text mention to a wikilink using the same rewrite as exact/alias auto-fix: [[Chosen]], or [[Chosen|surface]] when the surface text differs from the chosen note name (so picking Freddie for the surface Mercury yields [[Freddie|Mercury]]). The same masking and word-boundary guarantees apply, so it never relinks inside code or existing links, and it is idempotent.
This is interactive-only. In --auto / --fix --auto and any non-TTY (headless) run, ambiguous mentions remain flag-only and are never auto-resolved.
Frequent-unlinked-term semantics (open-world nudge)
Section titled “Frequent-unlinked-term semantics (open-world nudge)”frequent-unlinked-term is the open-world counterpart to unlinked-mention. Where unlinked-mention keeps known entities linked, this detection points at entities that probably should exist but don’t yet — it surfaces proper-noun-ish terms mentioned a lot across the vault that have no note. It attacks the failure mode where the AI agent (or you) never links something because it doesn’t know the entity exists in the first place. Create the note, and unlinked-mention keeps it wired up forever after.
Advisory only — this detection NEVER takes action. It is always reported as a warning and is never auto-fixable: there is no --fix path for it, --fix --auto ignores it, and interactive --fix lists it as a manual item only. Because it never acts, it is allowed to be a little noisy; the thresholds exist purely to keep the report readable, not for correctness. Just ignore any suggestion that isn’t a real entity.
The heuristic (and its honest limits). Discovering an unknown “thing” in prose without an LLM is inherently fuzzy. The detection approximates proper nouns with runs of Capitalized words (1–3 words: Rust, Steve Yegge, New York Times), counted only in prose. Known limits, stated plainly:
- No semantic understanding. A repeated capitalized non-entity (e.g. a recurring section title) can surface. That’s expected — ignore it.
- Sentence-start noise. Any word can be capitalized simply by starting a sentence. To suppress this, single-word candidates are held to a stricter bar (longer minimum length, full stopword filtering, and must appear at least once mid-sentence), and a small stopword list (days, months, pronouns, common sentence openers) is filtered. Leading filler words are stripped from phrases (
The Rust Foundation→Rust Foundation). - Multi-word phrases are favored over single words, because a 2–3 word capitalized phrase is far more likely to be a genuine proper noun.
Thresholds (defaults). A term is surfaced only when it appears ≥ 4 times in total across ≥ 2 distinct notes. These keep one-off capitalizations out of the report. (The thresholds live as documented constants — FREQUENT_TERM_DEFAULTS — in src/lib/audit/frequent-unlinked-term.ts; tune them there.)
Exclusions (the closed-world handoff).
- A term whose text matches an existing note name or a registered alias is never surfaced — that’s
unlinked-mention’s job. The two detections never overlap. - Terms that are already wikilinked are not counted: existing
[[wikilinks]], markdown links, fenced code blocks, inline code, and bare URLs are masked out (the same maskingunlinked-mentionuses), so only prose mentions count. A term that is always linked has zero prose mentions and can’t reach the threshold.
Because the threshold is vault-wide, this detection aggregates across all scanned notes and reports its findings under a single (vault-wide) heading rather than against one file. Each finding lists the term, how many times it was mentioned, and the notes it appeared in.
# Report frequent unlinked terms onlybwrb audit --only frequent-unlinked-term
# Suppress them (e.g. while focusing on fixable issues)bwrb audit --ignore frequent-unlinked-termMissing-body-section semantics (body structure)
Section titled “Missing-body-section semantics (body structure)”missing-body-section validates the markdown body, not the frontmatter. The schema heavily enforces YAML frontmatter, but the body has historically been unchecked: bwrb new and bwrb edit scaffold the heading sections declared in a type’s body_sections (e.g. a bug gets ## Steps to Reproduce), but nothing stops a user from later deleting or renaming one. This detection re-checks, at audit time, that every declared section heading is still present.
What it checks:
- For each section declared in the resolved type’s
body_sections(including nestedchildren), the body must contain a matching ATX heading — the exact title at the declaredlevel. Trailing whitespace and ATX closing hashes (## Title ##) are tolerated. - A heading written inside a fenced code block, inline code, or a link does not satisfy the requirement — the same body masking used by
unlinked-mentionis applied, so only real prose headings count. - If a heading with the right title exists but at the wrong level, it is still flagged (with the offending
lineNumber), and the fix appends a correctly-leveled heading rather than rewriting yours.
What it does not check: it validates heading presence/structure only — not whether the body content under a section is filled in (bullets, checkboxes, paragraph counts). It also does not validate body links; that is the job of the body-link checks (broken-body-wikilink, malformed-body-wikilink, broken-body-file-link), which never overlap with this structural check.
Auto-fixable. Adding a declared heading is a safe, deterministic, additive repair — --fix appends the missing section using the same scaffold bwrb new/bwrb edit emit (heading + the section’s content_type placeholder), so it never deletes or rewrites existing prose. It is idempotent: a re-run finds the now-present heading and does nothing (no duplicate headings). Types that declare no body_sections are never flagged.
# Report missing body sections onlybwrb audit --only missing-body-section
# Append missing sections automaticallybwrb audit --fix --auto --execute --all
# Suppress the checkbwrb audit --ignore missing-body-sectionBody-link validation semantics (link integrity)
Section titled “Body-link validation semantics (link integrity)”The body-link checks validate the actual links written in a note’s markdown body — the link-integrity counterpart to missing-body-section’s structure check. They are all flag-only: bwrb reports the problem (often with a suggestion) but never edits body prose links, because the intended target can’t be known safely.
There are three codes:
broken-body-wikilink— a well-formed[[Target]](also[[Target|display]],[[Target#heading]], and embeds![[Target]]) whose target resolves to no note. Resolution uses the same alias-aware, case-insensitive note index that relation fields use: a link written as[[an alias]]or[[REALNOTE]]resolves wherever the note’s name or a registered alias does. Relation fields also use theirsourcetype to resolve bare same-name targets when exactly one candidate matches that source; multiple matching source-type targets remain ambiguous and should be path-qualified. When a near-named note exists, a flag-only “did you mean?” hint is offered. An ambiguous body wikilink (its text matches more than one note) is not flagged — it is still a working Obsidian link (Obsidian resolves by proximity); ambiguity is only an error in relation fields, which is a separate frontmatter check.malformed-body-wikilink— bracket syntax that looks like a wikilink but is broken: an empty/whitespace-only target ([[]],[[ ]]) or an unclosed[[with no matching]].broken-body-file-link— a markdown file or image link,[text](path)/, whose relative target does not exist on disk. The path is resolved relative to the note’s own directory (a leading/is treated as the vault root, Obsidian-style), with percent-encoding (e.g.%20) decoded for the existence check. External targets are never checked: URLs with a scheme (https:,mailto:,tel:…), protocol-relative//hostlinks, and pure in-page anchors (#section).
All three respect code context: links written inside fenced code blocks or inline code are masked out and never flagged. Reported issues carry the offending lineNumber and the raw link value.
Relationship to unlinked-mention. These checks are the inverse of unlinked-mention and never overlap with it:
unlinked-mentionflags a known entity’s name appearing as plain text that should be a wikilink (it masks out existing[[...]]before scanning).broken-body-wikilinkflags an actual[[...]]link that points nowhere (it looks only at existing wikilinks).
They are also disjoint from frequent-unlinked-term (an open-world plain-text heuristic) and from the relation-field checks (stale-reference, malformed-wikilink, ambiguous-link-target), which validate frontmatter values only.
# Report broken body wikilinks onlybwrb audit --only broken-body-wikilink
# Suppress broken file/image link checksbwrb audit --ignore broken-body-file-linkNon-interactive mode
Section titled “Non-interactive mode”# Safe automation (no prompts)bwrb audit --fix --auto --execute --all# Report-only JSON for CI/daemonsbwrb audit --output jsonJSON recommendations for delete-eligible issues
Section titled “JSON recommendations for delete-eligible issues”bwrb audit --output json is report-only and never deletes files. For delete-eligible findings (such as orphan-file or invalid-type), JSON output includes recommendation metadata so automation can decide on follow-up actions.
{ "code": "invalid-type", "autoFixable": false, "meta": { "recommendation": { "action": "delete-note", "reason": "invalid-type", "interactiveOnly": true, "source": "audit-fix" } }}Phase 5: Type coercion fixes
Section titled “Phase 5: Type coercion fixes”# Auto-coerce unambiguous string scalarsbwrb audit --only wrong-scalar-type --fix --auto --execute --all
# Fix malformed wikilinks interactivelybwrb audit --only format-violation --fix --allEmpty required values ("", whitespace-only strings, or empty lists) are reported as empty-string-required and repaired interactively (or auto-filled when a default exists).
CI Integration
Section titled “CI Integration”# JSON output for CIbwrb audit --output json
# Fail build on schema violationsbwrb audit --output json || exit 1Type Resolution
Section titled “Type Resolution”Audit resolves each file’s type from its frontmatter type field:
- If
typeis missing: reportsorphan-fileand skips type-dependent checks - If
typeis invalid: reportsinvalid-typeand skips type-dependent checks - Type-dependent checks (
missing-required,invalid-option,wrong-directory) require valid type resolution
Use --type to filter by type; it does not fix missing type fields.
Exit Codes
Section titled “Exit Codes”| Code | Meaning |
|---|---|
0 | No violations found, or --fix --auto completed (remaining issues are reported) |
1 | Violations found in report-only mode, or --fix (interactive) left remaining issues |
See Also
Section titled “See Also”- Validation and Audit — Audit concepts
- bwrb bulk — Batch fix operations
- Targeting Model — Selector reference