docs/specs/2026-07-31-evidence-affecting-paths-assessment.md

Evidence-affecting path list — assessment

Status: In force. This is the evidence gate's PR-diff backstop list, designated as such by ADR-0005 and linked as the inventory from CONTRIBUTING.md and the pull request template. It stands until a maintained list supersedes it; the governance-docs ticket (#305) closed without producing one. Individual entries stay revisable — see "Judgment calls needing confirmation in review". (Origin: wayfinder ticket #303, autonomy map #298.) Amended 2026-08-23 by ADR-0014: every include-list entry now carries a tier, blocking or advisory. Read "Tiers" before using this list for anything; a hit no longer means one thing. Definition applied: CONTEXT.md, Assurance baseline — "An evidence-affecting change includes acquisition, parsing, extraction, storage, hashing, signing, trusted time, manifests, verification, redaction, export, reporting, AI analysis, and software distribution when it can alter an evidentiary result or its interpretation."

Purpose

This list is the mechanical PR-diff backstop for the evidence gate (decision record: #299). The primary trigger is the evidence-affecting label applied at triage; this list catches PRs whose diff touches an evidence path without the label. It is expressed as globs so a routine or CI step can match it against git diff --name-only. It was drafted as the reviewed source that a machine-consumed file would be seeded from; #305 closed without relocating or superseding it, so this document is the list itself until one is produced.

Method note: produced by a three-slice parallel survey of the codebase (main services; db/shared/ ai/extraction; extension/renderer/distribution edges), each reading files rather than classifying by name, followed by an adversarial completeness/overbreadth challenge. The challenge pass added six paths both surveyors missed, overturned one exclusion on a factual error, and removed two inconsistent inclusions — those changes are already folded into the list below.

Amendments since the survey: src/shared/urlPatterns.ts added to Acquisition (#227) — the ignored-URL matcher moved there out of the already-listed src/main/services/captureServer.ts, and the rename-detection-off rule only catches the move itself, not later edits to the new home. src/renderer/lib/api/** added to the exclusion list (#229) — the renderer query layer split out of src/renderer/lib/queries.ts into per-domain modules, so the reasoning recorded against that one file now applies to the directory. The call is unchanged (still excluded), and judgment call 2 below is restated against the new path; queries.ts itself is a re-export barrel that the last PR of that sequence deletes. extension/src/popup/PopupApp.tsx, extension/src/popup/pageStatus.ts and extension/src/messages.ts added to Acquisition (#387) — the popup's case routing and its capture button moved out of the already-listed extension/src/popup/popup.tsx, which is now a mount point, and the same rename-detection caveat applies: without these three the backstop would stop firing on the code that actually routes a capture. src/main/services/exclusionPolicy.ts added to Acquisition (#400) — the same move as #227, one step further: the decision about what may enter a case moved out of the already-listed src/main/services/captureServer.ts so that src/main/services/recapture.ts, a producer the capture server never sees, enforces the identical rule rather than a second copy of it. The rename-detection-off rule catches the move itself and nothing after, and this file is now the single place that answer is computed, so a later edit touching only it would otherwise match no entry. src/shared/urlCanonicalize.ts added to Acquisition (#392) — the canonical URL identity the extension attach routes (tag apply, note create, URL lookup) resolve against. It decides which Capture a Tag or Note binds to, and whether an attach request acquires new bytes at all (no canonical match means the supplied payload is ingested), so it shapes what enters a case the same way urlPatterns.ts does; the #227 argument applies unchanged. Shared so the extension and the server agree on the rules by construction rather than by convention. src/main/windowSize.ts added to the exclusion list (#515) — window sizing moved there out of the already-listed src/main/index.ts in #478 and landed on no list at all, so once that carve-out had settled a PR touching only the new file would have tripped nothing. The call is exclusion rather than inclusion: window geometry enters no capture, content hash, manifest entry, canonical JSON field or signature, which #478's Evidence impact section argued and its round-2 pre-pass verified. src/main/index.ts stays included at blocking tier for the invariants it does still hold. The Shared Case sync modules under src/main/services/sharing/ were added on 2026-10-14 with #1512, the first code that writes another installation's entries, files and rows into a Case. The four that decide what is stored (session.ts, entryAcceptance.ts, blobAcceptance.ts, workingRows.ts) are blocking; messages.ts and syncState.ts are advisory; and the transport interface with its in-process form, transport.ts, is an exclusion by the #227 and #515 reasoning: it carries bytes between installations, reads no message, and decides nothing about what the session accepts. IrohTransport (irohTransport.ts, #1809) is the same kind of file and joins that entry. nodeKey.ts, which keeps the transport's node key, is advisory: the node id the Owner's signed member-add records is that key's public half. The repo module the rows step writes through, src/main/services/db/workingRowRepo.ts, is already swept in by src/main/services/db/**. #1811 added a fifth blocking module, receivedEvidence.ts, which turns received entries into Capture and Exhibit rows. On 2026-10-19 #1881 added syncService.ts at advisory tier: it decides when sessions run and when the transport starts and stops, and reads no message and stores nothing, so what a session it starts may store is still decided in the blocking modules and the roster checks in session.ts. On 2026-10-20 #1882 added two test-only hooks for the two-instance e2e spec, both read only in an unpackaged build: src/main/captureServerPort.ts, the Capture Server port override, joins the exclusions beside src/main/windowSize.ts, since the server behind the port hashes and stores a Capture the same way on any port; and src/main/services/sharing/loopbackTransport.ts, the same transport interface over sockets on 127.0.0.1, joins the transport.ts entry. src/packages/transaction-record/** was added to Acquisition at blocking tier on 2026-10-17 with #1903, the capture engine's step 7 (D12 of the first-slice plan): it decides which of the browser's exchanges a Transaction Record holds as complete and how the inventory counts the rest. src/packages/evidence-package-layout/** was added to Integrity core at blocking tier on 2026-10-26 with #1904, the capture engine's step 1, which put the Transaction Record and PDF package paths in it. It had been on neither list, although Package Verification, verify.sh, Evidence Package export, the Shared Case sync and Case Archive export and import all take paths from it.

Known gap: carve-outs are caught by review, not by a check

Nothing detects a file being split out of an include-list path. The rename-detection-off rule under "List format" catches the move itself, because that diff still names the old path, and nothing after it — once the carve-out has merged, edits touching only the new home match no entry. That is why each amendment above was needed, and every one of them was written because a person or an agent noticed while reading a diff.

Whether that is the intended control or a gap worth closing with a check is open (#1410). Read this as an unresolved gap rather than a decision already taken. It is a live one: two further carve-outs from src/main/index.ts are on neither list today (#1409).

List format

These lists are the seed for a machine-consumed file, so entries follow fixed rules. Anything seeded from this document should be checked against them.

  • One repository-relative glob per entry. No table row or bullet combines two paths; a rule that needs two paths gets two entries. This is what makes the tables extractable by cell.
  • Paths are repository-relative POSIX paths with no leading ./ or /, so they compare directly against git diff --name-only output. Matching is case-sensitive, and directory separators are always /.
  • A ** segment crosses directory separators. An entry ending in a ** segment means that directory and everything beneath it, recursively. An entry with no wildcard matches exactly one file.
  • Run the diff with rename detection off (git diff --name-only --no-renames), so moving a file out of an evidence path still trips the backstop on the old path.
  • Only the include list feeds the matcher. The exclusion list is a review record of paths deliberately left out, not a subtraction pass applied on top. Blanket exclusion-over-inclusion would be wrong here: src/renderer/components/dashboard/** is excluded while src/renderer/components/dashboard/cases/DataExplorer.tsx and src/renderer/components/dashboard/cases/ImportCaseDialog.tsx are included, and a blanket override would silently drop them. If a future tool does consume both lists, the more specific pattern wins — an exact path beats a directory glob, and among globs the one with the longer literal prefix before its first wildcard beats the shorter.

Tiers

Added 2026-08-23 by ADR-0014. Every include-list entry carries a tier, and the tier decides what a hit costs.

blocking. A change on this path can alter an evidentiary result or its interpretation. The backstop finding is blocking, the PR owes the ADR-0004 obligations (an Evidence impact section, a known-answer test or a written justification, preserved backward verification), it gets human review, and it never auto-merges.

advisory. The path is real evidence surface, but it is shared with a lot of code that is not. A hit produces a one-line reviewer disposition naming which region the diff touched. It owes no Evidence impact section and does not by itself make the PR evidence-affecting. A reviewer who finds the diff does reach evidence behaviour relabels the PR blocking and the full obligations apply.

The evidence-affecting label applied at triage is unchanged and still the primary trigger. The tiers govern the mechanical backstop only. A labelled PR owes the full obligations whatever its diff touches.

Why the tiers exist

ADR-0005 asked for a mechanical backstop because "looks harmless" is the judgment that fails under review pressure. That reasoning holds. What did not hold is treating every entry as equally load-bearing: judgment calls 1, 3 and 10 below predicted, before the pilot ran, that ipcHandlers.ts/ipc.ts wholesale, pnpm-lock.yaml and the distribution block would fire on changes that could not touch an evidentiary result, and pre-authorised dropping them if that proved out.

It proved out. Measured 2026-08-23 over every PR that has carried the label, matching this document's own include list (extracted by cell per the rules above) against git diff --name-only --no-renames for each merge:

Count
PRs labelled evidence-affecting41
of those, merged38
merged PRs in the repository overall349
labelled merges hitting a narrow-block glob26
labelled merges hitting only broad-block globs11
labelled merges hitting no include-list glob at all1

Broad-block means the cross-process contracts block, the software distribution block, or src/main/services/db/**. The eleven include the cross-case recent-activity feed, the UI density setting, the tests/ typecheck gating and its reapply, the preload namespace rename, and two PRs hardening the release supply chain. The one no-hit is #532, a threat-model documentation change labelled by triage judgment alone, which is the label working as designed.

Four of the twenty-six narrow hits are incidental brushes rather than evidence work, and they are the more misleading class because they read as substantive:

  • #786 hit src/renderer/components/export/** by adding data-tour="export" to a div in ExportMenu.tsx. The whole diff is a coach-mark tour.
  • #455 and #357 hit the same glob changing error handling in ExportComplete.tsx and ExportMenu.tsx. Neither shapes a package.
  • #478 hit src/main/index.ts moving window sizing out to resolveWindowSize. That file is listed for the evidence-viewer invariants, webview navigation blocking and sandbox webPreferences, and the diff went nowhere near them. The file it moved the sizing into, src/main/windowSize.ts, is on the exclusion list below (#515).

So 16 of 38 labelled merges, 42%, touched no path where the change could have altered an evidentiary result.

The tiers do not clear all sixteen. Re-running the same measurement against the tiered list, 13 of 38 now classify advisory or no-hit and 25 stay blocking. The gap is src/main/services/db/**, kept blocking deliberately because it holds the evidentiary records, which leaves #677 (the cross-case recent-activity feed) and #435 firing on a repo read. That is a known residual, not an oversight: narrowing db/** to something that distinguishes a read from a write is a real piece of work and it is not this one.

Known exposure of the advisory tier

Recorded rather than resolved, because the tier assignment is a deliberate trade.

src/main/services/settings.ts is advisory, and it persists the operator identity that src/main/services/certification.ts embeds verbatim in an export certification. A change there can therefore reach a shipped package without a blocking finding. The judgment is that the certification path itself is blocking and is where such a change becomes visible. If an advisory-tier defect ever does reach main through this route, that is the evidence to promote the entry, and it should be promoted rather than argued with.

src/main/index.ts stays blocking despite its only observed fire being incidental. One file's noise does not buy dropping the evidence-viewer invariants, and the better fix for the specific carve-out that caused it was to record the carved-out file as an exclusion (#515), which is now done.

Include list

Acquisition (extension → capture server)

PathTierWhy
extension/src/background.tsblockingOrchestrates MHTML/screenshot/text capture; caches response headers anchored into the signed manifest
extension/src/content.tsblockingIn-page capture: screenshot stitching, DOM mutation during capture, selector matching over page text
extension/src/toast.tsblockingInjects DOM before/during capture — its markup can land inside captured bytes
extension/src/utils/api.tsblockingBuilds the multipart capture payload that becomes hashed evidence and manifest fields
extension/src/utils/headers.tsblockingDeterministic header normalization anchored into the hash-chained manifest
extension/src/popup/popup.tsxblockingRoutes a capture to a case (chain-of-custody routing)
extension/src/popup/PopupApp.tsxblockingThe popup itself since #387: picks the case a capture is routed to, and gates the capture button on the ignore rules
extension/src/popup/pageStatus.tsblockingDerives what the popup tells the operator about a page, including whether a capture would be refused
extension/src/messages.tsblockingThe popup/background contract carrying the block reason and the rules-loaded flag the capture route gates on
extension/manifest.jsonblockingAcquisition permissions; ships in the release zip
src/main/services/captureServer.tsblockingIngest endpoint: upload validation, case routing, ingest-time selector matching
src/shared/urlPatterns.tsblockingThe ignored-URL matcher both sides run: it decides what never enters a case at all, and names the rule recorded as the reason for the absence
src/shared/urlCanonicalize.tsblockingCanonical URL identity for the extension attach routes: decides which Capture a Tag or Note binds to, and whether an attach request ingests its payload or binds to an existing Capture
src/shared/localAddress.tsblockingThe address classification the viewer's Capture link, the background capture and the viewer's Wayback link lookup run: it decides which links and redirects a background capture refuses to render and store (#1726), and which link addresses are never sent to archive.org (#1737). Added 2026-10-16
src/shared/waybackLinkBatch.tsadvisoryDecides which link addresses a Wayback batch discloses to archive.org (#1738): the list the confirmation shows after de-duplication and the 50-address cap, and main's refusal of a confirmed list that names an address twice or one its own address check rejects. Advisory, because the answers are not stored, anchored or exported, so it changes no evidentiary result; the refusal rule it calls stays in src/shared/localAddress.ts, and the single-link refusal sits in src/main/ipcHandlers.ts, also advisory. Added 2026-10-26
src/main/services/exclusionPolicy.tsblockingResolves the per-case exclusion list into the answer the acquisition routes check before ingest, so it decides what those routes refuse. Not every route asks: the pipeline self-test (POST /api/captures/test) is a documented exception, so read the call sites rather than assuming coverage is total
src/main/services/serverToken.tsblockingAuthenticates ingest; weakening admits spoofed captures into the evidence chain
src/main/services/session.tsblockingActive-case state deciding which case a capture is filed under
src/main/services/recapture.tsblockingRecapture queue re-acquiring URLs as new evidence
src/main/services/backgroundRenderer.tsblockingHeadless render producing the artifacts that get hashed
src/main/services/localAddressPolicy.tsblockingApplies that classification to recapture links and each redirect hop, and holds the test-only loopback switch a packaged build ignores (#1726). Added 2026-10-16
src/main/services/egress.tsblockingThe Egress (ADR-0032): the proxy configuration every render, the Wayback pane and update checks receive, the routed fetch for the TSA request, the Wayback lookup and the filter lists, and what a signed capture entry records about the route (#1695). Added 2026-10-17
src/main/services/consentBlocker.tsblockingSuppresses consent overlays during recapture — alters acquired page content
src/packages/transaction-record/**blockingTurns the browser's network events into the exchanges a Transaction Record holds and the inventory counts of what it lacks, and a main document's securityDetails into Bound TLS Details
src/main/services/transactionRecorder.tsblockingSets up each debugger session of a background render, decides which events and bodies reach the Transaction Record and under what budget, and whether the record is present or failed (#1929). Added 2026-10-26
src/main/services/testCertificateAllowance.tsblockingHolds the test-only switch that makes a render accept one TLS leaf, which a packaged build ignores (#1929). Added 2026-10-26

Integrity core (hash, manifest, signing, trusted time, verification)

Assurance boundary for this block, stated so the rationales do not overclaim: the manifest chain and the RFC 3161 token cover different things, and neither substitutes for the other. The timestamp request's message imprint is the capture's contentHash (buildTimestampRequest in src/main/services/timestamp.ts), so a granted token dates the captured bytes — it says nothing about whether the manifest chain around them is intact. The chain, conversely, covers manifest entries but is signed with the installation-local keypair generated and held by this install (src/main/services/signingKey.ts), so it detects edits made without that key and is not independent of the Operator who holds it.

PathTierWhy
src/main/services/manifest.tsblockingHash-chained, signed audit manifest append/verify. The chain detects edits made without the installation-local signing key; it is not an Operator-independent guarantee
src/main/services/signingKey.tsblockingManifest signing keypair generation/storage/signing — the key the chain's assurance is bounded by
src/main/services/timestamp.tsblockingRFC 3161 TimeStampReq construction (message imprint = the capture's contentHash) and TSA round-trip. The token dates the content; it does not prove manifest-chain integrity
src/main/services/timestampWorker.tsblockingAsync timestamping worker writing manifest entries
src/main/services/staging.tsblockingThe Staging Pool (#1148): upload hashes bytes on arrival outside the chain, commit writes the exhibit entry and the row, discard removes both
src/main/services/trustedTime.tsblockingPer-capture trusted-time status resolution
src/main/services/tsaTrust.tsblockingEmbedded TSA trust anchors shipped in evidence packages
src/main/services/tlsCertChain.tsblockingTLS chain corroboration anchored into signed manifest entries
src/main/services/installationId.tsblockingInstallation identity in manifest entries and certification
src/shared/verify/**blockingVerification core: canonical JSON, chain, signature, RFC 3161 token, package verifier
src/verifier/**blockingStandalone verifier CLI (fs+crypto-only constraint)
src/packages/evidence-package-layout/**blockingThe Package Layout: the exporter, Package Verification and verify.sh read every Evidence Package path from it, so changing a name changes where verification looks for that file in every package already exported. It also names the member and lineage chain files in a Case directory, which the manifest, the Shared Case sync modules and Case Archive export and import read. Added 2026-10-26 with #1904, which put the Transaction Record and PDF paths in it
src/main/index.tsblockingEnforces evidence-viewer invariants (webview navigation block, sandbox webPreferences); boots DB/capture server/installation id

Capture lifecycle, storage, parsing, extraction

PathTierWhy
src/main/services/captureLifecycle.tsblockingIngest/delete pipeline: hashing, manifest entries, trusted-time reconciliation
src/main/services/captureStore.tsblockingOwner of capture artifact bytes on disk
src/main/services/exhibitNumbering.tsblockingTakes the next Exhibit Number from the signed chain (X45), the number new capture and Exhibit entries record and a citation names. Added 2026-10-01; it shipped off the list with #1660
src/main/services/exhibitBackfill.tsblockingStartup backfill that appends signed renumber and derivation entries and anchors each Derived File's outputHash. Added 2026-10-01; it shipped off the list
src/main/services/thumbnails.tsblockingRenders the thumbnail bytes the backfill hashes into a signed derivation entry; those Derived Files ship in evidence packages and the package verifier binds them. Moved from the exclusions on 2026-10-01
src/main/services/storage.tsblockingStorage root and case directory layout
src/main/services/mhtmlDecoder.tsblockingMHTML parsing feeding text extraction
src/main/services/dataExtractor.tsblockingIOC extraction pipeline entry
src/main/services/extraction/**blockingSource selection, sanitizer, IOC adapter, validators — determine extracted results
src/main/services/db/**blockingAll evidentiary records: schema, repos, dbAdmin row edit/restore/purge
src/main/services/selectorLifecycle.tsblockingRetro-matching writes selector_matches rows that ride in case archives
src/main/services/safeRegex.tsblockingRegex evaluation deciding those persisted matches (follows the selector cluster call below)

Annotation, redaction, notes-to-evidence binding

PathTierWhy
src/main/services/annotations.tsblockingPersists annotation shapes incl. redaction rects burned into exports
src/main/services/burnAnnotations.tsblockingComposites annotations/redactions onto exported pixels
src/main/services/renderAnnotationsSvg.tsblockingShape rendering for burn-in — a wrong redact rect leaks or destroys content
src/main/services/noteAnchorResolver.tsblockingBinds notes to hash-covered text
src/shared/noteDoc.tsblockingNote document model + text derivation (feeds FTS and exported reports)
src/shared/noteAnchor.tsblockingAnchor model deciding which evidence a note points at
src/renderer/components/captures/annotation/**blockingAnnotation editing incl. the redact tool

Export, reporting, archive

PathTierWhy
src/main/services/export.tsblockingEvidence-package export: hashing, evidence.json, verification, burn-in
src/main/services/pdfExport.tsblockingPDF disclosure rendering
src/main/services/reportHtml.tsblockingForensic report renderer inside packages
src/main/services/certification.tsblockingExport certification document
src/main/services/verifyRunbook.tsblockingBy-hand verification runbook shipped in packages
src/main/services/verifyScript.tsblockingverify.sh, the runbook's executable form, shipped in packages and hashed into packageHash. Added 2026-09-19 with #1156: it binds exhibit and derived-file bytes to the chain, so a defect here reports a tampered package as verified
src/main/services/verifyProcedure.tsblockingThe verification step list: step numbers, titles, citations and jq recipes that verify.sh, VERIFY.md and the report read. Added 2026-10-03; a defect here misnumbers or misspells a step in every package
src/main/services/caseArchive.tsblockingArchive export/import with id remapping
src/main/services/importIdMaps.tsblockingChecks and composes the import id maps that bind a renamed row to its signed entries and decide which Derived Files a Case Archive carries (#1017)
src/main/services/zip.tsblockingEvidence container write
src/main/services/zipRead.tsblockingEvidence container read
src/main/services/csvEscape.tsblockingEscaping behind CSV exports of case data
src/renderer/components/export/ExportDialog.tsxblockingExport options directly shape the package. Narrowed from src/renderer/components/export/** on 2026-08-23. See the tier amendment

AI analysis

PathTierWhy
src/main/services/ai/**blockingAnalysis pipeline + chat client (truncation changes model input, hence output)
src/renderer/components/captures/AnalysisTab.tsxblockingTriggers analysis, selects model/prompt, saves results
src/renderer/components/settings/AIConfig.tsxblockingWrites defaultModel and analysisSystemPrompt — same rationale as AnalysisTab

Interpretation surfaces (renderer)

PathTierWhy
src/renderer/components/captures/ForensicsTab.tsxblockingHash chain / chain status / headers display
src/renderer/components/captures/ProvenanceBadge.tsxblockingIntegrity + trusted-time status rendering
src/renderer/components/captures/getProvenanceColor.tsblockingStatus-to-meaning mapping
src/renderer/components/captures/MhtmlViewer.tsxblockingEvidence rendering invariants (JS disabled, navigation blocked)
src/renderer/components/captures/WaybackPanel.tsxblockingPins Wayback corroboration refs: builds each pin from the chosen snapshot and the lookup's checkedAt. Replaced the WaybackTab.tsx entry on 2026-10-12; #856 deleted that file, and the entry matched nothing from then on
src/renderer/components/captures/CaptureDownloadMenu.tsxblockingPer-capture evidence export surface
src/renderer/components/overview/VerifyBar.tsxblockingCase-level verification display
src/renderer/components/overview/overviewModel.tsblockingThe actual verified/tampered bucketing VerifyBar shows
src/renderer/components/dashboard/cases/ImportCaseDialog.tsxblockingArchive verification display + overrideTamper flow
src/renderer/components/dashboard/cases/DataExplorer.tsxblockingThe Data screen shell: per-row SHA-256, anchored versus not-anchored state, Integrity Exceptions and the manifest ledger (#1149). Extraction results display + reprocess trigger before that
src/renderer/components/data/**blockingThe Data screen's models and parts (#1149): what the artifact table says about anchoring, verification buckets and hashes is decided here
src/main/services/exhibits.tsblockingThe Exhibit model read paths (#1147): inventory with the anchored/pooled discriminator, manifest snapshot verdict and signer segments, per-Exhibit verify. Added 2026-09-14; it shipped off the list
src/renderer/components/settings/DatabaseAdmin.tsxblockingDirect DB administration over evidence tables
src/renderer/components/settings/db/**blockingThe DB administration views behind it (browse, row edit, utilities)
src/renderer/components/settings/OperatorConfig.tsxblockingOperator identity embedded verbatim in export certification

Shared Case sync

The sync session (#1512) is the one path by which another installation's chain entries, Exhibit bytes and working-layer rows enter a Case directory and database. What it accepts is decided in five modules, and who is a member in three more (#1810); the rest of the directory moves bytes or records state.

PathTierWhy
src/main/services/sharing/session.tsblockingRuns the pull: proves the peer's key against the roster, asks for entries, files and rows, and writes the merge entry that names the heads received. A defect here stores something the acceptance modules never saw, or signs a merge for a head this replica does not hold
src/main/services/sharing/entryAcceptance.tsblockingAppends a received chain segment only when the stored chain plus the segment verifies under the member's key and continues the stored head; stops before an entry from a newer schema. Decides which remote entries exist on this replica
src/main/services/sharing/blobAcceptance.tsblockingWrites a received file beside its path, hashes it from disk and renames it in only when the hash is the one a verified entry anchors; refuses paths outside the Case directory. Decides which remote bytes become visible
src/main/services/sharing/workingRows.tsblockingSigns this installation's rows on every write, tombstones instead of deleting in a Shared Case, and applies a received row only under its author's roster key and above the stored version. When a Capture of a Shared Case is deleted, keeps every other author's rows on it as signed and touches only this installation's: it tombstones them when the Capture reaches members and removes them when no member can hold it (a legacy html or imported Capture) (#1877). When a Case is first shared, signs only this installation's unsigned rows of it that are provably its own (#1910): never an older tag; a tag application only when its tag and its note or Exhibit reach members; no row that needs a Capture or Exhibit no member can hold; and, in a Case imported from any archive, this installation's own included, only rows that need a Capture or Exhibit created since the import. Later writes on an older tag sign it as before #1910. Decides what another member's notes, tags and annotations say here
src/main/services/sharing/receivedEvidence.tsblockingWrites the captures, exhibits and derived_files rows for another member's evidence only once every file its entry anchors is on disk with the anchored hash, and applies that member's deletion by removing the Capture and its files (#1811). Records a deletion of a Capture this replica never held when the same chain's current segment also created it, keeps the record whatever another chain claims, and refuses a capture or exhibit entry naming a deleted Capture (#1877). Decides which received Captures and Exhibits this replica lists and which it removes, and whether other members' rows on a deleted Capture are accepted
src/main/services/sharing/messages.tsadvisoryThe CBOR codec and frame reader: parses every frame against a strict schema at the trust boundary and passes it on. It admits nothing by itself, since each message is checked by the module above that consumes it
src/main/services/sharing/membership.tsblockingWrites the Owner's signed member-add, member-revoke and exclude entries (#1810), and refuses before writing any roster the Shared Case walk would call invalid. On the first share, signs the Owner's older rows of the Case that are provably its own, in the same transaction (#1910), and none when the Owner's chain records an archive-export or holds a line that does not parse as an entry. Decides which installation and key become a member, and which Exhibits the Owner marks excluded, which exports list
src/main/services/sharing/rosterCache.tsblockingRebuilds case_members from the current segment of the Owner's chain, and keeps an empty chain file for each member in good standing (#1810). Decides which keys every session checks a peer against
src/main/services/sharing/joinHandshake.tsblockingThe join handshake (#1810): verifies the join signature over this Owner's node id before the invite is claimed, and on the joiner checks the welcome before it writes the roster that the first session trusts
src/main/services/sharing/base32.tsadvisoryBase32 for the invite string (#1810). A defect here makes an invite unreadable, never admits a member
src/main/services/sharing/invite.tsadvisoryEncodes and parses the invite string at a trust boundary (#1810). The fields it returns are checked by the join handshake, not here
src/main/services/sharing/inviteRegistry.tsadvisoryHolds the hash of each open invite's secret in memory, and refuses an unknown, cancelled, used or expired one (#1810). It decides who may ask to join; the Owner's approval and the signed member-add decide who joins
src/main/services/sharing/nodeKey.tsadvisoryStores the transport's node key (#1809). The node id the Owner's signed member-add records is its public half, so a key that changed would leave rosters naming a node this installation no longer is; it signs no entry. A session checks a peer's identity by its signing key in hello, whose proof names the Case, the signer and its side, and keeps a peer's ticket only when it names that member's recorded node
src/main/services/sharing/sharingService.tsadvisoryRoutes each incoming stream to the join handshake or the sync session by its first message, and composes the commands behind the sharing:* channels (#1810). It runs one Case's sessions one after another (Sync now, the sync service's round and a join's first session), reads the roster again before each member, refuses to sync a Case whose own roster row is revoked, and ends a dialed session whose peer leaves the dial, the stream open, or any read or write idle for 30 s (#1881). It decides nothing a session stores. Two sessions on one Case at once cannot fork a chain: entry acceptance refuses a segment that does not continue the stored head and appends in the same synchronous call, so the serialization spares a spurious peer failure and a repeated transfer
src/main/services/sharing/syncService.tsadvisoryDecides when sessions run (#1881): a round at launch and every 60 s through the sharing service's per-Case single-flight, and the transport's start and stop. It reads no message and stores nothing. Which members a round dials comes from the roster's revokedAtIndex, read in sharingService.ts, and a session it should not have started still meets both sides' roster checks in session.ts
src/main/services/sharing/syncState.tsadvisorysync-state.json and members.json, neither of them evidence, and the version high-water mark the row signing reads. A defect here can make a peer ignore a row as old, never accept one it should not

Cross-process contracts and configuration

PathTierWhy
src/main/ipcHandlers.tsadvisoryShapes inputs/outputs of evidence operations (see judgment call 1)
src/preload/index.tsadvisorySingle privileged chokepoint for every evidence channel
src/shared/ipc.tsadvisoryTyped payloads for evidence operations (see judgment call 1)
src/shared/types.tsadvisoryDomain contracts for captures, provenance, trusted time, exports
src/shared/schemas.tsadvisoryZod validation at trust boundaries (upload, manifest entry, settings)
src/shared/constants.tsadvisoryManifest filename/schema version, TSA URL, size caps, default analysis prompt
src/main/services/settings.tsadvisoryPersists TSA URL, operator identity, capture prefs, key encryption
src/main/services/waybackMachine.tsblockingCorroboration lookups pinned as evidence references

Software distribution

PathTierWhy
package.jsonadvisoryelectron-builder build block, dependency pins/overrides
pnpm-lock.yamladvisoryThe actual dependency pin compiled into shipped bundles (see judgment call 3)
.github/workflows/release.ymladvisoryBuilds/publishes installers, updater manifests, extension zip
electron.vite.config.tsadvisoryCompiles the exact app bytes the release workflow publishes
extension/vite.config.tsadvisoryCompiles the exact extension bytes the release workflow publishes
scripts/build-verifier.mjsblockingBuild of the distributed verifier binary
sea-config.jsonblockingSEA definition of that verifier binary
src/main/services/updater.tsadvisoryUpdate delivery

Notable exclusions

A review record of paths deliberately left out, in the same one-glob-per-entry form as the include list — see "List format" for why this list is not applied as a subtraction pass over the includes. It is notable exclusions, not an exhaustive complement of the include list.

PathWhy
src/main/ipcWrap.tsContent-agnostic IPC envelope
src/main/services/logger.tsOperational logging
src/main/services/logSafe.tsRedacts logs, not evidence — the definition's "redaction" is the annotation burn-in path
src/main/services/sessionLog.tsOperational logging
src/main/services/bugReport.tsBug-report zip deliberately excludes captures and the DB
src/main/services/diagnostics.tsOperational telemetry
src/main/services/openrouter.tsAPI-key test + model catalog only; cannot alter analysis output. Distinct from the included src/main/services/ai/openrouter.ts chat client
src/main/services/deepLink.tsNavigation plumbing
src/main/services/extensionPath.tsExtension setup plumbing
src/main/windowSize.tsMain-window geometry, and the BIRDBRAIN_WINDOW_SIZE override read only in an unpackaged build. It holds none of the invariants src/main/index.ts is listed for: no capture, content hash, manifest entry, canonical JSON field or signature depends on the size of the window the evidence is later viewed in. Split out of the included src/main/index.ts by #478, and left off both lists until #515
src/main/captureServerPort.tsThe Capture Server port, and the BIRDBRAIN_CAPTURE_SERVER_PORT override read only in an unpackaged build, for the two-instance e2e spec (#1882). The same reasoning as src/main/windowSize.ts: the server behind the port hashes and stores a Capture the same way on any port, and a packaged build always listens where the extension looks
src/main/services/sharing/transport.tsThe transport interface and its in-process form, MemoryTransport (#1512). A transport carries bytes between installations: it reads no message and decides nothing about what the session accepts, which the four blocking modules above do. The same reasoning as src/main/services/exclusionPolicy.ts's inclusion in reverse (#227) and as src/main/windowSize.ts (#515): the evidentiary decision lives elsewhere. irohTransport.ts (#1809), the same interface over Iroh, joins this entry, and so does loopbackTransport.ts (#1882), the test-only form over sockets on 127.0.0.1 that the two-instance e2e spec selects through BIRDBRAIN_E2E_SHARING_BRIDGE, read only in an unpackaged build
src/renderer/lib/api/**High-churn typed passthrough; evidence parameters originate in included dialogs and are enforced in main (but see judgment call 2)
src/renderer/lib/queries.tsSame rationale — the re-export barrel left behind by the split, deleted by the last PR of #229
src/renderer/components/captures/AddUrlsBox.tsxRemoved by the challenge pass as inconsistent with the standard applied to src/renderer/lib/queries.ts and src/renderer/components/settings/CapturePreferences.tsx — UI-side plumbing whose values are enforced in main
src/renderer/components/captures/useVerifyMutation.tsSame rationale as src/renderer/components/captures/AddUrlsBox.tsx above
src/renderer/components/captures/CaptureViewer.tsxRenderer chrome
src/renderer/components/captures/CaptureList.tsxRenderer chrome
src/renderer/components/captures/CaptureDetailsPanel.tsxRenderer chrome
src/renderer/components/settings/CapturePreferences.tsxCapture-preference UI; the values it writes are enforced in main
src/renderer/components/layout/**Renderer chrome
src/renderer/components/dashboard/**Renderer chrome, except the two files listed in the include list — see the precedence rule in "List format"
src/renderer/components/tags/**Renderer chrome
src/renderer/components/search/**Renderer chrome
src/renderer/components/status/**Renderer chrome
src/renderer/components/ui/**Shared UI primitives
src/renderer/components/notes/**Note-editing UI; the evidence binding lives in the included src/shared/noteAnchor.ts / src/main/services/noteAnchorResolver.ts
src/renderer/components/selectors/**Selector-management UI; matching lives in main
src/renderer/hooks/**Renderer chrome
src/renderer/routes/**Renderer chrome
src/renderer/stores/**UI-only state
src/renderer/styles/**Styling
.github/workflows/ci.ymlNo distributed artifacts
.github/workflows/security.ymlNo distributed artifacts
.github/workflows/docs.ymlNo distributed artifacts
scripts/coverage-*.mjsDev tooling
scripts/gen-linux-icons.mjsDev tooling
scripts/rebuild-native.mjsDev tooling
extension/src/fonts/**Popup cosmetics, never touch the captured page
extension/src/popup/popup.cssPopup cosmetics, never touch the captured page
extension/src/popup/popup.htmlPopup shell, never touches the captured page
src/renderer/components/export/ExportMenu.tsxLaunch and confirmation UI. It opens the dialog and reports the result; it shapes no package. Split out of src/renderer/components/export/** on 2026-08-23
src/renderer/components/export/ExportComplete.tsxSame rationale as src/renderer/components/export/ExportMenu.tsx
src/renderer/components/export/ExportProgress.tsxSame rationale as src/renderer/components/export/ExportMenu.tsx
src/main/services/db/diagnosticsRepo.tsRead-only database facts for the Diagnostics panel. Swept in by src/main/services/db/** and named as an accepted false positive by judgment call 8; made explicit on 2026-08-23
src/renderer/components/settings/db/ConfirmDialog.tsxGeneric confirmation dialog. Swept in by src/renderer/components/settings/db/**, same judgment call 8 rationale

Judgment calls needing confirmation in review

  1. src/main/ipcHandlers.ts and src/shared/ipc.ts wholesale. Both also carry pure-UI channels, so the gate fires on many non-evidence PRs. The backstop cannot distinguish a payload-type change to an evidence channel from a new UI list query. Accept the noise, or split the files later. Resolved 2026-08-23: both are advisory. The files are not split; the tier absorbs the noise instead.
  2. src/renderer/lib/api/** excluded while src/preload/index.ts is included. Both are typed passthroughs. As excluded, a hostile change like hardcoding overrideTamper: true in the import mutation (src/renderer/lib/api/cases.ts) would not trip the backstop. Including it gates most renderer PRs. Current call: excluded — the label-at-triage trigger and human review are expected to catch that class.
  3. pnpm-lock.yaml. Included by the same logic as package.json (the lockfile is the actual pin that determines shipped code), but it gates every dependency-bump PR. Current call: include; drop it if dep-bump noise proves unacceptable during the pilot. Resolved 2026-08-23: it did. pnpm-lock.yaml is advisory, still listed.
  4. src/main/services/session.ts / src/main/services/serverToken.ts. Included on misattribution/spoofed-ingest rationales; arguably workflow state and authn rather than evidence processing.
  5. src/main/services/csvEscape.ts. CSV exports are convenience exports, not signed packages; kept because they are still disclosed case data.
  6. Notes cluster (src/main/services/noteAnchorResolver.ts, src/shared/noteDoc.ts, src/shared/noteAnchor.ts). Notes never enter evidence packages; inclusion leans entirely on the "interpretation" clause (what evidence a note points at, and note text in exported reports/archives).
  7. Selector cluster (src/main/services/selectorLifecycle.ts, src/main/services/safeRegex.ts). Originally excluded as "matches never enter the evidence package" — the challenge pass found that factually wrong (src/main/services/caseArchive.ts exports and re-imports selectors and selectorMatches). Included on that basis; if selector matches are ruled non-evidentiary despite riding in archives, drop both.
  8. Accepted glob false positives. src/main/services/db/** sweeps in the read-only src/main/services/db/diagnosticsRepo.ts; src/renderer/components/settings/db/** sweeps in the generic src/renderer/components/settings/db/ConfirmDialog.tsx. Accepted: a per-file list would silently miss future files added to these directories. Resolved 2026-08-23: the globs stay, and both named files are now explicit exclusions. The precedence rule in "List format" makes the specific exclusion win over the directory glob, so future files in those directories are still swept in.
  9. extension/src/toast.ts / extension/src/popup/popup.tsx. One step removed from captured bytes (toast markup can be captured; the popup routes evidence to a case). A stricter list could drop extension/src/popup/popup.tsx.
  10. Distribution block (src/main/services/updater.ts, the build configs, .github/workflows/release.yml, pnpm-lock.yaml). Included under the definition's "software distribution" clause. If release PRs get their own review channel instead, this whole section could move out of the backstop. Resolved 2026-08-23: the section is advisory rather than moved out, except scripts/build-verifier.mjs and sea-config.json, which define the binary a receiving party runs and stay blocking.