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:
- The owner has commits on this file →
authored - No commits by the owner →
reference
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 --followper file across ~18k tracked documents would mean 18k git invocations. Instead onegit log --name-only --no-renamespass 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:
- Outbound → you are
sender, the handles arerecipient - Inbound → the handle is
sender, you arerecipient - Mail
Cc:→cc
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):
- Code extensions →
code - Documentation names (
README,LICENSE,SECURITY,CHANGELOG…) →document, but only when the extension is not itself a code extension. Otherwisesecurity.rbandlicense.pywould be misfiled as prose — a bug this code had and now has a regression test for. - Conversations, or a source that pins the class →
communication
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;