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
8 changes: 6 additions & 2 deletions architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -2390,8 +2390,11 @@ and never resolves a component named `Return`.
The first executed `<Return>` claims the state before evaluating its expression,
so two returns racing through one body cannot both believe they were first. The
second fails the body, evaluates nothing, and the first value is not published —
there is no first-wins or last-wins fallback. A body that executes none reports
that instead, which is how a bounded search that found nothing reports itself.
there is no first-wins or last-wins fallback. A body that executes none settles
as a missing selection: the body ran and its flow reached no `<Return>`, which is
a structural outcome and carries nothing about what the author concluded. A
document that deliberately aborts, or that exhausted a bounded search, expresses
that as authored control flow with `<Fail>` and the sentence its author wrote.

**Selection is not a decision a document makes about itself.** A document's own
eval block reaches further than it looks: a named context by name, because
Expand Down Expand Up @@ -3539,6 +3542,7 @@ Status is measured against main.
| `xmd syntax` | describes every structural construct and every selected component the production `run` profile would let a document write in the contextual working directory, as deterministic Markdown or as version-1 JSON, from one catalog. Inspection only: it registers the run profile's declarations in a bounded scope and reads the filesystem for which files exist and, for a selected Markdown component, that file's frontmatter. It runs no body, imports no repository TypeScript module, installs no provider, mints no authority and writes no journal. An include it cannot enumerate — a selection-relevant symbolic link to a directory beneath it included — fails the whole request rather than printing a healthy subset | built on the #632 stack |
| document validation | validates one supplied root projection and the recursive Markdown source closure normal component selection discovers, returning deterministic version-1 document diagnostics and `valid`, `invalid` or `not-statically-checkable` invocation outcomes without evaluating document code or installing operational host behavior | built on the #654 stack |
| `<PrintErrors>` / `printErrors(fn)` | prints failures | built on 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 |
Expand Down
107 changes: 107 additions & 0 deletions packages/core/src/components/Fail.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/**
* `<Fail>` — stop authored work with a message the reader can act on
* (specs/executable-mdx-spec.md §6.8.2).
*
* ```md
* <Fail message="Review aborted; nothing was saved or run." />
* ```
*
* A document that has decided it cannot continue has something to say about
* why, and until now the only way to end a value body was to reach its end
* without a `<Return>` — which reports that the body selected no value, not
* what the author concluded. This raises the author's own sentence instead, at
* the position the author wrote it.
*
* ## An ordinary failure, deliberately
*
* There is no new error mode here and no root special case. This is the
* ordinary failure of a function component (§6.8.1): the engine dismantles the
* invocation, names it, positions it at the opening tag, and propagates it. The
* message is the author's exact string — nothing is prefixed, classified or
* interpolated into it, because the author already wrote the sentence they
* wanted the reader to see.
*
* It carries no `printErrors()` declaration, and that omission is the whole of
* its recovery policy. A text root therefore stops here by default, and an
* author who wants the document to continue says so with `<PrintErrors>` around
* the region — the same way they would for any other component that fails. A
* value body installs `throw`, which a printing boundary does not replace, so
* an enclosing `<PrintErrors>` cannot turn a deliberate abort into a successful
* result; the authored failure settles the body before missing-`<Return>`
* settlement can report anything about it.
*
* ## What it refuses, and why it refuses it first
*
* The one form is self-closing, declared here and dispatched by canonical core,
* so a paired spelling never expands its children: an author who wrote a body
* meant something this component does not do. The closed schema takes one
* required non-empty `message`, so a missing, empty, non-string or unknown prop
* is refused by the ordinary props boundary before anything runs.
*
* `as` is the one refusal this body makes for itself, because `as` is valid for
* a text component by default. There is no value to capture: this renders
* nothing and returns nothing, it raises. Asking {@link hasBinding} before
* raising is what makes a mistaken capture a report about the capture rather
* than the authored message arriving under a name that would never be bound.
*
* ## What it owns
*
* Nothing. No authority, no context, no provider, no resource, and no durable
* operation of its own — the only journal activity around it is what ordinary
* execution already owns, importing the selected component and recording the
* root's outcome. Replay of a completed root restores that outcome without
* re-expanding the root.
*/

import type { Operation } from "effection";
import { hasBinding } from "../component-api.ts";
import type { FormDeclaration, InvocationForm } from "../invocation-identity.ts";
import type { Json } from "../types.ts";

export const props = {
type: "object",
properties: {
message: { type: "string", minLength: 1 },
},
required: ["message"],
additionalProperties: false,
};

/** An invocation `<Fail>` will not raise from. */
export class FailInvocationError extends Error {
constructor(message: string, options?: ErrorOptions) {
super(message, options);
this.name = "FailInvocationError";
}
}

const BINDING = "<Fail> raises its message and binds nothing, so `as` is not accepted.";

const PAIRED = '<Fail> is self-closing and has no content: write <Fail message="…" /> instead.';

function* Fail(props: Record<string, Json>): Operation<string> {
if (yield* hasBinding()) {
throw new FailInvocationError(BINDING);
}
throw new Error(String(props.message));
}

/**
* The one form this component runs, and what it says about the other.
*
* A refusal is a `FailInvocationError` about the invocation, never the authored
* message: an invocation this component would not run raised nothing, and
* reporting the author's sentence for it would say the document had decided
* something it never reached.
*/
export const form: FormDeclaration = {
forms: "self-closing",
fn: Fail,
refuse: (_props: Record<string, Json>, written: InvocationForm | undefined) =>
new FailInvocationError(
written === "paired"
? PAIRED
: "<Fail> was called without the invocation the engine issued, so which form it was " +
"written as cannot be established.",
),
};
9 changes: 9 additions & 0 deletions packages/core/src/components/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import type { ComponentRegistry, RegistryEntry } from "../types.ts";
import { form as codeBlockForm, props as codeBlockProps } from "./CodeBlock.ts";
import Elicit, { props as elicitProps, returns as elicitReturns } from "./Elicit.ts";
import TempDir, { props as tempDirProps } from "./TempDir.ts";
import { form as failForm, props as failProps } from "./Fail.ts";
import Fetch, { props as fetchProps } from "./Fetch.ts";
import { form as fileForm, props as fileProps } from "./File.ts";
import { form as fileDeleteForm, props as fileDeleteProps } from "./FileDelete.ts";
Expand Down Expand Up @@ -131,6 +132,14 @@ export const CORE_REGISTRY: ComponentRegistry = new Map<string, RegistryEntry>([
as: "Optional. Captures the rendered directory path instead of emitting it.",
context: "Markdown expanded with the temporary directory as its working directory.",
}),
core("Fail", failForm, parseJsonObject(failProps), {
description:
"Stop authored work with an actionable failure. " +
'`<Fail message="No acceptable candidate was approved." />` raises the message where it ' +
"is written.",
as: null,
context: null,
}),
core("Fetch", Fetch, parseJsonObject(fetchProps), {
description:
'Fetch a URL. `<Fetch url="https://example.com/status" as="response" />` binds the ' +
Expand Down
2 changes: 2 additions & 0 deletions packages/core/tests/component-registration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,7 @@ describe("Tier CR — resolution order", () => {
const dir = yield* useFixture();
for (const name of [
"CodeBlock",
"Fail",
"Fetch",
"File",
"Glob",
Expand All @@ -388,6 +389,7 @@ describe("Tier CR — resolution order", () => {
const dir = yield* useFixture();
for (const name of [
"CodeBlock",
"Fail",
"Fetch",
"File",
"Glob",
Expand Down
Loading
Loading