Skip to content

citations

Ary Rabelo edited this page Jul 22, 2026 · 3 revisions

Now I have enough to write the page.

Source Citations

Relevant source files

  • src/repodocs/citations.py
  • tests/test_fix_citation_repair.py

Overview

Every generated wiki page must tie its prose to real locations in the source repository. src/repodocs/citations.py defines the citation format, a validator (citation_error), a deterministic repair pass (repair_citations) that fixes honest-but-malformed citations, and an enforcement gate (enforce_citations) that blocks publishing when a page's claims aren't backed by a valid citation.

Sources: src/repodocs/citations.py:L1-L12

Citation format

A full citation is a Markdown link whose label and href both encode a path and line range:

<a href="https://github.com/aryrabelo/repodocs/blob/51ba8b18a6abbf0ed1789af067cb1c932993f747/path/to/file.py#L10-L42" target="_blank" rel="noopener">path/to/file.py:L10-L42</a>

Two regexes recognize this shape. CITATION_RE matches the bare label [path:La-Lb], and FULL_CITATION_RE matches the label plus its linked href [path:La-Lb](href). The href itself is parsed by _CITE_HREF (defined in gitlinks.py), which requires a non-http(s) repo-relative path followed by #La[-Lb].

Sources: src/repodocs/citations.py:L12-L12, src/repodocs/citations.py:L38-L38

Validating a citation: citation_error

citation_error(repo, label_path, a, b, href) checks one citation and returns a reason string when it is dishonest, or None when it's valid. It never echoes file contents. The checks, in order:

  1. The href must match _CITE_HREF (a repo-relative path#La[-Lb] anchor).
  2. The label's path must equal the href's path.
  3. The label's line range must equal the href's line range.
  4. The range must be non-degenerate (a >= 1, a <= b).
  5. The cited path must resolve inside the repo via safe_repo_file (rejects missing files and paths that escape the repo).
  6. The end line b must not exceed the file's real line count (count_lines).

Sources: src/repodocs/citations.py:L55-L75

Repairing malformed citations: repair_citations

Models sometimes generate citations that are honest in intent but malformed in mechanical ways. repair_citations(repo, text) rewrites the text deterministically, canonicalizing each [label](href) link whose href is a repo-relative line anchor into [path:La-Lb](path#La-Lb). It handles three specific cases:

Case Repair
Label missing the L prefix, or a single-line label (path:3) Normalized to path:La-Lb matching the href
Path doesn't exist but uniquely matches a tracked file by suffix (model dropped a leading src/<pkg>/ prefix) Path rewritten to the unique tracked match
End line overshoots the file's real length (off-by-one, e.g. citing L6 in a 5-line file) Clamped to the file's actual line count

The rewrite only fires when the label and href agree on path and range to begin with (repl bails via lm.group(1) != hm.group(1) or (la, lb) != (ha, hb)), and when resolve() finds a tracked file with the start line still in bounds. Anything ambiguous — path/range disagreement, no unique suffix match, or a start line past EOF — is left untouched for enforce_citations to block later.

resolve(path, a, b) first tries safe_repo_file directly; if that fails, it searches git ls-files output for tracked paths ending in "/" + path and only proceeds if exactly one match exists. It then clamps b to min(b, count_lines(target)) and requires 1 <= a <= end.

Sources: src/repodocs/citations.py:L81-L124

The test suite in tests/test_fix_citation_repair.py exercises each repair case directly: clamping an end-of-file overshoot (test_repair_clamps_end_of_file_overshoot), leaving an already-valid citation untouched, leaving a hallucinated/missing path untouched, leaving a start-past-EOF citation untouched, resolving a dropped src/pkg/ prefix via a unique tracked-file suffix match (and combining that with an overshoot clamp), adding a missing L prefix to a label, normalizing a single-line citation to a range, and leaving a label/href path mismatch untouched as ambiguous.

Sources: tests/test_fix_citation_repair.py:L1-L74

Requiring evidence: requires_evidence

requires_evidence(md_text) decides whether a page needs at least one citation. It walks the page line by line, tracking whether it is inside the ## Relevant source files section. Bullet list items (- / * ) are exempt only while inside that section — a bullet list elsewhere in the body still counts as content requiring a citation. Lines starting with sources: (case-insensitive) are always skipped. The function returns True as soon as it finds any other non-empty content, meaning the page has prose beyond its title and source-file list and therefore needs backing citations.

Sources: src/repodocs/citations.py:L127-L145

Collecting violations: citation_problems

citation_problems(repo, mds) scans a list of generated Markdown files and returns (page, citation-or-'-', reason) tuples for every violation. For each file:

  1. Text is passed through _strip_noncounted, which removes HTML comments (_HTML_COMMENT_RE), fenced code blocks (_FENCED_CODE_RE), and inline code spans (_INLINE_CODE_RE) — so a citation hidden inside a comment or code sample can't satisfy the gate; only citations in rendered prose count.
  2. Every FULL_CITATION_RE match is validated with citation_error; failures are recorded as problems, successes increment a valid counter.
  3. Every bare CITATION_RE label match that wasn't already covered by a full-citation span is itself flagged as "citation label has no linked href" — a [path:La-Lb] with no linked href is a violation, not just a malformed linked one.
  4. If no valid citation was found anywhere in the page and requires_evidence says the page has real content, the page itself is flagged with "content page has no valid source citation".

Sources: src/repodocs/citations.py:L148-L174

Locating content pages and enforcing the gate

wiki_content_pages(out, subdirs) collects the generated *.md pages under the output directory, excluding index.md. With subdirs=True (the default), it also includes one level of subdirectories other than assets/ — this is how translated pages (e.g. out/pt/*.md) are picked up.

Sources: src/repodocs/citations.py:L177-L185

enforce_citations(repo, out, subdirs, cmd) is the blocking gate: it calls citation_problems over wiki_content_pages, and if any problems exist, calls die(...) with a message naming the command (cmd), listing up to 12 page cite -- reason entries, and a (+N more) suffix if there are more. This is what prevents publishing pages whose "source-cited" promise doesn't hold.

Sources: src/repodocs/citations.py:L188-L194

Non-blocking linting: lint_citations

Separately, lint_citations(repo, out, pages) performs a lighter-weight, warning-only check: for each page's output file, it scans for bare CITATION_RE labels (not full linked citations) and reports to stderr any citation whose file doesn't resolve via safe_repo_file, or whose range is out of bounds (a < 1 or a > b or b > n). Unlike enforce_citations, this does not raise or block — it prints "citation lint (warnings):" with details, or "citation lint: ok" if nothing is found.

Sources: src/repodocs/citations.py:L15-L35

Citation lifecycle

Source Citations diagram

Sources: src/repodocs/citations.py:L81-L124, src/repodocs/citations.py:L148-L194

Clone this wiki locally