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 |
|---|---|
|
|
|
|
|
|
|
|
acknowledgement-only methods |
|
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, andRuntimeInfoobjects.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_edgerequest 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
htmltext 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
uidto equal itsedge_id;collapse sibling fragments into a
blockpayload;require clients to submit fragment
uidwhen the contract saysedge_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.jsonengine/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.