Surface Console#
Surface ships a local Surface Console server for any producer that emits a Surface-format run snapshot. The current CLI, routes, and generated assets still use console; treat that as an implementation name, not product language.

Quick start#
From a Builder or producer project directory:
npx surface console
The Surface Console reads .surface/runs/latest.json by default. The server reads the file fresh on every request, so no restart is needed when the producer writes a new run.
Pass a custom path or port explicitly:
surface console --read-model .surface/runs/latest.json --port 4242
Use --port 0 to let the operating system assign a free port. The startup
message prints the assigned URL after the server has bound it.
Or use a config file:
{
"port": 4242,
"readModelPath": ".surface/runs/latest.json",
"vocab": {
"projectName": "My project",
"projectKind": "repo governance"
},
"theme": {
"brandName": "Veritas"
}
}
surface console --config surface.config.json
Merge multiple producer bundles into one view#
surface console also accepts repeatable --input bundle paths, mirroring surface report --input. This is additive to --read-model: any --input switches the console onto a merge-and-project path. With more than one input the console validates each producer bundle, merges them order-independently (mergeBundlesDetailed), and projects the merged ledger into the read model. Each input bundle is watched, so editing any producer bundle refreshes the view live.
npx surface console \
--input examples/console-multi-producer/ci-producer.bundle.json \
--input examples/console-multi-producer/review-producer.bundle.json \
--input examples/console-multi-producer/security-producer.bundle.json
The merged view adds two multi-producer affordances:
- Producer attribution. Every claim card names the producer(s) that asserted it, built from each input bundle's optional
producerId(falling back tosource). An identical shared claim contributed by several producers dedups to one card that still lists all of them. Because a merged bundle never carries a top-levelproducerId(merge.md§5 rule 3), attribution is carried as projection metadata keyed by claim id, built during the merge step — not read back off the merged bundle. - Merge collisions. Same claim id, different content across producers is surfaced in a dedicated "Merge collisions" section that names the colliding producers. The lexicographically-first content is kept and every losing record is reported there, never silently dropped (
merge.md§6). Theexamples/console-multi-producer/demo ships a build-digest collision between the CI and security producers.
Implementation note: surface console, *.console.json, /console.js, and /console.css are current implementation names. Product-facing language should describe the Operator experience as the Surface Console.
Developer map#
Surface keeps its own standalone Surface Console. Keep product-specific trust behavior in this repo, and use Kontour UI only as the shared presentation system across Kontour products.
| Area | Location | Purpose |
|---|---|---|
| CLI command | src/cli.ts, src/commands/console.ts |
Parses surface console flags and starts the local server. |
| Local server and API routes | src/console/server.ts |
HTTP transport only: routing, SSE, and request/response mechanics. Delegates domain orchestration to the runtime. |
| Console runtime | src/console/runtime.ts |
SurfaceConsoleRuntime owns read-model loading, run catalog, projection creation, HTML assembly, and Claim Package authoring — usable without starting an HTTP server. |
| Projection contract | src/console/projection.ts, src/console/claim-detail-projection.ts, src/console/types.ts |
Converts producer read models into the UI shape consumed by the Console, including the per-claim detail projection (guidance, gap labels, policy facts, integrity scope) the browser renders from. |
| Standalone UI shell | src/console/shell.ts |
Owns the dependency-free HTML shell served by /. |
| Standalone UI asset sources | src/console/client/parts/, src/console/styles/parts/ |
Owns the editable browser behavior and Kontour UI-compatible token aliases for the standalone Surface Console. |
| Generated UI asset constants | scripts/build-console-assets.mjs, src/console/client/index.js, src/console/styles/index.css, src/console/assets.generated.ts, src/console/script.ts, src/console/styles.ts |
Concatenates ordered client and CSS parts, then converts JS/CSS assets into TypeScript constants consumed by /console.js and /console.css; regenerate with npm run build:console-assets. |
| Browser coverage | tests/browser/console.spec.ts |
Starts bin/surface.mjs console and verifies the real standalone page on desktop and mobile. |
| Trust-state chip | scripts/build-console-assets.mjs, scripts/build-trust-state-css.mjs, scripts/check-console-token-drift.mjs, tests/browser/support/ui-trust-state.ts |
Renders claim statuses with Kontour UI's trust-state chip. At build time the Console bundles ui's renderer (@kontourai/ui/trust-state) and inlines @kontourai/ui/trust-state.css, and the Trust Panel gets the same rules with ui's light values as fallbacks, so the CLI and the panel stay standalone. The --check modes fail when a generated copy differs from the installed @kontourai/ui; the token gate fails when a --k-trust-* token is missing or drifts. |
| Trust Snapshot Viewer assets | scripts/build-pages-extras.mjs, scripts/pages-extras/ |
Adds the viewer page to the Pages site, with token CSS copied from the installed public @kontourai/ui package; the trust panel browser tests load the same page. |
| Package boundary guard | tests/package-files.test.ts, scripts/check-package-contents.mjs |
Keeps Kontour UI dev-only, prevents React/runtime leakage, and verifies published package contents. |
Run directory convention#
Producers write one file per run:
.surface/runs/
<run-id>.console.json # full read model for that run
latest.json # index → { latestRunId, readModelPath }
latest.json is either a full read model or an index pointer. If it contains kind: "surface-console-index" and a readModelPath, the server resolves the model from that path relative to the repo root. Otherwise it uses the file directly.
Run history#
When multiple *.console.json files exist in the run directory, the Surface Console toolbar shows a run picker that lets you compare runs without restarting the server.
The /api/runs endpoint returns a sorted list of available runs:
GET /api/runs
→ [{ runId, generatedAt, claimCount, verifiedCount, attentionCount, fileName }, ...]
Select a specific run by appending ?run=<runId> to the /api/read-model endpoint, or by using the run picker in the UI.
Surface Console features#
Metric chips#
The header row shows summary chips for total claims, verified count, and attention count. Each chip is clickable and filters the claim list to matching claims. The active filter is highlighted with a raised border; clicking again clears it.
Claim feed#
Each claim card shows:
- Claim ID and surface path
- The claim status as Kontour UI's shared trust-state chip: its display label (Verified, Needs refresh, Disputed, Pending review, No evidence, and so on), a glyph unique to the status, and a line style, with the status's own colour in light and dark. The card's left edge uses the same colour. A product
vocab.statusLabelsoverride replaces the visible label; the default label stays available to assistive technology. - Impact level badge
- Policy ID when present
Master-detail layout (desktop)#
On viewports wider than 900 px the Surface Console uses a sticky side panel instead of a modal overlay. Clicking a claim slides the detail panel in from the right without covering the claim list. The list and panel are visible simultaneously.
Claim detail#
The detail panel shows evidence, events, policy context, and integrity scope for the selected claim. The integrity scope is where producers can expose what a verified claim is anchored to: a source revision, working-tree digest, file fingerprints, or producer configuration hashes. For claims that are not yet verified, the panel shows contextual guidance describing what is needed to reach the verified state. For a stale claim the guidance follows why it is stale: the read-model claim's staleReason, or else the reason on its freshness-breach gap; only a commit reason is described as outdated evidence.
Evidence summary is the human-readable producer summary. Observed result is reserved for structured runtime output such as a pass/fail result, command, exit code, stdout, or stderr. If a producer only supplies a summary, the Surface Console does not duplicate it as an observed result.
Eval summary#
When a producer runs an eval cycle (e.g., via veritas eval record), it can write a generic EvalSummary into the run snapshot. The Surface Console uses this to show post-hoc review context alongside the live trust state.
EvalSummary shape (see src/types.ts):
interface EvalSummary {
reviewed: boolean;
reviewedAt?: string;
confidence?: "low" | "medium" | "high";
outcome?: "accepted" | "accepted-with-changes" | "rejected";
falsePositiveCount?: number;
missedIssueCount?: number;
timeToResolutionMinutes?: number;
notes?: string[];
metadata?: Record<string, unknown>;
}
This is a producer-agnostic shape. Producers may put domain-specific fields under metadata.
API reference#
| Route | Method | Description |
|---|---|---|
/ |
GET | Surface Console HTML shell |
/api/read-model |
GET | Current (or ?run=<id>) read model JSON |
/api/console-model |
GET | Current (or ?run=<id>) Surface Console projection |
/api/runs |
GET | Sorted list of available run snapshots |
/api/claims |
POST | Add a claim to the local claim store |
/api/claims/:id |
PUT | Update a claim in the local claim store |
/api/claims/:id |
DELETE | Remove a claim from the local claim store |
/console.js |
GET | Compiled Surface Console script; current route name |
/console.css |
GET | Surface Console styles; current route name |
Surface owns the Surface Console shell, status model, claim browser, and metadata drilldown. Producers own the read model and vocabulary that make the Console meaningful for their domain.