TermVerify JSONL transcript protocol¶
This document defines the reviewed design for the first intended external
protocol: termverify.transcript/v1. The repository contains a canonical codec,
semantic lifecycle validator, non-exhaustive schema aid, and reviewed fixture and
property coverage. The archived Phase 1 handover completed deterministic
transcript resource governance through the fixed limits below and its amended
behavior-based fixture gate. Installed-schema and release controls remain intact
in the active pre-release successor before the first supported external artifact.
Neither that transfer nor the current runtime activates Phase 2.
Schema and runtime authority¶
The standard Draft 2020-12 schema is a non-exhaustive structural and
record-local validation aid. Executable schema tests verify the metaschema,
canonical records, closed member sets, and the local constraints encoded for the
current inception slice. Schema acceptance is not a conformance verdict: the
Python validator remains authoritative for complete protocol acceptance,
including record kinds and representable rules not yet encoded in the schema,
cross-record lifecycle, canonical ordering, and uniqueness projected onto
selected fields. In particular, terminal capabilities must be sorted and
network entries must be sorted and unique by (host, port) even though standard
schema-only validation cannot fully enforce those rules. Exhaustive per-kind
schema coverage or a custom vocabulary/validator is a separate compatibility and
distribution workstream, not part of this replay-identity slice.
The canonical committed schema is packaged as the installed resource
termverify/schemas/termverify.transcript/v1.schema.json in both wheel and
sdist. The public accessors termverify.transcript_schema_v1_bytes() and
termverify.transcript_schema_v1_json() return the exact resource bytes and a
fresh parsed object, and termverify.TRANSCRIPT_SCHEMA_V1_ID names the
documented $id. Isolated installed-artifact checks verify byte identity
between the installed resource and the committed schema.
The authoritative validator is reachable from the same place, so the aid is
never the easier import: termverify.parse_transcript(),
termverify.serialize_transcript(), and termverify.TranscriptValidationError
are on the public surface alongside the schema accessors, and remain
importable from termverify.transcript as identical objects.
The $id resolves: the canonical publication at
https://termverify.dev/schemas/termverify.transcript/v1.schema.json is a
byte-for-byte mirror of the committed resource, deployed from main by the
Pages workflow and verified after every deployment by a fetch check that
fails on any byte difference (see
docs/developer-guide/schema-publication.md).
During the declared inception period the publication mirrors main, so
published bytes follow the in-place v1 amendment rule below. The $id
remains identifier-first: resolution is a distribution convenience,
consumers must not fetch it at validation time, no library or required gate
depends on the site being reachable, and neither publication nor installed
access changes the schema's non-exhaustive role or runtime authority.
Each UTF-8 JSON Lines file is one transcript for one verified run. A line ends
with exactly one LF (\n); a final LF is required. Blank lines, comments, and
byte-order marks are invalid. Readers reject duplicate object member names and
unknown required semantics instead of silently choosing an interpretation.
Fixed v1 resource limits¶
The canonical parser and programmatic serializer enforce the same fixed, protocol-owned ceilings. They do not inherit interpreter recursion settings, host memory, or ambient configuration:
| Resource | V1 maximum | Counting rule |
|---|---|---|
| Transcript bytes | 32 MiB (33,554,432 bytes) | Entire JSONL byte sequence, including every final LF. |
| Line bytes | 4 MiB (4,194,304 bytes) | One canonical JSON object, excluding its LF. |
| Records | 10,000 | JSONL objects in one transcript. |
| JSON nesting | 64 | Simultaneously open objects and arrays in one record, including the envelope object. |
| Collection items | 16,384 | Immediate members in one object or items in one array. |
| JSON values | 100,000 | Container and scalar value nodes in one record; object keys are not value nodes. |
| Individual string bytes | 1 MiB (1,048,576 bytes) | UTF-8 bytes of one decoded string value or object key. |
| Record string bytes | 2 MiB (2,097,152 bytes) | Aggregate UTF-8 bytes of every decoded string value and object key in one record. |
The parser checks total bytes, framing, record count, line bytes, and lexical JSON
nesting before JSON decoding. After decoding, it checks collection, value, and
string budgets before semantic validation. Brackets inside JSON strings do not
contribute to lexical nesting. The serializer traverses exact Python JSON values
iteratively, applies the same structured-value budgets before RFC 8785
canonicalization, then checks each canonical line and accumulates transcript
bytes before joining output. Strings longer than the individual byte ceiling in
code points are rejected before UTF-8 measurement, bounding that measurement;
malformed Unicode is normalized to TranscriptValidationError rather than
leaking a codec exception. A limit violation is a
TranscriptValidationError; interpreter recursion or allocation-dependent
behavior is not a conformance result. Every successfully serialized transcript
therefore remains admissible to the parser under the same ceilings.
Canonical serialization¶
Transcript producers use the JSON Canonicalization Scheme (RFC 8785) for each object, encoded as UTF-8 and followed by LF. Therefore object-member order, whitespace, string escaping, and number rendering do not vary by producer. Values that could lose precision in another JSON implementation, including seeds and monotonic IDs, are JSON strings rather than numbers.
Canonical bytes are the basis for fixture comparison and transcript digests; the digest algorithm and value, if emitted, are an optional metadata field and never replace parsing and semantic validation.
Common envelope¶
Every line is an object with these required members:
| Member | Type | Meaning |
|---|---|---|
protocol |
string | Exactly termverify.transcript/v1. |
run_id |
string | Caller-supplied stable identifier for this run. |
seq |
non-negative integer | Zero-based, contiguous line sequence. |
id |
string | Stable unique record identifier within run_id. |
kind |
string | Record type from the v1 set below. |
payload |
object | Data defined by kind; no omitted required members. |
run_id and id use lowercase ASCII letters, digits, ., _, and -; they
must be non-empty and generated by the caller or deterministic harness, never
from adapter ambient randomness. The recommended identifiers are the stable
run-<fixture-or-request-name> and record-<zero-padded-seq> forms. A reader
rejects a repeated id, a changed run_id, or a non-contiguous seq.
An extension member starts with x- and has no meaning to a generic v1 reader.
All other members are reserved. A producer must not use an extension to change
the meaning of a required v1 member.
Record ordering and lifecycle¶
A valid transcript contains exactly this lifecycle shape:
run.startedatseq: 0;capability.resultrecords in configuration-table order until all requested constraints are applied, the first is unsupported, or an adapter failure terminates negotiation;- if initialization completes, exactly one initial readiness observation;
- zero or more single-flight input epochs;
- exactly one terminal record:
run.finished,run.failed, orrun.unsupported.
An adapter failure may terminate negotiation before any capability result or
after any applied prefix. The transcript then contains only run.started, that
prefix, and run.failed. A subject that exits or an adapter that fails after
negotiation but before readiness may likewise terminate without an initial
observation. run.unsupported remains negotiation-only and has no body records.
Execution epochs and causality¶
The initial readiness observation is the first semantic body record after all seven capability results. Zero or more startup diagnostics may precede it, but no input may. Its position, rather than an additional payload member, declares that initialization completed and the subject is ready for input.
After readiness, v1 is single-flight. Each epoch contains:
- exactly one input record;
- zero or more diagnostics caused while handling that input;
- exactly one observation that closes the epoch at deterministic quiescence, unless a terminal record closes it first.
The closing observation is the complete normalized evidence available at
quiescence, not an output chunk. Transcript order supplies causality, so another
input before that observation is invalid. Observations and diagnostics while
idle are invalid: diagnostics do not create hidden epochs. input.stop is the
final accepted input. Its drain epoch may contain diagnostics and an optional
final observation, followed by run.finished or run.failed; no later input is
valid.
A terminal record may also occur while idle when the subject exits naturally or the adapter fails. This ordered model intentionally excludes unsolicited body events in v1. General asynchronous work requires a future explicit polling, draining, or correlation contract rather than wall-clock quiet-window polling.
No record follows a terminal record. Input records are ordered by dispatch;
observations are ordered by the point at which the adapter captured them; event
arrays inside an observation are ordered by application emission. Records never
claim wall-clock ordering: all timing uses the configured manual clock in
integer milliseconds. A capture failure is represented by run.failed, not by
silently dropping a record.
run.started has a payload.config object containing the requested
deterministic constraints. capability.result reports each constraint's actual
outcome before the adapter accepts the first input. input.* records describe
the requested user action; observation records contain the resulting
structured evidence; diagnostic carries non-oracle information such as a
normalized process warning.
run.started.payload is closed except for x- extensions and requires exactly
config plus subject. subject is a versioned replay selector with this v1
shape:
{
"format": "termverify.replay-subject/v1",
"application": {"id": "example.app", "version": "1", "build": "build-1"},
"fixture": {"id": "basic", "version": "1"},
"adapter": {"id": "example.direct", "version": "1"},
"normalizer": {"id": "example.identity", "version": "1"},
"state_schema": {"id": "example.state", "version": "1"}
}
Every selector value uses the same lowercase ASCII identifier grammar as
run_id. Application identity separates its application version from the exact
build selector. Fixture identity selects the registered invocation without
embedding a raw command line or environment. Adapter, normalizer, and state
schema identities bind the interpretation needed for replay. An optional
platform object has exactly normalized os and architecture selectors.
Volatile hostname, account, absolute path, raw argv, and environment details
are not replay selectors and are forbidden as generic subject members. Every
subject and nested selector is closed except for uninterpreted x- extensions.
Terminal record payloads are:
| Kind | Required payload members |
|---|---|
run.finished |
exit: {"kind": "code", "value": integer} or {"kind": "signal", "value": non-empty string} |
run.failed |
error: {"code": non-empty string, "message": string, "details"?: JSON value} |
run.unsupported |
constraint, code, message, and optional details; constraint and code are non-empty strings |
Every defined record payload and nested generic protocol object is closed to
its listed members plus uninterpreted x- extensions. In particular,
capability.result permits effective, tier, and delivery only with
applied and reason only with unsupported; delivery additionally
requires the delivered tier. Application-defined JSON values such as
observation state, event data, diagnostic details, and error details
remain open semantic values rather than generic protocol objects. The
delivery.env object (present only with the spawn-env channel, below) is a
value map of delivered environment-variable names, not a generic protocol
object: its member names are exact variable names with no x- extension
semantics.
message is diagnostic only. Consumers use stable code values for behavior;
v1 reserves adapter-start-failed, adapter-runtime-failed,
constraint-unsupported, and constraint-not-enforced.
Requested deterministic constraints¶
run.started.payload.config has all of the following required members:
| Member | Required shape and v1 rule |
|---|---|
seed |
canonical decimal string representing an unsigned 64-bit integer: "0" or a nonzero digit followed by digits, with no leading zeroes |
clock |
{"mode": "manual", "initial_ms": non-negative integer} |
locale |
literal "C", or a well-formed RFC 5646 language tag |
timezone |
non-empty string naming the requested zone; only literal "UTC" may be reported as applied, so any other request is refused through unsupported (or the run fails before its receipt) |
terminal |
columns and rows positive integers; capabilities sorted unique strings |
filesystem |
{"mode": "sandbox", "root_id": string} |
network |
{"mode": "deny"} or {"mode": "allow-list", "allowed": [{"host": string, "port": integer 1–65535}]}; allowed is sorted by host then port and has no duplicates |
The configuration object and each defined nested object are closed except for
uninterpreted x- extensions. Raw command lines, environment variables, host
paths, and other invocation or machine identity are not deterministic-constraint
members and cannot be added as generic configuration fields at any nesting level.
Locale validation applies the RFC 5646 language-tag syntax only, including
normal, private-use, and fixed grandfathered forms plus the RFC prohibition on
duplicate variants and extension singletons. It does not consult the IANA
Language Subtag Registry, reject deprecated tags, or claim registry-backed
validity. RFC 5646 language tags are case-insensitive, while the separate C
sentinel is literal and case-sensitive. The codec preserves the caller's
spelling and case exactly; v1 performs no locale normalization or
preferred-value rewriting. Validation is therefore independent of the host
locale, installed locale data, environment, and network.
timezone is a plain non-empty string: the zone the run requests. There is no
timezone registry. V1 can only ever apply the literal UTC — applying a named
zone requires zone data this protocol deliberately never consults — so a closed
registry existed only to validate requests that must then be refused anyway, at
the cost of pinning v1 to one TZDB release with no registry-version selector in
the record (adversarial review 2026-07-24, finding P4; registry removed by owner
decision 2026-07-24, preserved in Git history). Any future reintroduction, when
a vertical actually demands non-UTC zones, is a fresh design with a version
selector rather than a revival of this one.
The codec preserves the requested spelling exactly and performs no alias, case,
or preferred-name normalization; it never consults ambient zoneinfo,
operating-system data, environment, or network state, so a transcript records
what was asked for. A non-empty string is all that either the schema or the
runtime requires of the request, and an x- extension cannot change what a
request means.
Truthfulness therefore rests on the receipt, never on the request's vocabulary.
A timezone request is not enforcement evidence: v1 receipts permit only literal
UTC as an effective value, and an adapter that cannot apply a requested zone
terminates through structured unsupported rather than fabricating one.
Requesting Mars/Olympus is valid and unhelpful in exactly the way requesting
Europe/Berlin is: both are refused, and the refusal is the record.
The configuration requests a constraint; it does not itself prove enforcement. What an adapter owes is a truthful receipt per constraint, never containment it does not have. The adapter-facing contract is:
- It applies each requested constraint through an explicit mechanism and
records which one. A constructive in-process runtime injects the requested
seed and manual clock through application-facing ports; a cooperation port
instead delivers the requested value to the subject through the channel named
in the delivery record, and does nothing further. Advancing manual time is
always an explicit
input.clock_advancedrecord, never a wall-clock wait. - It reports the effective value it actually applied, and configures the requested terminal dimensions and capabilities before the subject can observe them.
- It never records a stronger mechanism than it used. The filesystem and
network constraints in particular are applied at whatever tier the adapter
can support — an
os-tier sandbox root or socket denial where a mechanism exists, otherwisedelivered, where the requested policy is handed to the subject and honoring it is subject cooperation. Adelivered-tier receipt is not containment: no receipt, claim, or document may imply that it is (owner decision 2026-07-18,cooperation-tier-constraint-ports.md). - An adapter that cannot apply a requested constraint at any tier must record
it
unsupportedand terminate throughrun.unsupported. A verified run may includedelivered-tier constraints, and its constraint claims are no stronger than its weakest tier — which is why the tier is recorded per constraint rather than summarized per run. - It emits one
capability.resultfor each attempted constraint through the first unsupported constraint, withconstraint,status, and status-dependenteffective/tier/delivery/reasonmembers.statusis exactlyappliedorunsupported.appliedstates that the adapter carried out the constraint's application step and recorded the value it applied; the mandatorytierstates what that step was worth — atosandconstructivethe constraint itself is in force, while atdeliveredwhat was applied is the delivery, and whether the subject honors the delivered value is not observable. There is no third status: the status word never claims enforcement the tier disclaims, and a constraint the adapter can neither enforce nor deliver isunsupported. The first unsupported result terminates the transcript withrun.unsupportedand no input dispatch.
capability.result.payload.constraint is one of seed, clock, locale,
timezone, terminal, filesystem, or network; it follows the table order
above. For applied, effective is required and is the applied configuration
value, and tier is required and states the enforcement tier (below). For
unsupported, reason is required and the next record is the matching
run.unsupported terminal record.
Enforcement tiers¶
termverify.enforcement-tier/v1 is a closed, case-sensitive vocabulary owned
by this protocol version exactly like the key registries: exact membership, no
aliases or normalization, runtime validation authoritative.
Post-freeze membership or meaning changes require a new vocabulary version.
Its members, in decreasing claim strength:
| Tier | Claim |
|---|---|
os |
The constraint is applied by an operating-system mechanism at the subject boundary; evidence exists at the OS level. |
constructive |
The constraint is applied by construction of the controlled in-process runtime: the emitting port asserts that the subject reaches the constrained resource only through it. Stating it truthfully is part of the injected application's port contract. |
delivered |
The requested value was delivered to the subject, exactly as recorded, through the channel named in the delivery record; honoring it is subject cooperation. Nothing is enforced. |
Every applied capability result carries a mandatory tier. A
delivered-tier result additionally carries a mandatory delivery object
naming the channel through which delivery flowed; no other tier may carry a
delivery. The channel member is a closed set fixed by this protocol
version (like network.mode, not a registry): exactly one of the following
shapes.
channel |
Members | Claim |
|---|---|---|
spawn-env |
env (required, non-empty), cwd (required iff the constraint is filesystem, forbidden otherwise) |
The recorded environment variables (and, for filesystem, working directory) were placed into the subject's spawn environment. |
hello-config |
none beyond channel |
The constraint's run.started.config members — already recorded and validated in this transcript — were delivered to the subject in session.hello.config (termverify.control/v1). |
wire-message |
none beyond channel |
The value was delivered as a control-protocol message during the run; the protocol's own message records are the evidence. |
spawn-env member rules are unchanged: env maps non-empty variable names
(no = or NUL) to non-empty values (no NUL); cwd is a non-empty NUL-free
string. A channel tag names where delivery flowed; the claim never widens —
every channel claims delivery exactly as recorded, subject cooperation for
honoring, and nothing enforced.
Compatibility (amendment of 2026-07-20, owner decision on issue #173): the
pre-amendment bare form {"env": ..., "cwd"?} with no channel member
remains accepted and is normalized to {"channel": "spawn-env", ...} at the
ingest boundary; a form carrying env together with a channel other than
spawn-env is invalid. (The canonical spawn-env form itself carries both
channel and env, per the channel table above — it is the required
emitter output, not an invalid combination.) Emitters produce only the
channel-tagged form. Normalization is
performed by the codec's compat rules — named, pure, total,
normalize-toward-canonical functions applied between structural decode and
validation (_COMPAT_RULES in the runtime); they never relax acceptance,
and validation proper sees only the canonical form.
Membership in the
vocabulary is not evidence that an emitter exists: which tier a negotiation
path may state is fixed by the accepted cooperation-tier design and validated
fail-closed at runtime during receipt binding (an adapter's own terminal
negotiation may state os; ports injected into the terminal adapter may state
only delivered; ports negotiated by the direct adapter may state only
constructive). The same posture applies to channels: wire-message is
admitted before any emitter exists. A transcript records the stated tier but
cannot know the
emitting path, so transcript validation checks vocabulary membership and the
tier/delivery pairing only. A receipt never claims the subject honored a
delivered value, and no tier claims containment.
This leaves applications responsible for exposing controllable ports; it keeps the deterministic core independent from ambient time, randomness, terminal, filesystem, and network state.
Inputs and observations¶
V1 input kinds are input.key, input.text, input.resize, input.mouse,
input.clock_advanced, input.clipboard_set, and input.stop. Every input
payload includes a non-negative integer at_ms manual-clock value and has the
following additional required members. No member not listed as optional is
permitted in a generic v1 input payload, except an x- extension member with
no generic v1 meaning.
| Kind | Required payload members |
|---|---|
input.key |
keys: ordered array containing one canonical termverify.key/v1 semantic chord |
input.text |
text: Unicode string |
input.resize |
positive integer columns and rows |
input.mouse |
action: press, release, move, or scroll; non-negative integer column and row; button (left, middle, or right) for press/release; non-zero integer delta for scroll |
input.clock_advanced |
positive integer delta_ms; at_ms equals the preceding manual time plus delta_ms |
input.clipboard_set |
text: Unicode string |
input.stop |
no additional members |
Semantic key chords¶
TermVerify owns the closed termverify.key/v1 registry. An input.key record
represents one simultaneous semantic chord; a sequence of keystrokes requires
multiple input records and therefore multiple quiescent input epochs. The
keys array contains zero or more modifiers followed by exactly one base key.
Modifiers are unique and, when present, appear in this canonical order:
Control, Alt, Shift, Meta.
The exact v1 component registry has 99 entries:
- modifiers:
Control,Alt,Shift,Meta; - named bases:
Enter,Tab,Escape,Backspace,Delete,Insert,ArrowUp,ArrowDown,ArrowLeft,ArrowRight,Home,End,PageUp,PageDown, andF1throughF12; - modified-only bases: lowercase ASCII
athroughz, ASCII0through9,Space, and the printable ASCII punctuation row! " # $ % & ' ( ) * + , - . / : ; < = > ? @ [ \ ] ^ _ ` { | } ~.
A modified-only base requires at least one of Control, Alt, or Meta.
Shift alone does not make a printable base valid. Thus ["Control", "c"],
["Alt", "1"], ["Control", "Space"], ["Control", "/"], and
["Alt", "<"] are valid; ["c"], ["1"], ["Space"], ["/"],
["Shift", "a"], and ["Shift", "<"] are invalid. Unmodified printable
insertion, including an ordinary space, punctuation, or uppercase letter,
uses input.text.
Names are exact and case-sensitive. V1 performs no trimming, case folding,
Unicode normalization, alias rewriting, or modifier reordering. Toolkit names,
OS virtual-key codes, physical-key locations, curses names, and terminal escape
sequences are not protocol values. In particular, Ctrl, Cmd, Option,
Esc, Return, lowercase enter, chord strings such as Ctrl+C, and encoded
bytes such as \u001b[A are invalid. Space is TermVerify's semantic name for
the space key when it participates in an approved modified chord; it is not a
raw whitespace identifier.
Neither input.key nor input.text is a raw terminal-byte channel. An adapter
maps a valid semantic value to its own application boundary and must fail rather
than silently translate an unknown value, alias, or ambiguous escape sequence.
An x- extension cannot add registry entries or change chord meaning. The
reviewed registry order is digest-bound in executable tests using newline-joined
UTF-8 names with a final LF; its SHA-256 is
51955be77ab11b23240c642edd0e4f08dbd56389b82f99bbe2ee87871ce9d0a0.
The registry and its membership predicate are on the package's public
surface as termverify.KEY_NAMES and termverify.is_key_chord; the module
that defines them is private and is not a supported import path.
Companion registry: termverify.key-encoding/v1¶
The terminal execution path has a companion registry,
termverify.key-encoding/v1 (public entry point:
termverify.encode_key_chord), that maps each of the 1382 valid
termverify.key/v1 chords either to exactly one xterm-legacy normal-mode
byte string or to the explicit fail-closed verdict
unencodable. It is committed data plus committed arithmetic owned by
TermVerify — never derived from terminfo, toolkit enums, OS virtual-key
codes, or other ambient host state — and it is not a transcript value: an
input.key record stays semantic, no record carries encoded bytes, and this
registry can version independently of the transcript protocol. A chord is
encodable exactly when the legacy encoding represents every chord component
by definition; when the only candidate bytes would drop a modifier
(Control+Enter, Control+/), alias one modifier to another (Meta as
Alt on letters), or pass a NUL hazard (Control+Space), the registry
returns unencodable and the adapter fails rather than misrepresent the
chord. Four
byte collisions inherent to the legacy byte space are disclosed:
["Control", "m"]/["Enter"] (CR), ["Control", "i"]/["Tab"] (HT), and
their two Alt-prefixed forms. The full enumeration — each chord joined
with +, then => and the space-joined two-digit lowercase hex code
points of its encoding or the word unencodable, newline-joined UTF-8 with
a final LF — is digest-bound in executable tests; its SHA-256 is
72a17da549238053c88a925cf6bf2bbe93ed2b8564c7a09188075987fcdcda95. The
pre-freeze inception policy above applies to this registry as well; details
of the adapter behavior live in the
ConPTY developer guide.
For input.mouse, button and delta are forbidden for move; delta is
forbidden for press and release; and button is forbidden for scroll.
Clipboard values are sensitive evidence and their
capture/redaction policy requires persistence through
the evidence-aware writer; the raw codec is non-persistent.
diagnostic has required at_ms, non-empty string code, and string
message members; optional details holds a JSON value. It is non-oracle
information and cannot change a run's terminal status.
An observation payload has these required members:
at_ms: manual-clock value at capture;state: application-defined JSON value after a documented normalizer;events: ordered array of{"type": non-empty string, "data": JSON value}objects;ui: an object with the required members below.
Terminal output records read-boundary-free: the recorder merges each run of
adjacent events whose type is terminal.output and whose data is exactly
{"chunk": <string>} into one event with the chunks concatenated, because
native read boundaries are OS scheduling noise, not subject behavior
(issue #195). Any other event passes through unchanged and ends the run of
adjacency. This is recorder behavior, not codec acceptance: the codec does
not reject a transcript that was produced without coalescing.
ui.regions is an ordered array of objects with non-empty id and role
strings and a bounds object containing non-negative integer column and
row plus positive integer columns and rows. Region IDs are unique within
the observation. ui.focus is a region ID or null; ui.cursor is
{"column": non-negative integer, "row": non-negative integer, "visible": boolean};
and ui.mode is a string or null. A non-null focus must name one of the
observation's regions.
frame (a normalized rendered terminal frame) and process (lifecycle detail)
are optional evidence layers. When present, frame is
{"lines": [string, ...], "columns": positive integer, "rows": positive integer}
with len(lines) == rows; process is {"state": "running"} or
{"state": "exited", "exit": <run.finished exit object>}. The shipped
comparator compares whole record payloads by exact canonical equality, so a
frame difference and a state difference are both reported and a matching
frame cannot conceal a domain mismatch; it does not rank domain evidence above
rendering evidence. Application-specific values belong
under documented x- members or a registered normalizer, not under new generic
v1 member names.
When a transcript ends with run.finished, every exited-process observation
has the same exit kind and value as run.finished.payload.exit.
An exited-process observation is the final body record. Uninterpreted x-
extensions do not participate in the exit comparison. The relationship with
the other terminal outcomes is asymmetric.
run.unsupported has no body and therefore no process observation. A
run.failed transcript may retain independently captured exited-process
evidence; that evidence is orthogonal to the adapter or harness failure and has
no terminal exit value to match.
Compatibility and evolution¶
termverify.transcript/v1 readers must reject a different protocol value.
Governance status (owner decision 2026-07-24): prototyping stage — no
protocol is frozen. The inception freeze fired on 2026-07-19 when
termverify 0.1.0 was published to PyPI, and the owner suspended it on
2026-07-24: the publication created no external client, the freeze's first
test required a recorded exception within days, and the only known users are
early internal projects that exist to drive TermVerify's design. Rationale,
scope, and exit criterion:
docs/agent/design/prototyping-stage-protocol-governance.md.
While the prototyping stage lasts, the inception policy applies to every
TermVerify protocol and registry (termverify.transcript/v1,
termverify.key/v1, termverify.key-encoding/v1, and the JSONL control
protocol): reviewed contract corrections — including
incompatible ones — update the current version in place rather than creating
fictional compatibility history. No version bump, compatibility shim, or
per-change exception decision is required. Existing repository fixtures are
migrated in the same reviewed change, and this document is updated in the
same change. No backward compatibility is owed to any artifact, including
the published 0.1.0; version identifiers such as /v1 name the current
shape so readers can reject foreign input, and are not stability promises.
The prototyping stage ends only by an explicit recorded owner decision that
TermVerify is usable by external clients — not by any release or publication
event. After that boundary, only optional x- extensions are additive
within a version, and new generic semantics, member types or meanings,
canonicalization, ordering rules, or stable error codes require a new
protocol version; the same applies to termverify.key/v1 membership,
spelling, component roles, modifier ordering, and chord validity, for which
ambient toolkit or host registry growth never changes a published version.
(Historical note: the printable-ASCII punctuation widening — issue #155,
docs/agent/design/key-v1-punctuation-bases.md — was recorded as a
"one-time post-freeze exception" during the 2026-07-19 – 2026-07-24 window
in which the freeze was considered active; that framing is historical and
carries no precedent force under the prototyping stage.)
An inception transcript without subject is invalid and no tool may guess its
identity from ambient or undocumented out-of-band context. A caller with the
required stable selectors may explicitly reconstruct a current v1 transcript;
automatic migration is outside the codec. A future new version supplies its own
fixtures and migration/replay policy. A reader may preserve unknown x-
members when rewriting a transcript, but must not manufacture or interpret them.