Skip to content

Latest commit

 

History

History
751 lines (644 loc) · 42 KB

File metadata and controls

751 lines (644 loc) · 42 KB

Architecture

GitContribute is a local-first research workbench. GitHub is an explicit input source; SQLite is the durable system of record. Commands that inspect the corpus must not silently fetch data, execute repository code, or mutate GitHub.

Component map

                    explicit network reads
                            |
                            v
CLI ---------+         GitHub adapter ------ GitHub API
MCP ---------+----> application service
TUI ---------+              |
                            +----> DeepWiki adapter -----> public DeepWiki MCP
                            |
                            +----> acquisition/workspace adapters ----> git
                            |
                            +----> validation runner -----------------> process
                            |
                            v
                      SQLite corpus
                  observations + projections
                            |
                            v
              offline search, radar, health, dossiers, thread briefs,
             investigations, evidence, readiness, and drafts

The dependency direction is toward product-owned contracts:

  • internal/app owns use cases and side-effect decisions.
  • internal/corpus owns persistence, transactions, migrations, and local query behavior.
  • internal/github, internal/deepwiki, internal/acquire, internal/workspace, and the evidence runner are adapters for external capabilities. DeepWiki prose is untrusted derived context, is not persisted, and never updates GitHub projections.
  • internal/cli, internal/mcpserver, and internal/tui translate user or protocol input into application calls. They do not own product rules. MCP prompts are static workflow guidance; they cannot grant new authority or turn repository content into instructions.

Third-party SDK and database types terminate inside their adapters. The application and domain packages expose product-owned values and interfaces. Inbound adapters import those contracts directly; they do not re-export product-owned types, errors, or constants.

Capability boundaries

Capability Examples Network Local write Process execution GitHub mutation
Corpus read search, health, dossier show, research brief, readiness, MCP resources no no no no
Corpus write investigations, start-thread, evidence, lenses, tracking, cluster governance no yes no no
Derived projection refresh explicit clusters refresh OWNER/REPO no yes no no
Private MCP runtime installation explicit setup --mode mcp no yes no no
Global CLI installation explicit setup --mode cli or --mode both npm registry dependent yes npm only no
Setup verification all applied setup modes no no git --version no
GitHub read sync, crawl, hydrate, bounded search/source acquisition yes yes no no
Coverage workflow explicit corpus.ensure_coverage yes, bounded yes no no
DeepWiki external read public repository structure, contents, questions yes no no no
Git acquisition acquire, workspace create remote-dependent yes git only no
Local merge check compare already-fetched revisions no no git only no
Validation validation run/repeat with explicit execution no by default yes yes no

Version 1 has no GitHub mutation path. Adding one requires a separate application capability and protocol annotation; it must not be hidden behind a read operation.

Validation definitions remain shell-free and execution requires an explicit authorization flag. Repeat groups bound attempts, concurrency, per-run and overall timeouts, captured output, and sampler frequency. The generic process runner records spawn/execution/shutdown boundaries and uses gopsutil for PID-plus-start-time process-tree metrics. Declared mcp_stdio definitions use the official MCP SDK for initialize and tools/list milestones; protocol state is never inferred from arbitrary stdout text. Metric availability and cleanup status are persisted separately from semantic pass/fail classification.

Corpus model

The corpus separates source history from convenient current state:

  • Observations are append-only records of source payloads and provenance.
  • Projections are normalized repository and thread rows used by local queries.
  • Facet observations store paginated child data such as issue comments, reviews, and review comments.
  • Facet coverage records whether a facet fetch completed and the source revision it represents.

GitHub actors are first-class projections rather than denormalized author strings. A stable provider node ID is the preferred identity; observed logins are aliases, so a rename does not split one contributor into two profiles. Profile fields remain nullable because absence may mean undisclosed, unauthorized, or not acquired. Social accounts, public organizations, pinned items, repository relationships, and contribution periods are independent facets with their own observation time, source time, authorization scope, and completeness. A child table is replaced only after its complete bounded fetch; an incomplete observation advances coverage without destroying the last complete projection.

