PathMX Spaceholder, Input, and Response Convergence
Status and implementation
This contract was accepted and implemented on 2026-07-20 through the linked
execution plan. PathMX now uses one plural Responses value shape, shared text
and choice definitions, explicit Source/Block owners, context.responses,
ctx.responses, normal Action authoring, and semantic Player controls.
The earlier @response.field, singular Block response, separate
questions.submit* codecs, arbitrary hidden-JSON Response form, and
ctx.response contracts are superseded and have no runtime compatibility
branch. Attached @spaceholder directives remain the separate generated
Source/image materialization adapter described below.
Thesis
PathMX needs one durable model for actor-supplied values without forcing every domain to use the same visible authoring syntax.
- An Input defines a value and how it is collected.
- A Spaceholder places an Input editor or Response projection in Source.
- Responses are the current actor-supplied values stored on one explicit Source or Block owner.
- A Question adapts natural Markdown into the same Input definitions and Response values, adding assessment semantics only when needed.
- A Submission records optional attempt or grading evidence beside Responses and never duplicates their values.
One deep Responses module should own definition normalization, owner resolution, form decoding, validation, readable persistence, and Action-plan composition. Occurrence parsing, Questions, Literate Components, and generated asset materialization remain focused adapters around that module.
Accepted domain language
Input: an owner-local definition for one actor-supplied value. It owns the stable ID, logical type, label, requiredness, validation, and type-specific configuration. Definitions are compiled facts, not duplicated persisted data.
Response: the current JSON-safe value produced through one Input.
Responses: the owner-local map of current Response values stored under
responses. It contains no Actor identity, history, grade, or Action Run
metadata.
Spaceholder: a stable Source-visible occurrence that edits an Input
([> id, ...]) or projects its current Response ([= id]). Placement and
presentation belong to the occurrence; value semantics belong to the Input.
Question: interaction or assessment semantics composed from one or more Inputs. A Question is not a second response storage system.
Submission: optional attempt, grading, or Action receipt evidence stored beside Responses. An ordinary onboarding save needs no Submission.
The current generated-content behavior remains a materializing Spaceholder adapter. It keeps ownership of deferred work, generated files, Includes, AI adapters, and provenance. It does not become the inline editor or the Response store.
Authored grammar
The accepted first-slice grammar is:
input-occurrence = "[>" ws reference *(ws? "," ws? argument) ws? "]"
output-occurrence = "[=" ws? reference ws? "]"
reference = identifier | quoted-identifier
argument = name ws? "=" ws? scalar
Use descriptive argument names. The earlier p= and t= abbreviations are
rejected.
Text Input
**What would you like to learn?**
[> topic, placeholder="e.g. Understand the greenhouse effect", minLength=3, maxLength=120]
type=text and required=true are defaults. Set required=false only when an
empty value is valid:
[> note, label="Optional note", required=false]
The nearest preceding readable prompt supplies the accessible label when
label is omitted, so authors do not repeat a visible heading or bold prompt
inside the occurrence. Use an explicit label when surrounding prose does not
name the control clearly. A standalone occurrence with an explicit label
renders that label visibly; inline prose and adjacent visible prompts remain
the visual context without duplicated copy.
Output occurrence
We will begin with **[= topic]**.
Output values use the Input type's deterministic plain-text projection. Text uses the normalized string; choice maps stored IDs to current labels and joins multiple labels predictably. Every projection is HTML-escaped and is never reparsed as Markdown, HTML, directives, or component code.
Choice Question
Natural Markdown remains the primary choice authoring surface:
<!--
type: question
id: experience
responses: {}
actions:
submit: responses.save
-->
# How much experience do you have?
- [ ] I'm completely new #new
- [ ] I know the basics #basics
- [ ] I've used this before #experienced
The Question Block ID becomes the single implicit Input ID. Every choice
requires an explicit owner-local #id; labels may change without migrating
stored values. Missing or duplicate option IDs are Build diagnostics. Do not
generate persistent IDs from label hashes.
Single-choice values are strings. Multiple-choice values are string arrays.
The same Input configuration names apply through optional Block input: data:
input:
required: true
multiple: true
minSelections: 1
maxSelections: 2
Block input: is valid only when the Question implies exactly one Input.
Questions with several named Inputs configure each [>] occurrence directly.
The first implementation does not mix a natural choice list and editable text
Spaceholders in one Block; use separate Blocks under a shared Source Responses
owner so each interaction retains one semantic control container.
Parsing boundaries
Bare and quoted references normalize to the same conservative local-ID
grammar. Quoting does not allow whitespace or punctuation in identity. Use a
backslash for literal openers (\[> and \[=). Do not parse occurrences in
code spans, fenced code, link or image syntax, raw HTML attributes, or existing
PathMX directives.
Source value and owner contract
type PathMXResponseValues = Readonly<Record<string, JsonValue>>
type PathMXSourceData = Readonly<{
responses?: PathMXResponseValues
}>
type PathMXBlockData = Readonly<{
id: string
responses?: PathMXResponseValues
}>
Values are direct semantic keys:
responses:
topic: Chess
outcome: Play a complete game confidently
experience: basics
interests:
- tactics
- endgames
responses is also the explicit owner declaration. An editable occurrence
without a containing owner is a Build diagnostic.
Owner resolution is deterministic:
- Bind
[>],[=], and a Question's implicit Input to the nearest containing Block or Source withresponsesdata. - Require a stable authored Block ID for Block ownership.
- Require Input IDs to be unique within the resolved owner.
- Permit one edit occurrence and any number of output occurrences per ID.
- Do not search other owners implicitly.
Use a Source owner when onboarding values feed later or generated Blocks:
---
type: path
responses: {}
---
<!--
type: question
id: learning-goal
actions:
submit: learning.begin
-->
# Build a path around your goal
[> topic, label="What would you like to learn?"]
---
# Your first step
We will begin with **[= topic]**.
Use Block ownership for isolated checkpoints. Explicit cross-owner reference syntax is deferred; move the owner upward when values are intentionally shared.
Normalized Input definitions
The first slice uses a closed internal union rather than a public Input Type registry:
type PathMXInputBase = Readonly<{
id: string
label: string
required: boolean
}>
type PathMXTextInputDefinition = PathMXInputBase & Readonly<{
type: "text"
placeholder?: string
minLength?: number
maxLength?: number
}>
type PathMXChoiceOption = Readonly<{
id: string
label: string
markdown: string
}>
type PathMXChoiceInputDefinition = PathMXInputBase & Readonly<{
type: "choice"
multiple: boolean
minSelections?: number
maxSelections?: number
options: readonly PathMXChoiceOption[]
}>
type PathMXInputDefinition =
| PathMXTextInputDefinition
| PathMXChoiceInputDefinition
type PathMXResponseOwner =
| Readonly<{ type: "source"; sourceId: string }>
| Readonly<{ type: "block"; sourceId: string; blockId: string }>
Marker attributes and Question adapters call one normalizer and validator. Unknown configuration and type-incompatible properties are Build diagnostics. Browser validation is immediate UX; the server Action always revalidates.
Add no public Input Type contribution until one non-core prototype proves a seam that does not expose Source mutation, Actor authority, Player state, or Question concepts. Core text and Question choice implementations are not, by themselves, sufficient evidence for a stable third-party registry.
Markdown and component lowering
One scan discovers occurrences and exact ranges in each candidate Markdown region before ordinary rendering:
- Parse occurrences and normalized definitions.
- Replace each occurrence with an implementation-private marker carrying an occurrence key and provenance, never current values or authored HTML.
- Let Includes, Literate Component expansion, Markdown rendering, and normal transforms preserve the marker.
- Resolve the final consuming owner and render the edit or output occurrence.
Do not rescan the rendered document or make Runtime discover authored syntax
from DOM text. Component-definition Sources never own instance Responses.
Components receive a deep-frozen ctx.responses projection from the consuming
owner. Duplicate local IDs in one owner remain an error until an explicit
binding mechanism is designed.
Action authoring contract
Normal forms submit successful controls as response.<inputId>. Repeated
choice names decode as arrays. Reserved Action routing fields remain separate,
and the browser never supplies owner authority.
type PathMXResponseSubmission = Readonly<{
values: Readonly<Record<string, JsonValue>>
}>
type PathMXResponsePlanner = Readonly<{
owner: PathMXResponseOwner
definitions: readonly PathMXInputDefinition[]
current: PathMXResponseValues
set(submission: PathMXResponseSubmission): PathMXResponseValues
}>
responseSubmission() is a reusable PathMXActionInputContract for normal
form decoding. Its Journal value contains only a canonical Response hash,
field count, and byte count, never raw learner values. An authoring Action
using that contract receives a resolved, plan-scoped context.responses
service:
export const beginLearning = defineAuthoringAction({
id: "learning.begin",
version: "1",
title: "Begin learning",
target: "block",
input: responseSubmission(),
result: learningResult(),
capability: {
operations: ["source.replace"],
targetScope: "attached-source",
maxOperations: 1,
},
plan(context, submission) {
const responses = context.responses.set(submission)
context.plan.appendBlocks(
context.target.source,
createStartingPath(responses),
)
return context.plan.complete({ status: "started" })
},
})
context.responses.set resolves the nearest owner from the trusted current
target, validates against current compiled definitions, stages a readable
Responses edit in the existing authoring plan, and returns the normalized,
deep-frozen next map. A typed validation failure short-circuits through the
Action framework as an invalid-input outcome with field issues; Action code
does not translate it into a Source conflict.
Contextual validation happens after the Run is admitted because it depends on the trusted current parsed Source and owner. Extend the existing planning and Run outcome unions explicitly:
Add these exact branches to the existing unions:
type PathMXInvalidActionPlanningResult = Readonly<{
type: "invalid"
issues: readonly PathMXActionInputIssue[]
}>
type PathMXInvalidActionRunOutcome = Readonly<{
type: "invalid"
code: "invalid-input"
message: string
issues: readonly PathMXActionInputIssue[]
}>
The Actor Runner finalizes this as a non-writing Run, the Server returns 422,
and the Player retains the submitted controls and renders field feedback.
Issue field names use the rendered successful-control name
(response.<inputId>) so the generic Action form feedback path needs no
Response-specific lookup branch.
The built-in responses.save is the smallest use of this same primitive.
Unassessed Questions may map directly to it. Assessed Question Actions may add
attempt, grade, or feedback planning, but they must call
context.responses.set and must not own another value codec or store.
An Action never invokes responses.save as a second Action. Response edits and
generated Blocks remain in one authoring plan and compile to one expected
Source version, at most one source.replace, one workspace application, one
incremental change, and one Action completion event.
Response mutation semantics
context.responses.set applies one interaction-scoped update:
- reject unknown submitted IDs;
- normalize values through their current definitions;
- validate all Inputs rendered by the submitted interaction;
- replace values for those Inputs;
- remove a previous value when an optional rendered Input is omitted;
- preserve unrelated values belonging to the same owner; and
- stage no Source edit when the normalized owner map is unchanged.
Missing required values, invalid values, and unknown IDs leave Source
unchanged. Source Version expectations remain the conflict authority. Direct
author or agent edits to responses are ordinary Source Changes, not
retroactive Action Runs.
Player interaction contract
Build emits normal POST forms and a small semantic DOM contract on each generated Response control container:
<fieldset
data-pathmx-response-controls
data-pathmx-response-submission="explicit"
data-pathmx-response-enter="modifier"
>
<!-- ordinary labeled controls -->
</fieldset>
Single choice may use:
<fieldset
data-pathmx-response-controls
data-pathmx-response-submission="change"
>
<!-- ordinary radio controls -->
</fieldset>
Build derives this policy from Input definitions. The Player finds successful controls inside the marked container and never switches behavior on Question type, Action ID, or field-name prefix.
changesubmits a valid choice immediately.explicitretains the generated submit affordance.plainpermits Enter on the appropriate single-line control.modifierreserves ordinary Enter and uses the documented modified chord.- Native form validity controls immediate affordance state.
- Entering a Response Beat focuses the first appropriate text control; Tab remains normal browser navigation.
- The normal form works without Player enhancement.
The Player observes DOM for the interaction surface only. Action completion comes from the server's correlated Action completion/projection receipt, never from waiting for arbitrary DOM mutation.
Thermo-nuclear maintainability constraints
The strict review approves the proposed ownership model only with the following implementation constraints.
Deepen the Response seam without creating a god module
Move the reusable internals evolved from plugins/core/responses/ into one
focused internal packages/build/src/responses/ boundary. Keep the default
plugin entrypoint thin. Split pure definition/value logic, owner resolution,
occurrence lowering, form decoding/rendering, and Action planning into focused
modules rather than growing save-action.ts, questions/index.ts, or a new
universal handler.
The convergence should delete the separate Question field, text, and choice value codecs and submission Actions where they add no assessment behavior. A net-new parallel set of Input-specific codecs that leaves those paths intact is a blocker even if tests pass.
Decompose authoring context before adding feature branches
packages/build/src/actor/action-authoring.ts is already approximately 739
lines. Do not push it toward or past 1,000 lines with Response-specific owner,
validation, and edit branches. First extract its existing context state and
plan builder into cohesive internal modules. Add context.responses through
one typed binding point backed by the Responses boundary; do not add a generic
service registry or several optional context flags.
The existing plan builder remains the only Source edit composer. Improve it to batch related edits in one editable Source session. The Responses module must not introduce another edit session, operation collector, transaction, or Source parser.
Reuse the canonical form and Player seams
The existing Action form renderer remains the only form wrapper. The shared
Input renderer writes the accepted semantic policy on its control container;
Questions and Literate Components do not each emit their own policy dialect.
The Player replaces the current Action-ID table and field-prefix checks with
one semantic-container reader. It must not retain the old table as fallback or
grow action-specific branches in contextual-actions.ts, response-control.ts,
and action-forms.tsx simultaneously.
These constraints are implementation acceptance conditions, not optional cleanup after the feature lands.
Invariants and diagnostics
| Code | Phase | Meaning |
|---|---|---|
missing-response-owner | Build | An editable or output occurrence cannot resolve an explicit owner. |
unstable-response-owner | Build | A Block owner has no stable authored ID. |
duplicate-input-id | Build | Two definitions claim one owner-local ID incompatibly. |
duplicate-input-editor | Build | More than one edit occurrence claims one owner-local ID. |
unknown-input-type | Build | An occurrence or adapter requests an unsupported type. |
invalid-input-config | Build | Configuration is unknown, malformed, or incompatible with the type. |
missing-choice-id | Build | A persisted choice option has no explicit ID. |
duplicate-choice-id | Build | Choice IDs repeat inside one Input. |
undeclared-response-key | Build/Action | Stored or submitted data names no owner-local Input. |
missing-required-response | Action | A required rendered Input has no submitted value. |
invalid-response-value | Build/Action | A stored or submitted value cannot normalize or validate. |
stale-source | Action | The expected Source Version no longer matches. |
Existing stored values containing undeclared or invalid keys fail Build without discarding data. Missing optional output renders empty content. Missing required output renders the documented empty state rather than a prompt or fabricated value.
Ordering and performance
- Scan each candidate Markdown region once: O(markdown length).
- Resolve definitions and values with owner-local maps: O(occurrences).
- Decode and validate one submission once: O(successful controls).
- Reuse the parsed Source, Blocks, graph, and compiled Input definitions already available to the Action context.
- Do not rescan the Root, reconstruct a parallel graph, reread Source from the filesystem, or reparse unchanged Source merely to validate Responses.
- Batch Response and generated-Block edits through one editable Source session before compiling the replacement.
- Keep normalized definitions and Response values explicit in Compiled Block cache identity.
The accepted Action Source Change convergence contract remains authoritative: uncontended warm submit-to-applied p95 should remain below 200 ms in the supported performance runner. Large Roots must not add Root-sized Response planning work. Bursts use the existing bounded mutation admission, Source-Version conflicts, interactive projection priority, coalescing, and backpressure rather than an Input-specific queue or cache.
Implementation boundary and sequence
The maintainability move is to deepen the existing
plugins/core/responses/ seam in place, then delete duplicate Question and
Component codecs. Do not build a second durable store beside it.
Implementation followed this sequence:
- Responses core — rename singular Source data to plural
responses, add normalized definitions, owner resolution, shared validation, staged plan updates, and the reusable submission input contract. - Occurrence lowering — implement
[>]and[=], exact provenance, text rendering, Source/Block ownership, and output projection. - Question adapter — lower natural single/multiple choice and text/field Questions into shared definitions; delete separate field/text/choice value codecs where the shared module replaces them.
- Action composition — expose plan-scoped
context.responses, reshape the built-inresponses.save, preserve assessed Question wrappers only where they add real semantics, and prove one atomic Response-plus-Block plan. - Player policy — emit the semantic Response container contract and remove the Action-ID policy table and field-prefix matching.
- Literate Components — migrate to deep-frozen
ctx.responsesand the shared action path without DOM-derived persistence identity. - Repository and Starter migration — migrate all current Sources,
examples, tests, docs, and the learning Starter in the same coordinated
breaking change; remove
@response.field, singularresponse, and the old Question form Actions rather than carrying compatibility branches. - Materialization and legacy cleanup — keep generated Source/image Spaceholders focused, remove only superseded Model/Point remnants, and reconcile the domain guide after caller audits.
The implementation used the parsed Build graph, focused Source builds, and a zero-caller audit for the coordinated repository and Starter cutover. It did not retain a one-time migration executable because there were no deployed or third-party callers to migrate. A future external migration must remain graph-aware, fail on ambiguous old/new coexistence, and stay out of Runtime compatibility code.
Test surface
- Grammar and escape fixtures for prose, standalone paragraphs, emphasis, blockquotes, lists, tables, code, links, raw HTML, malformed arguments, and duplicate IDs.
- Build fixtures for Source and Block owners, edit/output reuse, readable YAML, strict existing-value validation, Includes, and Literate Components.
- Question fixtures proving natural choice and multi-field authoring lower to the same definitions and Responses without losing assessment semantics.
- Action contract tests for normalization, missing/extra/duplicate fields, optional clearing, unrelated-key preservation, no-op saves, stale versions, limits, journal-safe hashes, and Response-plus-generated-Block atomicity.
- Player tests for generated policy rather than Action IDs, validity, focus, Tab, Enter, IME, pending/retry, restored values, and narrow layouts.
- Contained browser/server tests using disposable Sources, the real bundled Player, HTTP, WebSocket, storage, Action workspace, and incremental Build.
- Stress tests for a 1,000-Source warm Root, 50 disjoint Actors, same-Source conflicts, active recovery/deferred work, slow consumers, and causal resync.
- A dedicated warm performance run enforcing the accepted p95-under-200 ms submit-to-applied target.
The Starter acceptance scenario enters onboarding values, saves them, creates the personalized next Blocks in the same Action, receives the correlated completion event, and presents restored Responses after reload without a full page refresh or duplicate Source mutation.
Accepted review decisions
The maintainer accepted these decisions during the July 19–20 drill:
- Use long-form
[> id, ...]and[= id]syntax. - Let a visible heading or bold prompt supply the label by default; use the
explicit
labelconfiguration only when surrounding prose is insufficient. - Keep one Response value shape, definition/validation module, and mutation path for Questions and future Spaceholders.
- Preserve natural Markdown choice authoring; use the Question Block ID as the implicit Input ID and require explicit choice IDs.
- Use Input for definitions, Spaceholder for occurrences, Responses for current values, Question for interaction/assessment, and Submission for optional attempt evidence.
- Store direct values under plural
responses; remove heterogeneous legacyresponse.text,response.choice, and field shapes. - Resolve the nearest explicit Source or Block Response owner; use Source ownership for flow-wide learner state.
- Reuse the same named configuration and server validator for marker and Question inputs.
- Make save interaction-scoped, atomic, strict about submitted IDs, and preserving of unrelated owner values.
- Make
context.responses.setplan-scoped so custom Actions can save and generate Blocks in one Source replacement. - Make Player behavior depend on generated semantic form policy rather than Action IDs or DOM-change completion heuristics.
- Keep generated Source/image materialization separate and keep Input Type extensibility internal until a non-core implementation proves the seam.
Accepted gate and stop conditions
The maintainer accepted this consolidated packet on 2026-07-20 as the implementation contract.
Stop if implementation would:
- add parallel
response,responses, or proposedinputsvalue stores; - require Action code to reconstruct owners, definitions, form codecs, or Source edits itself;
- require DOM instance identity or values on component-definition Sources;
- expose a public Input Type registry before a non-core prototype;
- fold generated-file materialization into the inline Response module;
- add an Input-specific build, projection, graph, queue, or cache; or
- miss the existing Action convergence correctness and warm performance suite.