diff --git a/architecture.md b/architecture.md index e81761b9..3ee295f3 100644 --- a/architecture.md +++ b/architecture.md @@ -3545,7 +3545,7 @@ Status is measured against main. | `` | stops authored work with the sentence its author wrote, raised where it is written. An ordinary overridable core default — never structural, never reserved, so a repository `Fail.md` is chosen ahead of it — with a closed schema of one required non-empty `message` and **self-closing only**: a paired spelling never enters its body, and `as` is refused by the body itself because there is nothing to bind. Every refusal reports the invocation and happens before the authored message, so a document that never reached its decision is never reported as having made one. A valid invocation is the ordinary failure of a function component: an `Error` carrying the exact authored message, positioned at the opening tag, rendering nothing and binding nothing. It carries no `printErrors()` declaration, which is what leaves recovery to an authored `` region under ordinary text-root modes; a value body's `throw` is not replaced there, so the authored failure settles the body ahead of missing-`` settlement. No authority, context, provider, resource, module state or durable operation of its own: replay of a completed root restores the recorded outcome without re-expanding the body | built on the #659 stack | | `` | binds one name in the current environment from exactly one source: the content it renders, or the exact value `value` names, bound by reference and never through the JSON boundary component props cross — the scanner resolves no JSON for that one prop, and expansion projects none. Which source it has is read from what the author wrote, before either one runs, so a construct naming both expands no child and evaluates no expression. It opens no scope, owns no resource, adds no middleware boundary and writes no journal record — replay reconstructs both sources through ordinary expansion | built on the #527 stack | | `` | shows any supplied string inside one fenced Markdown code block, choosing a fence one backtick longer than the value's longest backtick run and never shorter than three, so the value cannot close it. An ordinary overridable text default: `value` and the optional single-token `language` are ordinary props on the closed schema, so a repository `CodeBlock.md` receives them the way it receives any other prop. Self-closing only, declared to canonical dispatch rather than decided in the body, so a paired spelling is refused before the body runs and its content never expands; prop validation refuses a missing or non-string `value`, a refused `language` and any unknown prop ahead of both. The value is returned unchanged — nothing trimmed, normalized, escaped or removed — and the framing line feeds belong to the envelope, with no line feed after the closing fence. Exactness is a promise at the function-component return and the `as` capture boundary; the `DocumentOutput` middleware contract governs emitted output after it. It owns no scope, resource, authority or durable effect of its own, so partial and completed replay are the ordinary ones | built on the #658 stack | -| `` | renders one supplied value as JSON text where the element was written, from one native two-space `JSON.stringify` call. An ordinary overridable core default whose operand is a capture: the exact evaluation result arrives by reference and is never mutated, cloned, replaced or frozen. It binds nothing, and `as`, content and a missing `value` are all refused before the operand evaluates. A value with no JSON text and a serialization that threw are distinct failures, each positioned at the invocation, emitting no partial output and preserving the original error as its cause. No scope, resource, authority or JSON-specific durable effect: replay reaches it through ordinary expansion, and a surrounding `` or `` keeps its own record of the text it consumed | built on the #452 stack | +| `` | renders one supplied value as JSON text where the element was written, from one native two-space `JSON.stringify` call. An ordinary overridable core default whose operand is a capture: the exact evaluation result arrives by reference and is never mutated, cloned, replaced or frozen. Optional `as` is the engine's ordinary text capture rather than anything of this component's: it binds that exact string and the invocation emits nothing, which is what the single-child `` wrapper did. An expression-valued `as`, a literal `as` that names no binding, content, a missing `value` and an unknown prop are all refused before the operand evaluates. A value with no JSON text and a serialization that threw are distinct failures, each positioned at the invocation, emitting no partial output, binding nothing, and preserving the original error as its cause. No scope, resource, authority or JSON-specific durable effect: replay reaches it through ordinary expansion, and a surrounding `` or `` keeps its own record of the text it consumed | built on the #452 stack, with ordinary text capture enabled by #666 | | root failure settlement | a text root installs the fail-capable `output` mode for its whole body, so an uncaught, undecided failure is the document execution's own outcome, whether or not the root declares `` | built on the #453 stack | | `` region `output` mode | an undecided error fails the document execution | built on main | | `` rendering selection | chooses which regions of a body render, and buffers a root that declares one; it decides nothing about failure | built on main | diff --git a/packages/cli/tests/document-suites/syntax/Syntax.test.md b/packages/cli/tests/document-suites/syntax/Syntax.test.md index febd894e..6d9eccf6 100644 --- a/packages/cli/tests/document-suites/syntax/Syntax.test.md +++ b/packages/cli/tests/document-suites/syntax/Syntax.test.md @@ -42,7 +42,9 @@ rows read. category.kind)} /> - entry.name)} /> + + entry.name)} /> + entry.name === "Json")} /> entry.name)} /> entry.name === "Documented")} /> @@ -127,6 +129,13 @@ rows read. + + + + + + + diff --git a/packages/core/src/components/Json.ts b/packages/core/src/components/Json.ts index 5ac87851..402e3106 100644 --- a/packages/core/src/components/Json.ts +++ b/packages/core/src/components/Json.ts @@ -4,9 +4,9 @@ * * `` introduces a value, this renders it, and `` turns text back * into a validated value. The whole transformation is one supplied value to one - * piece of JSON text at the position the element was written, so there is no - * option to choose: no `indent`, no replacer, no sorting, and no trailing - * newline. A file that needs one is written with one. + * piece of JSON text — written where the element is, or captured by `as` — so + * there is no option to choose: no `indent`, no replacer, no sorting, and no + * trailing newline. A file that needs one is written with one. * * The operand is a **capture**, so the exact evaluation result arrives by * reference (§6.5). An ordinary prop would cross the component JSON boundary @@ -16,10 +16,13 @@ * * ## Shape is decided before the operand runs * - * `as`, content, and a missing `value` are refused before `capture()`, because - * an operand expression can call a getter, a function, or anything else the - * author wrote. A malformed invocation therefore costs nothing, in the same way - * ``'s path arithmetic runs before its children do (§6.13). + * The engine validates `as` first and either refuses it or strips it, so a + * malformed binding name never reaches here. What is left is checked in the + * same spirit: content, then the syntactic presence of `value`, and only then + * the captured operand and its serialization. An operand expression can call a + * getter, a function, or anything else the author wrote, so a malformed + * invocation costs nothing — the same way ``'s path arithmetic runs + * before its children do (§6.13). * * `capture()` stays outside the `try` below. An expression that throws before it * produces a value is that invocation's ordinary captured-expression failure, @@ -46,7 +49,7 @@ */ import type { Operation } from "effection"; -import { capture, hasBinding, hasCapture, hasContent } from "../component-api.ts"; +import { capture, hasCapture, hasContent } from "../component-api.ts"; import { printErrors } from "../component-failures.ts"; export const props = { @@ -63,16 +66,12 @@ export class JsonRenderError extends Error { } } -const BINDING = " renders JSON text and binds nothing, so `as` is not accepted."; const CONTENT = " renders the value it is given, not content: write ."; const MISSING = " requires a `value` prop: write ."; const NO_TEXT = " serialization produced no JSON text for this value."; const THREW = " serialization of this value failed."; export default printErrors(function* Json(): Operation { - if (yield* hasBinding()) { - throw new JsonRenderError(BINDING); - } if (yield* hasContent()) { throw new JsonRenderError(CONTENT); } diff --git a/packages/core/src/components/registry.ts b/packages/core/src/components/registry.ts index ae8b0551..a9fa2be5 100644 --- a/packages/core/src/components/registry.ts +++ b/packages/core/src/components/registry.ts @@ -195,7 +195,7 @@ export const CORE_REGISTRY: ComponentRegistry = new Map([ description: "Render a value as JSON text. `` writes the JSON where you " + "put it.", - as: null, + as: "Optional. Captures the JSON text instead of emitting it.", context: null, }, { captures: ["value"], forms: ["self-closing"] }, diff --git a/packages/core/tests/component-registration.test.ts b/packages/core/tests/component-registration.test.ts index 2c096e7d..5de61f19 100644 --- a/packages/core/tests/component-registration.test.ts +++ b/packages/core/tests/component-registration.test.ts @@ -502,9 +502,14 @@ describe("Tier CR — what a document gets", () => { expect(String(yield* run(dir))).toContain("MINE"); }); - it("CR22b: a repository Json.md replaces core's serializer, end to end", function* () { + it("CR22b: a repository Json.md replaces core's serializer, `as` and all", function* () { const dir = yield* useFixture(); - yield* writeTextFile(join(dir, "doc.md"), "\n"); + // `as` is the engine's, so the override takes it the way every text + // component does: what it rendered is captured and read back later. + yield* writeTextFile( + join(dir, "doc.md"), + '\n\nCAPTURED:{captured}\n', + ); // The override declares `value` as an ordinary prop: `captures` belongs to // the registration core made, and a repository file makes none. yield* writeTextFile( @@ -512,10 +517,13 @@ describe("Tier CR — what a document gets", () => { ["---", "props:", " value: { type: object }", "---", "", "MINE"].join("\n"), ); - // Core's own would have rendered the object as JSON text; the repository - // file renders a word instead, so the output says which one ran. + // Core's own would have bound the object as JSON text; the repository file + // renders a word instead, so the binding says which one ran. + // The override's own rendering keeps the line structure a Markdown + // component produces, so the marker and the word are matched together + // rather than spelled as one literal. const rendered = String(yield* run(dir)); - expect(rendered).toContain("MINE"); + expect(rendered).toMatch(/CAPTURED:\s*MINE/); expect(rendered).not.toContain("serialized"); }); diff --git a/packages/core/tests/json-component.test.ts b/packages/core/tests/json-component.test.ts index a17c5857..7fb41d52 100644 --- a/packages/core/tests/json-component.test.ts +++ b/packages/core/tests/json-component.test.ts @@ -205,7 +205,9 @@ describe("Tier JSON — what the text says", () => { it("J3: the text lands in place, with nothing added around it", function* () { const result = yield* run("beforeafter\n", { source: { ok: true } }); - expect(result.output).toContain('before{\n "ok": true\n}after'); + // The complete rendering, untrimmed: a newline this component added would + // be invisible to an assertion that trimmed the ends first. + expect(result.output).toBe('before{\n "ok": true\n}after\n'); }); it("J4: ordinary interpolation still coerces rather than serializing", function* () { @@ -277,6 +279,71 @@ describe("Tier JSON — the operand arrives live", () => { }); }); +describe("Tier JSON — `as` captures the text instead of emitting it", () => { + it("J5b: a valid `as` binds the exact text and writes nothing where it stands", function* () { + const source = { name: "widget", version: 2 }; + const values: Record = { source }; + const result = yield* run('beforeafter\n', values); + + expect(result.observed).toEqual([]); + // The string itself, not a value, an object, or a rewritten one. + expect(values.captured).toBe(JSON.stringify(source, null, 2)); + // The authored sentinels close up, so the invocation emitted nothing at + // all — a duplicate emission alongside the binding would separate them. + expect(result.output).toBe("beforeafter\n"); + }); + + it("J5b: capturing is the exact wrapper, for every kind of value", function* () { + const cases: ReadonlyArray = [ + ["object", { name: "widget", nested: { depth: 1 } }], + ["array", [1, ["two"], { three: true }]], + ["scalar", "widget"], + ["nothing", null], + ]; + for (const [name, value] of cases) { + // Separate runs against equivalent environments: one document says it + // with `as`, the other with the wrapper it replaces. + const direct: Record = { [name]: value }; + const directRun = yield* run(`\n`, direct); + const wrapped: Record = { [name]: value }; + const wrappedRun = yield* run(`\n`, wrapped); + + expect(directRun.observed).toEqual([]); + expect(wrappedRun.observed).toEqual([]); + expect(direct.captured).toBe(JSON.stringify(value, null, 2)); + expect(wrapped.captured).toBe(direct.captured); + expect(directRun.output.trim()).toBe(""); + expect(wrappedRun.output.trim()).toBe(""); + } + }); + + it("J6b: a captured invocation still evaluates once and serializes once", function* () { + const reads: unknown[] = []; + const marker = { + get id() { + reads.push(this); + return 7; + }, + }; + let evaluated = 0; + const values: Record = { + give: () => { + evaluated += 1; + return marker; + }, + }; + const result = yield* run('\n', values); + + expect(result.observed).toEqual([]); + expect(values.captured).toBe('{\n "id": 7\n}'); + expect(evaluated).toBe(1); + // Read once, from the very object the document named: a projection taken + // to build the binding would show up as a second entry here. + expect(reads).toEqual([marker]); + expect(result.output.trim()).toBe(""); + }); +}); + describe("Tier JSON — the invocation shape is decided first", () => { /** A `value` expression that fails the test if anything evaluates it. */ function tripwire(): { values: Record; evaluated: () => number } { @@ -292,16 +359,36 @@ describe("Tier JSON — the invocation shape is decided first", () => { }; } - it("J5: a literal `as` is refused, binds nothing, and never evaluates `value`", function* () { + it("J5: an expression-valued `as` is refused before either expression runs", function* () { const wire = tripwire(); - const env = { ...wire.values }; - const result = yield* run('\n', env); + let named = 0; + const env: Record = { + ...wire.values, + name: () => { + named += 1; + return "captured"; + }, + }; + const result = yield* run("\n", env); - expect(reported(result)).toContain("binds nothing"); + expect(reported(result)).toContain("must be a string literal"); + // Neither expression ran: `as` names a binding, so it is refused on the + // authored text, and the operand belongs to an invocation that never was. + expect(named).toBe(0); expect(wire.evaluated()).toBe(0); expect("captured" in env).toBe(false); }); + it("J5: an `as` that names no binding is refused before `value` evaluates", function* () { + const wire = tripwire(); + const env = { ...wire.values }; + const result = yield* run('\n', env); + + expect(reported(result)).toContain("must be a valid JavaScript identifier"); + expect(wire.evaluated()).toBe(0); + expect("not a name" in env).toBe(false); + }); + it("J5: paired content is refused before `value` evaluates", function* () { const wire = tripwire(); const result = yield* run("text\n", wire.values); @@ -325,10 +412,13 @@ describe("Tier JSON — the invocation shape is decided first", () => { }); it("J5: an unknown prop is still the engine's own refusal", function* () { - const result = yield* run('\n'); + const wire = tripwire(); + const result = yield* run('\n', wire.values); expect(reported(result)).toContain("Prop validation failed for "); expect(reported(result)).toContain("indent"); + // The closed schema answers before the operand is asked for. + expect(wire.evaluated()).toBe(0); }); }); @@ -430,6 +520,41 @@ describe("Tier JSON — the two ways serialization fails", () => { expect(reaches(result.offered[0]!.error, boom)).toBe(true); }); + it("J7/J5b: a captured no-text failure binds nothing and emits no partial JSON", function* () { + const values: Record = { nothing: undefined }; + const result = yield* run('beforeafter\n', values); + + expect(reported(result)).toContain(NO_TEXT); + expect(reported(result)).not.toContain(THREW); + // Atomic: the destination is absent rather than holding an empty string, + // and the authored bytes on either side are all that reached the document. + expect("captured" in values).toBe(false); + expect(result.output).toContain("before"); + expect(result.output).toContain("after"); + expect(result.output).not.toContain("{"); + }); + + it("J7/J5b: a captured throwing getter keeps its cause and leaks no prefix", function* () { + const boom = new Error("the getter refused"); + const value = { + visible: "VISIBLE", + get broken(): never { + throw boom; + }, + }; + const values: Record = { value }; + const result = yield* run('\n', values); + + expect(reported(result)).toContain(THREW); + expect(reported(result)).not.toContain(NO_TEXT); + expect("captured" in values).toBe(false); + // The half `JSON.stringify` had built reaches neither the binding nor the + // document. + expect(result.output).not.toContain("VISIBLE"); + expect(result.offered.length).toBe(1); + expect(reaches(result.offered[0]!.error, boom)).toBe(true); + }); + it("J5/J7: an operand expression that throws stays a captured-expression failure", function* () { const boom = new Error("the expression refused"); const result = yield* run("\n", { @@ -457,9 +582,9 @@ function useFixture(): Operation { describe("Tier JSON — durability", () => { /** - * A registered source for the hostile value, because `` binds nothing - * and a document cannot write a counting hook without an eval block. Its - * return binds by reference, so what reaches `` is this exact object. + * A registered source for the hostile value, because a document cannot write + * a counting hook without an eval block. Its return binds by reference, so + * what reaches `` is this exact object. */ function source(value: unknown) { return { @@ -502,7 +627,13 @@ describe("Tier JSON — durability", () => { it("J9: no JSON effect is journaled, partial replay re-serializes, completed replay does not", function* () { const dir = yield* useFixture(); - yield* writeTextFile(dir + "/doc.md", '\n\n\n'); + // The captured path is what replay has to reach: the text is bound here and + // read back from a later authored position, so a run that skipped the + // component would render nothing rather than stale JSON. + yield* writeTextFile( + dir + "/doc.md", + '\n\n\n\n{text}\n', + ); let hooks = 0; const value = { diff --git a/packages/core/tests/syntax-catalog.test.ts b/packages/core/tests/syntax-catalog.test.ts index e2db7c26..cf02f02e 100644 --- a/packages/core/tests/syntax-catalog.test.ts +++ b/packages/core/tests/syntax-catalog.test.ts @@ -909,6 +909,37 @@ describe("Tier SY: complete component contracts", () => { expect(entry.context).toBe(undefined); }); + it("SY24b2: reports 's whole contract, optional `as` and all", function* () { + const catalog = yield* catalogFor({}, []); + const entry = find(builtIn(catalog), "Json"); + + // The whole row: what an author reads about `` and what a repository + // override is measured against are the same fields, so a drift in either + // is a failure here. + expect(entry).toEqual({ + kind: "component", + name: "Json", + origin: { kind: "registered", origin: "@executablemd/core", reserved: false }, + sourceKind: "registered", + inspectability: "complete", + forms: ["self-closing"], + // Closed and empty: `value` is a capture, and a schema cannot describe a + // value it never sees. + props: { type: "object", properties: {}, additionalProperties: false }, + captures: ["value"], + // Text, and no declared value return: `as` captures what the component + // rendered rather than a value it validated. + returnMode: "text", + returns: { type: "string" }, + description: + "Render a value as JSON text. `` writes the JSON where you " + + "put it.", + as: "Optional. Captures the JSON text instead of emitting it.", + }); + // Contentless, so there is no body to document. + expect(entry.context).toBe(undefined); + }); + it("SY24c: describes completely enough to copy the invocation", function* () { const catalog = yield* catalogFor({}, []); diff --git a/site/routes/docs/components.tsx b/site/routes/docs/components.tsx index 5d225384..d49f0b04 100644 --- a/site/routes/docs/components.tsx +++ b/site/routes/docs/components.tsx @@ -768,9 +768,10 @@ export default define.page(function Components() {