Actor acquisition is deliberately atomic. github.search_users stores only identity observations. Exact github.sync_users profile reads and the github.sync_user_* facet tools perform no implicit fan-out into one another. corpus.search_actors, corpus.get_actors, corpus.get_actor_facets, and corpus.search_contributions are offline and snapshot-bound. See docs/actor-corpus.md for the typed provider-to-SQLite mapping.

An explicit repository-context sync checks a fixed, bounded set of conventional CONTRIBUTING.md and AI-policy paths. Found text is stored as an untrusted repository-level contribution_guidance facet with exact file provenance. Offline readers may classify only predefined policy statements; repository text cannot introduce instructions or grant capabilities.

Thread-header sync never fetches pull-request detail facets per listed item. Repository metadata and fixed policy paths share one explicit context budget. Thread-header sync is a separate capability over an existing repository projection: pageable selections consume as many list requests as fit, while exact selections are admitted atomically. Batch allocation is stable by input order. Each executor consumes the immutable plan produced before network or corpus writes, and reports the same planned_requests and request_budget values afterward.

The issue-list endpoint used by thread-header sync does not expose pull-request merge state. A header-only closed pull request therefore stores merge state as unknown, not false. Explicit pr_details hydration makes the value known, and later header refreshes cannot erase that observation. Offline filters for merged=true or merged=false, dossier outcome groups, precedent/seed classification, portfolio attention, and health merge rates include only explicitly observed outcomes. Unknown closed outcomes are surfaced separately; MCP thread output omits the nullable merged field until it is known. Health also consumes the repository-level threads coverage fact instead of inferring completeness from the number of rows returned. External merge rates are nullable: when no explicitly observed closed external outcome exists, JSON reports null and the health coverage explains why the rate is unavailable. A known zero merge rate remains distinct from an unknown rate.

Pull-request portfolios use the ordinary repository and thread projections. github.sync_pull_request_portfolio is the only public portfolio producer. Its discriminated selection is either authored discovery (optionally scoped to one repository) or an explicit bounded set; identity lookup, authored discovery, and scalar status refresh are internal phases rather than separately advertised operations. REST pr_details and pr_reviews facets are combined with typed GraphQL facets for checks, unresolved review threads, detailed merge state, merge queue, closing issues, and changed files. Each facet has independent coverage; an incomplete refresh preserves the previous complete child snapshot but marks the newer coverage incomplete. Offline portfolio reads therefore return unknown instead of treating missing checks as passing or missing overlap signals as no overlap.

Feedback and CI are separate producer capabilities. Feedback preserves issue comments, submitted reviews, inline comments, and GraphQL review-thread topology as independently covered channels. CI observations bind checks and statuses to the pull request's observed head SHA. An unsupported log request fails before submission rather than producing fabricated log coverage.

Repository-wide feedback indexing is a separate bounded workflow:

github.index_pull_request_feedback
  -> jobs.get
  -> corpus.search_pull_request_feedback(feedback_author=exact_login)
  -> gitcontribute://pull-request-feedback/{owner}/{repo}/{number}
  -> gitcontribute://pull-request-feedback/{owner}/{repo}/{number}/{channel}/{feedback_id}

The index job enumerates provider pull-request pages with state=all and stores its next page, request count, item bound, and completeness in a repository-scoped discovery checkpoint. Each discovered PR is then passed through the exact feedback adapter, so open, closed, and merged PRs retain their observed head SHA, merge state, channel coverage, and raw facet payloads. The normalized pull_request_feedback_fts projection is rebuildable from complete raw observations; a newer incomplete or stale observation can advance coverage without replacing the previous complete child snapshot. Offline search reports discovery and facet coverage separately from match count and returns the exact index, feedback-sync, or PR-details recovery action needed to resolve an unknown. Search never performs network access.

The live MCP server exposes workflow.get_catalog_contract as a read-only catalog-parity contract. It reports the running version, whether the server is in all or read_only mode, a deterministic fingerprint of the registered tools/list definitions, and explicit availability of each feedback route. This makes a stale registration distinguishable from an intentionally restricted catalog. Clients must create a fresh MCP connection after setup, upgrade, or registration changes before comparing the contract.

