✨ Stop authored work deliberately with <Fail> - #662
Merged
Conversation
`<Fail message="…" />` is a new ordinary `@executablemd/core` default that raises the author's own sentence where it is written. A repository `Fail.md` overrides it under the existing resolution order. It accepts only the self-closing form and a closed schema of one required non-empty `message`. Paired content, `as`, and every malformed prop are refused before the authored message can be raised, 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 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; a value body's `throw` is not replaced there, so the authored failure settles the body ahead of missing-`<Return>` settlement. The specification and architecture now say that missing `<Return>` is the structural settlement error for a body that selected no value, and that a deliberate abort or a bounded exhaustion is expressed with `<Fail>`.
taras
force-pushed
the
agent/issue-659-fail-component
branch
from
August 30, 2026 15:08
573cdf7 to
3e082d5
Compare
taras
marked this pull request as ready for review
August 30, 2026 15:08
PR #662: ✨ Stop authored work deliberately with7 files, +691 / -10 Scope🟡 701 lines changed. PRs under 400 receive more thorough review. Structural✅ No structural bloat detected. Slop✅ Slop indicators look low. Static AnalysisOxlint: 2 diagnostics across 1 file (2 rules) no-shadow (1): packages/core/src/components/Fail.ts CorrectnessNo extraneous code patterns detected. |
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.
Closes #659.
Why
A document that has decided it cannot go on has something to say about why, and until now it had no way to say it. Ending a value body meant reaching its end without a
<Return>, and the resulting message —The root document declares \returns` but produced no value.` — reports a structural fact about the body's flow, not what the author concluded. A reader of that run learns that nothing was selected, never why.What changes
<Fail>is a new ordinary@executablemd/coredefault that raises the author's own sentence where it is written.Before:
After:
A repository
Fail.mdoverrides it under the existing resolution order, exactly as it would override<Parse>or<Json>.How it works
Nothing here is new machinery. Form dispatch, prop validation, invocation publication, source-position capture and failure propagation are the paths every function component already takes; the component adds a schema, a body and a
FormDeclaration, and the engine does the rest.The one design decision worth naming is an omission: the implementation carries no
printErrors()declaration. That omission is the recovery policy. A text root therefore stops at the failure by default, and an author asks for continuation explicitly by writing<PrintErrors>around the region. A value body installsthrow— the one mode a printing boundary does not replace — so an enclosing<PrintErrors>there cannot turn a deliberate abort into a successful result.Review guide
Start with:
packages/core/src/components/Fail.ts— the whole implementation is 30 lines under its own explanation.Then review:
specs/executable-mdx-spec.md§6.8.2 — the public contract, beside §6.8.1 whose failure path it usespackages/core/src/components/registry.ts— onecore("Fail", failForm, …)entrypackages/core/tests/fail-component.test.ts— Tier FAIL, 16 casesarchitecture.mdedits that recast missing<Return>Look carefully at:
printErrors()inFail.ts. Copying a built-in such as<Json>and keeping its wrapper would make a plain<Fail>silently continue everywhere. FAIL1 and FAIL6 exist to catch exactly that.asis valid for a text component by default, so the body askshasBinding()before it raises. Every refusal is aFailInvocationErrornaming the invocation, never the author's sentence.What must stay true
Fail.mdwins. — enforced by registering an ordinary default and staying out ofRESERVED_STRUCTURAL/STRUCTURAL_DECLARATIONS; checked byCR16b,CR17andFAIL10.throw new Error(String(props.message))with no prefix, classification or interpolation; checked byFAIL1andFAIL8.hasBinding()guard running before the throw; checked byFAIL7a–FAIL7g, each using a distinctive sentinel message and asserting it appears nowhere in the failure, the output, the observed segments or the offered failures.FAIL1(observedis empty, so nothing printed it) andFAIL5(an authored<PrintErrors>prints it once and the later sibling runs).throwerror mode, whichusePrintErrors()deliberately does not replace; checked byFAIL2andFAIL6, which require the authoredErrorto reach the completion by identity andproduced no <Return> valueto appear nowhere.FAIL9.How to verify it
The evidence is written so that absence of output is never the proof. A sibling that must not run is a registered
<Ran>component recording its own mark; a projected child that must not expand is that same component written inside the element.FAIL1proves a plain root ends with the exact authored message and starts nothing after it, and fails if aprintErrors()wrapper were ever added.FAIL2proves a value root settles on the authoredErrorby object identity even with a<Return>written later in the body, and fails if the missing-<Return>diagnostic replaced it.FAIL3proves an unselected<If>reaches nothing — asserted against the journal'simport_componentnames, not against absent output, so a silently-executed-and-swallowed failure could not pass it.FAIL4separates loop exhaustion from the failure chosen after it: the<Loop>records both of its own iterations, then the sibling<Fail>names exhaustion and the work after it never begins.FAIL5proves an authored<PrintErrors>reports it exactly once (counted, not merely present) and the later sibling runs.FAIL6proves<PrintErrors>in a value root does not downgrade it, and fails if a recovered failure let missing-<Return>settlement speak instead.FAIL7a–FAIL7gcover paired content, an empty paired spelling,as, and a missing, empty, non-string or unknown prop. Each asserts the child tripwire never fired and the sentinel message appears nowhere, so a refusal that happened after the body decided to raise would fail.FAIL8pinsname, the exact message, and thepath,offset,lineandcolumnof the opening tag.FAIL9proves the failed root's journal holdsimport_component/__root__,import_component/Ran,import_component/Failandcloseand nothing else, then hands the same stream to a second run: the same failure comes back, no event is appended, and the<Ran>tripwire written before the failure does not record a second run.FAIL10proves a repositoryFail.mdrenders its own content and no failure is offered at all.SY24casserts the complete catalog row with onetoEqual, so the absence ofasandcontextprose is pinned alongside origin,reserved: false, forms, schema, captures, return mode and description. Any drift in what an author reads fromxmd syntaxfails here.Beyond the focused command, the suites that enumerate core defaults were run because a new one changes what they see:
document-validation,generated-xmdandpackages/cli/tests/syntax-cli.test.ts(Tier SX) together giveok | 29 passed (169 steps) | 0 failed.deno task lintexits 0.Rebased onto #658
#658 (
<CodeBlock>) merged as879c4ff6while this PR was being opened, so the branch is rebased onto it. The two features are independent and neither contract changed; the three conflicts were all additive collisions in the same neighbourhood, resolved by keeping both:component-registration.test.tsCR16b/CR17 — one alphabetical name list now carrying bothCodeBlockandFail.syntax-catalog.test.ts— both changes claimed the idSY24b. ✨ Render arbitrary text safely with<CodeBlock>#658's<CodeBlock>row keeps it; the<Fail>row is renumberedSY24c.specs/executable-mdx-spec.md§5.3 — one inventory sentence listing both.registry.tsandarchitecture.mdauto-merged. Bothcore(…)entries and both construct-inventory rows are present.Scope
Included
<Fail>: the component, its form declaration, its registration and its catalog rowFailrows added to the existing selection and override tests<Return>architecture.mdreturn-state paragraph and construct-inventory rowIntentionally unchanged
<Return>messages and their implementation. What changes is their documented meaning — they report a body whose flow selected no value — not their text or their runtime behavior.<Return>semantics. Ownership, single-execution and validation are untouched.printErrors()and<PrintErrors>. No new error mode, structural branch, expansion case, middleware API, public error class or root special case is introduced;<Fail>uses the failure path that already exists.@executablemd/core.New abstractions
FailInvocationErrorexists so an invocation this component would not run reports itself rather than the author's sentence — a document that never reached its decision must never be reported as having made one. It is module-local and not exported frommod.ts, matchingJsonRenderErrorandFileDeleteError.Risks and limitations
printErrors()on this component would silently recover every plain<Fail>in every document.FAIL1andFAIL6are the guard; do not weaken them.scripts/tests/component-form-dispatch.test.tswas not run locally: it refuses without a compileddist/xmd, which is its documented precondition. CI builds for it.Scope confirmation