Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 106 additions & 0 deletions docs/concepts/trace-schema.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# Trace/Result Schema & Migration Policy

`rampart.core.serialization` defines RAMPART's canonical, versioned
`Result`-record format. The current implementation provides the neutral
`ResultRecord.to_dict()` / `ResultRecord.from_dict()` round-trip and the
`serialize_result()` / `deserialize_result()` convenience functions. Existing
xdist and reporting consumers are not yet wired to this module.

This page defines how the schema may evolve as consumers adopt it.

## Versioning

- Every serialized record carries one root `version` field. The current schema
is **`rampart.trace.v1`**.
- The record version is **independent** of transport or projection versions,
including the existing xdist envelope version (`rampart.xdist.v2`). Each
version describes its own layer and may evolve separately.
- There is a **single root version** — nested types (`Turn`, `Payload`,
`EvalResult`, …) do not carry their own versions.

## What is and is not a breaking change

- **Additive-optional = no bump.** A new optional field that older readers may
ignore, and whose absence has a defined default, does not change the major.
- **Missing = not recorded (not "false").** An absent optional field means the
producer *did not record it* — never that its value was empty, false, or zero.
Readers supply a default for *shape* only; consumers must not infer a semantic
negative from absence. A v1 record with no `manifest_snapshot` means "the
manifest was not captured," not "there was no manifest."
- **Structural change = major bump.** Removing, renaming, or retyping a field,
or changing its meaning or nesting, bumps `vN → vN+1` with a changelog and a
migration note.

## Reader posture

- Readers tolerate unknown fields and **fail closed on an unknown major** — a
record is never best-effort parsed across a major boundary.
- Forward compatibility is **additive-only within a major**. A newer major read
by an older framework fails closed by design.
- Schema descriptions and validators derived from this format must remain open
to unknown properties within a major version.

## Enum posture

- The closed enums — `SafetyStatus`, `EvalOutcome`, `ObservabilityLevel`, and
`PayloadFormat` — **fail closed** on an unknown value. A serialized safety
result must never silently misread one; there is no warn-and-degrade path.
- `HarmCategory` is the sole exception: it travels as a **passthrough string**
and is never coerced, so a new harm label from a future producer round-trips
unchanged on an older reader.

## Value domain

- Free-form mappings must already contain JSON-safe values. The canonical codec
does not coerce unsupported objects with `str()` or `repr()`.
- Numeric values must be finite. Transport-specific normalization is outside
the canonical schema.
- `rampart.trace.v1` does not define a durable representation for binary or
opaque payload artifacts. Encoding or decoding one fails closed rather than
coercing it to text.
- Transport bookkeeping keys are removed from top-level `Result.metadata`;
nested user mappings are preserved.

## Migration mechanics

Only `rampart.trace.v1` exists today. If a later structural change introduces a
new major:

- writers emit the latest supported major;
- support for an older major uses an explicit adjacent upcaster
(`vN-1 → vN`);
- migrating persisted data is an explicit operation; reading never rewrites an
artifact in place; and
- encountering an unsupported major fails closed.

## Reserved additive fields (named now, populated later)

These record-level wire-only collar slots are reserved by name so they can be
added without a major bump:
`manifest_snapshot`, `evaluation_fingerprint`, `replay_provenance`,
`population_ref`, plus `artifacts` / `target` / `provenance`. A field that is
truly *intrinsic to a result* instead lands as an additive-optional field on
`Result`, inside the referenced `result` body. Either way each is
additive-optional; none is populated at v1.

Other future fields follow the same general rule: optional additions with a
defined absence behavior do not require a major bump; structural changes do.

## Support window

Starting with the first release that writes durable trace records by default,
RAMPART supports reading `vN` and `vN-1` for **two subsequent framework
releases** (one deprecation cycle). The window is keyed on releases, not time.
Any major bump includes a changelog entry and migration note.

```mermaid
flowchart TD
change([proposed schema change]) --> q1{"adds a field only?"}
q1 -- no --> struct["structural:<br/>remove / rename / retype /<br/>change meaning or nesting"]
q1 -- yes --> q2{"optional with a<br/>well-defined default?"}
q2 -- no --> struct
q2 -- yes --> add["additive-optional"]
add --> nobump["NO bump<br/>(new optional fields)<br/>old readers ignore unknown keys"]
struct --> bump["bump major vN → vN+1<br/>+ changelog + migration note"]
bump --> reader["readers: fail closed on<br/>unknown major"]
```
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ nav:
- Attacks: concepts/attacks.md
- Probes: concepts/probes.md
- PyRIT Integration: concepts/pyrit.md
- Trace Schema & Migration: concepts/trace-schema.md
- Attacks:
- attacks/index.md
- XPIA: attacks/xpia.md
Expand Down
Loading