Thread resolution remains outside this read/index workflow. A future mutation must accept exact repository, PR, and thread identifiers plus the expected head SHA and must be separately authorized; indexing never auto-resolves a thread.

Portfolio relationships and derived resolution records are local product contracts. Their normalized snapshots carry rule versions and exact source observation references. Explicit timeline events may produce a resolution; closing-issue relationships remain relationship evidence until completion is independently observed. Lexical similarity alone never becomes a root-cause claim. Corpus portfolio and resolution reads perform no network access.

Repository and thread projections use this ordering:

(source_updated_at, observation_sequence)

A newer source timestamp wins. If timestamps are equal or unavailable, the local observation sequence breaks the tie. This prevents late or retried work from replacing a newer projection.

Duplicate-cluster projection

Duplicate clusters are a versioned derived projection over stored repository threads. Listing a repository's clusters, inspecting one cluster, finding a member's cluster, and MCP corpus.find_clusters are pure corpus reads: they do not score candidates, write SQLite, fetch GitHub, or execute a process.

Refresh is a separate, explicit local-write capability:

read one repository snapshot
  -> identify source + governance + rule versions
  -> close the read transaction
  -> generate a lossless bounded candidate-pair set
  -> score candidate pairs exactly with cancellation
  -> reconcile durable governance
  -> begin write transaction and recheck revisions
  -> atomically replace the visible projection

A projection identity consists of the repository, a full SHA-256 source revision, a monotonic governance revision, and a similarity rule version. The source revision covers every candidate field consumed by scoring or persisted output. An empty completed projection has an identity too, so unchanged empty repositories do not recompute on every refresh. If source or governance changes during computation, commit returns a stale-input error and leaves the previous complete projection visible.

duplicate-v1 scores pairs at a 0.30 threshold. Candidate generation is lossless: every pair with an explicit thread reference or shared normalized title token is scored exactly. A pair with neither signal can contribute at most 0.15 from body, labels, and author, so it cannot reach the threshold. The 10,000,000-comparison budget remains a worst-case population bound; sparse repositories score fewer pairs. Refresh statistics distinguish all possible_pairs from exact scored_pairs. Cancellation is checked during preparation and candidate evaluation. Similarity preparation, candidate selection, and scoring live in internal/similarity; internal/clustering remains storage-free, while internal/corpus owns snapshots, governance transactions, and atomic projection replacement.

Membership overrides are durable governance, not direct projection edits. Adding an override and advancing its repository governance revision happen in one transaction. The next explicit refresh applies the decision. A governed canonical choice may change the displayed canonical member, but the stable cluster identity remains anchored to the engine-selected canonical member so the next refresh can recover its history.

Complete facet replacement

Hydration buffers every page before writing. ApplyFacetObservationSet then replaces the previous facet and advances coverage in one transaction:

fetch page 1..N -> validate complete set -> begin transaction
                 -> compare source ordering
                 -> replace observations
                 -> advance coverage
                 -> commit

Cancellation, a page error, or a stale source revision leaves the previous complete set visible. An empty complete set is meaningful: it replaces old children and records complete coverage with zero items.

Persistent job records

Long-running application operations return stable job IDs. Job state and events live in SQLite; active execution remains process-bound. A stale running job is marked failed after restart and is never silently replayed, because external reads and host operations cannot all promise safe automatic replay. The agent may inspect the stored request and explicitly resubmit an idempotent operation after reviewing the failure.

Each JobExecutor registers an opaque owner ID and periodically updates its lease. A new executor reconciles only running jobs whose owner is absent or stale. It must never fail jobs owned by another live process.

queued -> running -> succeeded
                  -> failed
                  -> cancelled

Terminal states do not transition again. Cancellation is first persisted, then delivered to an in-process worker directly or observed by one executor-wide poll that checks all active job IDs together. Executors cap both running and admitted jobs, so nested operation-specific worker pools cannot create unbounded process-wide concurrency. Reconciliation uses an immediate SQLite transaction so a heartbeat cannot interleave between the liveness read and stale-owner update. MCP job reads expose structured phase, completed-item, total-item, percentage, and retry-delay fields. Concise polling omits stored request and result payloads; detailed mode retrieves them after a finalist is terminal. Batch reads and cancellation preserve input order and isolate per-item failures; free-form job event text is not an MCP progress contract.

