Skip to content

[FEAT]: Canonical versioned trace/result serializer (WS0-05 PR 1) - #185

Open
Nina Chikanov (nina-msft) wants to merge 12 commits into
microsoft:mainfrom
nina-msft:nina-msft-trace-result-schema-gate
Open

Nina Chikanov (nina-msft) wants to merge 12 commits into
microsoft:mainfrom
nina-msft:nina-msft-trace-result-schema-gate

Conversation

@nina-msft

@nina-msft Nina Chikanov (nina-msft) commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

WS0-05 — PR 1: Canonical versioned trace/result serializer

Establishes the neutral schema API and migration policy. Existing reporting and xdist consumers are not rewired in this PR.

What this lands

  • Result.to_dict() / Result.from_dict() — the canonical, unversioned body codec, backed by one cached Pydantic adapter over Result. The existing standard dataclasses remain the live models; there is no parallel body model or set of nested encode/decode functions. Encoding uses Python-mode adapter output with explicit enum/datetime serializers, followed by a canonical reader pass before returning the body. This extra pass preserves existing reader depth support without introducing a new depth cap; interpreter/parser recursion limits still apply.
  • Adapter-local policies — a private GetPydanticSchema hook configures copies of generated core-schema nodes. Public fields retain plain dictionary/datetime annotations; no class-wide Pydantic configuration or trace validators are attached to the live dataclasses. Other Pydantic adapters retain their normal behavior. The canonical adapter explicitly supplies its datetime/Path resolution namespace; types.py is unchanged from the PR base.
  • Thin ResultRecord envelope in rampart/core/serialization.py. serialize_record(record=...) -> str serializes an explicitly constructed ResultRecord to JSON text; deserialize_record(data=...) -> ResultRecord reads JSON text. ResultRecord.to_dict() / from_dict() remain available for dictionary conversion and projections. Every record carries the single root version rampart.trace.v1; unsupported versions fail closed through the decoder registry. Body dicts are fragments, not standalone durable records.
  • Optional attributionpytest_nodeid and result_index, omitted when not recorded. Stable identity fields and durable artifact descriptors remain deferred until their implementations exist.
  • Strict boundary validation — all nine nested types, finite numbers, JSON-only free-form maps, closed enums, and passthrough harm-category strings. Missing fields use declared defaults without retaining original field presence; explicit null is accepted only for nullable fields. Payload IDs are required on read rather than generated. Python ISO timestamp formatting is preserved. Strings and mapping keys, including attribution, must contain Unicode scalar values: Python surrogate code points are rejected without lossy normalization, while valid JSON surrogate-pair escapes for characters such as emoji remain supported. Malformed JSON, unsupported values, recursion-limit failures, and adapter serialization ValueErrors surface as SchemaError.
  • Trace-only payload restrictions — binary formats and non-null artifacts fail before filesystem access; ordinary live binary payload construction and independent Pydantic validation are unchanged.
  • Nonmutating metadata hygieneResultRecord.to_dict() filters only top-level transport bookkeeping from encoded output, including _rampart_source_worker. Body-only serialization and record decoding retain these keys. Re-encoding filters them from output without modifying the referenced result; nested user maps remain intact.
  • Generated open JSON Schema at schemas/trace.v1.schema.json. ResultRecord.json_schema() composes the adapter-derived body and versioned envelope, with narrow customizations for trace payload restrictions and the Request invariant. Runtime class docstrings are omitted in favor of concise trace-specific descriptions. scripts/generate_trace_schema.py --check is the CI drift gate.
  • Explicit structural/semantic distinction — passing JSON Schema validation is necessary but not sufficient for decoding. Integer notation, Unicode scalar strings, Python ISO datetime parseability, and finite representable numbers remain decoder requirements. These requirements and runtime nesting limits are documented rather than claimed as JSON Schema guarantees.
  • Migration and integration policy at docs/concepts/trace-schema.md: additive optional fields require no bump; structural changes require a new major; absence means not recorded, not a semantic negative. Reserved future field names and the release-based support window are documented. Transport normalization must prepare a separate result before encoding; a binary text placeholder is a lossy transport view, not a durable replay artifact.

