Skip to content

bwrb audit

Validate vault files against schema and report issues, with optional interactive repair.

Terminal window
bwrb audit [options] [target]

The target argument is auto-detected as type, path (contains /), or where expression.

OptionDescription
-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, --allTarget all files (explicit vault-wide selector)
OptionDescription
--only <issue-type>Only report specific issue type
--ignore <issue-type>Ignore specific issue type
--strictTreat 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-fuzzyDisable the unlinked-mention fuzzy “did you mean?” tier entirely
--mention-link-onceWith --fix --auto: link at most one unlinked-mention occurrence per note/target pair, overriding config.mention_link_once
--no-mention-link-onceWith --fix --auto: link every eligible unlinked-mention occurrence, overriding config.mention_link_once
--check-schema-docsAlso report schema types/fields that have no description
OptionDescription
--fixInteractive repair mode (writes by default; requires explicit targeting)
--autoWith --fix: automatically apply unambiguous fixes
--dry-runWith --fix: preview fixes without writing
--executeWith --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.

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:

Terminal window
bwrb audit --all --fix --only retention-due --retention-action archive
bwrb audit --all --fix --only retention-due --retention-action archive --execute

Before 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 (currently orphan-file and invalid-type).
  • Delete is never the default choice.
  • bwrb audit --fix --auto and bwrb audit --fix --auto --execute never delete files.
  • bwrb audit --fix --output json is invalid (interactive repair and JSON mode are intentionally separated).
OptionDescription
--output <format>Output format: text, json
TypeDescription
orphan-fileFile in managed directory but no type field
invalid-typeType field value not recognized in schema
missing-requiredRequired field is missing
empty-string-requiredRequired field is empty/whitespace/empty list
invalid-optionField value not in allowed option values
unknown-fieldField not defined in schema (warning by default)
wrong-directoryFile 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-locationAn 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-ownerA 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-fromReserved forked-from provenance is not a UUID string (error; flag-only)
missing-lineage-idA note declares forked-from but lacks its own valid UUID id (error; flag-only)
dangling-forked-fromforked-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-idTwo or more notes use the same stable UUID id (error; flag-only)
fork-cycleFollowing immediate forked-from references forms a cycle (error; flag-only). Traversal is cycle-safe and always terminates
format-violationField value doesn’t match expected format (wikilink, etc.)
stale-referenceWikilink points to non-existent file
trailing-whitespaceTrailing spaces/tabs on raw frontmatter key: value lines (warning; auto-fixable)
wrong-scalar-typeScalar value has wrong type for schema
illegal-aliasesAn 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-mentionA 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-termA proper-noun-ish term mentioned frequently across the vault that has no note yet (warning; advisory heuristic, never auto-fixable — see below)
missing-body-sectionA 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-wikilinkA 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-wikilinkWikilink bracket syntax in the body that is broken — an empty target ([[]]/[[ ]]) or an unclosed [[ (warning; flag-only)
broken-body-file-linkA relative markdown file/image link in the body — [text](path.md) / ![alt](img.png) — whose target does not exist on disk (warning; flag-only)
relative-date-cycleA Relative Dates equal chain loops back on itself (warning; flag-only)
relative-date-contradictionMultiple equal constraints on a relative-date field resolve to different positions (warning; flag-only)
relative-date-bound-violationA resolved relative date violates an after or before bound (warning; flag-only)
relative-date-invalid-refA relative-date anchor reference is missing or ambiguous (warning; flag-only)
missing-successorA 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-recurrenceA 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-dueA 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.

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.

Terminal window
# Check all files (report only)
bwrb audit
# Check only tasks
bwrb audit --type objective/task
# Check specific directory
bwrb audit --path "Ideas/**"
# Check files with specific status
bwrb audit --where "status=active"
# Check files containing TODO
bwrb audit --body "TODO"
Terminal window
# Only missing required fields
bwrb audit --only missing-required
# Ignore unknown fields
bwrb audit --ignore unknown-field
# Strict mode: unknown fields are errors
bwrb audit --strict
# Allow specific extra fields
bwrb audit --allow-field custom --allow-field legacy
Terminal window
# Fix issues across the entire vault (explicit targeting required)
bwrb audit --all --fix
# Preview fixes across the entire vault
bwrb audit --all --fix --dry-run --auto
# Interactive fix mode (writes by default; requires explicit targeting)
bwrb audit --all --fix
# Interactive fix mode for a subset
bwrb audit --path "Ideas/**" --fix
# Preview fixes without writing
bwrb audit --path "Ideas/**" --fix --dry-run
# Auto-apply unambiguous fixes (requires --execute)
bwrb audit --path "Ideas/**" --fix --auto --execute
# Preview auto-fixes
bwrb audit --path "Ideas/**" --fix --auto
# Auto-link only the first eligible unlinked mention per note/target pair
bwrb audit --path "Ideas/**" --fix --auto --execute --mention-link-once

trailing-whitespace is a raw-frontmatter hygiene warning with narrow scope:

  • It inspects YAML frontmatter key: value lines 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:

CommandResult
bwrb audit --fixInteractive mode. If you confirm the fix, the line trim is written.
bwrb audit --fix --autoPreview only when trailing-whitespace is present (no writes).
bwrb audit --fix --auto --executeApplies trailing-whitespace auto-fixes to disk.

If a run contains trailing-whitespace and --auto is used without --execute, treat that run as preview-only.

Terminal window
# Preview trailing-whitespace auto-fixes (no writes)
bwrb audit --only trailing-whitespace --fix --auto
# Apply trailing-whitespace auto-fixes
bwrb audit --only trailing-whitespace --fix --auto --execute

Example 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:

TierWhat it matchesBehavior
Exact / aliasThe 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).
FuzzyA capitalized phrase that is a near (small Levenshtein distance) match to a known name, e.g. Steve YegSteve Yegge. Single capitalized common words are skipped.Review item (“did you mean?”) — never auto-linked. Distance threshold is configurable.
AmbiguousA surface that matches multiple distinct entities/aliases, e.g. MercuryNever 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 mention
before: 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 (Ada does not match inside Adafruit or Canada). Multi-word names and aliases are case-insensitive, but single-word note names are case-exact (Time can match the Time note; time cannot). Common English single-word note names such as Time, Push, or Current are 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_notes distinct non-self notes and the non-canonical-case occurrence share is strictly greater than mention_corpus_noncanonical_ratio. Dropped names are also removed from fuzzy suggestions and frequent-unlinked-term nudges; 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.

Terminal window
# Report unlinked mentions only
bwrb audit --only unlinked-mention
# Auto-link trusted (exact/alias) mentions across the Notes directory
bwrb audit --path "Notes/**" --fix --auto --execute

Fuzzy 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.

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, where n is an integer in 05. 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_threshold in your vault config (integer 05). The flag, when present, overrides the config value for that run.
Terminal window
# Raise the fuzzy cap for one run (candidate length still applies)
bwrb audit --only unlinked-mention --mention-fuzzy-threshold 3
# Turn off fuzzy suggestions entirely
bwrb 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.

Corpus calibration is enabled by default and is intentionally conservative:

  • mention_corpus_calibration: true by default. Set to false to restore static-list-only single-word name behavior.
  • mention_corpus_min_notes: default 3. The word must appear in at least this many distinct non-self notes’ prose before damping can apply.
  • mention_corpus_noncanonical_ratio: default 0.5. The non-canonical-case share must be strictly greater than this value, so exactly 0.5 keeps 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
}
}

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 excluding reference also excludes a book type that extends it.
  • mention_exclude_paths: vault-relative path globs to exclude with the same path matching used by --path, such as Imports/** or Books/.

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 FoundationRust 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 masking unlinked-mention uses), 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.

Terminal window
# Report frequent unlinked terms only
bwrb audit --only frequent-unlinked-term
# Suppress them (e.g. while focusing on fixable issues)
bwrb audit --ignore frequent-unlinked-term

Missing-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 nested children), the body must contain a matching ATX heading — the exact title at the declared level. 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-mention is 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.

Terminal window
# Report missing body sections only
bwrb audit --only missing-body-section
# Append missing sections automatically
bwrb audit --fix --auto --execute --all
# Suppress the check
bwrb audit --ignore missing-body-section
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 their source type 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) / ![alt](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 //host links, 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-mention flags a known entity’s name appearing as plain text that should be a wikilink (it masks out existing [[...]] before scanning).
  • broken-body-wikilink flags 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.

Terminal window
# Report broken body wikilinks only
bwrb audit --only broken-body-wikilink
# Suppress broken file/image link checks
bwrb audit --ignore broken-body-file-link
Terminal window
# Safe automation (no prompts)
bwrb audit --fix --auto --execute --all
# Report-only JSON for CI/daemons
bwrb audit --output json

JSON 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"
}
}
}
Terminal window
# Auto-coerce unambiguous string scalars
bwrb audit --only wrong-scalar-type --fix --auto --execute --all
# Fix malformed wikilinks interactively
bwrb audit --only format-violation --fix --all

Empty required values ("", whitespace-only strings, or empty lists) are reported as empty-string-required and repaired interactively (or auto-filled when a default exists).

Terminal window
# JSON output for CI
bwrb audit --output json
# Fail build on schema violations
bwrb audit --output json || exit 1

Audit resolves each file’s type from its frontmatter type field:

  • If type is missing: reports orphan-file and skips type-dependent checks
  • If type is invalid: reports invalid-type and 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.

CodeMeaning
0No violations found, or --fix --auto completed (remaining issues are reported)
1Violations found in report-only mode, or --fix (interactive) left remaining issues