✨ Let <Json> capture its text with as - #667
Merged
Merged
Conversation
`<Json>` refused a valid literal `as` in its own body, so a document that wanted the JSON text rather than its placement had to write the single-child `<Let>` wrapper around it. Removing that refusal opts the component back into the ordinary text capture every text component already has: the engine validates and strips `as`, binds the exact string the component returned, and emits nothing at the invocation site. `value` remains the one public prop and the one raw captured operand, `JSON.stringify(value, null, 2)` still runs once, and the two serialization failures stay distinct — under `as` each leaves the destination absent and leaks no partial JSON. Validation order is unchanged apart from what the engine now owns: an expression-valued `as` and a literal that names no binding are refused on the authored text, before either expression runs. Closes #666
PR #667: ✨ Let
|
The registry's description already said what <Json> does; only the field was untrue. Restoring the original sentence keeps this PR to the one prose change the behavior requires.
taras
marked this pull request as ready for review
August 30, 2026 18:42
taras
enabled auto-merge (squash)
August 30, 2026 18:43
taras
added a commit
that referenced
this pull request
Aug 30, 2026
… making it (#260) Rebased onto `<Json as>` (#667), which supplies what the single-child `<Let>` wrapper was standing in for. Both wrappers in the packaged prompt command document are gone; `<Json value={check.diagnostics} as="problems" />` binds the serialized text directly, and the bytes reaching `<CodeBlock>` are the same bytes — the repair turn and the review presentation still carry the complete structured problems, now proven by parsing the fence back rather than by matching a substring of it. The default session's directory is now a scope-owned resource rather than a directory with an ensure attached afterwards. The release is registered before the `mkdir` that could create it, so there is no window in which a leaf exists that nothing is responsible for: a failure between making it and using it hands it back like every other ending. What the release knows is whether the directory was ever handed over, and that decides which question it is answering. Once established, the exit has exactly two outcomes and no third. Still the empty directory it was given, and the leaf is removed, non-recursively. Anything else — content that appeared, or a directory that vanished — is preserved as found and fails the command terminally. A leaf that disappears under a live conversation was silently accepted before, which was wrong twice over: nothing here is allowed to remove it, so its absence is interference, and treating interference as a clean exit would let admission, the save and the execution proceed on a conversation something else had already reached into. A directory establishment never handed over is a different question, already answered by what establishment reported. The release leaves that report and the directory's contents alone, while still handing back an empty leaf it did create. Explicitly named directories are untouched by any of this: they stay, because the next `--session` derives the same ACPX session identity from where they are. Claude-Session: https://claude.ai/code/session_015HcqB9kJM9KFnMNToAuZF6
taras
added a commit
that referenced
this pull request
Aug 30, 2026
…260) (#657) * ✨ Add xmd prompt, the authorship command (#260) `xmd prompt "<request>"` asks the configured ACP agent for a complete executable Markdown root, validates it without executing any of it, repairs its definite defects by asking again, shows a person the exact bytes, and runs the approved ones through the ordinary supplied-source path under the `<prompt>` identity. It adds authorship around one normal document; it adds no second execution model, props model or journal. Authorship sits outside durability. The catalog, the fresh generator session, every repair and the review create no journal and replay nothing, and the generator's scope closes before the optional exclusive `--save` and before execution. A defect the draft authored is repairable and earns one of three turns; a defect the command line authored terminates without spending a turn on something the agent cannot fix. `xmd run` and `xmd prompt` now share one execution field set, one resolved Agent configuration, and one props source resolution. Claude-Session: https://claude.ai/code/session_015HcqB9kJM9KFnMNToAuZF6 * 🐛 Settle one agent stack, refuse -e, and report a run's failure (#260) Three review findings on the xmd prompt command. An inline document reached the generator. `preparePropsPhase` skips the parse that carries the other commands' `-e` refusal, so a supplied document was dropped in silence and the command generated a different one. Refused in prompt's own branch, before the catalog, the provider, the review, the save, the journal or any execution. The agent configuration was resolved twice, once for generation and again for the document, so one command line had two chances to read DEFAULT_AGENT_NAME differently. A dispatch now settles it once and hands the resolved value to both consumers; DocumentMode carries the settled stack rather than the flags. The executed program still gets a fresh ordinary provider and inherits neither the generator session nor its system prompt. A document failing its own <Testing> boundary printed a bare message where a run prints a `tests failed:` heading. reportFailure moves to a module both commands share. Claude-Session: https://claude.ai/code/session_015HcqB9kJM9KFnMNToAuZF6 * ✨ Ship the plan program as a packaged Markdown document (#260) The first document-backed command needs its Markdown where the command is: a source checkout, a published npm package, and a compiled binary with no checkout. The document is the deployed program, so it ships as a file rather than as a generated string. src/documents/prompt-plan.md is that program. A package declares what it ships by putting it in that directory; test documents and scenario fixtures live under src/ too, and a build that swept those up would publish fixtures and grow every binary carrying them. The command locates it from its module's own URL, never from the working directory and never through the component search path, so it finds the same policy whatever directory a person stands in and no repository file can answer for it. Each build keeps the directory beside its module: both deno compile sites embed it, and the npm build copies it into the emitted tree, because dnt emits the module graph only and an asset nothing imports would otherwise reach Node and Bun missing while Deno stayed green. A regression holds the two compile sites to the documents that exist, since that list is the one thing no build discovers. The host still runs the TypeScript authorship loop; wiring the plan program into the plan profile follows. Claude-Session: https://claude.ai/code/session_015HcqB9kJM9KFnMNToAuZF6 * 🐛 Make plan approval and exhaustion mutually exclusive (#260) A <Return> selects the value a value body publishes; it does not end that body, and a later failure wins over a value already selected. The approve branch returned the candidate and broke out of the review loop, then execution left the Session and reached the unconditional exhaustion <Fail>. Every approval settled as the ten-draft failure and no approved candidate could reach the host. Approval now binds the exact candidate to a null-sentinel result, and one exhaustive branch after the Session either returns it or fails. Only the arm taken runs, so an approved candidate has no later sibling failure to overtake it. Bounds, prompt wording, diagnostics, presentation, Session structure and the authored abort remain as they were. The review schema also needed an explicit string type: Ajv strict mode refuses minLength on an untyped property, so every review was refused before a person saw anything. Diagnostics reach their code block through <Json> rather than a JSON.stringify call in an expression, so serializing them is a document construct like everything else this policy does. The regression runs the packaged document itself, resolved against first-party declarations with an empty include list, against a scripted agent turn, a scripted approval and a test validator. It fails with the exhaustion message before this change. Claude-Session: https://claude.ai/code/session_015HcqB9kJM9KFnMNToAuZF6 * ✨ Turn a request into a Plan with a packaged command document (#260) `xmd prompt` now executes two roots with a scope boundary between them. First `src/documents/prompt-command.md`, the checked-in first-party Markdown that implements the conversion and its review workflow; then the Plan it returned, through the ordinary supplied-source path. The TypeScript authorship loop — `author()`, `evaluate()` and `review()` — is gone: what the assistant is asked, how many drafts may be fixed, how many you are shown and what happens when you approve nothing are readable Markdown now, and arguable. A Plan is not a script with comments. The shipped generation, repair and revision instructions ask for every requested outcome as reader-facing prose with each component placed immediately after the sentences describing what it does, so the source can be audited and the execution followed by the same narrative. That authorship rule is the document's and human review's; it is never a hidden validation rule. The prompt profile is what that document runs inside. The assistant gets an empty directory of this host's own, no additional directories, no MCP servers, no native tools and a strict denial answered inside the provider — so nothing composed around it can widen a ceiling with nothing in it, and `--approve-all` configures the approved Plan rather than the conversation that wrote it. The document itself reaches no Files, command, service or network capability, runs against no repository component search, and cannot opt out of a failing turn ending it: a turn that streamed half a program and failed presents nothing. `<ValidateCandidate>` is declared to that execution rather than resolved from a repository, and it keeps the two kinds of failure apart. A defect a draft authored comes back as structured facts the document may repair; a defect the command line authored raises out of it, so no policy can catch a caller's mistake and call it feedback. The host takes the returned Plan back as untrusted text. Only after every Prompt task, provider and Elicitation resource has torn down does it validate those exact bytes again and resolve their props — a revision that changed a property's declared type changes what the run receives — and only then does `--save` create the file and the Plan run. `--session <name>` names the assistant session; without it each invocation places its own. The profile's working directory is one fixed host-owned path because a session's key includes the directory it lives in, and a location that changed every time would leave that option unable to name anything. Evidence runs the packaged document itself against a scripted agent and a scripted review: the shipped words reach the turn, ten presentations bound the review with no revision on the tenth, an interleaved Plan returns byte for byte, and what a person reads says each thing once however many rounds it took. Claude-Session: https://claude.ai/code/session_015HcqB9kJM9KFnMNToAuZF6 * 🐛 Give each prompt session its own directory and a reachable exhaustion (#260) Four corrections to the prompt profile and the command document it runs. One directory per logical session, at `~/.xmd/prompt/sessions/<sha256(logical-session-name)>`, replaces the single `~/.xmd/prompt` every invocation shared. A shared directory was one ambient location two conversations could both stand in; a fresh one each time would have left `--session` unable to name anything, since a session's key includes the directory it lives in. The digest gives both: a generated name reaches a location nothing else does, and the same explicit name reaches the same one, so the provider continues the session it established rather than placing a second. The leaf is the digest and never the name — a caller's string that becomes a path is a caller's string that can escape one. Emptiness is now proven rather than assumed. The directory is created empty and required to be empty before the provider is constructed or a session is materialized, and anything already in there is a terminal refusal naming the path. Nothing is deleted: what is in a directory this host did not authorize anything to write to is not this command's to clean up. Stopping on the tenth draft says two different things, and now the document says both. When that draft still has problems, `abort` is the only choice offered and there was never a Plan to approve, so it routes to the authored exhaustion message. Every other abort — including one on a tenth draft that could have been approved — stays the ordinary ending. The branch after the Session remains an exhaustive fallback. Final admission is now shown to be independently effective. A scripted draft uses a repository component that exists while the command document runs and is removed as its scope tears down; the unchanged approved bytes then fail the host's second validation, in that order, with no save, journal or execution. Both validations run the production path — leaving the component in place makes the case pass and execute, which is what makes it evidence. Claude-Session: https://claude.ai/code/session_015HcqB9kJM9KFnMNToAuZF6 * 🐛 Own the profile root in tests, and give a default session's directory back (#260) Two ownership corrections: one about which directories a test may touch, and one about which directories an invocation may keep. The suite no longer reads, creates or removes anything under a real `~/.xmd/prompt`. Snapshotting that root and deleting whatever appeared after the snapshot was unsafe on its face — absence from a snapshot says nothing about who created a directory, so an overlapping case or a real invocation could have its state removed by a test that never owned it. Where profile session directories live is now a host dependency: production keeps its default, and a harness names a tree it made itself, passed as a value with no flag, environment variable, document prop or replaceable context anywhere near it. `createPromptHarness` requires that root, so no case can fall back to the host's, and each root is a per-scope temporary directory removed whole — which is safe precisely because the scope created everything in it. The one case that needs two invocations to share a root says so explicitly. Only an explicitly named session needs its directory afterwards. Its identity is what a later `--session` derives the same ACPX session from, so it stays, is required empty on the way in every time, and is never cleaned. An invocation-unique default names nothing anybody can ask for again, so its directory belongs to the command: after the command document and every provider, Prompt task and Elicitation resource inside it has torn down, exactly one cleanup is attempted, and it settles before final admission, the save or the execution begins. Still empty, and the leaf is removed non-recursively. No longer empty, and the directory and its contents are preserved and the command fails terminally — this host authorized nothing to write there, and deleting a stranger's files to get on with the work is the opposite of what a ceiling is for. Being an `ensure` is what makes an abort, a failed turn and a cancellation settle the same way a success does. Which of the two applies is a trusted host value — whether the caller wrote `--session` — never inferred from the shape of the generated name, and never visible to the command document. Both outcomes are held by evidence that fails without them: disabling the cleanup leaves the leaf behind on success and before admission, and making the removal recursive turns the preserved-and-refused case into a silent success. Claude-Session: https://claude.ai/code/session_015HcqB9kJM9KFnMNToAuZF6 * ✨ Capture the problems with <Json as>, and claim the directory before making it (#260) Rebased onto `<Json as>` (#667), which supplies what the single-child `<Let>` wrapper was standing in for. Both wrappers in the packaged prompt command document are gone; `<Json value={check.diagnostics} as="problems" />` binds the serialized text directly, and the bytes reaching `<CodeBlock>` are the same bytes — the repair turn and the review presentation still carry the complete structured problems, now proven by parsing the fence back rather than by matching a substring of it. The default session's directory is now a scope-owned resource rather than a directory with an ensure attached afterwards. The release is registered before the `mkdir` that could create it, so there is no window in which a leaf exists that nothing is responsible for: a failure between making it and using it hands it back like every other ending. What the release knows is whether the directory was ever handed over, and that decides which question it is answering. Once established, the exit has exactly two outcomes and no third. Still the empty directory it was given, and the leaf is removed, non-recursively. Anything else — content that appeared, or a directory that vanished — is preserved as found and fails the command terminally. A leaf that disappears under a live conversation was silently accepted before, which was wrong twice over: nothing here is allowed to remove it, so its absence is interference, and treating interference as a clean exit would let admission, the save and the execution proceed on a conversation something else had already reached into. A directory establishment never handed over is a different question, already answered by what establishment reported. The release leaves that report and the directory's contents alone, while still handing back an empty leaf it did create. Explicitly named directories are untouched by any of this: they stay, because the next `--session` derives the same ACPX session identity from where they are. Claude-Session: https://claude.ai/code/session_015HcqB9kJM9KFnMNToAuZF6 * ✨ Make the approved Plan the result, and the workflow readable end to end (#260) The command document is now a workflow somebody can read straight through. It opens by saying what `xmd prompt` is for — a Prompt describes steps, the components carry them out, and the coding agent turns both into one document that explains and executes the sequence — and every stage after that is a heading: creating the first draft, checking and repairing it, reviewing it, continuing from the decision, returning the approved Plan. Every Plan now begins with one descriptive level-one title, and the generation, repair and revision turns each ask for it alongside the steps-beside-components structure, so a replacement cannot quietly drop either. The worked example is a titled Plan itself. This is an authorship and human-review requirement, not a rule hidden in the checker: `<CheckDraft>` reports structural facts and would accept a titleless Plan that a person would send back. `<ValidateCandidate>` is `<CheckDraft>` throughout. It checks a draft rather than validating a candidate, and nothing about what it does changed. The review choices are the words a person reads — Approve, Request changes, Stop — with no internal spelling behind them. A tenth draft that still has problems now offers something better than only stopping: Explain what went wrong makes one more ordinary turn in the same Session, carrying only the final diagnostics, because the conversation already holds the Prompt, the catalog and every draft. It asks for an explanation and explicitly not another Plan; its answer is inert text, reopens no draft limit, is reported as the coding agent's own words, and ends the command. So the host's instruction layer no longer demands that every answer be document source — it says only that an answer belongs to the message that asked for it, and each authored turn owns its own shape. The approved Plan is the command's result. By default it goes to stdout, byte for byte, with nothing this command added, so it can be piped, diffed or read before anybody commits to it. `--output` puts those bytes in a file instead, exclusively created. `--run` runs it. With both, the file is written first and only a successful write is followed by the run. `--save` is gone rather than aliased; nothing has been released to keep compatible with. That makes most execution flags conditional, so they are refused rather than ignored: a caller who asks for a journal, a permission mode or an exec deadline from a command that runs nothing has not been answered. `--include`, `--agent-provider`, `--default-agent`, `--session` and `--timeout` keep working always — they build the catalog, settle the agent, name the conversation, admit properties and bound the command. A journal exists only when `--run` begins. Claude-Session: https://claude.ai/code/session_015HcqB9kJM9KFnMNToAuZF6 * 🐛 State the whole Plan requirement in every turn that asks for one (#260) A repair or a revision was told to "keep" the draft's title. That is the wrong instruction for a replacement: a draft whose title is missing, in the wrong place, or does not describe the Plan has nothing worth keeping, and an instruction to keep it asks the coding agent to carry the defect forward. Each Plan-producing turn — the first draft, each repair, each revision — now states the complete requirement on its own: optional frontmatter and then one descriptive level-one heading as the first body content, the Prompt's complete sequence as readable steps, every requested outcome, a meaningful order, and each component beside the prose describing what it does. Replacements are told to write the title the Plan needs rather than the one the last draft had. None of that reaches `<CheckDraft>`, `validateDocument()` or TypeScript. It is what the workflow asks for and what a person reviews, which is where a judgement about whether a title describes a Plan belongs. The prose under "Continue from your decision" claimed a final invalid draft left only one choice. It leaves two: ask the coding agent what went wrong, or stop. The governing documents still described the earlier command. They were swept deliberately rather than by replacing words: the opening example is a titled Plan, the flow shows the four destinations, and the failure table, timeout paragraph, acceptance rows, terminology rows, construct inventory, executable-MDX identity section, ACP lifecycle paragraph and root-props admission paragraph no longer say that an approved Plan always runs, that `--save` is the output option, that stopping is called abort, that admission is followed unconditionally by execution, or that every success writes a journal. The evidence follows the same rule. The approved introduction is pinned whole, punctuation included, rather than sampled; the requirement block is asserted against the actual initial, repair and revision turns and counted three times in the shipped source; and the run-only preflight table covers every spelling, including `-V` and `--secret-detection`. The test-local `validateCandidate` is `checkDraft`, so a terminology sweep finds no survivor of the old name. Claude-Session: https://claude.ai/code/session_015HcqB9kJM9KFnMNToAuZF6 * 🐛 Use the approved apostrophe, and refuse --save by name (#260) The introduction claimed to be the approved wording and was one byte away from it: `coding agent's plan` with an ASCII apostrophe where the contract has U+2019. The regression that was supposed to catch that had copied the same byte, so it pinned the file to itself rather than the file to the contract. Both now carry E2 80 99, and the fixture's comment says why it is the byte and not the sentence that is being held. Two contract passages still described the earlier command. The ACP lifecycle said the profile's scope closes before "the save and the execution"; it closes before final admission and before whichever result was asked for. The command spec said `xmd prompt` executes two root documents; it always executes the command document and executes a second only under `--run`. The terminology sweep reached the comments, messages and test names that the earlier passes left behind: "the save" in the deadline and teardown comments, the harness's `--save`, the profile's two directory failures now saying "nothing was output or run", and the review-decision comments and test names that still called stopping an abort. The unrelated abort in `runDocument` is left alone — it is about a document's own failure, not this command's. `--save` had no real proof it was gone. The case that passed it also passed two mutually exclusive permission flags, so the earlier failure decided the outcome and the assertion would have held whether or not the option still worked. That case no longer carries it, and a focused one does — on a command line that is otherwise entirely valid. Making that case honest exposed something worth fixing. `--save out.md` was refused, but as "unrecognized argument: out.md": the scanner passed the unknown option to a parser that stops at the first option it does not define and drops the rest, so what a caller heard about was the value rather than the flag. Fixed preflight now refuses an option this command does not define, by name, and names `--output` when the option was `--save`. Claude-Session: https://claude.ai/code/session_015HcqB9kJM9KFnMNToAuZF6 * 🤖 Say that the second root execution is optional (#260) The module contract opened by claiming `xmd prompt` owns two root document executions, and its flow diagram ended by running the second one unconditionally — one line after the line listing stdout, an `--output` file, a run, or both as the four ways the bytes are delivered. A reader following the diagram would conclude that a default invocation runs the Plan it just printed. Every invocation executes the packaged prompt command document. A second root is executed only under `--run`. The diagram now shows the four deliveries as the alternatives they are, and the paragraph after it puts the complete scope boundary where it belongs: before that optional second execution, so whatever result follows has already let go of the conversation that wrote it. The same claim opened the prompt profile's header and the Tier PR introduction, and three nearby comments still had execution as the thing that follows approval rather than one of the things that might. Corrected to the approved bytes, or to the run that may follow. Prose only: no runtime behaviour, no tests and no evidence changed. Claude-Session: https://claude.ai/code/session_015HcqB9kJM9KFnMNToAuZF6 * 🐛 Refuse a valued --run instead of reading it as the switch (#260) An option's name is read up to its first `=`, so `--run=false` arrived under the name of the switch and was taken for it. That set the flag saying execution had been requested, which is also what satisfies the gate refusing options that only configure a run — so `--run=false --journal trace.jsonl` was accepted. What followed is worth stating precisely, because it is not what the flag being set suggests. The ordinary parser reads `--run=false` as false and `--run=true` as the field's default, which is also false. So the command took the write path either way: `--run=false --journal <path>` completed successfully, created no journal, and never told the caller that the journal they asked for was not a thing this invocation would produce; and `--run=true` was silently answered by a command that wrote the Plan instead of running it. One half of the command believed a run had been requested and the other half did not, and neither told anybody. Every valued spelling — `--run=false`, `--run=true`, `--run=` — now fails fixed preflight, before it establishes that execution was requested and before it can answer for the gate. Nothing is loaded, built, contacted, opened, created or executed. The regressions fail without it: the scanner cases find no error, and the command-level case exits 0 where it now exits 1 — the old behaviour was a success, not an incidental failure, which is what makes the tripwires worth asserting. Claude-Session: https://claude.ai/code/session_015HcqB9kJM9KFnMNToAuZF6 * 🐛 Assert the session and output-file tripwires on the --run= refusal (#260) The refusal has to prove authorship never began, and the two tripwires that say so most directly were implied rather than asserted. A provider count of zero already rules out an established session, but the claim worth making is the narrow one: no session was established, and no directory was made for one to run in. The absence of an output file was likewise folded into an empty working directory rather than stated. Evidence only: no behaviour, no message and no other case changed. Claude-Session: https://claude.ai/code/session_015HcqB9kJM9KFnMNToAuZF6 * 🐛 Pin the valued --run refusal at the parser boundary it crossed (#260) The defect was a disagreement between two layers: the scanner read `--run=false` as the switch while the parser read it as false. The command-level regression for it hand-built the scanned result and called `runPrompt` directly, so it never reached `preparePropsPhase`, the parser or dispatch — the boundary the bug lived on. It would have passed with the two layers still disagreeing. A real invocation now covers it, for `--run=false`, `--run=true` and `--run=`: the exact refusal, empty stdout so no approved Plan escaped, no unavailable-agent error because reaching a provider is what produces one, and an empty working directory holding neither the journal it named nor anything else. Both existing cases stay, because the three prove different things. The scanner table is the grammar. This one is the boundary. The direct one names the phases that stayed at zero, which a subprocess cannot see. Without the refusal this case fails at `Expected: 1 / Received: 0`, and its stderr carries the unavailable-agent error — the command line reached a provider, which is the authorship this refusal exists to prevent. Evidence only: no production change. Claude-Session: https://claude.ai/code/session_015HcqB9kJM9KFnMNToAuZF6 --------- Co-authored-by: Taras Mankovski <74687+taras@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
<Json>refused a valid literalasin its own body. A document that wanted theJSON text rather than its placement — to hand it to
<CodeBlock>, a<File>, ora later
<Prompt>— had to write a wrapper whose only job was to catch the text:Every other text component already takes an optional
as, and the engine alreadyowns it.
<Json>was the one opting out.Closes #666.
What changes
Before:
failed with
<Json> renders JSON text and binds nothing, soasis not accepted.After: it binds the exact string
JSON.stringify(value, null, 2)produced andemits nothing where the element stands. For one run in the same environment it is
exactly the single-child
<Let>wrapper above — and only that wrapper.Whitespace, additional content or
selectinside<Let>describes a differentoperation.
Uncaptured
<Json>is byte-for-byte what it was.How it works
The production delta is the removal of one guard. A function component that
exports no
returnsis already a text component, soexpandComponent'sexisting path binds its returned value when
asis present and renders it onlywhen
asis absent.<Json>gained no prop, noreturnsand no registrationoption — it stopped blocking a path that was already there.
Review guide
Start with:
packages/core/src/components/Json.tsThen review:
specs/executable-mdx-spec.md§6.12 — the capture, its equivalence, and therevised validation order
packages/core/src/components/registry.ts— the one new author-facingstring, the
asfield, and everything around it left alonepackages/core/tests/json-component.test.ts— the newasdescribe block andthe reworked durability document
architecture.md, the catalog tests, and the CLI syntax documentLook carefully at:
as. A serialization that fails must leave thedestination absent, not empty, and must leak no partial JSON.
asand a literal that names no bindingare refused on the authored text, before either expression runs.
What must stay true
JSON.stringify(value, null, 2), called once —enforced by the single native call in
Json.ts, checked byJ1,J6andJ6b(a counting getter proves building the binding takes no second read).valueremains the one raw captured operand — enforced bycaptures: ["value"]on core's registration alone, checked byJ4/J6(identity of the very object) and by
CR22c, where a repositoryJson.mdstill receives
valueacross the ordinary prop boundary.engine reaches the binding, checked by the two
J7/J5bcases.asis the engine's — noasin the props schema, noreturnsexport;checked by
SY24b2's complete row andCR25c.J9, which asserts no eventdescription matches
/json|serial|stringify/i.How to verify it
J3asserts the complete untrimmed rendering'before{\n "ok": true\n}after\n',and fails if removing the guard moved the text or added a newline.
J5b: a valid \as` binds the exact text …compares the binding to the exact native string and asserts the output is exactly"beforeafter\n"`. It fails ifa value or object is bound instead of the string, or if the invocation emits
alongside the binding.
J5b: capturing is the exact <Let> wrapper …runs an object, array, scalar andnullthrough both spellings separately, and fails on wrapper-onlywhitespace or a scalar special case.
J7/J5bcases fail if a failure publishes a binding, if the twodiagnostics collapse, if the original cause becomes unreachable, or if the half
JSON.stringifyhad built escapes.ascases carry tripwires on both expressions and fail ifeither runs.
J9binds<Json>'s text and renders it from a later authored position, solive, partial-replay and completed-root reuse are all proved through the
captured path rather than the legacy emitted one.
CR22binvokes a repositoryJson.mdwithasand proves the binding holdsthe override's visibly different text, not core JSON.
SM12asserts the description and the newassentence in the JSON catalogand the rendered Markdown field. Inverting one word in it fails the suite
with
1 of 12 tests failed, which is how it was checked for vacuity.Scope
Included
<Json>'s component-specificasrefusalasfield in core's registry entry for<Json>. Itsdescriptionalready said what the component does and is unchanged
architecture.mdinventory rowfailures, invalid
as, override capture, inspection and captured replayIntentionally unchanged
JSON.stringify(value, null, 2), formatting, trailing newlines, interpolation,output middleware and failure settlement
valueas the single captured prop; no new prop and noreturnsas,<Let>, component resolution and override precedence — no filein the expansion or binding machinery is touched
components/BootstrapNpmPackage.mdkeeps its<Let>wrapper; the shorterspelling is available, not required
Generated or mechanical changes
None.
Risks and limitations
asto the schema, adding areturnsdeclaration, proving only that a binding exists rather than that itis the exact string, and letting a failure publish one.
SY24b2,J5b,J6band the twoJ7/J5bcases discriminate each of those directly.excluded site work, but two site sentences became false statements:
site/routes/docs/components.tsx("asis refused") andsite/routes/docs/reference.tsx("binds nothing"). Tier JSON rowJ12assertsthat the site documentation and §6.12 state the same contract, so leaving them
would have made the spec's own row untrue at this commit. Each was corrected in
one sentence. Revert those two hunks if the exclusion is read as covering prose
the change falsifies.
compatibility surface involved.
Scope confirmation