Semantic Inspector /api/v1 protocol
Status: Completed
Parent: Semantic Inspector
This page is the normative JSON contract between the Semantic Inspector backend and browser. The web application walkthrough explains when each resource is requested and which Sage operation supplies it; this page defines the bytes which cross that boundary.
The protocol is internal to one cargo sage inspect executable and its
embedded assets. It is versioned and tested exactly, but it is not a public
remote API compatibility promise.
Serialization rules
- JSON is UTF-8 without a byte-order mark.
- Checked response files use two-space indentation, LF line endings, and one trailing newline.
- Object fields appear in the order shown by the DTOs on this page. A missing
variant field is omitted rather than serialized as
null; fields explicitly declared as nullable are always present. - Semantic maps cross the protocol as ordered entry arrays. JSON object-key order is never used to encode semantic order.
- Enum and union variants use the exact lowercase kebab-case tags shown here.
- Product IDs and ephemeral handles are case-sensitive URL-safe strings.
Product IDs contain ASCII letters, digits,
_, and-; clients treat continuation, run, and revision handles as opaque rather than interpreting a prefix. - A
SymbolPathis a canonical slash-separated sequence of backend-authored, URL-safe ownership segments. The frontend treats the complete string as opaque. When used as a query value it is percent-encoded normally; clients do not parse or construct its segments. - Protocol snapshots are produced by the live server over checked-in Rust sample projects. Tests compare stable response bytes directly; a snapshot is never parsed and loaded as the server's semantic value.
The DTO notation below uses these string aliases:
SymbolPath = String // canonical backend-authored ownership path
ProductId = String // opaque URL-safe product identifier
ContinuationHandle = String // cont_<token>
RunHandle = String // run_<token>
RevisionId = String // rev_<token>
The comments constrain serialization; all five values remain opaque strings to the client.
Contractual HTTP behavior
Every /api/v1 JSON request sends:
Accept: application/json
Requests do not send a session token, expected revision, or browser-action
header. A browser action correlates its requests using the request_id
returned in each response.
Ordinary JSON responses send:
Content-Type: application/json; charset=utf-8
Cache-Control: no-store
The event stream instead sends Content-Type: text/event-stream and
Cache-Control: no-store. Static application assets are specified by the web
application rather than this JSON protocol.
Successful resource requests return HTTP 200. Missing resources and protocol or server failures use:
| HTTP status | Meaning |
|---|---|
| 400 | malformed JSON, invalid tag, or invalid path/query parameter |
| 404 | unknown route, unresolved symbol path, invalid ephemeral handle, or a product not listed for that symbol |
| 500 | unexpected inspector failure |
Every successful response has this field order:
Response<T> {
revision_id: RevisionId,
request_id: String,
run_id: RunHandle | null,
value: T,
}
Every error response, including an unresolved path or invalid ephemeral handle, repeats the backend's current revision:
ErrorResponse {
revision_id: RevisionId,
request_id: String,
run_id: RunHandle | null,
error: ApiError,
}
ApiError {
code: String,
message: String,
}
run_id is always present. It is null when satisfying the request performed
no recordable semantic work, including reads of retained run, continuation, or
revision data. The service assigns revision_id from one process-wide
sequence, including across a database reconstruction, so the browser never
needs a separate database-lifetime identifier.
For a success, revision_id identifies the coherent state which produced the
value and is still current when the owned response is frozen. For an error, it
is the backend's current revision when the error is constructed. A later
update is announced through the revision event stream. The browser never
installs a response whose ID differs from the revision it is displaying. It
discards every response-derived value, bootstraps a fresh directory, and
replays the current URL instead.
Domain outcomes do not use ErrorResponse: selection ambiguity, solver
ambiguity or overflow, candidate incompleteness, diagnostics from erroneous
Rust, and explicit unsupported Sage features remain variants or fields of the
corresponding value DTO. Cancellation caused by a concurrent update is retried
internally; it is not a public result status.
Shared scalar DTOs
Issue {
code: String,
message: String,
}
Badge {
label: String,
tone: "neutral" | "accent" | "success" | "warning" | "danger",
}
SymbolPresentation {
eyebrow: String | null,
badges: [Badge],
}
SymbolReference {
path: SymbolPath,
label: String,
presentation: SymbolPresentation,
}
Symbol kind, origin, source location, and provenance may be reflected inside product content, but they are not frontend control enums. Directory and header styling uses only the generic presentation fields above.
Session
GET /api/v1/session
Session {
protocol_version: "1",
target: CargoTarget,
workspace_root: String,
capabilities: [Capability],
retained_revisions: RetainedRevisionRange,
}
CargoTarget {
package: String,
target_kind: "lib" | "bin" | "example" | "test" | "bench",
target_name: String,
}
Capability =
"symbol-index"
| "products"
| "runs"
| "events"
| "revisions"
| "revision-comparison"
RetainedRevisionRange {
first: RevisionId | null,
last: RevisionId | null,
}
Capabilities appear in the order listed above. A delivery slice advertises only implemented capabilities; sample-project evidence never claims a capability whose route is intentionally absent in that slice.
Current revision
GET /api/v1/revision
This returns Response<null>. Its top-level revision_id is the backend's
current coherent revision. The browser requests it during bootstrap and may
request it again whenever it wants to check whether the view it is displaying
is current.
Complete local symbol index
GET /api/v1/symbols
SymbolIndex {
root: SymbolPath,
symbols: [SymbolSummary],
}
SymbolSummary {
path: SymbolPath,
parent: SymbolPath | null,
label: String,
display_path: String,
search_text: String,
presentation: SymbolPresentation,
children: ChildCompleteness,
}
ChildCompleteness =
{ status: "complete" }
| { status: "not-applicable" }
| { status: "incomplete", reason: Issue }
The array is a deterministic source/preorder traversal: parents precede their
children and siblings follow represented source order. Parent edges are the
complete local tree; there is no branch-fetch resource. children reports
whether the index's represented children are complete and is not a list or an
invitation to fetch another local resource. External symbols never appear.
Selecting a known symbol
The browser selects a local symbol using the canonical path already present in the complete symbol index. It selects an external symbol using a path from a reflected semantic reference:
GET /api/v1/symbol?path={symbol-path}
This returns Response<SelectedSymbol>. The query value uses ordinary percent
encoding; the frontend does not parse the path before sending it.
Local search text never crosses the API boundary. The browser filters the complete local index and sends the chosen row's path. External symbols are not text-searchable in this RFD and remain reachable through semantic links. Source-position selection is deferred until an editor-facing client requires it.
SelectedSymbol {
path: SymbolPath,
label: String,
display_path: String,
presentation: SymbolPresentation,
parent: SymbolReference | null,
products: [ProductDescriptor],
}
ProductDescriptor {
id: ProductId,
label: String,
href: String,
}
The product array contains only the pages valid for this symbol, in display
order. IDs are opaque and are not a frontend enum. The browser creates tabs
directly from this array and labels them from label; it does not maintain a
symbol-kind, origin, or product table. Each href is an exact relative
/api/v1 path. The server revalidates a product request. A direct request for
a product which the current list does not contain returns HTTP 404 with error
code product-not-found.
Symbol products
GET /api/v1/product?symbol={symbol-path}&product={product-id}
The response is Response<ProductPage>:
ProductPage {
id: ProductId,
title: String,
content: RenderNode,
}
The browser neither branches on id nor interprets the semantic meaning of
title. A source page, a diagnostic page, and a reflected typed body all use
the same response shape.
Generic rendering tree
RenderNode =
{
kind: "group",
layout: "block" | "row" | "columns",
children: [RenderNode],
}
| {
kind: "heading",
level: 1 | 2 | 3,
text: String,
}
| {
kind: "text",
text: String,
}
| {
kind: "code",
language: String,
text: String,
highlights: [CodeHighlight],
}
| {
kind: "notice",
tone: "neutral" | "info" | "warning" | "error",
title: String | null,
text: String,
}
| {
kind: "navigation",
target: SymbolReference,
}
| {
kind: "value",
value: ValueNode,
}
CodeHighlight {
start: u32,
end: u32,
role: String,
}
Code highlight offsets are UTF-8 byte offsets into text. The rendering union
is the complete frontend page vocabulary. An unknown node is a visible
protocol error. Product identifiers never select a renderer; the page's tree
does.
Reflected value tree
ValueNode =
{
kind: "record",
type_name: String,
fields: [ValueField],
}
| {
kind: "variant",
enum_name: String,
variant_name: String,
fields: [ValueField],
}
| {
kind: "sequence",
type_name: String,
items: [ValueNode],
}
| {
kind: "scalar",
type_name: String,
value: null | bool | number | String,
}
| {
kind: "reference",
target: SymbolReference,
}
| {
kind: "cycle",
identity: String,
}
| {
kind: "truncated",
summary: String,
continuation?: ContinuationHandle,
}
ValueField {
name: String,
value: ValueNode,
}
Records, variants, wrappers, options, and map-entry projections use these
ordinary nodes. Summaries are additive, and a reference retains both its
display label and canonical symbol path. The client matches this union
exhaustively; an unknown kind is a visible protocol error rather than a
silently omitted subtree.
Stash pointers and slices normally expose their values inline, including when
hash-consing makes two edges share an allocation. cycle.identity is a
display-local ID emitted only when reflection reenters an allocation currently
on its recursion stack; it is not a process address or semantic identity.
Truncation is explicit and can be continued without executing Sage work.
Continuations
GET /api/v1/continuations/{continuation-handle}
ContinuationValue {
continuation: ContinuationHandle,
items: [ValueNode],
next: ContinuationHandle | null,
}
The response is Response<ContinuationValue> with run_id: null and reads
only the retained owned observation. next is always present.
Run observations and traces
GET /api/v1/runs/{run-handle}
RunObservation {
run_id: RunHandle,
request: RunRequest,
root: TraceNode,
}
RunRequest =
{ kind: "symbol-index" }
| { kind: "symbol", target: SymbolPath }
| { kind: "product", target: SymbolPath, product: ProductId }
| { kind: "automatic-refresh", resource: String }
TraceNode {
phase: "bootstrap" | "selection" | "analysis" | "reflection" | "view-assembly",
source: "salsa" | "sage" | "solver" | "external-metadata",
operation: String,
key: TraceKey,
disposition: "executed" | "validated" | "reused" | "cancelled" | "observed",
child_order: "sequential" | "unordered",
observations?: Integer,
children: [TraceNode],
}
TraceKey =
{
kind: "semantic",
value: String,
}
| {
kind: "unmapped",
ingredient: String,
}
The response is Response<RunObservation> with run_id: null: reading an
already-retained run is not a child of the run being displayed.
Every tracked-query invocation is represented by the balanced span emitted by
the temporary Salsa fork, including an already-current memo fetch. Its
disposition is executed, validated, reused, or cancelled; nested spans
determine the tree rather than arrival adjacency. Sage and metadata spans use
observed when none of the Salsa dispositions applies. Repeated identical
leaf requests may be stored once with observations > 1; omission means one.
This is a lossless multiplicity encoding, not sampling.
child_order is recorded by the producer of the parent span. sequential
preserves capture order. unordered declares that sibling order is not part of
the contract. Checked serialization recursively canonicalizes every unordered
child subtree, serializes that subtree using this protocol, sorts the resulting
UTF-8 byte strings lexicographically, and retains duplicate multiplicity
through observations. Identical byte strings need no tie-breaker because
exchanging them cannot change the output.
An unmapped event retains a stable code-generated Salsa ingredient name. Raw
Salsa keys, timestamps, and arrival order appear only in interactive
diagnostics and test-failure artifacts. If even the ingredient cannot be named,
the checked key uses ingredient: "unknown"; the event remains visible, but a
test containing it cannot claim a closed exact dependency contract until that
family is mapped.
Revision events
GET /api/v1/events
Accept: text/event-stream
The stream emits these event names and JSON data payloads:
event: revision-advanced
data: RevisionAdvanced
event: workspace-reloaded
data: WorkspaceReloaded
The browser requests GET /api/v1/revision whenever this stream connects or
reconnects. Events are wakeups rather than a durable replay log; the revision
handshake detects an update published while the connection was absent.
RevisionAdvanced {
revision_id: RevisionId,
edit_batch: String,
changed_inputs: [InputIdentity],
}
WorkspaceReloaded {
previous_revision_id: RevisionId,
revision_id: RevisionId,
reason: Issue,
}
InputIdentity {
kind: "source-file",
path: String,
field: "text",
}
Keepalive comments carry no semantic data. The stream does not promise replay IDs; the current-revision handshake is the reconnect contract.
Revision history and comparison
GET /api/v1/revisions?cursor={cursor}
GET /api/v1/revisions/{revision-id}
GET /api/v1/revisions/compare?from={revision-id}&to={revision-id}&symbol={symbol-path}&product={product-id}
cursor is an opaque URL-safe string. The symbol path uses ordinary query
percent-encoding.
RevisionPage {
revisions: [RevisionSummary],
next_cursor: String | null,
}
RevisionSummary {
revision_id: RevisionId,
cause: RevisionCause,
input_delta_count: u32,
run_count: u32,
}
RevisionCause =
{ kind: "initial" }
| { kind: "input-edit", edit_batch: String }
| {
kind: "workspace-reload",
previous_revision_id: RevisionId,
reason: ErrorBody,
}
RevisionDetail {
summary: RevisionSummary,
input_deltas: [InputDelta],
runs: [RunHandle],
}
InputDelta {
input: InputIdentity,
old_hash: String,
new_hash: String,
diff: String,
}
RunComparison {
from_revision: RevisionId,
to_revision: RevisionId,
symbol: SymbolPath,
product: ProductId,
value_changed: bool,
executed_only_before: [TraceIdentity],
executed_only_after: [TraceIdentity],
reused_only_before: [TraceIdentity],
reused_only_after: [TraceIdentity],
}
TraceIdentity {
source: "salsa" | "sage" | "solver" | "external-metadata",
operation: String,
key: TraceKey,
observations?: Integer,
}
These resources return their matching Response<T> with run_id: null.
Comparison reports observed differences; it does not invent a causal
invalidation edge. RevisionCause records whether the retained revision is
the initial workspace state, an incremental input-edit batch, or a database
rebuild. A rebuild links to the last revision of the previous database
generation so that revision history does not disguise a wholesale reload as
ordinary incremental invalidation.
Source-driven contract snapshots
Protocol evidence starts from a checked-in Cargo project such as:
test-projects/semantic-inspector/db-drop-guard/
├── Cargo.toml
├── Cargo.lock
└── src/lib.rs
The black-box test launches cargo sage inspect in that project, waits for the
real workspace host and Axum server, and issues ordinary /api/v1 requests.
Reviewed snapshots capture stable response values together with the
actor-owned demand log. Focused provider tests use the same project to inspect
larger structural value and trace snapshots without paying a second transport
cost.
The sample project is input. Snapshots are output. There is no route manifest, dummy API server, scripted semantic provider, or JSON file which Sage reads to answer a protocol request. Snapshot-update mode may write a candidate expected output for review; a mismatch never passes merely because the implementation emitted new output.
The current real-process suite pins exact bytes and contractual headers for a representative revision, semantic product, malformed request, missing symbol, unadvertised product, and unknown route. It also verifies routed HTML and a missing asset. Exact source-driven snapshots for every successful DTO and the continuation and revision-history route families remain implementation work; the protocol destination is unchanged.
Browser integration uses the same live server and project. Pure rendering component tests may construct protocol nodes directly, but they establish only the rendering component—not semantic computation, API compatibility, or an end-to-end Inspector anchor.
Protocol evolution
The completed /api/v1 contract is a regression gate. A protocol change
updates this page, affected source-driven snapshots, frontend types, and
browser scenarios in the same commit. An incompatible redesign uses /api/v2;
adding a field or variant to /api/v1 is still a reviewed exact-contract
change even when old clients could theoretically ignore it.