Presence/Prose Contract Spike¶
Status: Partially landed Authority: Current
compose_journalrules live indocs/src/design/story/JOURNAL_COMPOSE_CONTRACT.md; this note remains the spike record for presence/prose integration. Current contract: seedocs/src/design/story/JOURNAL_COMPOSE_CONTRACT.mdfor the live journal-composition boundary andengine/src/tangl/story/STORY_DESIGN.mdfor current story runtime layering.
Last Updated: March 2026
Scope: story concept scaffolding plus contract tests; active block rendering
remains format_map-based, with a small opt-in JOURNAL composition consumer
for explicit dialog markup
Why This Spike Exists¶
presence/look is not just an appearance mechanic. It sits near a larger
future contract for prose-facing concept presentation:
how concepts are named or described in rendered text,
how focalization changes those references over time,
how character-specific speech resolves from intent,
and how visual or audio presence enriches prose and media without making the data model own those rendering concerns.
This spike exists to name those contracts and prove them with toy tests before changing the active story render path.
Current v38 Baseline¶
The live engine already has some of the right seams, but not the full prose subsystem yet.
Story JOURNAL handlers receive a real
PhaseCtx.PhaseCtxis ephemeral and primarily used for assembled scoped namespace access viactx.get_ns(node).Persistent session state lives on
Ledger.render_block_content()currently renders block text through namespace assembly plusstr.format_map, not Jinja or concept-driven prose filters.A lightweight
tangl.prosepackage now hosts dialog and micro-block parsing helpers, while the broader prose subsystem remains future work.Raw JOURNAL emission and post-merge composition are now distinct seams:
render_journalemits raw fragments,compose_journalcan replace the merged fragment batch afterward.
The first concrete
compose_journalconsumer is deliberately narrow: explicit dialog micro-block syntax can be rewritten into attributed discourse fragments, while ordinary block prose still flows through the existing content handler unchanged.That dialog rewrite can now bind speakers through the gathered namespace and attach speaker-facing presentation hints plus optional
dialog_immedia payloads sourced from presence-aware actors.That dialog path is now architecturally parallel to the media path, but not identical to it: dialog uses a post-merge transient microconcept (
DialogMuBlock) incompose_journal, while media uses a pre-provision transient microconcept (MediaShotPlan) duringadapt_spec(). Both are request-scoped composition over gathered namespace, but they intentionally remain separate seams for now.
That split still matters, but the current direction is now more specific: narrator-facing epistemic bookkeeping lives on the story concepts it is about, while the render environment remains ephemeral and is rebuilt per pass.
Target Contract¶
1. Presentable¶
The minimum prose-facing protocol should stay small:
@runtime_checkable
class Presentable(Protocol):
def get_label(self) -> str: ...
def get_nominal(
self,
*,
familiarity: Familiarity = Familiarity.ANONYMOUS,
det: DeterminativeType = DeterminativeType.DEFINITE,
) -> str: ...
def get_pronoun(self, pt: PT) -> str: ...
This is the prose participation floor. Anything richer remains optional.
2. Narrative State Placement¶
The current implementation direction is:
EntityKnowledgepersistent with the graph
stored directly on the story concept it is knowledge of
flat and diff-friendly:
state,nominal_handle,first_description,identification_source
HasNarratorKnowledgemixin applied to story concept carriers such as
Actor,Location,Role, andSettingstores
dict[str, EntityKnowledge]keyed by narrator key
DiscourseContextephemeral
built per render or projection pass
tracks focus and other transient discourse decisions
resolves narrator selection from
ctx.get_meta()["narrator_key"]when available, otherwise"_"
This keeps Ledger unchanged. Narrator knowledge is treated as concept-level
episodic bookkeeping, while the render environment remains per-pass state.
3. Speaker Profiles and Speech Acts¶
Speech is treated as a sparse override layer on top of language banks.
SpeakerProfileis derived from entity fields likenative_language,register, andvocabulary.Intent resolution falls back in this order:
per-entity override,
language bank,
English bank,
literal intent key.
This keeps character-specific mannerism lightweight while still making speech semantically addressable.
4. Presence as an Adapter Seam¶
Presence is an enrichment layer, not the prose engine itself.
Look,OutfitManager, andOrnamentationremain data-facing mechanics surfaces.Direct visual facets such as
HasSimpleLook,HasOutfit, andHasOrnamentationshould remain usable independently.HasLookis the visual bundle over those direct facets, not the future all-up presence bundle.A future
HasPresencefacade delegates to prose and media adapters.Lookshould not own narrative policy, focalization, or media backend logic.Prose and media consume presence data through adapter seams such as
describe_presence()andfrom_presence().
This keeps imports one-way:
mechanics.presenceshould not depend ontangl.prose,mechanics.presenceshould not depend ontangl.mediainternals,adapter layers consume presence data, not the other way around.
Compatibility with Current Runtime¶
The migration path should stay parallel and opt-in first.
get_ns()is the entity-local publication seam. Concepts and facets publish their own local symbol maps there.ctx.get_ns(node)remains the assembled scope accessor. Runtime code renders against the gathered view built bydo_gather_ns.Presence facets are exposed to JOURNAL in two explicit opt-in ways:
prose via namespace symbols consumed by the current
format_mappath,media via explicit
block.mediaentries withsource_kind="facet".
Role and setting namespace publication remain backward-compatible.
villainstill resolves to the provider actor, while additive aliases such asvillain_role,place_setting,role_edges, andsetting_edgesexpose role/setting carriers for separate epistemic state.The current
format_mappath remains the live renderer.Future voice/discourse behavior should target the post-merge
compose_journalseam rather than making mechanics objects own prose policy.A future Jinja-based prose renderer should sit beside it first, prove itself, and only replace it once it is a clear superset.
PhaseCtxmetadata already provides the first-pass narrator selection bridge throughnarrator_key; no ledger schema change is required.Richer chapter or section labeling is deferred; when revived it should build on existing stream markers and
since_stepretrieval rather than introducing a second journal-segmentation system.
Toy Spike Proof¶
This spike is backed by toy tests rather than active runtime scaffolding.
The proof should demonstrate:
anonymous versus named reference via concept-local
EntityKnowledgeandmeet(),discourse focus driving pronoun resolution,
speech-intent fallback through
SpeakerProfile,presence-aware prose and media adaptation without changing the base
Presentablecontract,narrator-key isolation via
ctx.get_meta()["narrator_key"],additive role/setting aliases preserving actor-versus-role knowledge distinction,
and the current engine truth that live story rendering is still
PhaseCtx.get_ns(node)plusformat_map.
These tests are intentionally self-contained. They prove the contract shape without claiming that the full prose subsystem already exists.
Forward Integration¶
The current first pass now includes:
Story concept carriers expose
EntityKnowledgethroughHasNarratorKnowledge.Role and setting namespace publication exposes additive aliases for role-level and setting-level knowledge.
Contract tests prove narrator-key selection through context metadata, while active block rendering remains unchanged.
The raw JOURNAL path can now be followed by a minimal post-merge
compose_journalseam for future voice/grouping work.That compose seam now has one concrete story-level consumer: syntax-opt-in dialog micro-block composition that rewrites authored dialog markup into attributed discourse fragments without changing the underlying block-content renderer.
Those attributed dialog fragments can now carry resolved speaker identity, style hints, and optional look-derived
dialog_impayloads when the speaker object exposes the relevant hooks.A broader unification with media is intentionally deferred. If prose, dialog, and media all converge on the same pattern of invoking request-specific entity contribution hooks against the gathered namespace, the next step should be a small shared contribution protocol or typed request object, not an eager generalization of
compose_journalinto a nested or DAG-style dispatch framework.
If this grows beyond concept-local bookkeeping later, the next phase should decide whether to build:
a real prose renderer entry point in parallel with the current block content handler, or
author/world-level composition handlers over the new
compose_journalseam for lighter-weight voice and grouping work.
Until then, the engine should keep treating this as a design target proven by tests, not as an active published runtime subsystem.