# Integrations Status: **draft**, part of `code-browser/review@2`. This document specifies how reviews are exported to other systems and how the app fetches what happens there. The review model and the store are in [REVIEW-FORMAT.md](../REVIEW-FORMAT.md) ("format §N" below). Each adapter has its own document: [github.md](github.md). It does not specify a user interface. The key words MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are to be read as in RFC 2119. ## 1. Scope This release does three things: - **Export.** The user sends one submitted review, with its threads, to one destination, once (§6). - **Attach.** The user attaches a destination to the change they are reviewing, without exporting anything (§3.1). Opening a pull request's URL attaches it. - **Fetch.** For a destination a review was exported to or that is attached, the app reads the destination itself (its description and checks) and everything in it (reviews, threads, replies, resolutions, edits and deletions, by anyone), and shows it beside the local review (§7). Fetching is **read-only**: it never writes to the destination. The app sends nothing after an export. Replies, resolutions, edits and retractions made in the app are local entries (format §10) and stay local; to answer at the destination, the user goes there. Sending them is later work (§9). ## 2. Terminology | Term | Meaning | | --- | --- | | adapter | Code that maps reviews onto one external system, named by a lower-case `system` such as `github`. | | destination | One place at a system that reviews are exported to or that is attached, such as one pull request (§3). | | export | Sending one review, with its threads, to one destination (§6). | | attachment | The record that a change (a repository and a change key, format §11.2) is shown with a destination, whether or not anything was exported there (§3.1, §5.5). | | description | The destination's own text, author and state: for GitHub, the pull request's body (§5.1). | | check | One CI result the destination reports for a commit, such as a GitHub check run or a GitLab pipeline job (§5.6). | | link | The record that a local review or thread was sent to a destination, with its external id (§5.2). | | remote entry | An item fetched from a destination: a review, thread or comment there (§5.3). | | viewer | The destination account the app acts as. | | marker | A hidden line in a sent body that names the local item (§6.3). | ## 3. Adapters and destinations An adapter maps reviews onto one external system: a forge's pull request reviews, tracker issues, SARIF, or (later) git refs. The core never requires one. A **destination** is one place at a system, named by the adapter with a canonical `locator` (for GitHub, `//pull/`) on a `host`. Each adapter declares the syntax of its locators and external ids. A `host` is lower-case ASCII (IDN names in punycode), with no port and no user info, and is always reached over `https`. - Only submitted reviews are exported. A review is exported to **at most one** destination, once. Several reviews MAY share one destination (a review and its re-review, on one pull request). - Exporting is always an explicit action by the user that names the destination. The system, host, locator and account come only from the user's choice or the app's own settings. A value derived from the clone (its remotes, files or config) MAY be offered as a suggestion, shown in full, and is never used without the user confirming it. Repository settings never set any of them ([SETTINGS.md](../SETTINGS.md)). - A review is **linked** to a destination when it has a link there or an export there that is not `cancelled`. - A destination is **in use** when a review is linked to it or it has an attachment (§3.1). Only destinations in use are fetched, apart from planning (§6.1). - Each adapter declares what it can do: `export`, `fetch`, `describe` (read the destination's description, §5.1) and `checks` (§5.6). An adapter that cannot export can still be attached and fetched. - An adapter that can be opened by URL also declares: | Declaration | GitHub | GitLab (for example) | | --- | --- | --- | | its **URL syntax**: which URLs name a destination, and the `host` and `locator` each names | `https://///pull/` | `https://///-/merge_requests/` | | the destination's **canonical URL**, from `host` and `locator` | the same, without a suffix | the same | | the **refs** that hold its head and base, fetched from the repository's remote when it is opened, and the remote-tracking ref the head goes to | `refs/pull//head` → `refs/remotes//pull/` | `refs/merge-requests//head` → `refs/remotes//merge-requests/` | | its **names**: the system's display name, what a destination is called, the short kind for a tab, and the short form of a locator | GitHub · pull request · `pr` · `#1287` | GitLab · merge request · `mr` · `!342` | The app tries each adapter's URL syntax on what is typed in the omnibar; opening one is the same flow for every system: find or clone the repository, fetch the declared refs, open the subject, attach (§3.1) and show the destination's tab. The core never names a system, a kind of destination or a ref layout of its own; it shows the adapter's names. ### 3.1 Attachments An attachment says: the change with this repository and change key is shown with this destination. It is how the user sees a pull request's description, checks and discussion while reviewing it, before, or without, exporting there. - The **change key** is the subject's, as format §11.2 defines it: for a subject opened from a destination's URL, that destination's canonical URL (§3), lower-cased; for a change review of a branch, the branch. A subject without a change key, and an audit (base = head), cannot be attached. - A change has **at most one attachment**. A destination MAY be attached to several changes of its repository (a branch, and the same pull request opened by URL). - Attaching is an explicit action, and the rules of §3 apply: the system, host and locator come from the user's choice. Two actions attach: - **Opening a destination's URL** (any adapter's URL syntax, §3, typed or pasted in the omnibar). The URL is the user's choice; it is attached with no further question, and `confirmed` is set. - **Attach** on a change review. The app MAY suggest destinations from the clone's remotes, the one whose head is the subject's head first; each is shown with its system, host, locator and account, and nothing is attached until the user picks one. The pick sets `confirmed`. - **Detaching** deletes the attachment. The destination's row and its remote entries and checks are then deleted if nothing else uses it (§3): no link, no export, no other attachment. Detaching never touches reviews. - An attachment changes nothing about reviews: no review or draft is created by attaching, and a draft's change key is not changed by it. Browsing a pull request by its URL therefore leaves an attachment and the fetched copy in the store, and nothing else. - **Exporting** a review of an attached change offers the attached destination first (§6.1). The user still confirms the plan. - A restore clears `confirmed` on every destination (§5.1), attached ones included. ## 4. Authority | Data | Authoritative | Rule | | --- | --- | --- | | reviews, threads, evidence, coverage, entries | the store | never changed by a fetch | | what exists at a destination: its description, checks, reviews, threads, comments, their text and state | the destination | the fetched copy is overwritten by each fetch | | which destination a change is shown with | the store (attachments) | changed only by the user attaching or detaching (§3.1) | | a thread's status in the store | local `resolve` entries | a remote resolution does not create or change one | | a linked thread's status at the destination | the destination | shown beside the local status | | a linked review's standing and verdict | the store, for the local standing; the destination, for the verdict it shows | shown side by side | A linked item therefore has two states: its local state, and its state at its destination. Readers show both where they differ (for example "resolved here, open on GitHub", "edited on GitHub", "deleted on GitHub", or "superseded here, still requesting changes on GitHub"). Neither is folded into the other. ## 5. Tables These tables are part of the store and follow format §4: its conventions (`-- time`, `-- oid`, `-- bool`, `-- json` stand for the checks of format §4.3), migrations, transactions and backups. Migration 1 creates §5.1 to §5.4 without the description columns of §5.1; migration 2 adds those columns, and §5.5 and §5.6. ### 5.1 Destinations ```sql CREATE TABLE destinations ( id INTEGER PRIMARY KEY, repo TEXT NOT NULL REFERENCES repos(id), system TEXT NOT NULL CHECK (system NOT GLOB '*[^a-z0-9-]*' AND system <> ''), host TEXT NOT NULL CHECK (host NOT GLOB '*[^a-z0-9.-]*' AND host <> ''), -- §3 locator TEXT NOT NULL, -- adapter-defined, canonical (§3) url TEXT, -- §5.3 URL rule title TEXT, head TEXT, -- oid: the destination's head at the last fetch viewer TEXT, -- the account's public name, e.g. its login viewer_id TEXT, -- the account's stable id at the system confirmed INTEGER NOT NULL DEFAULT 0, -- bool: the user confirmed this destination generation INTEGER NOT NULL DEFAULT 0, -- §7 fetched TEXT, -- time of the last complete fetch fetch_error TEXT, paused_until TEXT, -- time (§6.6) data TEXT, -- json: adapter-defined; never an endpoint (§8) -- the description (§7 step 1), for adapters that declare `describe`: body TEXT, -- markdown as the destination returns it; NULL when it has none truncated INTEGER NOT NULL DEFAULT 0, -- bool: body was cut to fit author TEXT, -- display name, as for remote entries (§5.3) author_id TEXT, state TEXT CHECK (state IN ('open', 'draft', 'merged', 'closed')), -- as the adapter maps it remote_state TEXT, -- the destination's own state word, verbatim base_ref TEXT, -- the branch it merges into, as the destination names it head_ref TEXT, -- the branch it merges from created TEXT, -- time, as the destination claims updated TEXT, -- time, as the destination claims checks_omitted INTEGER NOT NULL DEFAULT 0, -- checks of the head not stored (§5.6) UNIQUE (system, host, locator) ) STRICT; ``` The description columns follow the bounds and checks of §5.3: `body` is cut at 1 MiB, the others at 1 KiB, and all of them are untrusted text, displayed as format §5.3 requires. Each complete fetch overwrites them; the store keeps no history of them. - A destination belongs to one repository. A review is exported only to a destination of its own `repo`. - `confirmed` is set by confirming an export plan (§6.1) or by **re-confirming** the destination: an action that shows its system, host, locator and account and sets `confirmed = 1`. A restore clears it (format §4.6): a restored store may come from anywhere, so no destination in it is fetched until the user confirms it again. ### 5.2 Links ```sql CREATE TABLE links ( id INTEGER PRIMARY KEY, destination INTEGER NOT NULL REFERENCES destinations(id), review_id TEXT NOT NULL REFERENCES reviews(id), thread_id TEXT, external_id TEXT NOT NULL, -- the item at the destination url TEXT, sent TEXT NOT NULL, -- time sent_digest TEXT NOT NULL, -- 'sha256:' + hex of the body sent, marker removed (§6.3) FOREIGN KEY (review_id, thread_id) REFERENCES threads(review_id, id), UNIQUE (destination, external_id) ) STRICT; CREATE UNIQUE INDEX links_local ON links(destination, review_id, ifnull(thread_id, '')); CREATE INDEX links_by_review ON links(review_id); CREATE TRIGGER links_same_repo BEFORE INSERT ON links WHEN (SELECT repo FROM reviews WHERE id = NEW.review_id) IS NOT (SELECT repo FROM destinations WHERE id = NEW.destination) BEGIN SELECT RAISE(ABORT, 'a review is linked only to a destination of its repository'); END; CREATE TRIGGER links_frozen_update BEFORE UPDATE ON links BEGIN SELECT RAISE(ABORT, 'a link cannot change'); END; -- and the same BEFORE DELETE ON links. ``` - A link says: this review, or this thread of it, was sent to this destination and is `external_id` there. - A local item has at most one link per destination (`links_local`), and an external item belongs to at most one local item. - Links are written once, when an export is sent (§6.4), and never change or get deleted. A link whose external item was deleted stays; the fetched copy says it is gone (§5.3). ### 5.3 Remote entries ```sql CREATE TABLE remote_entries ( id INTEGER PRIMARY KEY, destination INTEGER NOT NULL REFERENCES destinations(id) ON DELETE CASCADE, external_id TEXT NOT NULL, -- adapter syntax (§3) kind TEXT NOT NULL CHECK (kind IN ('review', 'thread', 'comment')), parent_external_id TEXT, -- a comment's thread, if any; a thread's review, if any author TEXT NOT NULL, -- display name, as the destination gives it author_id TEXT, -- the author's stable account id edited_by TEXT, -- account id of the last editor, when the destination says body TEXT, -- NULL for a thread with no text of its own, and once deleted truncated INTEGER NOT NULL DEFAULT 0, -- bool: body was cut to fit verdict TEXT CHECK (verdict IN ('approve', 'request-changes', 'comment')), -- reviews, as mapped remote_state TEXT, -- the destination's own state word, verbatim resolved INTEGER, -- bool: threads only resolved_by TEXT, anchor_path TEXT, -- as the destination anchors it, checked (below) anchor_side TEXT CHECK (anchor_side IN ('head', 'base')), anchor_commit TEXT, -- oid, when the destination gives one anchor_start INTEGER, anchor_end INTEGER, outdated INTEGER, -- bool: the destination says so url TEXT, created TEXT, -- time, as the destination claims updated TEXT, -- time, as the destination claims first_fetched TEXT NOT NULL, -- time last_fetched TEXT NOT NULL, -- time seen_generation INTEGER NOT NULL, missed INTEGER NOT NULL DEFAULT 0, -- complete fetches in a row that did not see it deleted TEXT, -- time the store learnt of the deletion data TEXT, -- json: adapter-defined, at most 64 KiB UNIQUE (destination, external_id), CHECK (kind = 'thread' OR resolved IS NULL), CHECK (kind = 'review' OR verdict IS NULL), CHECK (anchor_start IS NULL OR (1 <= anchor_start AND anchor_start <= anchor_end)) -- plus a byte limit on every text column: 1 MiB for body, 64 KiB for data, 1 KiB for the rest ) STRICT; CREATE INDEX remote_by_parent ON remote_entries(destination, parent_external_id); ``` - A remote entry is what the destination holds, by anyone, the viewer included. The fetched copy of an item the app sent is the remote entry with the link's `external_id`. "By the viewer" means `author_id` equals the destination's `viewer_id`; it is compared when needed, never stored. - **Bounds.** Adapters fit fetched values to the column limits before writing: a body over 1 MiB is cut at a UTF-8 boundary and `truncated` set; other text is cut likewise. A fetch never fails because an item is too large. - **Checks.** An anchor path that does not meet format §5.2, or lines that are not a valid range, are stored as `NULL`, with the raw value in `data`. A remote anchor is displayed only when its lines are inside the blob. While a thread is not `outdated`, its lines are those of the destination's current head: adapters map it so (GitHub's `line` is), and the app places it in the code only when that head is the one shown. A URL column holds only an `https` URL on the destination's `host` (or a host the adapter declares); anything else is stored as `NULL`. The app checks a URL the same way again before opening it. - Remote entries are ordered by `created`, then `external_id`. - `parent_external_id` is text, not a foreign key: a reply may arrive before its thread. Readers resolve it within the destination. - A remote verdict is shown as that account's verdict at the destination. It never counts as the user's verdict (format §11.1). - **Edits** at the destination overwrite the fetched columns. The store keeps no history of remote text. For an item the app sent, readers compare the link's `sent_digest` with the digest of the fetched body (§6.3) and show "edited at the destination" when they differ. - **Deletions** (§7) set `deleted` and clear `body` to `NULL`, keeping the row (its author, times and position) as a tombstone. An item seen again by a later fetch is back: its upsert clears `deleted` and `missed` and restores `body`. - Everything here is untrusted and displayed as format §5.3 requires. Readers show a remote entry that is a link's fetched copy as the destination state of that local item, not as a separate item. Other remote entries of a destination (other people's reviews, threads and replies) are shown with the reviews linked to it. A view that needs one order of a thread's comments (such as whether a thread waits on the user) takes the remote entries in their order, then the posted local entries about that thread in `pos` order. A comment is the user's when it is local or by the viewer. ### 5.4 Exports ```sql CREATE TABLE exports ( id INTEGER PRIMARY KEY, destination INTEGER NOT NULL REFERENCES destinations(id), review_id TEXT NOT NULL REFERENCES reviews(id), plan TEXT NOT NULL, -- json: the adapter's placement, as the user saw it progress TEXT, -- json: steps done, with the external ids they created state TEXT NOT NULL CHECK (state IN ('sending', 'sent', 'failed', 'cancelled')), maybe_sent INTEGER NOT NULL DEFAULT 0, -- bool: an attempt may have created something (§6.5) retryable INTEGER NOT NULL DEFAULT 0, -- bool: the last failure may succeed on retry (§6.6) account TEXT, -- account id of the last attempt attempts INTEGER NOT NULL DEFAULT 0, last_error TEXT, -- no credentials (§8) created TEXT NOT NULL, -- time finished TEXT -- time ) STRICT; CREATE UNIQUE INDEX exports_once ON exports(review_id) WHERE state <> 'cancelled'; CREATE INDEX exports_by_destination ON exports(destination); -- links_same_repo's check, BEFORE INSERT ON exports, and: CREATE TRIGGER exports_submitted BEFORE INSERT ON exports WHEN (SELECT state FROM reviews WHERE id = NEW.review_id) <> 'submitted' BEGIN SELECT RAISE(ABORT, 'only submitted reviews are exported'); END; ``` `exports_once` keeps a review to one export. A `sent` export is the record of what went where, and is kept. | From | To | When | | --- | --- | --- | | (new) | `sending` | the user confirms the plan (§6.1) | | `sending` | `sent` | the adapter confirms every step (§6.4) | | `sending` | `failed` | any failure (§6.6), or the app finds it `sending` at start | | `failed` | `sending` | the user retries it | | `failed` | `cancelled` | the user cancels it, after the adapter has undone what it left unpublished (§6.4) | An export found `sending` when the app starts was interrupted: it becomes `failed`, with `maybe_sent = 1` and `retryable = 1`. ### 5.5 Attachments ```sql CREATE TABLE attachments ( repo TEXT NOT NULL REFERENCES repos(id), change_key TEXT NOT NULL CHECK (change_key <> ''), -- format §11.2 destination INTEGER NOT NULL REFERENCES destinations(id), attached TEXT NOT NULL, -- time via TEXT NOT NULL CHECK (via IN ('url', 'pick')), -- §3.1: opened by URL, or picked PRIMARY KEY (repo, change_key) ) STRICT; CREATE INDEX attachments_by_destination ON attachments(destination); CREATE TRIGGER attachments_same_repo BEFORE INSERT ON attachments WHEN NEW.repo IS NOT (SELECT repo FROM destinations WHERE id = NEW.destination) BEGIN SELECT RAISE(ABORT, 'a change is attached only to a destination of its repository'); END; ``` - Attaching inserts the `repos` row for the repository when there is none (format §6.1 inserts it with the first review otherwise). - Attaching a change that already has an attachment replaces it, in one transaction that also applies the detach rule of §3.1 to the destination it had. ### 5.6 Checks ```sql CREATE TABLE remote_checks ( id INTEGER PRIMARY KEY, destination INTEGER NOT NULL REFERENCES destinations(id) ON DELETE CASCADE, commit_oid TEXT NOT NULL, -- oid the checks are for: the destination's head at the fetch name TEXT NOT NULL, -- as the destination names it, e.g. "ci / test (node 22)" state TEXT NOT NULL CHECK (state IN ('pending', 'running', 'passed', 'failed', 'skipped', 'cancelled', 'neutral')), remote_state TEXT, -- the destination's own words, verbatim url TEXT, -- §5.3 URL rule started TEXT, -- time, as the destination claims finished TEXT, pos INTEGER NOT NULL, -- the destination's order data TEXT, -- json: adapter-defined, at most 64 KiB UNIQUE (destination, pos) -- plus the byte limits of §5.3 ) STRICT; ``` - Checks describe one commit: the destination's head. Each complete fetch of an adapter that declares `checks` replaces **all** of the destination's rows in the transaction of §7 step 3. Checks have no tombstones and no history: a check that is gone is simply not there. - Readers show checks with their `commit_oid`, and say so when it is not the subject's head ("checks are for 9a0b7c2, a newer head"). - Only status, name, times and a link are stored. **Logs are never fetched**: they are the destination's, opened there through `url`. - A destination holds at most 500 checks: the first 500 in its order. The number of the head's checks not stored (past the cap, or of a kind the adapter does not map) is the destination's `checks_omitted`, replaced with the checks, so that readers can say "and 12 more". ## 6. Exporting ### 6.1 Planning Planning creates the destination row, or reuses the one with the same `(system, host, locator)`, and runs a complete fetch (§7): the user naming the destination confirms it for that fetch. The adapter then computes a **plan** from the review, its threads and the fetched state. When the review's change is attached (§3.1), its destination is the one offered first. A review cannot be planned when it is not submitted, already has an export that is not `cancelled`, or has a `repo` other than the destination's. The app says why. The plan has: - **The review:** what it becomes there, with its verdict (§6.2). - **Threads.** Each thread that is not private, with its placement: its own item there (on lines or on a file), part of the review's body, or dropped. - **The code.** "The diff" is the destination's diff at planning time. A thread whose `anchor_commit` is not in the destination's history is placed as a file or body thread, with a `shape` row. The plan records the destination's head. - **Evidence** is not quoted unless the user opts in for this export. When they do, the plan shows the exact text that will be sent for each quoted log. - **Supersedes.** When the review supersedes one linked to the same destination, the plan says which remote review it replaces, and whether the destination still shows that one's verdict (§4). Only an approval or a request for changes is a verdict there; an earlier comment (or a review whose verdict is gone) stays there as a comment. - **Entries.** Posted entries about the review are not sent. The plan says how many, as an `info` row. - A **loss report**: rows of kind `drop` (not sent), `shape` (sent in another form) or `info` (sent as expected, worth knowing). The app MUST show the loss report before sending, and MUST NOT send what the user has not seen planned. When the user confirms, in one transaction, the destination's `confirmed` is set and the export is inserted as `sending`, and sending starts (§6.4). When the user cancels, the destination's row and its remote entries are deleted if nothing uses it (§3); otherwise the row is kept as it was. ### 6.2 Mapping declaration Each adapter MUST declare, in its code and its document, how it maps: | Local | The adapter declares | | --- | --- | | change review and verdict | what it becomes, and what happens to each verdict | | audit | what it becomes, or that audits are refused (most forges have no equivalent) | | thread, by anchor | where it goes: on lines, on a file, in the review's body, or dropped | | thread kind, severity, suggestion, symbol | how they are rendered | | evidence | how it is quoted when the user opts in, and how much of the log | | `generated_by` | how it is shown | | coverage, entries, private threads | never sent | | each step | which remote state counts as done (§6.5) | and how it fetches: | Remote | The adapter declares | | --- | --- | | each kind of item it reads | which `kind` of remote entry it becomes, and its `parent_external_id` | | review states | the `verdict` each maps to, if any | | thread state, anchors, outdated | how they fill the columns of §5.3 | Everything a mapping drops or reshapes MUST appear in the loss report. ### 6.3 Markers and quoted text Every body an adapter sends SHOULD carry a **marker** naming the local target (format §3.4), in a form the destination keeps but does not show, when the destination has one. Markers let the adapter find what it sent when a confirmation was lost (§6.5). - A body carries exactly one marker, as its final line. Only the final line of a fetched body is read as a marker. - Text the user did not write (evidence logs and commands, excerpts, paths, symbol names) MUST be quoted so that it cannot end its quote. Text that may span lines (logs, commands, excerpts) goes in a fence longer than any run of backticks inside it. Paths and symbol names go in a code span likewise, after every control character in them (LF, CR, TAB and the rest) is replaced by a visible escape such as `\n`, and `|` by `\|` inside a table. It then cannot add a marker, a mention, a link, an image or markup. - **Digest.** The digest of a body, sent or fetched, is `sha256:` and the hex SHA-256 of its UTF-8 after converting it to LF line endings (format §5.1) and, if its final line is a marker, removing that line and the LF before it. Nothing else is changed. A sent body is digested as sent, with the adapter's prefix lines and after any cap. Adapters SHOULD test the digest on a real round trip to their destination. A marker proves nothing: anyone can write one. Markers are read **only** by the check of §6.5. A marker anywhere else is ignored, and a fetch never creates links. ### 6.4 Sending 1. In one transaction: set `state = 'sending'`, `account` to the viewer's id, and increment `attempts`. 2. If `maybe_sent` is set, run the check of §6.5 first, as the `account` of the attempt that may have sent. 3. Before the first step, the adapter reads the destination's head. If it differs from the plan's, the export fails with `last_error` "head moved" and `retryable = 0`: the anchors were planned against another diff. The way on is to cancel it and plan again. 4. The adapter renders each request from the stored rows and `plan`. Each finished step is recorded in `progress`, with the external ids it created, in a transaction of its own. An attempt resumes after the last finished step. 5. On confirmation, in one transaction: set `state = 'sent'` and `finished`; insert the links (the review, and each thread placed as its own item, with every thread's placement in `progress`); and upsert the returned items as remote entries, with `seen_generation` set to the destination's `generation`. Then fetch (§7). 6. On failure, as §6.6 says. Sending never retries by itself. A failed export is retried or cancelled by the user. Before an export with progress is cancelled, the adapter MUST undo the remote state its steps left unpublished (for GitHub, delete the pending review). Until that succeeds, the export stays `failed`, with the reason in `last_error`. Cancelling an export with `maybe_sent` set leaves at the destination whatever it may have created; a fetch shows it as a remote entry by the viewer. ### 6.5 Uncertain sends Before a step with `maybe_sent` set is sent again, the adapter runs a complete fetch (§7) and looks for its marker. - If the fetch does not complete, the export fails again with `retryable = 1`. It is never sent without a completed check. - A **match** is a remote entry of that destination whose marker names the step's local item, whose `author_id` equals the export's `account`, and which, where the destination tells, no one else has edited (`edited_by`). With several matches, the one with the earliest `created`, then the lowest `external_id`, is used, and the others are reported to the user. - If the match is in the adapter's done state for that step (§6.2), the step is finished with it. If not (for GitHub, a pending review), it is recorded in `progress`, and sending resumes at the next unfinished step. - With no match, `maybe_sent` is cleared and the step is sent. - If the destination cannot hold markers, the app asks the user whether to send again, saying it may duplicate what was sent. ### 6.6 Failures and rate limits An adapter reports each failure as one of: | Kind | Examples | Result | | --- | --- | --- | | **not sent** | DNS failure, connection refused, TLS failure, rate limit, a refusal before any write | `failed`, `retryable = 1` | | **maybe sent** | timeout or reset after the request was written, a 5xx | `failed`, `retryable = 1`; on a creating step also `maybe_sent = 1` | | **permanent** | validation, permission, gone | `failed`, `retryable = 0`, with the reason in `last_error` | The app offers retry only when `retryable` is set, and cancel always. A rate limit pauses the **host**: every destination of that `system` and `host` gets `paused_until` (the `retry-after` time when given, else 60 s), and neither fetches nor sends until then. ## 7. Fetching A fetch reads one destination and upserts what it finds. It runs when a linked review is opened, when a subject with an attachment is opened, when the user asks, when attaching (§3.1), when planning (§6.1), after an export is sent (§6.4), and MAY run periodically (no more than once a minute per destination). Periodic fetches of a destination that is in use only through attachments run only while a window shows a subject attached to it. A fetch never runs for a destination that is paused or not `confirmed`, except when planning. A fetch never writes to the destination, and MUST NOT change reviews, threads, evidence, coverage, entries, links, exports or attachments. Every fetch is **complete**: it reads the whole destination. 1. The adapter reads the destination itself (for GitHub, the pull request) and records `url`, `title`, `head`, `viewer` and `viewer_id`, and, when it declares `describe`, the description columns (§5.1). If that fails with "not found" or "forbidden", the adapter cannot tell a deletion from lost access (an expired token, another account). The fetch stops with `fetch_error`, and nothing is marked deleted. 2. It increments `generation`, then reads every item, page by page. Each page is written in one transaction: upsert every item by `(destination, external_id)`, setting `last_fetched` and `seen_generation` to the current `generation` and `missed` to 0. An item whose id does not match the adapter's syntax (§3) stops the fetch with `fetch_error`. 3. When every page has been read without error, and the checks for the head of step 1 (adapters that declare `checks`), in one transaction that also sets `fetched`, clears `fetch_error` and replaces the checks (§5.6): every row that is not deleted and has `seen_generation` below `generation` gets `missed` incremented, and those that reach 2 are marked deleted. Lists can skip an item (a write between pages, a lagging index), so one miss is not a deletion. An adapter MAY mark an item deleted at once when a direct read of it says "not found" while the destination itself is readable. A fetch that did not complete MUST NOT change `missed` or mark anything deleted; the next fetch starts again from step 1. 4. A destination holds at most 20000 remote entries. A fetch that would go past that stops with `fetch_error`, marks nothing deleted, and stops periodic fetches of it until the user asks. Upserting by `(destination, external_id)` makes a fetch idempotent: fetching the same state twice changes nothing but `last_fetched` and `seen_generation`. Incremental fetching is not specified; an adapter that needs it comes with a migration. After a fetch that marks deletions, the app SHOULD checkpoint the WAL, so that deleted text leaves the live files (backups keep it, format §4.6). ## 8. Credentials Credentials are never in the store. Tokens stay where they are kept today (the `github.auth` setting): the OS keychain, or `gh auth token`, read by the main process when needed and never sent to the renderer. - A token is looked up by the destination's exact `host` (for example `gh auth token --hostname `, or the keychain entry for that host). A host with no token of its own gets none. - Adapters MUST build every request from `host`, `locator` and external ids that match the adapter's syntax. They MUST NOT send credentials to a URL taken from fetched data or from `data`, `plan` or `progress`, and MUST NOT follow a redirect to another host with them. - Credential lookup MUST NOT use git credential helpers or any repository-local config. Tools such as `gh` are found on the user's `PATH` (never in the current directory) and run with a fixed working directory outside every repository. - The store, its backups, the app's logs, error reports and anything sent to the renderer MUST NOT contain tokens, authorization headers or cookies. Adapters MUST scrub these from `last_error`, `fetch_error`, `data` and `progress`. `destinations.viewer` and `viewer_id` hold only public account names and ids. - Fetched bodies and URLs are stored as the destination returns them: they are what anyone with access there sees. Adapters SHOULD remove query parameters they know to grant access (for GitHub, `jwt` on private attachment links) from fetched URL columns. An adapter's `plan`, `progress` and `data` JSON are private to it. It MUST read what its own earlier versions wrote, or fail the export with `retryable = 0` for the user to cancel. ## 9. Later Not in this release, each coming with a migration: - **Sending entries.** Replies, resolutions, edits and retractions sent to a destination, and replies to fetched items. This needs an outbox of operations per destination, with the uncertain-send check of §6.5 on every creating operation, detection of edits made at the destination before an edit is sent, and rules for targets that were never sent, were placed in a review body, or were deleted there. - **Exporting again.** Sending threads added to, or changed in, a review after it was exported. Reviews are immutable, so today this is a re-review. - **Drafts as pending reviews.** Sending a draft to a destination that has pending reviews (GitHub's are visible only to their author), and replacing it as the draft changes. - **Check logs.** Fetching a failed check's log to show beside the code. It is more untrusted text, and larger, than anything fetched today. - **More of the destination.** Labels, assignees, requested reviewers, milestones, linked issues. - **Reactions**, and a local "concur", mapped to each other. - **Several accounts per host.** One viewer per destination is assumed. - **Remote images and attachments** in fetched bodies: they are not loaded (format §5.3), and not stored. - **Retention** of sent exports and tombstones.