Bounded batch operations

Batch MCP operations preserve input order and return an outcome per input key. Independent GitHub reads use fixed server-side concurrency ceilings; code acquisition uses a lower ceiling because it performs Git processes and local writes. A single unavailable or retryable item does not erase successful siblings. Callers should retry only items marked retryable and use the provided recovery hint for unavailable inputs. Duplicate batch keys are rejected before submission instead of being silently collapsed, because collapsing would make the returned outcome count differ from the requested input count. Index requests also reject two remotes for the same repository as ambiguous.

Agent tool contract

MCP exposes one canonical gitcontribute:// resource namespace. Historical resource aliases are not advertised or routed. Tool names follow capability boundaries (corpus, github, code, workspace, validation, workflow, and research) rather than mirroring low-level API endpoints. Frequently chained operations may be consolidated only when they share one authority and one failure boundary; a read must never hide a refresh, write, or process run. The CLI exposes the unified all catalog. Internal catalog projections are used only by controlled tool-selection evaluations. The catalog keeps code/workspace execution, external derived research, diagnostics, portfolio, and advanced similarity as separately annotated capabilities rather than separate user-configured profiles.

Feedback routing is intentional: use github.index_pull_request_feedback -> jobs.get -> corpus.search_pull_request_feedback for repository-wide comment audits, and github.sync_pull_request_feedback -> jobs.get when exact PR numbers are already known. corpus.search_threads remains the generic offline text search for issue or pull-request records; it is not the comment-level feedback projection.

Tool inputs are strict and bounded, output distinguishes total population from returned/truncated items, and errors state how the caller can recover. MCP SDK annotations describe observable effects: pure external reads are read-only and idempotent with open-world access, while reads that also persist projections remain write operations. Catalog changes require realistic multi-call agent evaluations, including held-out queries, tool-call count, errors, latency, and context size; scripted schema checks alone do not establish good tool choice.

Recovery-plan actions are sealed variants. The concrete input type derives the action discriminator, the output schema advertises the variants with oneOf, and decoding rejects unknown, mismatched, or multiple payloads. Application code therefore receives a typed action rather than validating a discriminator against a nullable argument bag.

The canonical source-audit workflow is a machine-readable contract exposed by workflow.get_source_audit_contract:

corpus.get_coverage -> typed exact/repository recovery -> jobs.get
         -> corpus.get_threads/get_thread_facets -> duplicate checks
         -> explicit github.sync_threads -> jobs.get
         -> receipt attachment -> evidence/draft handoff

Each transition declares its required token, result type, allowed next actions, retryability, side-effect authority, and incomplete semantics. Post-job handoffs use a discriminated FollowUpAction; generic tool argument bags and arguments reconstructed from prose are not part of the contract.

corpus.ensure_coverage is the explicit network-authorized recovery boundary. It durably owns repository bootstrap, thread-header synchronization, selected facet hydration, coverage verification, and immutable snapshot creation. Ordinary coverage and corpus reads remain offline.

Immutable read identity

Reusable research reads use scoped CorpusSnapshotToken records rather than a global revision alone. Tokens bind scope, observation watermark, source manifest, derived versions, completeness, provenance, and an immutable payload digest. Resolution fails closed with snapshot_unavailable or snapshot_expired; current mutable projections are never substituted.

Read-only search, precedent, coverage, code-search, fix-pattern preview, and research-brief responses also bind their query digest to an observation watermark. Because these operations have no local-write capability, their ephemeral: identities are explicitly non-durable and report completeness, truncation, and unknown coverage inline. A caller that needs reuse across calls must cross the explicit corpus.ensure_coverage write boundary and use the resulting durable snapshot token.

Code indexes are separate immutable artifact records addressed as gitcontribute://artifact/code-index/<artifact-digest>. Re-indexing an unchanged source commit emits a new artifact identity and does not mutate old resource payloads. See ADR 0005.

