Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -3545,7 +3545,7 @@ Status is measured against main.
| `<Fail message>` | 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 `<PrintErrors>` 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-`<Return>` 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 |
| `<Let as="name">` | 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 |
| `<CodeBlock value={…} />` | 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 |
| `<Json value={…} />` | 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 `<Prompt>` or `<File>` keeps its own record of the text it consumed | built on the #452 stack |
| `<Json value={…} />` | 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 `<Let>` 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 `<Prompt>` or `<File>` 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 `<Output>` | built on the #453 stack |
| `<Output>` region `output` mode | an undecided error fails the document execution | built on main |
| `<Output>` rendering selection | chooses which regions of a body render, and buffers a root that declares one; it decides nothing about failure | built on main |
Expand Down
11 changes: 10 additions & 1 deletion packages/cli/tests/document-suites/syntax/Syntax.test.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ rows read.

<Let as="catalog" value={JSON.parse(json.stdout)} />
<Let as="categoryKinds" value={catalog.categories.map((category) => category.kind)} />
<Let as="builtInNames" value={catalog.categories[1].entries.map((entry) => entry.name)} />
<Let as="builtInEntries" value={catalog.categories[1].entries} />
<Let as="builtInNames" value={builtInEntries.map((entry) => entry.name)} />
<Let as="jsonEntry" value={builtInEntries.find((entry) => entry.name === "Json")} />
<Let as="userEntries" value={catalog.categories[2].entries} />
<Let as="userNames" value={userEntries.map((entry) => entry.name)} />
<Let as="documented" value={userEntries.find((entry) => entry.name === "Documented")} />
Expand Down Expand Up @@ -127,6 +129,13 @@ rows read.
<AssertEquals actual={orderedNames.includes("Opaque")} expected={true} />
</Test>

<Test name="SM12: the built-in <Json> entry publishes its prose in both formats">
<AssertEquals actual={jsonEntry.description} expected="Render a value as JSON text. `<Json value={config} />` writes the JSON where you put it." />
<AssertEquals actual={jsonEntry.as} expected="Optional. Captures the JSON text instead of emitting it." />
<AssertStringIncludes actual={markdown.stdout} expected="Render a value as JSON text. `<Json value={config} />` writes the JSON where you put it." />
<AssertStringIncludes actual={markdown.stdout} expected="**`as`:** Optional. Captures the JSON text instead of emitting it." />
</Test>

<Test name="SM11: the built-in category holds the run profile, including <Session>">
<AssertEquals actual={builtInNames.includes("Session")} expected={true} />
<AssertEquals actual={builtInNames.includes("Prompt")} expected={true} />
Expand Down
23 changes: 11 additions & 12 deletions packages/core/src/components/Json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
*
* `<Let>` introduces a value, this renders it, and `<Parse>` 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
Expand All @@ -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
* `<File>`'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 `<File>`'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,
Expand All @@ -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 = {
Expand All @@ -63,16 +66,12 @@ export class JsonRenderError extends Error {
}
}

const BINDING = "<Json> renders JSON text and binds nothing, so `as` is not accepted.";
const CONTENT = "<Json> renders the value it is given, not content: write <Json value={…} />.";
const MISSING = "<Json> requires a `value` prop: write <Json value={…} />.";
const NO_TEXT = "<Json> serialization produced no JSON text for this value.";
const THREW = "<Json> serialization of this value failed.";

export default printErrors(function* Json(): Operation<string> {
if (yield* hasBinding()) {
throw new JsonRenderError(BINDING);
}
if (yield* hasContent()) {
throw new JsonRenderError(CONTENT);
}
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/components/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ export const CORE_REGISTRY: ComponentRegistry = new Map<string, RegistryEntry>([
description:
"Render a value as JSON text. `<Json value={config} />` 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"] },
Expand Down
18 changes: 13 additions & 5 deletions packages/core/tests/component-registration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -502,20 +502,28 @@ 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"), "<Json value={{ serialized: true }} />\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"),
'<Json value={{ serialized: true }} as="captured" />\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(
join(dir, "Json.md"),
["---", "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");
});

Expand Down
Loading
Loading