Skip to content

Let <Json> bind its rendered text with as #666

Description

@taras

Story

As an Executable Markdown author, I want <Json> to bind the JSON text it
already renders, so I can pass that text to another component without wrapping
the serializer in <Let>.

<Json value={assessment.diagnostics} as="findings" />
<CodeBlock value={findings} language="json" />

The direct form is equivalent to capturing the rendered component explicitly:

<Let as="findings"><Json value={assessment.diagnostics} /></Let>

The equivalence applies to that exact wrapper. Authored whitespace, additional
content, or select inside <Let> would describe a different operation.

Architecture decision

<Json> remains a text component. It has no returns declaration: its one
result is the JSON string it already renders. The engine's ordinary as
mechanism decides whether that string is emitted or bound.

This reverses one component-specific decision from #452. It does not introduce
a new binding mechanism or turn <Json> into a value component. Under the
existing text-component contract, a successful invocation without as renders
its returned string, while the same invocation with as binds that string and
emits nothing.

Public contract

These two invocations have the same observable outcome:

<Let as="findings"><Json value={assessment.diagnostics} /></Let>
<Json value={assessment.diagnostics} as="findings" />

Run separately in the same environment:

  • on success, each binds the exact result of the existing single
    JSON.stringify(value, null, 2) call under findings and emits nothing at
    its invocation site;
  • the value expression is evaluated once and serialization runs once;
  • on failure, each reports the same positioned <Json> failure, binds nothing,
    and emits no partial JSON; and
  • live execution, partial replay, and completed-root reuse retain their existing
    behavior.

The existing uncaptured spelling is unchanged:

<Json value={assessment.diagnostics} />

It renders the same JSON text at the position where it is written, with no
component-added trailing newline.

as stays engine-owned. A valid literal binding name is accepted and never
crosses into <Json> as a prop. An expression-valued or invalid binding name is
the engine's ordinary refusal and evaluates no operand.

The remaining invocation checks keep their existing ordering:

  • paired content, including whitespace, is refused before value evaluates;
  • a missing value is refused before serialization;
  • unknown props are refused by the closed props schema; and
  • only after the invocation is valid does <Json> evaluate the captured
    value and serialize it.

The raw operand seam is unchanged. value remains the registration's one
captured prop, so the exact evaluation result arrives by reference without an
ordinary-prop JSON projection. Allowing result binding does not change operand
capture.

Serialization and failure semantics remain those settled by #452: native
two-space JSON.stringify runs once; a non-string result is distinct from a
thrown serialization; the original thrown cause remains reachable; the source
value is not cloned, replaced, frozen, or mutated by the component; and no
partial JSON survives a failure.

Resolution, ownership, and replay

<Json> remains an ordinary overridable core default. A repository Json.md
is selected ahead of it and uses the engine's ordinary as behavior for its own
text result; none of core's captured-prop declaration travels with the override.

The change introduces no scope, resource, provider, authority, durable identity,
or JSON-specific journal event. A partial replay reaches ordinary expansion and
serializes the value reconstructed in that execution. A completed-root replay
reuses the retained terminal result without invoking <Json> or the operand
again. A surrounding <Prompt>, <File>, or other durable component continues
to own the text it consumes.

Syntax catalog wording

Update the first-party component description to:

Render a value as JSON text for a prompt, file, or code block. <Json value={diagnostics} /> places that text where you put it.

This description leads with the author's purpose and shows a representative
invocation. It deliberately does not explain as, because the catalog renders
that structured field beside the description.

Update the dedicated as description to:

Optional. Captures the JSON text instead of emitting it.

Inspection otherwise remains unchanged: ordinary non-reserved core origin,
self-closing form, captured value, text return mode, and effective
{ type: "string" } return schema. This issue does not change the syntax
catalog schema or its renderer.

Acceptance

  1. An uncaptured <Json value={value} /> renders exactly the same JSON text as
    before, including its fixed two-space formatting and absence of a trailing
    newline.
  2. <Json value={value} as="text" /> binds the exact string the uncaptured form
    would render and emits nothing at the invocation site.
  3. The direct as form and the exact single-child <Let> wrapper above bind
    identical strings for representative object, array, scalar, and null
    operands.
  4. A counted expression and a counted getter or toJSON hook prove one operand
    evaluation and one serialization in the captured form.
  5. A no-text result and a thrown serialization in the captured form keep their
    existing distinct diagnostics and cause behavior, produce no binding, and
    emit no partial JSON.
  6. Invalid as, paired content, a missing value, and an unknown prop remain
    effect-free on the same ordering boundaries, except that a valid literal
    as is no longer refused.
  7. The captured form adds no JSON-specific durable event; partial replay
    reserializes the reconstructed operand and completed-root reuse invokes
    neither the component nor its hooks.
  8. Inspection reports the unchanged text-component contract plus the new
    optional-as documentation, and rendered xmd syntax shows the revised
    component description and as sentence together.
  9. A repository Json.md still overrides core and captures its own rendered
    text through ordinary component as semantics.

Scope

Included:

  • remove <Json>'s component-specific refusal of a valid as binding;
  • preserve invocation validation before operand evaluation;
  • add direct-equivalence, failure, replay, override, inspection, and rendered
    syntax evidence;
  • update the <Json> architecture inventory row and executable-MDX component
    contract; and
  • update the first-party description and dedicated as documentation above.

Out of scope:

  • no change to JSON formatting, serialization, captured-operand delivery,
    interpolation, output middleware, or error settlement;
  • no returns declaration, value-component conversion, new prop, new durable
    event, or generic component-binding redesign;
  • no change to <Let>; and
  • no change to the future packaged plan program for Add xmd prompt: turn a request into an approved executable Plan #260. That program is a
    motivating consumer but is not blocked on this issue and is not present on
    the reviewed main.

Relationship and handoff

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions