{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Response",
"description": "A submission bound to the exact offer it answers.\n\nBinds type, definition, instance, digest, revision, control values, idempotency key, and responder provenance — the ADR's own list. Nothing here authorizes on its own: the controller revalidates every field against the definition before it resolves anything.",
"type": "object",
"required": [
"definition",
"idempotency_key",
"instance",
"responder_provenance",
"revision",
"schema",
"values"
],
"properties": {
"definition": {
"description": "The definition answered — and, being a content id, its exact form digest. A definition that changed by one byte cannot be answered by a response minted against the old one.",
"type": "string"
},
"idempotency_key": {
"description": "Makes a retry the same submission rather than a second one.",
"allOf": [
{
"$ref": "#/definitions/IdempotencyKey"
}
]
},
"instance": {
"description": "The instance answered.",
"type": "string"
},
"responder_provenance": {
"description": "How the responder was authenticated, by reference — including which audience they answered from. Bound into the response's identity, so a submission cannot be re-attributed after the fact.\n\nThere is deliberately no second `responder: Audience` field beside this one. Two bound fields carrying the same fact can disagree, and nothing in a plain record keeps them equal; the identity table would then be pinning a contradiction. The audience a responder was authenticated FROM is part of the assertion, so it lives with it.",
"allOf": [
{
"$ref": "#/definitions/ResponderProvenance"
}
]
},
"revision": {
"description": "The revision the responder saw.",
"type": "integer",
"format": "uint64",
"minimum": 0.0
},
"schema": {
"description": "Versioned type tag; see [`RESPONSE_SCHEMA_V1`].",
"allOf": [
{
"$ref": "#/definitions/ResponseTag"
}
]
},
"values": {
"description": "The submitted values, each against the control it answers.",
"type": "array",
"items": {
"$ref": "#/definitions/Submission"
}
}
},
"additionalProperties": false,
"definitions": {
"AssertionKind": {
"description": "What established the responder's identity.",
"oneOf": [
{
"description": "The operator at the terminal that owns the session; authority comes from holding the terminal, not from a presented token.",
"type": "string",
"enum": [
"terminal-operator"
]
},
{
"description": "A signed assertion from an enrolled credential.",
"type": "string",
"enum": [
"signed-assertion"
]
},
{
"description": "No assertion was presented. Recorded, not hidden.",
"type": "string",
"enum": [
"unauthenticated"
]
}
]
},
"Audience": {
"description": "Who may answer, as distinct from what a surface can draw.",
"oneOf": [
{
"description": "The operator at the terminal that owns this session.",
"type": "string",
"enum": [
"terminal"
]
},
{
"description": "An authenticated, in-scope web attachment.",
"type": "string",
"enum": [
"web"
]
}
]
},
"ControlId": {
"type": "string",
"minLength": 1,
"pattern": "^[A-Za-z0-9_-]+$"
},
"ControlValue": {
"description": "What was submitted for one control, typed.\n\nTyped at the TYPE rather than at a downstream parser: a `String` here would make every consumer decide whether `\"TRUE\"`, `\"1\"`, and `\"yes\"` are the same toggle, which is the per-surface drift the epic exists to end. Canonicalization lives in the shape, so A3 revalidates rather than re-parses. Deliberately NOT `#[non_exhaustive]`, unlike the open vocabularies in this crate (`InteractionKind`, `SemanticRole`, `Audience`, …). This variant set is a security boundary: \"no variant can carry a secret\" is provable only by an exhaustive match, and `#[non_exhaustive]` would force every such match to end in a wildcard — after which adding a plaintext-carrying variant would compile silently everywhere. Adding a variant here SHOULD break every consumer that reasons about the set. Every variant is a STRUCT variant: serde's internally-tagged form cannot represent a newtype variant wrapping a primitive, and the named field is the clearer thing to freeze into A2.1's vectors anyway (`{\"kind\":\"toggle\",\"on\":true}`).",
"oneOf": [
{
"description": "One of the control's options was chosen.",
"type": "object",
"required": [
"kind",
"option"
],
"properties": {
"kind": {
"type": "string",
"enum": [
"choice"
]
},
"option": {
"description": "Which OPTION of the control named by the enclosing [`Submission`]. A distinct id space from `ControlId`, so the pair cannot name two things of the same kind with no rule saying which wins. Its charset is enforced on construction AND on deserialization, so the guarantee holds on the wire too.",
"allOf": [
{
"$ref": "#/definitions/OptionId"
}
]
}
},
"additionalProperties": false
},
{
"description": "Free text, as typed.",
"type": "object",
"required": [
"kind",
"text"
],
"properties": {
"kind": {
"type": "string",
"enum": [
"text"
]
},
"text": {
"description": "The submitted text.",
"type": "string"
}
},
"additionalProperties": false
},
{
"description": "A boolean. Travels as a bool, never as text.",
"type": "object",
"required": [
"kind",
"on"
],
"properties": {
"kind": {
"type": "string",
"enum": [
"toggle"
]
},
"on": {
"description": "Whether the control is on.",
"type": "boolean"
}
},
"additionalProperties": false
},
{
"description": "A secret, BY REFERENCE. There is deliberately no variant that can hold plaintext.",
"type": "object",
"required": [
"kind",
"reference"
],
"properties": {
"kind": {
"type": "string",
"enum": [
"secret"
]
},
"reference": {
"description": "The handle the host can resolve.",
"allOf": [
{
"$ref": "#/definitions/SecretRef"
}
]
}
},
"additionalProperties": false
}
]
},
"IdempotencyKey": {
"type": "string",
"minLength": 1
},
"OptionId": {
"type": "string",
"minLength": 1,
"pattern": "^[A-Za-z0-9_-]+$"
},
"ResponderProvenance": {
"description": "How a responder was authenticated, IDENTIFIED — never the credential itself.\n\nA2 gives responder provenance a place to live and binds it into the response's identity; A3 authenticates it and revalidates it. The separation matters: an [`Audience`] says which KIND of surface answered, which is not who did.\n\n**This record carries no credentials and no secrets.** It names the assertion by reference — a kind, a subject, and an opaque [`assertion`](ResponderProvenance::assertion) handle the host can resolve — so a durable, content-addressed response never becomes a disclosure liability. A digest in the record beats the bytes: it is tamper-evident without being a secret.",
"type": "object",
"required": [
"audience",
"kind",
"subject"
],
"properties": {
"assertion": {
"description": "An opaque handle the host can resolve to the assertion itself — a content id, a row id, or a session reference. `None` when the responder was unauthenticated, which is a fact worth recording rather than hiding: A3 decides whether that is admissible.",
"type": [
"string",
"null"
]
},
"audience": {
"description": "Which audience the assertion was presented from.",
"allOf": [
{
"$ref": "#/definitions/Audience"
}
]
},
"kind": {
"description": "What kind of assertion authenticated this responder.",
"allOf": [
{
"$ref": "#/definitions/AssertionKind"
}
]
},
"subject": {
"description": "The principal the assertion speaks for, as the host names it. A stable reference, never a credential.",
"type": "string"
}
},
"additionalProperties": false
},
"ResponseTag": {
"type": "string",
"const": "newt.interaction.response/v1"
},
"SecretRef": {
"type": "string",
"minLength": 1
},
"Submission": {
"description": "One control's answer: which control, and the typed value.",
"type": "object",
"required": [
"control",
"value"
],
"properties": {
"control": {
"description": "Which control this answers.",
"allOf": [
{
"$ref": "#/definitions/ControlId"
}
]
},
"value": {
"description": "What was submitted.",
"allOf": [
{
"$ref": "#/definitions/ControlValue"
}
]
}
},
"additionalProperties": false
}
}
}