Live acquisition and corpus artifacts

Explicit live acquisition follows one direction:

live GitHub request
  -> adapter conversion with provider provenance
  -> local observations and/or immutable digest-bound artifact
  -> compact MCP result plus opaque resource URI
  -> local resources/read

github.search_threads accepts a required nested repository reference and persists the returned issue or pull-request observations and an exact github-thread-search.v1 result artifact. A search page never advances repository-wide thread coverage and an empty page is not proof that no matching live thread exists. github.read_source_files accepts the same required nested repository reference, resolves one named ref to a commit, reads bounded repository-relative files in input order, and stores a source-bundle.v1 artifact. Commit SHA is the authoritative revision; GitHub blob SHA remains a separate file identity. Source content is untrusted text and is never merged into thread facets or code-index snapshots.

The two artifact resource families are gitcontribute://artifact/github-thread-search/<digest> and gitcontribute://artifact/source-bundle/<digest>. Their resource readers open the local corpus only and verify the digest before decoding the typed payload. The live operations have network-read/local-write annotations; the resource reads are offline and immutable.

GitHub transport

The GitHub adapter wraps go-github behind narrow read interfaces. For each logical request, the retry transport invokes the rate-limited base transport:

go-github -> bounded retry loop -> rate limiter -> HTTP transport

Putting the limiter inside the retry loop means every actual attempt consumes rate capacity. Only replayable reads are retried. Backoff honors GitHub rate headers, is bounded, observes context cancellation, and redacts URL userinfo before retry metadata is persisted.

Repository identities are parsed at input, provider, and persistence boundaries into a private, comparable domain.RepoRef. Interior code cannot construct an owner without a repository name, carry whitespace, or bypass the owner and repository grammar; it receives a parsed identity and uses explicit accessors. The zero value is reserved for optional scope and must be tested with IsValid. JSON decoding reparses the identity, and larger domain records hold it in named fields so its codec cannot be promoted over the enclosing record.

Pull-request merge knowledge is likewise a parsed domain.MergeStatus, not independent merged, merged_known, and merged_at fields. Constructors make unknown, observed-unmerged, and observed-merged outcomes explicit. SQLite and GitHub adapter reads reject contradictions such as an unknown outcome marked merged or an unmerged outcome with a merge timestamp; interior code cannot create those combinations. The relational schema keeps scalar columns for querying, but rows are reparsed before they enter application models.

Durable run and job lifecycles are read through private state values that bind statuses to their timestamps. Running work cannot be completed, queued jobs cannot already be started, terminal work requires a completion time, and only cancelled or cancellation-requested jobs carry a cancellation time. Terminal run transitions are conditional on the stored running state, while job transitions update the status and required timestamps atomically. Corrupt or unknown persisted combinations fail at the corpus boundary.

JSON inputs that express alternatives remain wire-compatible discriminated objects, but they are parsed before any durable job is submitted. Thread sync becomes either repository discovery with repository-only filters or an exact thread set. Portfolio sync becomes either authored discovery or an explicit pull-request set. Actor identity becomes either a canonical login or a node ID, and coverage becomes either a repository target or an exact typed thread. Workers receive these private variants rather than the original field bags, so mode-specific fields cannot be silently ignored and identity strings are canonicalized before duplicate detection. The normalized wire form, not the caller's mutable slices or pointers, is what the durable job records.

Acquisition and workspaces

Acquisition and workspace packages invoke git directly with prompts, hooks, global configuration, optional locks, and repository filesystem monitors disabled where applicable. Crawling and indexing never run repository code.

Managed mirrors are keyed by repository identity and remote, and acquisition uses both in-process and filesystem locks. Remote validation rejects embedded credentials before metadata is written. Transient worktrees are checked for cleanliness and removed after indexing. The indexer streams all selected blobs through one long-lived git cat-file --batch process and prepares one SQLite insert statement per snapshot transaction. A clean commit with a stored current-format snapshot is reused without rereading blobs or replacing its documents; the format version changes whenever indexing semantics change.

