Scrubbed extract of
Spatial-Cognition-Platform/ADR/ADR-SCP-004-Contract-First-Projection-Wire-Format.md— cited by the Chronicle (08-now-shipping). Redactions: phone / service detail / secrets / clearance / personal / internal hosts.
type: project project: Spatial-Cognition-Platform status: accepted created: 2026-08-27 tags:
- adr
- contracts
- truth-plane
- wire-format
ADR-SCP-004: Contract-first projection wire format; POC serves static snapshots
Status: Accepted Date: 2026-08-27 Deciders: Solo (Cory) Amended: 2026-08-30 — contract v0.2.0 (see Amendment — contract v0.2.0 below) Context Project: [[Spatial-Cognition-Platform]] · [[Concept-Layered-Planes-Architecture]] §1, §6
Context
The concept mandates graph-as-projection: the client consumes materialized projections of canonical facts, never the authoritative store. The POC has no backend service (schedule risk), but must not bake in a client/data coupling that a later Query Service breaks.
Decision
The wire contract is a first-class, versioned artifact — the platform’s most stable seam. Three JSON documents, each with a JSON Schema in contracts/ and checked-in fixtures:
SystemGraphSnapshot(Truth Plane projection, read-only to the client):contractVersion,generatedAt,source;entities[]{id, kind, name, attrs{}}with stable URI ids (entity://service/llm-proxy);assertions[]{id, subject, predicate, object, provenance{sourceSystem, evidenceRef, observedAt}}with a closed predicate set for v0 (depends_on, contains, exposes, consumes, deployed_on, observed_by); optionalartifacts[]{id, kind, uri, title}.ReasoningOverlay(Reasoning Plane projection, append-only):agentWork[]{id, type(observation|assumption|hypothesis|claim|decision|plan), content, author{kind: human|agent, model?}, createdAt, confidence, status, evidence[], derivedFrom[], contradicts[], supports[], revisedBy?}. Immutability semantics per [[ADR-SCP-006-Reasoning-Immutability-In-Client]].WorkspaceState(Experience Plane, the only client-writable document):workspaceId,views[]withplacements[]{ref, pos, rot, scale, pinned}, grouping/visibility rules, optional room-anchor binding. Canonical entities never carry coordinates ([[ADR-SCP-005-Workspace-State-Separation]]).
Transport for POC: the exporter (tools/snapshot-exporter) emits snapshot + overlay JSON; files are served as static HTTP from the homelab (any existing static host) and fetched by Scp.Unity.Net. A later Query Service (slice S4) serves the identical contract live — the client contract-side does not change; only the ISnapshotSource adapter gains a live implementation.
Rules:
contractVersionis semver, starting0.1.0; breaking change ⇒ minor bump pre-1.0 plus fixture updates in the same PR. CI validates every fixture against its schema (rung 1).- The exporter’s POC input is a hand-curated
inventory.yamldescribing the real homelab estate (seeded fromDeveloper/CLAUDE.md’s repo table); a Gitea-API-driven exporter replaces the input, not the contract, post-POC. - Provenance is mandatory on every assertion even in v0 — “the AI said so” is never an acceptable answer (concept §8), and retrofitting provenance is the expensive path.
Amendment — contract v0.2.0 (2026-08-30)
[[ADR-SCP-009-ADP-Integration-Boundary|ADR-SCP-009]] §4 pre-committed seven amendments to this contract, marked during the POC freeze and landing with Epic S4. [[../Research/SCP-ADP-Integration-Design|SCP ↔ ADP Integration Design]] §7 is the normative table. This section records them against this ADR, as §7 requires (“a contract change requires an ADR-SCP-004 amendment”), and resolves the three that were left underspecified.
The rule this obeys is the Decision’s own: any breaking change bumps the minor pre-1.0 with matching fixtures in the same PR, and CI validates every fixture against its schema at rung 1. Two of the seven are breaking — 7.1 and 7.2 — which is exactly what the pre-1.0 minor bump exists for. contractVersion goes 0.1.0 → 0.2.0.
The seven amendments
| # | Change | Schema | Breaking |
|---|---|---|---|
| 7.1 | AgentWork.confidence → nullable; add confidenceSource |
reasoning-overlay |
Yes (required → nullable) |
| 7.2 | provenance.evidenceRef retyped to EvidenceAnchor |
system-graph-snapshot |
Yes |
| 7.3 | Add optional AgentWork.correlation |
reasoning-overlay |
No (additive) |
| 7.4 | AgentWork.type += risk-assessment, option-enumeration, tool-call-justification |
reasoning-overlay |
No (enum widening) |
| 7.5 | Add optional AgentWork.summary |
reasoning-overlay |
No |
| 7.6 | Add AgentWork.sensitivity + AgentWork.redacted and the redacted render state |
reasoning-overlay |
No |
| 7.7 | (no schema change) — entity correlation IDs ride entities[].attrs, the schema’s intended open extension point |
system-graph-snapshot |
— |
They belong to two schemas and therefore have two deadlines: 7.1 gets more expensive once [[IP-SCP-7.1-Reasoning-Overlay-Content|IP-SCP-7.1]] freezes overlay content, 7.2 once [[IP-SCP-3.1-Homelab-Inventory-Authoring|IP-SCP-3.1]] freezes the inventory’s provenance rows.
7.1 — Confidence, resolved: confidenceSource is camelCase
AgentWork.confidence becomes nullable, and a required discriminator is added:
AgentWork.confidence : number | null
AgentWork.confidenceSource : "providerNative" | "reviewerAgent" | "engineDerived" | "unavailable" | "authored"
The member casing is camelCase on the SCP wire, verbatim from §7. [[ADR-011-Agent-Reasoning-Emission-Contract|ADR-011]]’s ProviderNative | ReviewerAgent | EngineDerived | Unavailable is Pascal because those are C# member names in ADP’s own model — Integration Design §3.5 quotes them as such. §7 is the pre-committed amendment text and is normative for the SCP wire form; §3.5’s Pascal is a quotation, not a second spelling of this field.
authored is the fifth value and has no ADP counterpart: it marks hand-written POC/demo content so it is self-identifying and permanently excludable from AI-MLOps-Platform calibration. Driver per §3.5: ADR-011 rejects prompt-engineered self-report and requires a source discriminator, and a required bare number forces the projection adapter to invent a confidence it does not have.
Recorded, not fixed: SCP’s
AgentWork.typeenum is kebab-case (risk-assessment) whileconfidenceSourceis camelCase. That is a genuine internal inconsistency. It is not resolved at v0.2.0 — normalising casing across the contract is a candidate for contract 1.0, where a deliberate breaking normalisation is cheap. Do not “fix” it inside a v0.2.0 IP.
7.2 — Evidence, resolved: retype evidenceRef in place; contentHash is the whole-file hash
provenance.evidenceRef changes type, not name:
provenance.evidenceRef : string -> EvidenceAnchor
EvidenceAnchor is adopted verbatim from repo-intel’s published contract (repo-intel/src/core/types.ts), all three fields required:
interface EvidenceAnchor {
path: string; // repo-relative path to the source file
lineRange: { start: number; end: number }; // 1-based INCLUSIVE
contentHash: string; // content hash of the source
}
JSON form on the SCP wire:
{
"path": "charts/aiservice/values.yaml",
"lineRange": { "start": 12, "end": 40 },
"contentHash": "<hex>"
}
contentHash is the hash of the WHOLE FILE, never of the anchored line-range snippet. This is repo-intel’s codebase-wide convention, shared by its indexer and its staleness checks; a snippet hash would make every SCP finding classify as obsolete the moment it is checked. Any IP that computes a contentHash must say “whole-file hash” in its deliverable text. A relaxed profile with optional lineRange / contentHash is not permitted: it would be a fork wearing the name of an import, and it would give back the staleness detection this amendment exists to buy (§3.4).
The field is not renamed. §7 row 7.2’s arrow targets the existing provenance.evidenceRef; §4.1’s “becomes the EvidenceAnchor.path” describes where the old string value migrates during back-fill, not a field rename. Reading it as a rename would add a second breaking change with no stated driver. One breaking change; fixtures diff on shape only. Renaming to evidenceAnchor is a clearer name and is rejected here — it doubles the breaking surface for cosmetics; if wanted, it belongs to the contract-1.0 normalisation alongside the casing item above.
Back-filling contentHash across the hand-curated inventory (25–40 entities / 40–80 assertions) is content authoring, not codegen: it is operator-executed, never locally dispatched, because its failure mode is inventing a provenance row with no evidence behind it.
7.3 — Correlation (additive)
AgentWork.correlation? : { sessionId?, routingDecisionId?, gateId?, requestExecutionId?, agentInstanceId?, toolUseId? }
All optional. Without it a projected object is unjoinable to the run that produced it, and an in-headset revision is unjoinable to anything — breaking AAT’s cross-lane join precisely where a human touched the work (§3.7).
7.4 — Type enum widening (additive)
AgentWork.type gains risk-assessment, option-enumeration, tool-call-justification — the three ADP ReasoningStepKind values with no SCP target, and the three most spatially interesting objects in the set: a four-axis risk shape, a multi-branch option fork, and the moment an agent decided to touch the world (§3.2).
7.5 — Summary (additive)
Optional AgentWork.summary. content remains the body. What you read at two metres and what you read when you pull the object to your face are different texts, and ADP already emits both as Summary / Detail (§3.3).
7.6 — Sensitivity and redaction, resolved: two fields, not one
§3.8 left the redacted wire representation as “design it deliberately.” It is designed here, so that the S4 service and the S6 client cannot diverge:
AgentWork.sensitivity : "public" | "sensitive" (required, default "public")
AgentWork.redacted : boolean (required, default false)
Two fields, and the split is the point. sensitivity is a property of the content. redacted is a property of this response to this viewer. A sensitive object served to a holder of adp-sensitive-viewer arrives with redacted: false and full text. The S4 service decides and stamps redacted; the Unity client never learns roles, tokens, or ADP authorization semantics — ADR-SCP-009 §2 keeps all of that server-side, and a single conflated field would have forced the client to reason about entitlement to render correctly.
When redacted: true:
summary=nullandcontent=null.- Everything structural still travels:
id,type,sequence,derivedFrom,revisedBy,confidence+confidenceSource,correlation. evidence[]entries keeppathandlineRange; any excerpt field is omitted. Per §3.8 and ADR-015: “Paths still travel; the path is not secret.”
Client contract: redacted: true renders the first-class redacted shape — real structure, real edges, real provenance, no text. It is never an error state, never an empty card, never a missing object.
7.7 — No change
Correlation IDs on entities ride entities[].attrs, which the schema already provides as its intended open extension point.
WorkspaceState is untouched at v0.2.0
None of the seven amendments touches WorkspaceState — a good result for [[ADR-SCP-005-Workspace-State-Separation|ADR-SCP-005]]: presentation state stayed genuinely separate across every seam in the integration design.
This is a freeze, not an absence of future need. Epic S7 (multi-view projections) does require contract changes here: the live DTOs are WorkspaceStateDto { contractVersion, workspaceId, views[], roomAnchor? } and ViewDto { id, placements[], groups? }, with no activeViewId and no rule-set linkage, so view switching and view rule-sets are a contract change despite §7’s “WorkspaceState unchanged” being read otherwise. Resolution: v0.2.0 leaves WorkspaceState exactly as §7 froze it. S7 adds two optional (therefore additive, non-breaking) fields under its own later minor bump with its own ADR-SCP-004 amendment:
WorkspaceState.activeViewId?ViewDto.ruleSetId?
S7 must not retro-fit these into v0.2.0.
An eighth candidate exists and is deliberately not auto-included
ViewDto’s docstring names a “candidate uniqueness constraint for contract v0.2.0”: duplicate placement refs inside a single view, currently resolved first-occurrence-wins on both directions — PlacementWriteCommand upserts the first match and SceneViewModelJoin reads the first match, so a duplicate-carrying document round-trips deterministically (Wave D defect fix, 2026-08-28).
It is not one of §7’s seven, and it is neither silently added nor silently dropped. S4’s contract IP raises it as an explicit Confidence Gate: adding the constraint makes today’s tolerated documents invalid (a breaking validation change, on a schema §7 otherwise leaves alone), while omitting it leaves a documented ambiguity resolved only by client convention. The decision is taken at that gate, in the open, not inside this amendment.
Consequences
Positive
- Client, exporter, and future services develop in parallel against fixtures — ideal for the multi-executor workflow.
- Time-travel (S5) falls out of snapshot files being immutable, timestamped documents.
- The schema is the canonical model’s public face; schema review is model review.
Negative / Risks
- Static snapshot = no live updates in the POC demo (accepted; S4 unlocks it).
- Hand-curated inventory can drift from reality; acceptable for one demo week, retired by the API exporter.
Alternatives Considered
Alternative 1 — Thin live service inside the POC week
Rejected because: biggest schedule risk for the least demo value; the contract seam gives the same end state later without client rework.
Alternative 2 — Data baked into StreamingAssets
Rejected because: hides the seam that makes the whole design honest; HTTP fetch costs hours, not days.
Alternative 3 — Graph database as the client’s source (query the DB directly)
Rejected because: violates graph-as-projection; couples the client to a storage choice the concept explicitly refuses to make authoritative.
Alternative 4 — Protobuf/FlatBuffers wire format
Rejected because: JSON + Schema is inspectable, diffable in PRs, and trivially produced by any future source system; binary efficiency is irrelevant at POC graph sizes. Revisit if snapshot payloads exceed ~10 MB or streaming deltas arrive (S4/S5).
Related
- [[ADR-SCP-005-Workspace-State-Separation]] — the write-side counterpart
- [[ADR-SCP-006-Reasoning-Immutability-In-Client]] — overlay semantics
- [[ADR-SCP-009-ADP-Integration-Boundary]] §4 — the seven amendments this ADR records, and the freeze that gated them
- [[../Research/SCP-ADP-Integration-Design|SCP ↔ ADP Integration Design]] §7 — the normative amendment table
- [[ADR-SCP-011-S4-Service-Repository]] — how v0.2.0 reaches the S4 service without a second copy of the contract