Attribution

How the pipeline decides who wrote something and how much to trust it. Every decision records its evidence, so a wrong answer is traceable to the rule responsible.

Precedence

Signals are tried in order; the first that produces an answer wins.

1. Git history — authoritative

For a tracked file, attribute/git.py supplies the contributors and their commit counts. The decision that matters:

That single test separates your own repository from a clone of someone else’s, which no other signal can do. It works per file, so a fork where you changed three files attributes those three to you and the rest to upstream.

Real output from this corpus:

reference  agave         owner=anza-xyz     top=Brooks           git-log:no-self-commits
reference  chainlink     owner=smartcontr…  top=Jordan Krage     git-log:no-self-commits
reference  bitchat       owner=rickmark     top=Vignesh Skanda   git-log:no-self-commits
authored   demuxusb      README.md                               git-log:self-commits:4/4
authored   integrity-coin README.md                              git-log:self-commits:4/4

Note bitchat: the remote is under your account, but you never touched that README, so it is reference. Ownership of the repository is not authorship of the file.

Performance. git log --follow per file across ~18k tracked documents would mean 18k git invocations. Instead one git log --name-only --no-renames pass per repository builds a path→authors map in memory. Measured: 60 repositories, 277,796 tracked paths, 29 seconds total.

The tradeoff is --no-renames. Following renames requires per-file history; a renamed file is attributed from commits touching its current path, which in practice still identifies the right person.

2. Embedded document metadata

PDF /Author, Office core properties, Markdown frontmatter author:.

Reliable when present, absent most of the time — no Markdown file in this corpus carries an author: key, so frontmatter is a bonus path, not a dependency.

Metadata is filtered through looks_like_tool_name(), because software writes its own name into these fields. Without the filter, python-pptx’s default template attributes every deck to the library’s author (“Steve Canny”) and openpyxl names itself on every spreadsheet. Tool names are kept in meta for provenance but never become authors.

A third party’s name in the metadata implies reference; the owner’s implies authored. A reference-shaped path still overrides an authored guess — “in Reference/ but carries my name” is usually a paper you collected.

3. Path convention

attribute/pathrules.py, first match wins. Data, not logic — adapting to a different layout means editing the table.

Pattern Trust
Reference/* reference
Documents/Paper/Reference/* reference
**/{Documentation,Datasheets,Manuals,Books,Papers,Specs,RFCs}/* reference
Personal/*, Projects/*, Notes/* authored
Documents/{Research,Paper}/* authored
any component named reference/papers/manuals/… reference
anything else source default

Vendored trees (node_modules, vendor, third_party, Pods, site-packages, …) are reference wherever they appear, checked before the path table.

4. Source default

Whatever garage add-source --trust recorded.

Identity resolution

An author owns many identities. Resolution is lookup-by-identity, create-on-miss, so one person arriving first as a git email and later as a PDF byline collapses onto a single row rather than duplicating.

Configure your own identities so the authored/reference split works:

"identity": {
  "name": "Your Name",
  "identities": [
    "git_email:you@example.com",
    "email:you@example.com",
    "git_name:Your Name"
  ]
}

Add every address you have committed under. An identity you omit reads as somebody else, and your own work is filed as reference.

Communications

The schema reserves three roles for messages and mail alongside author and committer (AuthorRole in db/models.py): sender, recipient, cc. The intended mapping is:

with trust authored for what you sent and received for what you did not.

Not yet populated. Nothing in attribute/ assigns these roles today: the resolver only ever emits author and committer, and communication sources are attributed through the same git → metadata → path → source-default chain as everything else. The roles exist so the schema does not need a migration when a conversation-aware attributor lands.

Corpus class

Independent of trust, decided by content shape with the source default as fallback (ingest/classify.py):

Structured config (.json, .yaml, .toml, .ini, .csv) counts as code. As documents it was 63% of this corpus — 461k of 726k chunks of CI workflows and dependency manifests — and pure retrieval noise. It is still indexable with --include-code, filed as code.

Inspecting decisions

garage extract <file>          # shows author hints and extracted metadata
garage search "..." --author "Name"
garage search "..." --trust authored     # only your own writing
garage search "..." --trust reference    # only QA'ed external material

The evidence column on document_authors records exactly which rule fired:

SELECT a.display_name, da.role, da.confidence, da.evidence
FROM document_authors da JOIN authors a ON a.id = da.author_id
WHERE da.document_id = 123;