Validation is a different capability. It executes only after the caller passes the explicit execution flag and records the command, working directory, environment allowlist, timeout, output bound, and result. The MCP definition tool accepts managed workspace IDs rather than arbitrary host paths. The application resolves each ID and verifies that it belongs to the selected investigation before persisting executable state. The explicit CLI remains a local-user interface and may accept a directly supplied path.

Observation definitions cross command and MCP boundaries as untrusted specs. The application parses a complete base-and-candidate contract before it enters the evidence service. Parsed observations have a private representation: their source and artifact-path relationship is established once, default occurrence is normalized, and regular expressions are compiled once for execution. Persistence decodes through the same parser, so malformed stored contracts do not re-enter the trusted model. Execution therefore consumes parsed values and does not repeat structural validation or regular-expression compilation.

Durable workflow JSON is parsed again on read. Concern, investigation, hypothesis, opportunity, validation, and evidence discriminators cannot enter application logic as unchecked strings; legacy empty states are canonicalized only where their historical meaning is unambiguous. Telemetry metrics decode as either an available value or an unavailable reason and reject payloads claiming both. External validation receipts atomically store their synthetic definition and run, while external evidence manifests atomically store the complete claim set. A failed import therefore leaves no orphan definition or partial manifest.

Bulk local-metadata and collection inputs are fully parsed before writable corpus access. Collection references are stored in canonical repository, thread, or UUID form, and malformed later members cannot follow earlier writes. Thread projections similarly parse kind, lifecycle state, repository key, and number before a transaction begins and again when SQLite rows are read.

Search and analysis

Search uses the local SQLite corpus and FTS5 indexes; agents query bounded application tools rather than receiving raw database access. Repository ranking weights owner/name at 10, topics at 5, and description at 2. Thread ranking weights title at 10, labels at 5, body at 2, and complete hydrated facet evidence at 0.5. Code ranking weights path at 5 and content at 1. README text remains available through code search when indexed; it is not silently treated as repository metadata because code coverage can be partial. Scoped code search returns the selected snapshot manifest even when no document matches, so absence can be separated from a missing or truncated index. Snapshots created before manifests were introduced report indexed_coverage_unknown; their zero skip counts are never presented as proof of complete coverage.

Repository coverage uses collection membership to represent presence: a returned domain.FacetCoverage is necessarily present, while a missing facet is absent from the collection. Its private constructor binds the facet name, observation time, completeness, and non-negative count. Immutable code-index artifacts similarly use their digest-bound manifest as the sole in-memory authority; duplicated query columns are checked against that manifest while decoding and discarded rather than exposed as a second source of truth.

Title, labels, body, and hydrated evidence are materialized into one search document per thread and ranked by one BM25 invocation. Ranks from the legacy thread and facet indexes are never compared; the facet index is used only to identify the matching evidence source and excerpt. A thread page and its exact count share one read transaction. Counts use a lean FTS match set rather than recomputing ranking and excerpts, and the first-page no-overflow case derives its total directly from the returned rows.

Relevance is the default. Equal-ranked results use newest source revision as the first tie-breaker. Repository and thread tools also expose sort=updated for tasks that explicitly ask for the newest matching records. Search returns bounded excerpts rather than complete thread bodies or files; exact-object tools provide details after the agent narrows candidates. Thread search indexes titles and bodies plus product-selected fields from complete hydrated issue comments, pull-request reviews, review comments, and opt-in timeline events. The searchable facet projection is replaced in the same transaction as its observations; a failed, cancelled, incomplete, or stale refresh cannot expose a partial search document. Transport pages are collapsed into one semantic facet document, and matches report the source facet plus a bounded excerpt. Untrusted discussion remains searchable data and cannot grant capabilities. Cursors encode their query and scope so they cannot be reused for a different search. Ordering always has a deterministic tie-breaker. Hydrated search text is materialized once per complete facet replacement and bounded to 262,144 characters per thread. Results expose match_truncated=true when that bound omitted text; complete API coverage must not be mistaken for complete search-text coverage.