The formatting is fixed: two spaces per level, keys in the order the object has them, and nothing else to choose. There is no{" "} - indent, pretty{" "} - or replacer option, and it binds nothing — as{" "} - is refused, because this renders text. + indent, pretty or replacer option. Writing + {" "} + as{" "} + captures that text for a later element instead of placing it here.

It also adds{" "} diff --git a/site/routes/docs/reference.tsx b/site/routes/docs/reference.tsx index e1403520..99270eef 100644 --- a/site/routes/docs/reference.tsx +++ b/site/routes/docs/reference.tsx @@ -71,8 +71,8 @@ export default define.page(function Reference() {

  • <Json value={"{...}"} />{" "} - renders one value as two-space JSON text where it is written, and - binds nothing. + renders one value as two-space JSON text where it is written, or binds + that text when as names a binding.
  • <Parse schema={"{...}"} as="name">{" "} diff --git a/specs/executable-mdx-spec.md b/specs/executable-mdx-spec.md index 76395550..fe2cbb79 100644 --- a/specs/executable-mdx-spec.md +++ b/specs/executable-mdx-spec.md @@ -6555,14 +6555,36 @@ Return JSON matching this schema: `` is self-closing and contentless, and takes exactly one public prop. `value` is required, and its presence is syntactic — `value={undefined}` supplies the prop and then fails serialization. There is no `indent`, `pretty`, compact, -replacer, key-order, schema or newline option, and `as` is not accepted: this -renders text and binds nothing. +replacer, key-order, schema or newline option. -**The invocation's shape is decided before its operand runs.** A literal `as`, -any paired invocation — including one whose content is only whitespace — and a -missing `value` are each refused before `value` is evaluated, so a malformed -invocation cannot trigger a getter, a function call, or any other work the -operand expression would do. +**`as` is optional, and it is the engine's, not this component's.** A document +that wants the text rather than its placement writes the binding on the +invocation: + +```md + + +``` + +The exact string `` produced is bound, and nothing is written where the +element stands. That first line is the ordinary text capture every text +component has, so it does for one run in the same environment exactly what the +single-child wrapper does: + +```md + +``` + +The equivalence is to that exact wrapper alone. Whitespace, additional content +or `select` inside `` describes a different operation. + +**The invocation's shape is decided before its operand runs.** `as={expression}` +and a literal `as` that names no binding are refused on the authored text, with +neither the `as` expression nor the operand expression evaluated. After that, +any paired invocation — including one whose content is only whitespace — a +missing `value`, and an unknown ordinary prop are each refused before `value` is +evaluated, so a malformed invocation cannot trigger a getter, a function call, +or any other work the operand expression would do. **The operand arrives by reference.** `value` is a capture (§5.3, §6.5): the engine does not resolve it, so it meets neither the prop JSON round trip nor the @@ -6589,7 +6611,9 @@ of course change its own state, because running them is what `JSON.stringify` does. **The output lands where the element was written**, as one function-component -result, so nothing is added around it and no partial prefix can escape. +result, so nothing is added around it and no partial prefix can escape. With +`as`, that same one result becomes the binding instead, and the invocation site +emits nothing at all. **Two failures, and they are different facts.** A result that is not a string means the value has no JSON text at all — root `undefined`, a function, a symbol @@ -6599,15 +6623,17 @@ means the value has no JSON text at all — root `undefined`, a function, a symb which of the two happened. The original thrown value is preserved as the cause through the normal component failure chain (§6.8.1); neither the error nor the rejected value is interpolated into the message, because doing so could invoke a -hostile accessor a second time. - -`` creates no binding, owns no scope or resource, and has no durable -effect or journal record of its own — resolving the component is the ordinary -`import_component` every component resolution produces. A live run or a partial -replay reaches it through normal expansion and serializes the value that -execution reconstructed; completed-root terminal reuse is unchanged. A -surrounding `` or `` keeps its own request or write record, -including the JSON text it consumed; `` neither duplicates nor redacts it. +hostile accessor a second time. Either failure is atomic under `as`: the +destination stays absent, and no partial JSON reaches the document. + +`` owns no scope or resource, and has no durable effect or journal record +of its own — resolving the component is the ordinary `import_component` every +component resolution produces, and an optional `as` is the engine's own capture +rather than a record of this component's. A live run or a partial replay reaches +it through normal expansion and serializes the value that execution +reconstructed; completed-root terminal reuse is unchanged. A surrounding +`` or `` keeps its own request or write record, including the JSON +text it consumed; `` neither duplicates nor redacts it. #### Parsing text: `` and `` @@ -9582,12 +9608,14 @@ visible warning blocks, gather into a separate error report). | J2 | Native container rules | One representative object and array show nested `undefined`, function, symbol and non-finite handling as `JSON.stringify` defines it | | J3 | Position | Adjacent authored bytes survive on both sides, and no newline is added | | J4 | Raw operand | The captured expression's exact result arrives by identity; the source object and array keep their contents, extensibility and frozen state; ordinary interpolation is unchanged | -| J5 | Shape first | `as`, paired content including whitespace, and a missing `value` each refuse before the operand evaluates, and no binding is created | -| J6 | Once | One expression evaluation and one `JSON.stringify` call; a `toJSON` hook runs exactly once | +| J5 | Shape first | An expression-valued `as`, a literal `as` naming no binding, paired content including whitespace, a missing `value` and an unknown prop each refuse before the operand evaluates, and none creates a binding | +| J5b | Capture | A valid `as` binds the exact JSON string and emits nothing between the authored bytes around it; an object, array, scalar and `null` bind identically through the direct spelling and through the single-child `` wrapper, run separately | +| J6 | Once | One expression evaluation and one `JSON.stringify` call, captured or not; a `toJSON` hook runs exactly once, and building the binding takes no second read | | J7 | Two failures | Root `undefined`/function/symbol fail as no JSON text; `bigint` and a cycle fail as serialization that threw; a throwing getter or `toJSON` preserves the exact cause, records the invocation position, and emits no partial JSON | | J7b | The authored `value={undefined}` | Fails as no JSON text and renders no `null`, while an authored `value={null}` still renders `null` | -| J8 | Resolution | `Json.md` overrides core's default; otherwise the name inspects as an ordinary `@executablemd/core` registration with a closed empty schema and the capture list `["value"]` | -| J9 | Durability | No JSON-specific effect is journaled, component resolution stays an ordinary import, partial replay re-serializes the reconstructed value, and completed replay runs neither the component nor its hooks | +| J7c | Captured failures | Both failures stay distinct under `as`, keep the original cause reachable, leave the destination absent, and leak no partial JSON | +| J8 | Resolution | `Json.md` overrides core's default and its own rendered text is what `as` captures; otherwise the name inspects as an ordinary `@executablemd/core` registration with a closed empty schema, the capture list `["value"]`, a text return and the documented optional `as`, in structured inspection and in rendered `xmd syntax` alike | +| J9 | Durability | Capturing adds no JSON-specific effect to the journal, component resolution stays an ordinary import, partial replay re-serializes the reconstructed value into the binding a later position reads, and completed replay runs neither the component nor its hooks | | J10 | Composition | `` sends and retains exactly the rendered JSON, with only the authored bytes around it | | J11 | Bootstrap migration | `components/BootstrapNpmPackage.md` builds its manifest with `` and renders it through ``; the complete written manifest is compared byte for byte, including its trailing newline, which the document authors rather than the component | | J12 | One taught contract | The construct inventory, §6.12, this plan, decision 100, the site component documentation and the reference all state the same `` → `` → `` direction and the same exclusions |