💥 Fail fast on function-component failures, collect explicitly - #251

Merged
taras merged 1 commit into
mainfrom
feat/fail-fast-components
Jul 31, 2026
Merged

💥 Fail fast on function-component failures, collect explicitly#251
taras merged 1 commit into
mainfrom
feat/fail-fast-components

Conversation

@taras

Copy link
Copy Markdown
Owner

Why

Function-component failures were collected into ErrorSegments by default, with fatality an opt-in carried by an internal abort marker. That is inverted: a component that fails now fails the operation it is part of, like any other Effection work, and continuing is an explicit scope-local choice.

Closes#249.

What changes

Default. An ordinary failure propagates after the invocation has been dismantled — projected content, then the component's own resources, then retained work. An Error propagates by identity with its type and cause; a non-Error becomes an Error carrying the original value as cause; body-plus-teardown propagates as the complete aggregate withInvocation() produced. Later siblings do not run.

Component.handleFailure(failure) is the one contextual seam. The default fails the operation. It is called only for an ordinary invocation failure and only after complete teardown. Durability failures, an already-selected DocumentationError, the content transport, and schema diagnostics are classified before it and never reach it.

Two collection forms, one middleware.collectFailures(fn) marks a component by exact function identity and returns the same object. <CollectFailures> is reserved engine syntax that expands its content in the caller's frame, preserving structured segments. Both install the same terminal middleware, so the nearest boundary handles a failure once.

Collection converts a failure into one diagnostic whose cause is the complete original, and does not change the ambient policy — under documentation a collected failure still stops the document.

How it works

The boundary is installed outside the whole withInvocation() call, and scoped:

scoped:
useFailureCollection() ← still installed during teardown
withInvocation:
definition.fn(props)
ordered teardown ← a failure here is still inside the boundary
catch → classify → handleFailure()

Outside, because middleware a component installs for itself is gone before its own teardown can fail. Scoped, because a component that collects its own failures must not quietly decide that for its siblings.

Removed

abortOrdinaryComponentFailures, abortsOrdinaryComponentFailures, the function-identity abort weak set, componentAbort, its error weak set, the aborted arm of fatalCause, hasOrdinaryFailure with its traversal, the agent fatally() wrappers, and the AgentProvider decoration. Agent components now simply throw; they stay fail-fast because that is the default.

Kept and updated: withInvocation(), InvocationTeardownError, durability precedence, AmbientErrorPolicy/settle()/DocumentationError, Component.raise and exactly-once observation, cause attribution, ContentError/tryContent()/the content transport.

What this changed for existing components

Core's <File>, <Glob>, <Parse>, <SafeParse> and <TempDir> have documented diagnostic-and-continue behavior — 44 tests assert it — so they are marked collectFailures(...). That is the decorator's purpose, and it preserves their behavior exactly rather than changing it in a refactor.

Test fixtures whose point is observing a diagnostic are likewise marked; fixtures asserting engine defaults were updated to the new semantics.

How to verify it

Tier CF (tests/failure-collection.test.ts), 11 cases covering the issue's matrix. Every one distinguishes a failed operation from a completed one containing a diagnostic — output text alone cannot tell those apart:

  • CF1–CF4: unmarked failure by identity with nothing after it; teardown-only; body-plus-teardown aggregate; non-Error normalization with the exact value in cause.
  • CF5–CF7: collectFailures reports once and continues; the marker is identity, not name; it collects a teardown-only failure.
  • CF8–CF11: <CollectFailures> handles a direct child and continues; reaches a nested component's failure and handles it once; does not collect durability; under a throwing policy reports once and still stops.

FC5/FC5b pin both halves end-to-end through execute(): an unmarked repository component fails the execution with nothing rendered, and the same component marked reports and continues.

Gate on Deno 2.9.1: fmt / lint / check / test (272 passed, 0 failed) / check:jsr, pnpm exec tsc --project tsconfig.node.json --noEmit, deno task build && ./dist/xmd test packages/core/src --raw (exit 0, 0 failures), git diff --check.

Scope

Per the issue: no WebForm, no Elicit, no remaining #202 migrations, and the TD8 daemon-liveness defect (#248) is deliberately not fixed here.

#247 stays open and will be rebased on this.

An ordinary failure now fails the operation it is part of, after the
invocation has been dismantled. Continuing is a scope-local choice:
collectFailures(fn) for a component, <CollectFailures> for a region.
Closes#249.

@github-actionsgithub-actionsBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found 5 redundant comments. Inline suggestions to remove them below.