Coverage

Hypothesis-generated body and record round-trips over nested JSON values, all nine nested types, enums, optional fields, and timestamps. Example-based coverage includes field exhaustiveness, version dispatch, attribution, additive tolerance, invalid JSON/types/cycles, independent-adapter isolation, schema semantics, and payload rejection before filesystem access. Additional regressions cover surrogate-containing text and mapping keys, valid Unicode/JSON surrogate-pair escapes, deep-nesting round-trips, and recursion/error boundaries with exception chaining. A prepared-copy boundary case demonstrates canonical reuse without modifying or relaxing the original result. hypothesis and jsonschema are development-only dependencies.

Scope

Reporting projections, xdist/failure-attachment adoption, and stable identity remain separate follow-up implementations. Existing transport coercion and payload-display behavior require explicit preparation and compatibility work, not a mechanical serializer replacement. No public transport-preparation API or artifact representation is introduced here.

Introduce rampart/core/serialization.py as the single, neutral
full-fidelity Result <-> dict round-trip (Decision D6 gate). Every
record carries a single root version (rampart.trace.v1) and decoding
dispatches on it, failing closed on an unknown major.

The canonical layer defines the supported value domain only: enums
encode to .value and fail closed on unknown values, harm_category is a
passthrough string, floats must be finite, and free-form maps must be
JSON-safe. Transport hygiene (ANSI stripping, float normalization,
repr() fallback, size caps) stays at the xdist boundary and is not
duplicated here. Binary payloads fail closed pending the WS7 artifact
resolver rather than being coerced to text.

Also lands the written migration policy (docs/concepts/trace-schema.md):
additive-optional = no bump, structural = major bump, missing = not
recorded, readers fail closed on unknown major, with named reserved
additive slots so WS8 provenance needs no hard migration.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

Describe only the canonical serializer behavior present on this branch,
express later migration and consumer work as policy constraints, generalize
future additions outside the reserved collar fields, and remove the ship gate.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Comment thread rampart/core/serialization.py Outdated
Comment thread rampart/core/serialization.py Outdated
Comment thread rampart/core/serialization.py Outdated
Comment thread rampart/core/serialization.py Outdated
Comment thread rampart/core/serialization.py Outdated
Comment thread rampart/core/serialization.py Outdated
Comment thread rampart/core/serialization.py Outdated
Comment thread rampart/core/serialization.py Outdated
Remove planning references, place constant comments before declarations,
drop the unused identity origin field, and make unsupported binary payload
messages independent of planned work.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Remove identity and case_id until their producer is implemented, keep the
existing pytest attribution fields optional, and make the reserved metadata
constant private.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Fail closed on malformed collection fields, incomplete population references,
and non-string harm categories. Validate result indices before serialization so
the encoder cannot emit boolean indices rejected by the decoder.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Replace handwritten nested codecs with a cached TypeAdapter on Result, keep ResultRecord as the versioned envelope, and generate the open schema with a CI drift check.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Comment thread rampart/core/result.py Outdated
Comment thread rampart/core/result.py Outdated
Comment thread rampart/core/types.py Outdated
Rename the record helpers to serialize_record and deserialize_record, accepting ResultRecord and JSON text respectively. Keep dictionary conversion on the existing record methods.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Restore public dataclass annotations, apply canonical policies through adapter-local schema hooks, document structural schema limits and transport preparation, and add generated round-trip coverage.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Restore types.py to its PR-base contents and supply datetime/Path resolution inside the canonical adapter.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@nina-msft
Nina Chikanov (nina-msft) marked this pull request as ready for review September 11, 2026 20:46
@nina-msft
Nina Chikanov (nina-msft) requested a review from a team September 11, 2026 20:46
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant