RedlineKnowledge base

Data schema and client state machine

Block identity

A block is the unit of review: heading, paragraph, list item, code fence or frontmatter field. Its id is <context>:<type>:<key> as produced by parseDocumentBlocks in the package-root semantic.js (T2.1 moves it there). The same function runs in three places and must stay the single implementation:

  1. the service, to diff merge-request layers and commit history;
  2. the remark plugin (T2.2), to stamp data-redline-block on rendered HTML;
  3. the tests, to compute expected ids.

A rendered element carries data-redline-block="<id>". Comments anchor to it, redlines decorate it, and changed-since marks it.

Schema additions

All additions are optional fields on existing objects, so the /v1 contract stays backward compatible. contracts/openapi.yaml is updated in T2.5.

Health

{
  "ok": true,
  "service": "fio-redline",
  "apiVersion": "1",
  "now": "2026-10-10T12:00:00.000Z",
  "gitlab": { "reachable": true, "checkedAt": "2026-10-10T11:59:50.000Z", "status": 200 }
}

gitlab.reachable is false when the probe (GET <internal>/api/v4/version, 3 second timeout, cached 15 seconds) throws or returns a status of 502, 503 or 504. status is the HTTP status or null on a network error.

Error code gitlab-unreachable

Any service request that needs GitLab responds 502 with { error: { code: "gitlab-unreachable", message: … } } when the upstream fetch throws (network error, timeout) or returns 502, 503 or 504. Other upstream failures keep the existing gitlab-api-error code.

Commit blockChanges and history blockHistory

Each commit in history.commits gains:

"blockChanges": [{ "blockId": "title:paragraph:paragraph", "type": "replace" }]

type is insert, delete or replace. It is null (not an empty array) when the service could not compute it, for example a document over the size limit.

history gains an aggregate, newest first per block:

"blockHistory": {
  "title:paragraph:paragraph": [
    { "sha": "bbbb…", "committedAt": "2026-07-29T12:00:00Z", "type": "replace" },
    { "sha": "aaaa…", "committedAt": "2026-07-28T12:00:00Z", "type": "insert" }
  ]
}

It covers only the commits returned in history.commits.

Identity mode

IDENTITY_MODE is cloudflare-access (default, current behaviour) or gitlab-oauth. In gitlab-oauth mode the Cf-Access-* headers are never read, GitLab OAuth is the only user identity, and the session’s accessEmail is the GitLab user’s email. Reads are governed by ALLOW_ANONYMOUS_READ as before. The /documents/state payload is unchanged; authentication.user is the GitLab user when connected.

A third mode, tailscale, is permitted if OAuth blocks the journey; see “Alternative identity” in network.md. In that mode authentication.user is the tailnet user and gitlabConnected is true whenever the identity headers are present, so the client machine is unchanged.

Client state machine

One store per (serviceUrl, siteId, documentPath) (T3.0) owns this machine. Every component reads the store; no component fetches on its own.

States

StateMeaning
staticNothing fetched yet. Build-time content is showing.
probingA fetch is in flight. Build-time content keeps showing.
liveA DocumentState payload is loaded and usable.
connect-requiredPayload loaded; the site allows comments, the reader has no GitLab session. A substate of live: live data shows, plus the connect prompt.
offline-networkThe service host did not answer at all. On the experiment network this means the reader is off Tailscale.
offline-gitlabThe service answered but GitLab did not.
unauthenticatedThe service answered 401.
errorAny other failure.

Classification

classifyRedlineFailure({ error, response, payload }) in client.js (T2.4):

  1. error is a TypeError, or response is absent → offline-network. (A browser fetch rejects with TypeError for DNS failure, refused connection, and CORS failure. All three mean “could not reach”.)
  2. response.status === 502 and payload.error.code === 'gitlab-unreachable' → offline-gitlab.
  3. response.status === 401 → unauthenticated.
  4. otherwise → error.

deriveRedlineState(payload) for a successful payload:

  • payload.capabilities.comment && !payload.permissions.comment && !payload.authentication.gitlabConnected → connect-required
  • otherwise → live

Transitions

FromEventTo
staticload()probing
any non-probingload()probing (previous payload retained)
probingresponse oklive or connect-required
probingfailurethe classified state
live, connect-requiredpoll tick (footer open, interval > 0)probing
offline-*, unauthenticated, errorpoll tickprobing (polling continues so recovery is automatic)
connect-requiredreader clicks connectbrowser navigates to /v1/auth/gitlab/start?returnTo=<page>; on return the page reloads into static

A load() while one is in flight returns the in-flight promise.

Messages

Defined once in client.js as REDLINE_MESSAGES and imported by components and tests. Exact strings:

StateMessage
static(none)
probingChecking for live changes…
liveLive · updated {time} where {time} is toLocaleTimeString()
connect-requiredConnect GitLab to comment. (rendered as a link to the start URL)
offline-networkLive review needs the private network. Showing history from the last build.
offline-gitlabGitLab is unavailable. Showing history from the last build.
unauthenticatedSign in to see live review.
errorLive review unavailable ({code}). where {code} is the error code or HTTP status

Every component root element exposes data-redline-state="<state>" so tests assert the state, then the message.

Invariants

  • Build-time content never disappears. Live data only adds to it, or replaces it in place; on failure the build-time content is what remains.
  • The connect link is shown only in connect-required, which requires a successful response. A reader off the network never sees a link to a GitLab they cannot reach.
  • The store never retries faster than 5 seconds.

Write path

For the experiment the only write path is comments. Whatever store holds them, the service enforces: an authenticated person behind every mutation; a mutation bound to one document; body and anchor size limits; one redline-audit log line per mutation with the actor. The existing GitLab implementation already does all four. If an interim store replaces it, the same four hold and a propagation task writes the interim records to GitLab issues or merge-request discussions, attributed in the body to the person. Drafts, approvals and merge remain feature-flagged off for the experiment site.

Git history

Loading the page's history…