// deno-lint-ignore require-yield
*handleFailure(failure: ComponentFailure): Operation<ErrorSegment> {
// The default is to fail, so a component that goes wrong stops the work it
// was part of rather than quietly becoming a note in the output.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// was part of rather than quietly becoming a note in the output.

/** The invocation itself, and what a failure of it means. */
const invoke = function* (): Operation<Segment[]> {
// Detached and frozen: what a component reads about its call site is a copy,
// so nothing it does can reach the element the parser built.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// so nothing it does can reach the element the parser built.

const outcome = yield* handle.tryProject({ kind: "slot", name: slotName });
// A documentation failure is presented in the public shape, as
// `content()` does, so a component recovering from one sees the
// same thing either way.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// same thing either way.

// teardown together.

// Not the document's failure to render: a journal that no longer describes
// this run, or a policy that has already decided the document fails.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// this run, or a policy that has already decided the document fails.

): FunctionComponentDefinition {
return { kind: "function", name, props: NO_PROPS, fn: body };
// These fixtures exist to be observed failing, so they collect rather than
// stopping the expansion the assertion is about.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// stopping the expansion the assertion is about.

@github-actions

Copy link
Copy Markdown

PR #251: 💥 Fail fast on function-component failures, collect explicitly

22 files, +737 / -463

Scope

🔴 PR has 1200 lines changed. Split into focused PRs.

🟡 1200 lines changed. PRs under 400 receive more thorough review.

🟡 22 files changed. Are all changes related?

Structural

🟡 Type declarations with no consumers: FatalFailure.
SymbolDeclared atRefs in diffWhy flagged
FatalFailurepackages/core/src/errors.ts:1311referenced ≤1× within the added diff (pre-existing usages not counted)

Slop

✅ Slop indicators look low.

Static Analysis

✅ Oxlint found no issues.

Correctness

No extraneous code patterns detected.

@taras
taras merged commit 97d9c35 into mainJul 31, 2026
9 checks passed
@taras
taras deleted the feat/fail-fast-components branch August 27, 2026 01:34
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Make function-component failures fail-fast by default with explicit collection boundaries

1 participant

@taras
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all \u003cpre\u003e\u003ccode\u003e blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks"); } } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); } })(); (function(){ try { var __m = "github.com"; var __re = new RegExp('^' + "github\\.com" + '
Skip to content

💥 Fail fast on function-component failures, collect explicitly - #251

Merged
taras merged 1 commit into
mainfrom
feat/fail-fast-components
Jul 31, 2026
Merged

💥 Fail fast on function-component failures, collect explicitly#251
taras merged 1 commit into
mainfrom
feat/fail-fast-components

Conversation

@taras

Copy link
Copy Markdown
Owner

Why

Function-component failures were collected into ErrorSegments by default, with fatality an opt-in carried by an internal abort marker. That is inverted: a component that fails now fails the operation it is part of, like any other Effection work, and continuing is an explicit scope-local choice.

Closes#249.

What changes

Default. An ordinary failure propagates after the invocation has been dismantled — projected content, then the component's own resources, then retained work. An Error propagates by identity with its type and cause; a non-Error becomes an Error carrying the original value as cause; body-plus-teardown propagates as the complete aggregate withInvocation() produced. Later siblings do not run.

Component.handleFailure(failure) is the one contextual seam. The default fails the operation. It is called only for an ordinary invocation failure and only after complete teardown. Durability failures, an already-selected DocumentationError, the content transport, and schema diagnostics are classified before it and never reach it.

Two collection forms, one middleware.collectFailures(fn) marks a component by exact function identity and returns the same object. <CollectFailures> is reserved engine syntax that expands its content in the caller's frame, preserving structured segments. Both install the same terminal middleware, so the nearest boundary handles a failure once.

Collection converts a failure into one diagnostic whose cause is the complete original, and does not change the ambient policy — under documentation a collected failure still stops the document.

How it works

The boundary is installed outside the whole withInvocation() call, and scoped:

scoped:
useFailureCollection() ← still installed during teardown
withInvocation:
definition.fn(props)
ordered teardown ← a failure here is still inside the boundary
catch → classify → handleFailure()

Outside, because middleware a component installs for itself is gone before its own teardown can fail. Scoped, because a component that collects its own failures must not quietly decide that for its siblings.

Removed

abortOrdinaryComponentFailures, abortsOrdinaryComponentFailures, the function-identity abort weak set, componentAbort, its error weak set, the aborted arm of fatalCause, hasOrdinaryFailure with its traversal, the agent fatally() wrappers, and the AgentProvider decoration. Agent components now simply throw; they stay fail-fast because that is the default.

Kept and updated: withInvocation(), InvocationTeardownError, durability precedence, AmbientErrorPolicy/settle()/DocumentationError, Component.raise and exactly-once observation, cause attribution, ContentError/tryContent()/the content transport.

What this changed for existing components

Core's <File>, <Glob>, <Parse>, <SafeParse> and <TempDir> have documented diagnostic-and-continue behavior — 44 tests assert it — so they are marked collectFailures(...). That is the decorator's purpose, and it preserves their behavior exactly rather than changing it in a refactor.

Test fixtures whose point is observing a diagnostic are likewise marked; fixtures asserting engine defaults were updated to the new semantics.

How to verify it

Tier CF (tests/failure-collection.test.ts), 11 cases covering the issue's matrix. Every one distinguishes a failed operation from a completed one containing a diagnostic — output text alone cannot tell those apart:

  • CF1–CF4: unmarked failure by identity with nothing after it; teardown-only; body-plus-teardown aggregate; non-Error normalization with the exact value in cause.
  • CF5–CF7: collectFailures reports once and continues; the marker is identity, not name; it collects a teardown-only failure.
  • CF8–CF11: <CollectFailures> handles a direct child and continues; reaches a nested component's failure and handles it once; does not collect durability; under a throwing policy reports once and still stops.

FC5/FC5b pin both halves end-to-end through execute(): an unmarked repository component fails the execution with nothing rendered, and the same component marked reports and continues.

Gate on Deno 2.9.1: fmt / lint / check / test (272 passed, 0 failed) / check:jsr, pnpm exec tsc --project tsconfig.node.json --noEmit, deno task build && ./dist/xmd test packages/core/src --raw (exit 0, 0 failures), git diff --check.

Scope

Per the issue: no WebForm, no Elicit, no remaining #202 migrations, and the TD8 daemon-liveness defect (#248) is deliberately not fixed here.

#247 stays open and will be rebased on this.

An ordinary failure now fails the operation it is part of, after the
invocation has been dismantled. Continuing is a scope-local choice:
collectFailures(fn) for a component, <CollectFailures> for a region.
Closes#249.

@github-actionsgithub-actionsBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found 5 redundant comments. Inline suggestions to remove them below.

// deno-lint-ignore require-yield
*handleFailure(failure: ComponentFailure): Operation<ErrorSegment> {
// The default is to fail, so a component that goes wrong stops the work it
// was part of rather than quietly becoming a note in the output.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// was part of rather than quietly becoming a note in the output.

/** The invocation itself, and what a failure of it means. */
const invoke = function* (): Operation<Segment[]> {
// Detached and frozen: what a component reads about its call site is a copy,
// so nothing it does can reach the element the parser built.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// so nothing it does can reach the element the parser built.

const outcome = yield* handle.tryProject({ kind: "slot", name: slotName });
// A documentation failure is presented in the public shape, as
// `content()` does, so a component recovering from one sees the
// same thing either way.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// same thing either way.

// teardown together.

// Not the document's failure to render: a journal that no longer describes
// this run, or a policy that has already decided the document fails.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// this run, or a policy that has already decided the document fails.

): FunctionComponentDefinition {
return { kind: "function", name, props: NO_PROPS, fn: body };
// These fixtures exist to be observed failing, so they collect rather than
// stopping the expansion the assertion is about.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// stopping the expansion the assertion is about.

@github-actions

Copy link
Copy Markdown

PR #251: 💥 Fail fast on function-component failures, collect explicitly

22 files, +737 / -463

Scope

🔴 PR has 1200 lines changed. Split into focused PRs.

🟡 1200 lines changed. PRs under 400 receive more thorough review.

🟡 22 files changed. Are all changes related?

Structural

🟡 Type declarations with no consumers: FatalFailure.
SymbolDeclared atRefs in diffWhy flagged
FatalFailurepackages/core/src/errors.ts:1311referenced ≤1× within the added diff (pre-existing usages not counted)

Slop

✅ Slop indicators look low.

Static Analysis

✅ Oxlint found no issues.

Correctness

No extraneous code patterns detected.

@taras
taras merged commit 97d9c35 into mainJul 31, 2026
9 checks passed
@taras
taras deleted the feat/fail-fast-components branch August 27, 2026 01:34
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Make function-component failures fail-fast by default with explicit collection boundaries

1 participant

@taras
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

💥 Fail fast on function-component failures, collect explicitly - #251

Merged
taras merged 1 commit into
mainfrom
feat/fail-fast-components
Jul 31, 2026
Merged

💥 Fail fast on function-component failures, collect explicitly#251
taras merged 1 commit into
mainfrom
feat/fail-fast-components

Conversation

@taras

Copy link
Copy Markdown
Owner

Why

Function-component failures were collected into ErrorSegments by default, with fatality an opt-in carried by an internal abort marker. That is inverted: a component that fails now fails the operation it is part of, like any other Effection work, and continuing is an explicit scope-local choice.

Closes#249.

What changes

Default. An ordinary failure propagates after the invocation has been dismantled — projected content, then the component's own resources, then retained work. An Error propagates by identity with its type and cause; a non-Error becomes an Error carrying the original value as cause; body-plus-teardown propagates as the complete aggregate withInvocation() produced. Later siblings do not run.

Component.handleFailure(failure) is the one contextual seam. The default fails the operation. It is called only for an ordinary invocation failure and only after complete teardown. Durability failures, an already-selected DocumentationError, the content transport, and schema diagnostics are classified before it and never reach it.

Two collection forms, one middleware.collectFailures(fn) marks a component by exact function identity and returns the same object. <CollectFailures> is reserved engine syntax that expands its content in the caller's frame, preserving structured segments. Both install the same terminal middleware, so the nearest boundary handles a failure once.

Collection converts a failure into one diagnostic whose cause is the complete original, and does not change the ambient policy — under documentation a collected failure still stops the document.

How it works

The boundary is installed outside the whole withInvocation() call, and scoped:

scoped:
useFailureCollection() ← still installed during teardown
withInvocation:
definition.fn(props)
ordered teardown ← a failure here is still inside the boundary
catch → classify → handleFailure()

Outside, because middleware a component installs for itself is gone before its own teardown can fail. Scoped, because a component that collects its own failures must not quietly decide that for its siblings.

Removed

abortOrdinaryComponentFailures, abortsOrdinaryComponentFailures, the function-identity abort weak set, componentAbort, its error weak set, the aborted arm of fatalCause, hasOrdinaryFailure with its traversal, the agent fatally() wrappers, and the AgentProvider decoration. Agent components now simply throw; they stay fail-fast because that is the default.

Kept and updated: withInvocation(), InvocationTeardownError, durability precedence, AmbientErrorPolicy/settle()/DocumentationError, Component.raise and exactly-once observation, cause attribution, ContentError/tryContent()/the content transport.

What this changed for existing components

Core's <File>, <Glob>, <Parse>, <SafeParse> and <TempDir> have documented diagnostic-and-continue behavior — 44 tests assert it — so they are marked collectFailures(...). That is the decorator's purpose, and it preserves their behavior exactly rather than changing it in a refactor.

Test fixtures whose point is observing a diagnostic are likewise marked; fixtures asserting engine defaults were updated to the new semantics.

How to verify it

Tier CF (tests/failure-collection.test.ts), 11 cases covering the issue's matrix. Every one distinguishes a failed operation from a completed one containing a diagnostic — output text alone cannot tell those apart:

  • CF1–CF4: unmarked failure by identity with nothing after it; teardown-only; body-plus-teardown aggregate; non-Error normalization with the exact value in cause.
  • CF5–CF7: collectFailures reports once and continues; the marker is identity, not name; it collects a teardown-only failure.
  • CF8–CF11: <CollectFailures> handles a direct child and continues; reaches a nested component's failure and handles it once; does not collect durability; under a throwing policy reports once and still stops.

FC5/FC5b pin both halves end-to-end through execute(): an unmarked repository component fails the execution with nothing rendered, and the same component marked reports and continues.

Gate on Deno 2.9.1: fmt / lint / check / test (272 passed, 0 failed) / check:jsr, pnpm exec tsc --project tsconfig.node.json --noEmit, deno task build && ./dist/xmd test packages/core/src --raw (exit 0, 0 failures), git diff --check.

Scope

Per the issue: no WebForm, no Elicit, no remaining #202 migrations, and the TD8 daemon-liveness defect (#248) is deliberately not fixed here.

#247 stays open and will be rebased on this.

An ordinary failure now fails the operation it is part of, after the
invocation has been dismantled. Continuing is a scope-local choice:
collectFailures(fn) for a component, <CollectFailures> for a region.
Closes#249.

@github-actionsgithub-actionsBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found 5 redundant comments. Inline suggestions to remove them below.

// deno-lint-ignore require-yield
*handleFailure(failure: ComponentFailure): Operation<ErrorSegment> {
// The default is to fail, so a component that goes wrong stops the work it
// was part of rather than quietly becoming a note in the output.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// was part of rather than quietly becoming a note in the output.

/** The invocation itself, and what a failure of it means. */
const invoke = function* (): Operation<Segment[]> {
// Detached and frozen: what a component reads about its call site is a copy,
// so nothing it does can reach the element the parser built.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// so nothing it does can reach the element the parser built.

const outcome = yield* handle.tryProject({ kind: "slot", name: slotName });
// A documentation failure is presented in the public shape, as
// `content()` does, so a component recovering from one sees the
// same thing either way.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// same thing either way.

// teardown together.

// Not the document's failure to render: a journal that no longer describes
// this run, or a policy that has already decided the document fails.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// this run, or a policy that has already decided the document fails.

): FunctionComponentDefinition {
return { kind: "function", name, props: NO_PROPS, fn: body };
// These fixtures exist to be observed failing, so they collect rather than
// stopping the expansion the assertion is about.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// stopping the expansion the assertion is about.

@github-actions

Copy link
Copy Markdown

PR #251: 💥 Fail fast on function-component failures, collect explicitly

22 files, +737 / -463

Scope

🔴 PR has 1200 lines changed. Split into focused PRs.

🟡 1200 lines changed. PRs under 400 receive more thorough review.

🟡 22 files changed. Are all changes related?

Structural

🟡 Type declarations with no consumers: FatalFailure.
SymbolDeclared atRefs in diffWhy flagged
FatalFailurepackages/core/src/errors.ts:1311referenced ≤1× within the added diff (pre-existing usages not counted)

Slop

✅ Slop indicators look low.

Static Analysis

✅ Oxlint found no issues.

Correctness

No extraneous code patterns detected.

@taras
taras merged commit 97d9c35 into mainJul 31, 2026
9 checks passed
@taras
taras deleted the feat/fail-fast-components branch August 27, 2026 01:34
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Make function-component failures fail-fast by default with explicit collection boundaries

1 participant

@taras
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length \u003e 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

💥 Fail fast on function-component failures, collect explicitly - #251

Merged
taras merged 1 commit into
mainfrom
feat/fail-fast-components
Jul 31, 2026
Merged

💥 Fail fast on function-component failures, collect explicitly#251
taras merged 1 commit into
mainfrom
feat/fail-fast-components

Conversation

@taras

Copy link
Copy Markdown
Owner

Why

Function-component failures were collected into ErrorSegments by default, with fatality an opt-in carried by an internal abort marker. That is inverted: a component that fails now fails the operation it is part of, like any other Effection work, and continuing is an explicit scope-local choice.

Closes#249.

What changes

Default. An ordinary failure propagates after the invocation has been dismantled — projected content, then the component's own resources, then retained work. An Error propagates by identity with its type and cause; a non-Error becomes an Error carrying the original value as cause; body-plus-teardown propagates as the complete aggregate withInvocation() produced. Later siblings do not run.

Component.handleFailure(failure) is the one contextual seam. The default fails the operation. It is called only for an ordinary invocation failure and only after complete teardown. Durability failures, an already-selected DocumentationError, the content transport, and schema diagnostics are classified before it and never reach it.

Two collection forms, one middleware.collectFailures(fn) marks a component by exact function identity and returns the same object. <CollectFailures> is reserved engine syntax that expands its content in the caller's frame, preserving structured segments. Both install the same terminal middleware, so the nearest boundary handles a failure once.

Collection converts a failure into one diagnostic whose cause is the complete original, and does not change the ambient policy — under documentation a collected failure still stops the document.

How it works

The boundary is installed outside the whole withInvocation() call, and scoped:

scoped:
useFailureCollection() ← still installed during teardown
withInvocation:
definition.fn(props)
ordered teardown ← a failure here is still inside the boundary
catch → classify → handleFailure()

Outside, because middleware a component installs for itself is gone before its own teardown can fail. Scoped, because a component that collects its own failures must not quietly decide that for its siblings.

Removed

abortOrdinaryComponentFailures, abortsOrdinaryComponentFailures, the function-identity abort weak set, componentAbort, its error weak set, the aborted arm of fatalCause, hasOrdinaryFailure with its traversal, the agent fatally() wrappers, and the AgentProvider decoration. Agent components now simply throw; they stay fail-fast because that is the default.

Kept and updated: withInvocation(), InvocationTeardownError, durability precedence, AmbientErrorPolicy/settle()/DocumentationError, Component.raise and exactly-once observation, cause attribution, ContentError/tryContent()/the content transport.

What this changed for existing components

Core's <File>, <Glob>, <Parse>, <SafeParse> and <TempDir> have documented diagnostic-and-continue behavior — 44 tests assert it — so they are marked collectFailures(...). That is the decorator's purpose, and it preserves their behavior exactly rather than changing it in a refactor.

Test fixtures whose point is observing a diagnostic are likewise marked; fixtures asserting engine defaults were updated to the new semantics.

How to verify it

Tier CF (tests/failure-collection.test.ts), 11 cases covering the issue's matrix. Every one distinguishes a failed operation from a completed one containing a diagnostic — output text alone cannot tell those apart:

  • CF1–CF4: unmarked failure by identity with nothing after it; teardown-only; body-plus-teardown aggregate; non-Error normalization with the exact value in cause.
  • CF5–CF7: collectFailures reports once and continues; the marker is identity, not name; it collects a teardown-only failure.
  • CF8–CF11: <CollectFailures> handles a direct child and continues; reaches a nested component's failure and handles it once; does not collect durability; under a throwing policy reports once and still stops.

FC5/FC5b pin both halves end-to-end through execute(): an unmarked repository component fails the execution with nothing rendered, and the same component marked reports and continues.

Gate on Deno 2.9.1: fmt / lint / check / test (272 passed, 0 failed) / check:jsr, pnpm exec tsc --project tsconfig.node.json --noEmit, deno task build && ./dist/xmd test packages/core/src --raw (exit 0, 0 failures), git diff --check.

Scope

Per the issue: no WebForm, no Elicit, no remaining #202 migrations, and the TD8 daemon-liveness defect (#248) is deliberately not fixed here.

#247 stays open and will be rebased on this.

An ordinary failure now fails the operation it is part of, after the
invocation has been dismantled. Continuing is a scope-local choice:
collectFailures(fn) for a component, <CollectFailures> for a region.
Closes#249.

@github-actionsgithub-actionsBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found 5 redundant comments. Inline suggestions to remove them below.

// deno-lint-ignore require-yield
*handleFailure(failure: ComponentFailure): Operation<ErrorSegment> {
// The default is to fail, so a component that goes wrong stops the work it
// was part of rather than quietly becoming a note in the output.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// was part of rather than quietly becoming a note in the output.

/** The invocation itself, and what a failure of it means. */
const invoke = function* (): Operation<Segment[]> {
// Detached and frozen: what a component reads about its call site is a copy,
// so nothing it does can reach the element the parser built.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// so nothing it does can reach the element the parser built.

const outcome = yield* handle.tryProject({ kind: "slot", name: slotName });
// A documentation failure is presented in the public shape, as
// `content()` does, so a component recovering from one sees the
// same thing either way.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// same thing either way.

// teardown together.

// Not the document's failure to render: a journal that no longer describes
// this run, or a policy that has already decided the document fails.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// this run, or a policy that has already decided the document fails.

): FunctionComponentDefinition {
return { kind: "function", name, props: NO_PROPS, fn: body };
// These fixtures exist to be observed failing, so they collect rather than
// stopping the expansion the assertion is about.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// stopping the expansion the assertion is about.

@github-actions

Copy link
Copy Markdown

PR #251: 💥 Fail fast on function-component failures, collect explicitly

22 files, +737 / -463

Scope

🔴 PR has 1200 lines changed. Split into focused PRs.

🟡 1200 lines changed. PRs under 400 receive more thorough review.

🟡 22 files changed. Are all changes related?

Structural

🟡 Type declarations with no consumers: FatalFailure.
SymbolDeclared atRefs in diffWhy flagged
FatalFailurepackages/core/src/errors.ts:1311referenced ≤1× within the added diff (pre-existing usages not counted)

Slop

✅ Slop indicators look low.

Static Analysis

✅ Oxlint found no issues.

Correctness

No extraneous code patterns detected.

@taras
taras merged commit 97d9c35 into mainJul 31, 2026
9 checks passed
@taras
taras deleted the feat/fail-fast-components branch August 27, 2026 01:34
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Make function-component failures fail-fast by default with explicit collection boundaries

1 participant

@taras
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

💥 Fail fast on function-component failures, collect explicitly - #251

Merged
taras merged 1 commit into
mainfrom
feat/fail-fast-components
Jul 31, 2026
Merged

💥 Fail fast on function-component failures, collect explicitly#251
taras merged 1 commit into
mainfrom
feat/fail-fast-components

Conversation

@taras

Copy link
Copy Markdown
Owner

Why

Function-component failures were collected into ErrorSegments by default, with fatality an opt-in carried by an internal abort marker. That is inverted: a component that fails now fails the operation it is part of, like any other Effection work, and continuing is an explicit scope-local choice.

Closes#249.

What changes

Default. An ordinary failure propagates after the invocation has been dismantled — projected content, then the component's own resources, then retained work. An Error propagates by identity with its type and cause; a non-Error becomes an Error carrying the original value as cause; body-plus-teardown propagates as the complete aggregate withInvocation() produced. Later siblings do not run.

Component.handleFailure(failure) is the one contextual seam. The default fails the operation. It is called only for an ordinary invocation failure and only after complete teardown. Durability failures, an already-selected DocumentationError, the content transport, and schema diagnostics are classified before it and never reach it.

Two collection forms, one middleware.collectFailures(fn) marks a component by exact function identity and returns the same object. <CollectFailures> is reserved engine syntax that expands its content in the caller's frame, preserving structured segments. Both install the same terminal middleware, so the nearest boundary handles a failure once.

Collection converts a failure into one diagnostic whose cause is the complete original, and does not change the ambient policy — under documentation a collected failure still stops the document.

How it works

The boundary is installed outside the whole withInvocation() call, and scoped:

scoped:
useFailureCollection() ← still installed during teardown
withInvocation:
definition.fn(props)
ordered teardown ← a failure here is still inside the boundary
catch → classify → handleFailure()

Outside, because middleware a component installs for itself is gone before its own teardown can fail. Scoped, because a component that collects its own failures must not quietly decide that for its siblings.

Removed

abortOrdinaryComponentFailures, abortsOrdinaryComponentFailures, the function-identity abort weak set, componentAbort, its error weak set, the aborted arm of fatalCause, hasOrdinaryFailure with its traversal, the agent fatally() wrappers, and the AgentProvider decoration. Agent components now simply throw; they stay fail-fast because that is the default.

Kept and updated: withInvocation(), InvocationTeardownError, durability precedence, AmbientErrorPolicy/settle()/DocumentationError, Component.raise and exactly-once observation, cause attribution, ContentError/tryContent()/the content transport.

What this changed for existing components

Core's <File>, <Glob>, <Parse>, <SafeParse> and <TempDir> have documented diagnostic-and-continue behavior — 44 tests assert it — so they are marked collectFailures(...). That is the decorator's purpose, and it preserves their behavior exactly rather than changing it in a refactor.

Test fixtures whose point is observing a diagnostic are likewise marked; fixtures asserting engine defaults were updated to the new semantics.

How to verify it

Tier CF (tests/failure-collection.test.ts), 11 cases covering the issue's matrix. Every one distinguishes a failed operation from a completed one containing a diagnostic — output text alone cannot tell those apart:

  • CF1–CF4: unmarked failure by identity with nothing after it; teardown-only; body-plus-teardown aggregate; non-Error normalization with the exact value in cause.
  • CF5–CF7: collectFailures reports once and continues; the marker is identity, not name; it collects a teardown-only failure.
  • CF8–CF11: <CollectFailures> handles a direct child and continues; reaches a nested component's failure and handles it once; does not collect durability; under a throwing policy reports once and still stops.

FC5/FC5b pin both halves end-to-end through execute(): an unmarked repository component fails the execution with nothing rendered, and the same component marked reports and continues.

Gate on Deno 2.9.1: fmt / lint / check / test (272 passed, 0 failed) / check:jsr, pnpm exec tsc --project tsconfig.node.json --noEmit, deno task build && ./dist/xmd test packages/core/src --raw (exit 0, 0 failures), git diff --check.

Scope

Per the issue: no WebForm, no Elicit, no remaining #202 migrations, and the TD8 daemon-liveness defect (#248) is deliberately not fixed here.

#247 stays open and will be rebased on this.

An ordinary failure now fails the operation it is part of, after the
invocation has been dismantled. Continuing is a scope-local choice:
collectFailures(fn) for a component, <CollectFailures> for a region.
Closes#249.

@github-actionsgithub-actionsBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found 5 redundant comments. Inline suggestions to remove them below.

// deno-lint-ignore require-yield
*handleFailure(failure: ComponentFailure): Operation<ErrorSegment> {
// The default is to fail, so a component that goes wrong stops the work it
// was part of rather than quietly becoming a note in the output.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// was part of rather than quietly becoming a note in the output.

/** The invocation itself, and what a failure of it means. */
const invoke = function* (): Operation<Segment[]> {
// Detached and frozen: what a component reads about its call site is a copy,
// so nothing it does can reach the element the parser built.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// so nothing it does can reach the element the parser built.

const outcome = yield* handle.tryProject({ kind: "slot", name: slotName });
// A documentation failure is presented in the public shape, as
// `content()` does, so a component recovering from one sees the
// same thing either way.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// same thing either way.

// teardown together.

// Not the document's failure to render: a journal that no longer describes
// this run, or a policy that has already decided the document fails.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// this run, or a policy that has already decided the document fails.

): FunctionComponentDefinition {
return { kind: "function", name, props: NO_PROPS, fn: body };
// These fixtures exist to be observed failing, so they collect rather than
// stopping the expansion the assertion is about.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// stopping the expansion the assertion is about.

@github-actions

Copy link
Copy Markdown

PR #251: 💥 Fail fast on function-component failures, collect explicitly

22 files, +737 / -463

Scope

🔴 PR has 1200 lines changed. Split into focused PRs.

🟡 1200 lines changed. PRs under 400 receive more thorough review.

🟡 22 files changed. Are all changes related?

Structural

🟡 Type declarations with no consumers: FatalFailure.
SymbolDeclared atRefs in diffWhy flagged
FatalFailurepackages/core/src/errors.ts:1311referenced ≤1× within the added diff (pre-existing usages not counted)

Slop

✅ Slop indicators look low.

Static Analysis

✅ Oxlint found no issues.

Correctness

No extraneous code patterns detected.

@taras
taras merged commit 97d9c35 into mainJul 31, 2026
9 checks passed
@taras
taras deleted the feat/fail-fast-components branch August 27, 2026 01:34
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Make function-component failures fail-fast by default with explicit collection boundaries

1 participant

@taras
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

💥 Fail fast on function-component failures, collect explicitly - #251

Merged
taras merged 1 commit into
mainfrom
feat/fail-fast-components
Jul 31, 2026
Merged

💥 Fail fast on function-component failures, collect explicitly#251
taras merged 1 commit into
mainfrom
feat/fail-fast-components

Conversation

@taras

Copy link
Copy Markdown
Owner

Why

Function-component failures were collected into ErrorSegments by default, with fatality an opt-in carried by an internal abort marker. That is inverted: a component that fails now fails the operation it is part of, like any other Effection work, and continuing is an explicit scope-local choice.

Closes#249.

What changes

Default. An ordinary failure propagates after the invocation has been dismantled — projected content, then the component's own resources, then retained work. An Error propagates by identity with its type and cause; a non-Error becomes an Error carrying the original value as cause; body-plus-teardown propagates as the complete aggregate withInvocation() produced. Later siblings do not run.

Component.handleFailure(failure) is the one contextual seam. The default fails the operation. It is called only for an ordinary invocation failure and only after complete teardown. Durability failures, an already-selected DocumentationError, the content transport, and schema diagnostics are classified before it and never reach it.

Two collection forms, one middleware.collectFailures(fn) marks a component by exact function identity and returns the same object. <CollectFailures> is reserved engine syntax that expands its content in the caller's frame, preserving structured segments. Both install the same terminal middleware, so the nearest boundary handles a failure once.

Collection converts a failure into one diagnostic whose cause is the complete original, and does not change the ambient policy — under documentation a collected failure still stops the document.

How it works

The boundary is installed outside the whole withInvocation() call, and scoped:

scoped:
useFailureCollection() ← still installed during teardown
withInvocation:
definition.fn(props)
ordered teardown ← a failure here is still inside the boundary
catch → classify → handleFailure()

Outside, because middleware a component installs for itself is gone before its own teardown can fail. Scoped, because a component that collects its own failures must not quietly decide that for its siblings.

Removed

abortOrdinaryComponentFailures, abortsOrdinaryComponentFailures, the function-identity abort weak set, componentAbort, its error weak set, the aborted arm of fatalCause, hasOrdinaryFailure with its traversal, the agent fatally() wrappers, and the AgentProvider decoration. Agent components now simply throw; they stay fail-fast because that is the default.

Kept and updated: withInvocation(), InvocationTeardownError, durability precedence, AmbientErrorPolicy/settle()/DocumentationError, Component.raise and exactly-once observation, cause attribution, ContentError/tryContent()/the content transport.

What this changed for existing components

Core's <File>, <Glob>, <Parse>, <SafeParse> and <TempDir> have documented diagnostic-and-continue behavior — 44 tests assert it — so they are marked collectFailures(...). That is the decorator's purpose, and it preserves their behavior exactly rather than changing it in a refactor.

Test fixtures whose point is observing a diagnostic are likewise marked; fixtures asserting engine defaults were updated to the new semantics.

How to verify it

Tier CF (tests/failure-collection.test.ts), 11 cases covering the issue's matrix. Every one distinguishes a failed operation from a completed one containing a diagnostic — output text alone cannot tell those apart:

  • CF1–CF4: unmarked failure by identity with nothing after it; teardown-only; body-plus-teardown aggregate; non-Error normalization with the exact value in cause.
  • CF5–CF7: collectFailures reports once and continues; the marker is identity, not name; it collects a teardown-only failure.
  • CF8–CF11: <CollectFailures> handles a direct child and continues; reaches a nested component's failure and handles it once; does not collect durability; under a throwing policy reports once and still stops.

FC5/FC5b pin both halves end-to-end through execute(): an unmarked repository component fails the execution with nothing rendered, and the same component marked reports and continues.

Gate on Deno 2.9.1: fmt / lint / check / test (272 passed, 0 failed) / check:jsr, pnpm exec tsc --project tsconfig.node.json --noEmit, deno task build && ./dist/xmd test packages/core/src --raw (exit 0, 0 failures), git diff --check.

Scope

Per the issue: no WebForm, no Elicit, no remaining #202 migrations, and the TD8 daemon-liveness defect (#248) is deliberately not fixed here.

#247 stays open and will be rebased on this.

An ordinary failure now fails the operation it is part of, after the
invocation has been dismantled. Continuing is a scope-local choice:
collectFailures(fn) for a component, <CollectFailures> for a region.
Closes#249.

@github-actionsgithub-actionsBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found 5 redundant comments. Inline suggestions to remove them below.

// deno-lint-ignore require-yield
*handleFailure(failure: ComponentFailure): Operation<ErrorSegment> {
// The default is to fail, so a component that goes wrong stops the work it
// was part of rather than quietly becoming a note in the output.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// was part of rather than quietly becoming a note in the output.

/** The invocation itself, and what a failure of it means. */
const invoke = function* (): Operation<Segment[]> {
// Detached and frozen: what a component reads about its call site is a copy,
// so nothing it does can reach the element the parser built.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// so nothing it does can reach the element the parser built.

const outcome = yield* handle.tryProject({ kind: "slot", name: slotName });
// A documentation failure is presented in the public shape, as
// `content()` does, so a component recovering from one sees the
// same thing either way.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// same thing either way.

// teardown together.

// Not the document's failure to render: a journal that no longer describes
// this run, or a policy that has already decided the document fails.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// this run, or a policy that has already decided the document fails.

): FunctionComponentDefinition {
return { kind: "function", name, props: NO_PROPS, fn: body };
// These fixtures exist to be observed failing, so they collect rather than
// stopping the expansion the assertion is about.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// stopping the expansion the assertion is about.

@github-actions

Copy link
Copy Markdown

PR #251: 💥 Fail fast on function-component failures, collect explicitly

22 files, +737 / -463

Scope

🔴 PR has 1200 lines changed. Split into focused PRs.

🟡 1200 lines changed. PRs under 400 receive more thorough review.

🟡 22 files changed. Are all changes related?

Structural

🟡 Type declarations with no consumers: FatalFailure.
SymbolDeclared atRefs in diffWhy flagged
FatalFailurepackages/core/src/errors.ts:1311referenced ≤1× within the added diff (pre-existing usages not counted)

Slop

✅ Slop indicators look low.

Static Analysis

✅ Oxlint found no issues.

Correctness

No extraneous code patterns detected.

@taras
taras merged commit 97d9c35 into mainJul 31, 2026
9 checks passed
@taras
taras deleted the feat/fail-fast-components branch August 27, 2026 01:34
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Make function-component failures fail-fast by default with explicit collection boundaries

1 participant

@taras
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

💥 Fail fast on function-component failures, collect explicitly - #251

Merged
taras merged 1 commit into
mainfrom
feat/fail-fast-components
Jul 31, 2026
Merged

💥 Fail fast on function-component failures, collect explicitly#251
taras merged 1 commit into
mainfrom
feat/fail-fast-components

Conversation

@taras

Copy link
Copy Markdown
Owner

Why

Function-component failures were collected into ErrorSegments by default, with fatality an opt-in carried by an internal abort marker. That is inverted: a component that fails now fails the operation it is part of, like any other Effection work, and continuing is an explicit scope-local choice.

Closes#249.

What changes

Default. An ordinary failure propagates after the invocation has been dismantled — projected content, then the component's own resources, then retained work. An Error propagates by identity with its type and cause; a non-Error becomes an Error carrying the original value as cause; body-plus-teardown propagates as the complete aggregate withInvocation() produced. Later siblings do not run.

Component.handleFailure(failure) is the one contextual seam. The default fails the operation. It is called only for an ordinary invocation failure and only after complete teardown. Durability failures, an already-selected DocumentationError, the content transport, and schema diagnostics are classified before it and never reach it.

Two collection forms, one middleware.collectFailures(fn) marks a component by exact function identity and returns the same object. <CollectFailures> is reserved engine syntax that expands its content in the caller's frame, preserving structured segments. Both install the same terminal middleware, so the nearest boundary handles a failure once.

Collection converts a failure into one diagnostic whose cause is the complete original, and does not change the ambient policy — under documentation a collected failure still stops the document.

How it works

The boundary is installed outside the whole withInvocation() call, and scoped:

scoped:
useFailureCollection() ← still installed during teardown
withInvocation:
definition.fn(props)
ordered teardown ← a failure here is still inside the boundary
catch → classify → handleFailure()

Outside, because middleware a component installs for itself is gone before its own teardown can fail. Scoped, because a component that collects its own failures must not quietly decide that for its siblings.

Removed

abortOrdinaryComponentFailures, abortsOrdinaryComponentFailures, the function-identity abort weak set, componentAbort, its error weak set, the aborted arm of fatalCause, hasOrdinaryFailure with its traversal, the agent fatally() wrappers, and the AgentProvider decoration. Agent components now simply throw; they stay fail-fast because that is the default.

Kept and updated: withInvocation(), InvocationTeardownError, durability precedence, AmbientErrorPolicy/settle()/DocumentationError, Component.raise and exactly-once observation, cause attribution, ContentError/tryContent()/the content transport.

What this changed for existing components

Core's <File>, <Glob>, <Parse>, <SafeParse> and <TempDir> have documented diagnostic-and-continue behavior — 44 tests assert it — so they are marked collectFailures(...). That is the decorator's purpose, and it preserves their behavior exactly rather than changing it in a refactor.

Test fixtures whose point is observing a diagnostic are likewise marked; fixtures asserting engine defaults were updated to the new semantics.

How to verify it

Tier CF (tests/failure-collection.test.ts), 11 cases covering the issue's matrix. Every one distinguishes a failed operation from a completed one containing a diagnostic — output text alone cannot tell those apart:

  • CF1–CF4: unmarked failure by identity with nothing after it; teardown-only; body-plus-teardown aggregate; non-Error normalization with the exact value in cause.
  • CF5–CF7: collectFailures reports once and continues; the marker is identity, not name; it collects a teardown-only failure.
  • CF8–CF11: <CollectFailures> handles a direct child and continues; reaches a nested component's failure and handles it once; does not collect durability; under a throwing policy reports once and still stops.

FC5/FC5b pin both halves end-to-end through execute(): an unmarked repository component fails the execution with nothing rendered, and the same component marked reports and continues.

Gate on Deno 2.9.1: fmt / lint / check / test (272 passed, 0 failed) / check:jsr, pnpm exec tsc --project tsconfig.node.json --noEmit, deno task build && ./dist/xmd test packages/core/src --raw (exit 0, 0 failures), git diff --check.

Scope

Per the issue: no WebForm, no Elicit, no remaining #202 migrations, and the TD8 daemon-liveness defect (#248) is deliberately not fixed here.

#247 stays open and will be rebased on this.

An ordinary failure now fails the operation it is part of, after the
invocation has been dismantled. Continuing is a scope-local choice:
collectFailures(fn) for a component, <CollectFailures> for a region.
Closes#249.

@github-actionsgithub-actionsBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found 5 redundant comments. Inline suggestions to remove them below.

// deno-lint-ignore require-yield
*handleFailure(failure: ComponentFailure): Operation<ErrorSegment> {
// The default is to fail, so a component that goes wrong stops the work it
// was part of rather than quietly becoming a note in the output.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// was part of rather than quietly becoming a note in the output.

/** The invocation itself, and what a failure of it means. */
const invoke = function* (): Operation<Segment[]> {
// Detached and frozen: what a component reads about its call site is a copy,
// so nothing it does can reach the element the parser built.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// so nothing it does can reach the element the parser built.

const outcome = yield* handle.tryProject({ kind: "slot", name: slotName });
// A documentation failure is presented in the public shape, as
// `content()` does, so a component recovering from one sees the
// same thing either way.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// same thing either way.

// teardown together.

// Not the document's failure to render: a journal that no longer describes
// this run, or a policy that has already decided the document fails.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// this run, or a policy that has already decided the document fails.

): FunctionComponentDefinition {
return { kind: "function", name, props: NO_PROPS, fn: body };
// These fixtures exist to be observed failing, so they collect rather than
// stopping the expansion the assertion is about.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// stopping the expansion the assertion is about.

@github-actions

Copy link
Copy Markdown

PR #251: 💥 Fail fast on function-component failures, collect explicitly

22 files, +737 / -463

Scope

🔴 PR has 1200 lines changed. Split into focused PRs.

🟡 1200 lines changed. PRs under 400 receive more thorough review.

🟡 22 files changed. Are all changes related?

Structural

🟡 Type declarations with no consumers: FatalFailure.
SymbolDeclared atRefs in diffWhy flagged
FatalFailurepackages/core/src/errors.ts:1311referenced ≤1× within the added diff (pre-existing usages not counted)

Slop

✅ Slop indicators look low.

Static Analysis

✅ Oxlint found no issues.

Correctness

No extraneous code patterns detected.

@taras
taras merged commit 97d9c35 into mainJul 31, 2026
9 checks passed
@taras
taras deleted the feat/fail-fast-components branch August 27, 2026 01:34
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Make function-component failures fail-fast by default with explicit collection boundaries

1 participant

@taras
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

💥 Fail fast on function-component failures, collect explicitly - #251

Merged
taras merged 1 commit into
mainfrom
feat/fail-fast-components
Jul 31, 2026
Merged

💥 Fail fast on function-component failures, collect explicitly#251
taras merged 1 commit into
mainfrom
feat/fail-fast-components

Conversation

@taras

Copy link
Copy Markdown
Owner

Why

Function-component failures were collected into ErrorSegments by default, with fatality an opt-in carried by an internal abort marker. That is inverted: a component that fails now fails the operation it is part of, like any other Effection work, and continuing is an explicit scope-local choice.

Closes#249.

What changes

Default. An ordinary failure propagates after the invocation has been dismantled — projected content, then the component's own resources, then retained work. An Error propagates by identity with its type and cause; a non-Error becomes an Error carrying the original value as cause; body-plus-teardown propagates as the complete aggregate withInvocation() produced. Later siblings do not run.

Component.handleFailure(failure) is the one contextual seam. The default fails the operation. It is called only for an ordinary invocation failure and only after complete teardown. Durability failures, an already-selected DocumentationError, the content transport, and schema diagnostics are classified before it and never reach it.

Two collection forms, one middleware.collectFailures(fn) marks a component by exact function identity and returns the same object. <CollectFailures> is reserved engine syntax that expands its content in the caller's frame, preserving structured segments. Both install the same terminal middleware, so the nearest boundary handles a failure once.

Collection converts a failure into one diagnostic whose cause is the complete original, and does not change the ambient policy — under documentation a collected failure still stops the document.

How it works

The boundary is installed outside the whole withInvocation() call, and scoped:

scoped:
useFailureCollection() ← still installed during teardown
withInvocation:
definition.fn(props)
ordered teardown ← a failure here is still inside the boundary
catch → classify → handleFailure()

Outside, because middleware a component installs for itself is gone before its own teardown can fail. Scoped, because a component that collects its own failures must not quietly decide that for its siblings.

Removed

abortOrdinaryComponentFailures, abortsOrdinaryComponentFailures, the function-identity abort weak set, componentAbort, its error weak set, the aborted arm of fatalCause, hasOrdinaryFailure with its traversal, the agent fatally() wrappers, and the AgentProvider decoration. Agent components now simply throw; they stay fail-fast because that is the default.

Kept and updated: withInvocation(), InvocationTeardownError, durability precedence, AmbientErrorPolicy/settle()/DocumentationError, Component.raise and exactly-once observation, cause attribution, ContentError/tryContent()/the content transport.

What this changed for existing components

Core's <File>, <Glob>, <Parse>, <SafeParse> and <TempDir> have documented diagnostic-and-continue behavior — 44 tests assert it — so they are marked collectFailures(...). That is the decorator's purpose, and it preserves their behavior exactly rather than changing it in a refactor.

Test fixtures whose point is observing a diagnostic are likewise marked; fixtures asserting engine defaults were updated to the new semantics.

How to verify it

Tier CF (tests/failure-collection.test.ts), 11 cases covering the issue's matrix. Every one distinguishes a failed operation from a completed one containing a diagnostic — output text alone cannot tell those apart:

  • CF1–CF4: unmarked failure by identity with nothing after it; teardown-only; body-plus-teardown aggregate; non-Error normalization with the exact value in cause.
  • CF5–CF7: collectFailures reports once and continues; the marker is identity, not name; it collects a teardown-only failure.
  • CF8–CF11: <CollectFailures> handles a direct child and continues; reaches a nested component's failure and handles it once; does not collect durability; under a throwing policy reports once and still stops.

FC5/FC5b pin both halves end-to-end through execute(): an unmarked repository component fails the execution with nothing rendered, and the same component marked reports and continues.

Gate on Deno 2.9.1: fmt / lint / check / test (272 passed, 0 failed) / check:jsr, pnpm exec tsc --project tsconfig.node.json --noEmit, deno task build && ./dist/xmd test packages/core/src --raw (exit 0, 0 failures), git diff --check.

Scope

Per the issue: no WebForm, no Elicit, no remaining #202 migrations, and the TD8 daemon-liveness defect (#248) is deliberately not fixed here.

#247 stays open and will be rebased on this.

An ordinary failure now fails the operation it is part of, after the
invocation has been dismantled. Continuing is a scope-local choice:
collectFailures(fn) for a component, <CollectFailures> for a region.
Closes#249.

@github-actionsgithub-actionsBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found 5 redundant comments. Inline suggestions to remove them below.

// deno-lint-ignore require-yield
*handleFailure(failure: ComponentFailure): Operation<ErrorSegment> {
// The default is to fail, so a component that goes wrong stops the work it
// was part of rather than quietly becoming a note in the output.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// was part of rather than quietly becoming a note in the output.

/** The invocation itself, and what a failure of it means. */
const invoke = function* (): Operation<Segment[]> {
// Detached and frozen: what a component reads about its call site is a copy,
// so nothing it does can reach the element the parser built.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// so nothing it does can reach the element the parser built.

const outcome = yield* handle.tryProject({ kind: "slot", name: slotName });
// A documentation failure is presented in the public shape, as
// `content()` does, so a component recovering from one sees the
// same thing either way.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// same thing either way.

// teardown together.

// Not the document's failure to render: a journal that no longer describes
// this run, or a policy that has already decided the document fails.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// this run, or a policy that has already decided the document fails.

): FunctionComponentDefinition {
return { kind: "function", name, props: NO_PROPS, fn: body };
// These fixtures exist to be observed failing, so they collect rather than
// stopping the expansion the assertion is about.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// stopping the expansion the assertion is about.

@github-actions

Copy link
Copy Markdown

PR #251: 💥 Fail fast on function-component failures, collect explicitly

22 files, +737 / -463

Scope

🔴 PR has 1200 lines changed. Split into focused PRs.

🟡 1200 lines changed. PRs under 400 receive more thorough review.

🟡 22 files changed. Are all changes related?

Structural

🟡 Type declarations with no consumers: FatalFailure.
SymbolDeclared atRefs in diffWhy flagged
FatalFailurepackages/core/src/errors.ts:1311referenced ≤1× within the added diff (pre-existing usages not counted)

Slop

✅ Slop indicators look low.

Static Analysis

✅ Oxlint found no issues.

Correctness

No extraneous code patterns detected.

@taras
taras merged commit 97d9c35 into mainJul 31, 2026
9 checks passed
@taras
taras deleted the feat/fail-fast-components branch August 27, 2026 01:34
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Make function-component failures fail-fast by default with explicit collection boundaries

1 participant

@taras