FTS rank is retrieval evidence and must not be relabeled as a separately hand-written score. Match explanations report the actual lower-is-better BM25 rank and the indexed document or hydrated facet that supplied the excerpt; they do not guess token matches with a second string matcher. Freshness and coverage are separate facts. Lens ranking uses a bounded population and therefore does not support cursor pagination. Contribution Radar similarly ranks a bounded open-issue population, separates score from the explicit ready_to_code, needs_diagnosis, needs_coordination, and blocked eligibility states, and reports positive signals, risks, blockers, and unknown evidence. One repository run accepts and can return up to the same 500-issue population that it evaluates. Eligibility derives only from stored policy, labels, discussion, ownership, collision, and coverage facts. The cross-repository MCP form requires an explicit non-empty repository set and reports the evaluated total and truncated. Per-repo summaries distinguish considered, returned, per-repo truncation, and an internally capped population. It deliberately does not expose a cursor: callers raise the bounded limit or narrow the repository set instead of treating a newly derived ranking as a stable result snapshot. Radar normalizes PR text, authoritative closing-issue facets, issue/comment references, opt-in timeline cross-references, and duplicate projections into bounded related_work facts with exact source evidence. Quoted and code-fenced text is excluded from lexical relationship classification. Only an open PR with a closing relationship is an implementation blocker; open dependencies and non-closing PR relationships require coordination. Missing coverage is never silently converted into a negative score, but it prevents a ready-to-code claim. Health metrics, dossier generation, and thread research briefs also operate only on stored facts and report partial or missing coverage when required facets are incomplete. A research-brief section must carry a source reference or an explicit unknown reason; untrusted thread text remains data and cannot grant an adapter additional authority.

Seed extraction is also a strict offline read. It labels explicitly merged pull requests as positive examples and explicitly closed-unmerged pull requests as negative constraints. A closed issue is negative only when GitHub records not_planned or it carries a predefined rejection/supersession label; all other issues are context, not outcome evidence. The default seed view includes positive and negative evidence, while contextual issues require an explicit polarity selection. Repository-controlled titles and bodies may add excerpts to evidence but cannot determine polarity.

Starting an investigation from a thread is an explicit corpus-write capability. The investigation and seed hypothesis are committed in one transaction and carry the exact thread observation ID, source timestamp, and observation sequence used as their baseline. A partial unique origin key returns the existing open pair on repeated or concurrent requests; later thread projections do not rewrite that baseline, and a closed investigation releases the origin for a deliberate new start.

Evidence freshness is a read-time assessment over stored observations, not another persisted evidence relation. Source-backed evidence can record the repository, thread, facet, or guidance revision it used; readers compare those recorded revisions with the current winning local projections and return fresh, stale, unknown, or not_applicable. Freshness reads must not perform network access, execute processes, delete evidence, or silently treat stale evidence as invalid. Tracking exports require schema version 2 so evidence provenance is always explicit; unversioned and other schema versions are rejected before import writes begin.

Concern intake ledger

Repo-local concerns are durable intake records for findings that are not yet contribution hypotheses. They are distinct from precedent Seed records, which are read-only examples extracted from stored threads. Concerns use UUID identities, explicit lifecycle transitions, bounded FTS5 search, typed relationships, and source revisions evaluated by the existing evidence freshness reader.

Creating, listing, searching, updating, or linking a concern is corpus-only: it performs no GitHub access, worktree read, or process execution. Similarity and duplicate candidates remain typed links and never become a root-cause claim. Only the promotion operation may set promoted; it atomically creates the investigation and seed hypothesis, optionally creates an opportunity, and stores the downstream IDs on the concern. A status-only update cannot create a partially promoted record.

Concern protocol results expose opaque workspace and evidence IDs but omit absolute paths and source-reference URLs. Local provenance retains exact source revisions so fresh, stale, and unknown are derived at read time rather than persisted as mutable truth.

Contribution readiness is also a pure corpus-read capability. It re-evaluates a versioned rule set for one opportunity and returns deterministic checks with pass, warn, block, or unknown status, evidence references, and remediation text. Only objectively unsubmitable local states, such as an archived repository, closed target thread, failing candidate validation, or unresolved contradicting evidence, should block. Missing guidance, missing coverage, stale evidence, or incomplete validation usually remain warn or unknown. Readiness must not fetch GitHub, execute validation, mutate opportunities, or inspect repository-controlled code while evaluating a gate. MCP exposes the same report as an offline tool/resource and adds workflow prompts that point agents at local resources first. Those prompts must preserve the same side-effect boundary: they may suggest explicit tools, but they cannot authorize network reads, local writes, process execution, or GitHub mutation.

Contribution evidence manifests are explicit local-write exports. They use an in-toto Statement v1 envelope around a product-owned predicate and bind claims to repository, opportunity, and optional managed-workspace identities. The workspace identity covers HEAD, staged and unstaged patches, bounded untracked content, submodules, and commit metadata; every omitted resource becomes an explicit gap. Candidate validations are usable only when their pre/post snapshot is complete, unchanged, and equal to the exported candidate snapshot. Stored GitHub facets and evidence are evaluated independently, so missing, stale, or unknown data cannot become a passing claim.

Manifest generation never refreshes GitHub. Callers explicitly sync the exact repository or pull-request facets first, then export from SQLite and optional non-mutating Git reads. The content ID excludes generation/evaluation clocks, is recomputed before persistence, and rejects mismatched subjects or payloads. Drafts may store a manifest ID as structured metadata; renderers do not copy manifest claims into public prose.

Schema changes

Migrations are embedded from internal/corpus/migrations and applied by Goose, but an ordinary corpus read never applies them. Read-only opens inspect the schema and return a typed migration-required or newer-schema error. Existing corpora advance only through the explicit corpus migrate capability, which plans pending versions, creates a verified online backup by default, reports step boundaries, and holds an exclusive cross-process corpus lease. Normal readers hold shared leases; incompatible work fails fast instead of waiting on SQLite until an opaque deadline.

Schema history starts from the canonical 001_initial.sql baseline. Every supported corpus carries the canonical lineage identifier in SQLite's application_id; numeric migration versions are never used to infer lineage. Unmarked corpora and corpora from another lineage remain untouched and cannot be migrated in place, even when their numeric version happens to match. Use a matching release, or archive or move the incompatible corpus out of the configured database path before setup creates a corpus on the canonical baseline. Future schema changes may append migrations within this identified lineage.

setup may create a missing empty corpus. It must not migrate an existing corpus or rebuild derived indexes as a side effect of installing a launcher or coding-agent integration. Versioned private runtimes are staged independently; client registration is activated only after the configured corpus is usable.

Add migrations in numeric order; never edit an applied migration to change an existing database. Every migration needs a working Down section unless SQLite cannot represent the reversal safely, in which case the reason belongs in the migration and an ADR. Large data migrations need prerequisite indexes and a regression plan based on representative row counts.

Durable observations and derived projections have separate lifecycles. Search and clustering indexes carry product-owned rule versions and status. A read may report a stale or unavailable projection; rebuilding is an explicit local-write operation and never triggers GitHub access.

Corpus inventory is also read-only. The corpus-wide view performs grouped aggregation and returns at most one summary per repository or code-index scope; it does not load observation payloads or code documents into the application. Database and WAL bytes remain whole-file measurements because SQLite pages are shared. Logical observation-payload and code-content bytes are reported separately rather than attributed to individual database pages.

Semantic commit preparation is a two-step local read. First, workspace.inspect_commit_changes asks Git for a binary/full-index patch and untracked blob identities, then uses the maintained sourcegraph/go-diff parser to expose stable file and hunk units. Second, the agent supplies semantic grouping judgment to workspace.plan_semantic_commits; deterministic code rejects stale inventories, unknown or duplicate unit assignments, and invalid dependency graphs. Ambiguous units remain explicit. A verified reconstruction record binds one-to-one unit coverage to the exact source patch and untracked content identities. Neither operation stages files, applies patches, creates commits, changes refs, executes repository code, or contacts GitHub. Applying a plan is intentionally a separate future capability with an explicit mutation boundary.

Storage changes should include tests for upgrade behavior, rollback when supported, stale-write rejection, transaction atomicity, and deterministic query ordering.

Further decisions