Backend Fragment Contract Reconciliation

Status: active reconciliation note.

StoryTangl has two related but separate output surfaces:

  • the service/backend Python API, consumed directly by Python clients such as cmd2, Rich, Tk, and in-process tools;

  • the FastAPI REST server, which transcribes those Python objects to and from HTTP JSON.

The service/backend API is the contract owner for runtime widget-shaped data. The REST server is intentionally thin: authentication, request routing, transport serialization, HTTP error mapping, and transport-adjacent media dereferencing. It should not invent a second fragment model or merge fragments back into legacy display blocks.

Target Shape

Service story-session methods return typed Python objects:

Service method

Python return

create_story

RuntimeEnvelope

resolve_choice

RuntimeEnvelope

get_story_update

RuntimeEnvelope

get_story_info

ProjectedState

acknowledgement-only methods

RuntimeInfo

RuntimeEnvelope.fragments is an ordered list of independent BaseFragment instances. A choice’s fragment uid identifies the renderable choice fragment; its edge_id identifies the action to submit back to resolve_choice. These identities must remain distinct.

RuntimeEnvelope.ux_events is a typed side channel for transient client guidance. Inline command feedback, validation failures, achievements, and shell-level notices do not become journal fragments unless they are genuinely part of the narrative record.

Responsibility Split

Backend/service responsibilities:

  • Return typed Python RuntimeEnvelope, ProjectedState, and RuntimeInfo objects.

  • Preserve independent fragments and fragment uids.

  • Populate action-facing ChoiceFragment.edge_id, accepts, ui_hints, availability, and blockers.

  • Populate advisory envelope metadata such as info_affordances, info_state, world_id, ledger_id, and command grammar hints.

  • Accept the typed direct-edge / exploratory find_edge request union.

  • Resolve exploratory queries through Story policy before committing an edge.

  • Validate submitted choice payloads during action resolution.

  • Return typed UX events when an exploratory request cannot advance.

REST responsibilities:

  • Parse HTTP request bodies and query params into service-call arguments.

  • Enforce authentication and service-method access policy.

  • Serialize typed Python response objects into JSON-safe payloads.

  • Apply explicitly requested render profiles such as html text conversion.

  • Apply transport-adjacent media policies, such as RIT placeholder or media server URL conversion.

  • Preserve service-owned identities and semantic fields while adding only harmless compatibility aliases such as label.

REST must not:

  • rewrite a choice fragment uid to equal its edge_id;

  • collapse sibling fragments into a block payload;

  • require clients to submit fragment uid when the contract says edge_id;

  • turn exploratory command text into a synthetic choice or journal fragment;

  • create backend semantics from presentation-only hints.

Current Status

The service layer returns Python-native RuntimeEnvelope objects for story creation, updates, and edge resolution. resolve_choice() accepts either a DirectEdgeRequest(edge_id=...) or a FindEdgeRequest(find_edge=CommandEdgeQuery(...)). Story dispatch owns command matching against the current open Action surface. A unique match follows the ordinary ledger path; no match, ambiguity, or rejection returns the current turn with an inline, non-replayed UxEvent.

The first pinned backend contract test now asserts that a simple story emits sibling content and choice fragments, not a legacy block, and that uid and edge_id stay separate. Additional contract tests pin the authored Action.accepts and Action.ui_hints path through service envelopes and REST JSON. These are UI-facing intent contracts, even when the engine’s internal vocabulary also uses fields named kind; any future service adapter that maps UI intent onto engine mechanics should be explicit and narrow rather than handled by the REST serializer. RuntimeEnvelope.to_dto() projects fragments through the journal fragment DTO pathway, preserving concrete fragment subclass fields while omitting transport-only stream bookkeeping. In-process Python clients, diagnostic fixture generation, REST serialization, and remote Python rehydration all operate on that same widget-shaped payload surface. The reference CLI stores runtime updates from RuntimeEnvelope.to_dto() before rendering, while still accepting lightweight object-shaped stubs in tests and diagnostic harnesses. The same pattern applies to ProjectedState: service methods own the typed section/value model, ProjectedState.to_dto() emits the client-facing value_type discriminated DTO surface, REST and CLI use that projection, and remote Python clients decode it back into typed projected-state values.

The credentials demo now provides the first real mechanic-to-widget vertical slice. Entering its HasGame block asks the game handler for a current-state projection before any round has completed. The handler emits a candidate PieceFragment, a packet GroupFragment(group_type="zone"), and document PieceFragment members alongside the block’s authored prose and provisioned choices. ServiceManager.resolve_choice() returns those same typed siblings in the RuntimeEnvelope, and RuntimeEnvelope.to_dto() preserves their identities, zone references, structured properties, and text fallbacks. The engine-side field is named piece_kind because kind is reserved for constructor-form persistence; fragment DTO metadata maps it to and from the widget contract’s kind key.

The same slice exercises input in the reverse direction. The credentials game provisions one ChoiceFragment(accepts.kind="pieces") targeting the packet zone. GameHandler owns the small generic hooks that declare a move’s Accepts contract and resolve submitted widget data into an engine move. Selecting a document’s piece_id therefore becomes the existing credentials inspection move before rule evaluation; malformed or stale selections fail at that mechanics boundary. Action preserves the nested accepts discriminator through graph snapshots, while service and REST remain generic. The test in engine/tests/integration/test_credentials_widget_flow.py pins the complete path.

Nim provides the second use of the same hooks for bounded integer input. get_available_moves() remains the rules-facing list of legal takes, while get_provisioned_moves() projects those moves as one ChoiceFragment(accepts.kind="quantity"). The submitted quantity is validated against the current heap and resolved back to the ordinary integer move before the round runs. This distinction keeps client action aggregation out of the mechanics API and is the intended pattern for future “how many?” interactions.

The REST layer validates the same EdgeResolutionRequest union, calls the service method directly, and serializes RuntimeEnvelope.to_dto(). Any manual shaping remains limited to HTTP-adjacent concerns such as media profiles and optional markdown-to-HTML conversion. The serializer remains a transcription boundary and does not preserve retired request or fragment aliases.

Diagnostic Fixtures And Transcripts

engine/contrib/conformance/backend_widget_demo.py now generates the first backend-emitted diagnostic payloads:

  • engine/contrib/conformance/diagnostics/backend_widget_contract_runtime.json

  • engine/contrib/conformance/diagnostics/backend_widget_contract_projected_state.json

These are not canonical conformance fixtures yet. They prove that the current service layer can emit a real widget-shaped RuntimeEnvelope and ProjectedState covering content, typed choices, accepts, ui_hints, metadata.info_affordances, metadata.info_state, and generic projected-state values.

Diagnostic transcripts should be generated only after backend output can be captured as a real RuntimeEnvelope stream. The durable source of truth should be:

backend emitted RuntimeEnvelope
  -> canonical JSON fixture
  -> CLI/Rich/web rendered transcript

Until that path is real for a world/demo, transcript-like prose and UI mockups remain design references rather than regression baselines.