rift
Protocol

Core

Planned

The core schema holds the values every surface shares: the shape of a projection, the structure of source, the semantic facts providers resolve, and the changes the server applies.

Projection state

A change names its target with a ProjectionId, or targets the workspace tree by naming none. ProjectionState exposes the conditions a caller checks before paging through a changeset:

{
  "dirty": true,
  "unaccepted": false
}

dirty indicates that the projection differs from its workspace base. unaccepted indicates that its changeset contains a confirmation requirement.

The server MUST refuse publish unless accept names every such change; path and dependency conflicts remain visible only in the publication result. The changes come from the rift://changes resource.

Paths and files

ProjectPath is a normalized UTF-8 path relative to the workspace root. It uses /, contains no . or .. component, and cannot name .rift - projections and the server lock live under there, and a path that could address it would let a change reach the server's own state. Normalized means Unicode NFC: the server normalizes what it emits and accepts, then compares byte-for-byte. On a case-insensitive filesystem, the server MUST refuse a create or rename whose destination case-folds onto a different existing path.

Every identifier that embeds a path - FileId, a node id, the rift://fs form - carries a ProjectPath in canonical percent-encoding. The generated schema patterns do not carry the exclusions above, so the server re-validates each decoded value.

ProjectEntry is a visible directory, regular file or symlink. Visible means below the workspace root, outside .rift, and not excluded by the workspace's VCS ignore rules. Visibility governs reads; publication carries every changed path, visible or not.

Each variant carries what a consumer can act on:

  • a regular file - its size and executable bit
  • a symlink - its target, as base64, because the server never follows it and cannot assume the bytes are text
  • a directory - only its path, so an empty one is still listed once something creates it

A symlink is listed and never followed, in every traversal:

  • reads
  • materialization
  • the execution copy execute runs in
  • publication
  • prune

A copy of a symlink reproduces the link. The server MUST reject a path that addresses through a symlink component, because following it could leave the workspace.

Source locations

TextRange is a pair of half-open UTF-8 byte offsets, and SourceSpan puts one against a file. Rift uses byte offsets rather than line and column because every provider converts from whatever its own toolchain counts in exactly once, at its own boundary; after that, two providers' offsets are on one scale.

A NodeId is positional and witnessed:

rift://node/python/pkg/util.py@1204-1266#3f9a1c2e

The byte range names where the node stands; the fragment is the witness - the first eight hex characters of the SHA-256 of the node's source bytes, minted when a listing returned the identity. Resolution recomputes the witness before acting on the address, so the server refuses an identity minted before the file changed with a failed source_unchanged precondition rather than splicing into bytes that moved. The witness makes a node address safe to hold across calls: the address carries its own proof of freshness without depending on connection-scoped state.

Source discovery

Source resolvers run before syntax or semantic analysis. They inspect workspace files and resolved toolchain state, then publish a source catalog made of SourceUnit records. A resolver discovers installed dependencies; it does not download packages or change the environment.

SourceLocation names a unit's owner: project, dependency, standard library, or external source. SourceKind names how the source came to exist: authored or generated. The two stay separate because generated code can belong to the project or to a dependency.

A generated unit names its generator and can carry SourceMapping records back to its inputs. A synthetic declaration has no source unit.

{
  "id": "rift://source/pypi/pydantic/main.py",
  "location": {
    "kind": "dependency",
    "package": { "manager": "pypi", "name": "pydantic", "version": "2.8.2" }
  },
  "path": "pydantic/main.py",
  "source_kind": "authored",
  "languages": [{ "name": "python", "dialect": null }],
  "digest": "6f2a9c31",
  "generator": null,
  "mappings": []
}

An id is two parts: the resolver identity (pypi), then that resolver's own human-readable key for the unit (pydantic/main.py). digest is the short form every content digest takes on the wire - the first eight hex characters of a SHA-256, the same convention NodeId's witness uses; the server computes and compares the full digest internally, so it never travels whole.

SourceResolverSummary reports each resolver's identity, locations, catalog revision, and lifecycle state. Resolver revisions change with files, manifests, lockfiles, toolchains, installed packages, or generated-source metadata. Fact-provider revisions change after providers analyze a catalog revision.

Semantic facts

Providers return facts in families: symbols, the syntax nodes that write them, relationships between symbols, resolved types, diagnostics, and history. A family no provider can produce refuses with capability_unavailable. Signatures and documentation travel on the symbol itself, covered under the types family.

A read result carries the answer, pagination, and warnings. A condition the caller must weigh - an index that lags the tree - arrives as a typed entry in warnings; the Reads section on the MCP page states the paging and warning contract.

SymbolOrigin keeps source location separate from source kind. A synthetic symbol carries neither a location nor a package; dependency and standard-library symbols can carry facts even when their provider cannot return source bytes. The declaration's own source-catalog unit, where its location differs from the project, rides on the get_symbol hit itself, addressed by unit beside the path a project declaration carries.

History facts

The history family is resolved from the workspace's version-control history rather than from the current tree. A SymbolHistory is one symbol's timeline - each SymbolVersion names the RevisionId that touched it and what the revision did: introduced it, changed its body, changed its signature, moved it, removed it.

The history provider correlates declarations across revisions using their parsed structure and surrounding changes. A SymbolId can change when a language includes a module path in its qualified name; the timeline still records the move.

The walk is bounded by the configured history depth, and the timeline rides a get_symbol hit when the request asks for history.

Changes

An Edit is one filesystem effect: replace these bytes, create this file, delete it, move it, copy it, flip its executable bit. Edits in one set share one input state, cannot overlap, and apply atomically.

The change tools resolve down to a set of those, plus the preconditions the server checked, the effects the change had, the diagnostics it produced, and GuaranteeEvidence for every property a hook established.

When resolution yields no edits, the server returns a RefusalReason - unmet_precondition or unsupported - and leaves the targeted tree unchanged. When it yields edits, the change lands carrying its Advisory list - the concerns providers and hooks attached to exactly this change. A checked advisory is a verdict the emitter verified itself; an open warning carries the instruction that settles it.

In a projection, an edit with an unresolved concern - an open warning advisory, a hook that failed or did not finish, a deletion - carries a ConfirmationRequirement per problem, and the caller settles them by naming each in accept at publish. A change applied directly to the workspace has no publication to gate, so its confirmations list stays empty and its advisories are the caller's to act on.

A process can write directly into a projection directory without producing an Edit. The server reconciles that filesystem delta into a change whose origin is filesystem; its paths are authoritative when a binary file or symlink has no portable edit form. The imported change carries an external confirmation, so the server withholds publication until the caller reviews writes that bypassed a change tool.

Transport and infrastructure failures use ErrorData, with a code, a phase, and a retry directive saying whether the caller may resend the same request.

The full field definitions are in the generated reference.