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

Description

@taras

Purpose

Make ordinary function-component failures follow Effection's normal fail-fast
semantics by default. Continuing after such a failure becomes an explicit,
scope-local choice expressed by <CollectFailures> or collectFailures(fn).

This replaces the current inverse model, where the function-component boundary
turns ordinary body and teardown failures into ErrorSegments unless the
function or failure carries an internal abort marker.

Product contract

Default behavior

An ordinary failure from a function component fails its current Effection
operation by default:

  • the complete component invocation finishes teardown before the failure leaves
    the boundary;
  • an Error propagates by identity, preserving its type and cause;
  • a non-Error thrown value is normalized to an Error whose cause is the
    exact original value;
  • a body failure combined with teardown failure propagates as the complete
    aggregate produced by withInvocation();
  • later siblings do not execute unless an explicit collection boundary handles
    the failure.

Failing an Effection operation is not an unconditional process crash. The
structured scope unwinds and releases its resources, and an enclosing operation
may still recover deliberately.

Failure-handling middleware

Add one public contextual component operation with this effective contract:

exportinterfaceComponentFailure{readonlyname: string;readonlyposition?: SourcePosition;readonlyerror: Error;}interfaceComponentApi{handleFailure(failure: ComponentFailure): Operation<ErrorSegment>;}

The final spelling may change only to avoid a collision with an existing
repository convention; do not replace this with process-global hooks, events,
or mutable registration metadata.

The default implementation throws failure.error. The engine calls it only
for an ordinary function-component invocation failure and only after
withInvocation() has produced the complete body-and-teardown outcome.

The standard collection middleware converts the failure into one component
diagnostic, attributes the original failure as its cause, and reports it once
through Component.raise. It is terminal: it handles rather than delegates.

The following are classified before handleFailure and are never downgraded by
the standard collector:

  • durability, stale-input, and replay-divergence failures;
  • a DocumentationError already selected by the caller's throwing policy;
  • the private content-failure transport that restores already-reported
    ErrorSegments;
  • established schema-validation diagnostics that already have a structured
    document representation.

Component.raise remains the single observation point for an ErrorSegment.
handleFailure handles an operation failure; it does not replace or duplicate
error observation.

collectFailures(fn)

Export this public decorator from @executablemd/core:

exportfunctioncollectFailures<TextendsFunctionComponent>(component: T): T;

Usage:

exportdefaultcollectFailures(function*WebForm(props){// body, requested content, retained work, and teardown are inside the// collection boundary});

The decorator records the exact function identity and returns that same
function object with its type preserved. It does not create a delegating
generator and does not catch inside the component body.

When the engine invokes a marked function, it installs the standard collection
middleware outside the entire withInvocation() call. This placement is
required: middleware installed from inside the component would disappear
before invocation teardown could fail. Installing outside also makes the
handler available to nested components and projected content whose scopes are
created by that invocation.

The marker is function identity, never component name. A repository component
with the same name as a marked registered component does not inherit its
collection behavior.

<CollectFailures>

Add <CollectFailures> as a reserved engine-owned control construct. It accepts
no props and expands its content in the caller's ordinary expansion frame while
the same standard collection middleware is installed. It preserves structured
segments rather than rendering content to a string.

<CollectFailures>
<MayFail />
<StillRuns />
</CollectFailures>

Both forms establish the same dynamic scope: the nearest collection boundary
handles ordinary failures from the enclosed invocation tree.

Collection converts ordinary failures into ErrorSegments; it does not change
the ambient ErrorPolicy. The caller still settles the segment normally:

  • under collect (the root and <Output>), the diagnostic renders and later
    work continues;
  • under throw (documentation), the reported segment becomes a
    DocumentationError and execution stops.

Do not add a second "already handled" bit or make collection silently override
an enclosing throwing policy.

Structured-concurrency invariants

  • withInvocation() and its ordered teardown remain the component lifetime
    boundary.
  • Failure handling runs after projected content, the component body, and
    retained invocation resources have all stopped in the established order.
  • A collector never reports success before teardown completes.
  • If body and teardown both fail, collection reports one diagnostic whose
    cause is the complete aggregate, not only its first member.
  • Cancellation is not converted into a document diagnostic.
  • A collector does not retry an invocation.

Required cleanup

Remove the mechanisms that exist only because collection is currently the
default:

  • abortOrdinaryComponentFailures() and
    abortsOrdinaryComponentFailures();
  • the function-identity abort weak set;
  • componentAbort(), its error-identity weak set, and the aborted-failure arm
    of fatalCause();
  • hasOrdinaryFailure() and its ordinary-leaf traversal;
  • the agent fatally() wrappers and explicit componentAbort() calls;
  • the special decoration of AgentProvider;
  • tests and specification text that describe ordinary collection with fatality
    as an opt-in exception.

Agent components simply throw when their existing contracts say the document
must stop. They remain fail-fast because that is now the default. Prompt
failures that are deliberately recorded and returned remain unchanged.

Keep and update rather than remove:

  • withInvocation() and InvocationTeardownError;
  • durability-failure recognition and its precedence;
  • AmbientErrorPolicy, settle(), and DocumentationError;
  • Component.raise and exactly-once observation;
  • cause attribution for a failure deliberately converted to an ErrorSegment;
  • ContentError, tryContent(), and the private content transport;
  • consumer-boundary settlement between a component and its caller.

Put the public decorator and its private exact-function marker in a focused
module such as src/component-failures.ts; do not grow errors.ts with the
inverse of the machinery being removed.

Behavior matrix and tests

Add discriminating tests for all of the following:

  1. An unmarked repository TypeScript component throws an Error: execution is
    Err with the exact object, later siblings do not run, and no diagnostic is
    rendered.
  2. An unmarked registered component has the same behavior.
  3. An unmarked component's teardown throws only after its body returns: teardown
    finishes and the exact teardown failure propagates.
  4. Body and teardown both fail: the complete ordered aggregate propagates.
  5. A non-Error throw is normalized with the exact thrown value in cause.
  6. collectFailures(fn) converts an ordinary body failure into exactly one
    observed ErrorSegment under collect, preserves the original failure as
    cause, and later siblings run.
  7. The decorator collects a teardown-only failure after teardown completes.
  8. The decorator collects the complete body-plus-teardown aggregate as one
    diagnostic with the aggregate in cause.
  9. <CollectFailures> handles a direct child failure and continues to a later
    child under collect.
  10. Both forms reach failures in nested components and in content projected
    through content() without duplicate handling or observation.
  11. The nearest nested collection boundary handles once.
  12. Under an ambient throwing policy, both forms report once and the resulting
    DocumentationError still stops execution.
  13. Neither form collects durability/replay divergence.
  14. Uncaught ContentError still restores the original segments without
    reporting them again; explicit content recovery remains recovery.
  15. Input- and return-schema behavior remains in its existing structured
    diagnostic channel.
  16. A repository component named like a marked registered component remains
    unmarked and fail-fast.
  17. Existing Agent, observation, capture, projection, invocation-failure, and
    replay suites remain green after obsolete marker cases are removed.

Tests must distinguish successful operation completion containing a rendered
diagnostic from failed operation completion; checking output text alone is not
sufficient.

Specification updates

Update the executable MDX and component API specifications in present tense:

  • ordinary function-component operation failures propagate by default;
  • error collection is an explicit dynamic boundary;
  • handleFailure runs after complete invocation teardown;
  • Component.raise observation and operation-failure handling are distinct;
  • <CollectFailures> is reserved engine syntax;
  • collectFailures(fn) is keyed by exact function identity;
  • collection remains subordinate to the caller's ambient settlement policy;
  • durability failures and cancellation are not collectable diagnostics.

Update #202's behavior-preservation account where it describes the private
agent abort marker. This issue deliberately replaces that implementation while
preserving the user-visible fail-fast behavior of Agent components.

Sequencing

  1. Land this issue as one focused core semantic-refactor PR based on current
    main. Do not stack it on 🐛 Report a <Testing> boundary only once its body has finished #247.
  2. Keep 🐛 Report a <Testing> boundary only once its body has finished #247 open. After this issue lands, rebase 🐛 Report a <Testing> boundary only once its body has finished #247 and replace TB1's custom
    Component.raise throwing middleware with a real ordinary child component
    failure. Assert exact failure identity, absence of later output, and no
    testing boundary observation or journal entry. The ordering fix then lands
    on the new default.
  3. Continue Add a bundled local WebForm component for schema-backed user input #195 WebForm transport/server/browser work in parallel. Its public
    registered <WebForm> integration waits for this issue and chooses
    collectFailures(WebForm) only if the component contract explicitly intends
    recoverable rendered diagnostics. Do not inherit collection accidentally.
  4. Resume the remaining Register function components and migrate legacy component handlers #202 migrations after the semantic-refactor PR lands.
    Each migrated component is fail-fast unless its approved behavior requires
    an explicit collection boundary.
  5. Provider-neutral <Elicit> (Add provider-neutral Elicit through the core Context API #197) remains after the WebForm provider
    contract and the corrected Testing/error semantics are ready.

Non-goals

Verification

deno task fmt
deno task lint
deno task check
deno task test
deno task check:jsr
pnpm exec tsc --project tsconfig.node.json --noEmit
deno task build
./dist/xmd test packages/core/src --raw
git diff --check

All hosted checks must be green. A recurring failure from the independent TD8
daemon-liveness defect is tracked in #248 and must not be fixed in this PR.

Acceptance criteria

  • Ordinary function-component body and teardown failures propagate by default
    after complete cleanup.
  • Component.handleFailure is the one contextual handling seam for ordinary
    invocation failures.
  • <CollectFailures> and collectFailures(fn) install the same standard
    collection middleware only within their explicit dynamic scopes.
  • Collected failures are observed once, retain the complete original failure as
    cause, and remain subject to the caller's ambient settlement policy.
  • Durability failures, cancellation, content transport, and established schema
    diagnostics retain their distinct semantics.
  • The inverse abort-marker machinery and agent-specific fatal wrappers are gone.
  • Specifications and discriminating tests describe the simplified model.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
       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

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

      Description

      @taras

      Purpose

      Make ordinary function-component failures follow Effection's normal fail-fast
      semantics by default. Continuing after such a failure becomes an explicit,
      scope-local choice expressed by <CollectFailures> or collectFailures(fn).

      This replaces the current inverse model, where the function-component boundary
      turns ordinary body and teardown failures into ErrorSegments unless the
      function or failure carries an internal abort marker.

      Product contract

      Default behavior

      An ordinary failure from a function component fails its current Effection
      operation by default:

      • the complete component invocation finishes teardown before the failure leaves
        the boundary;
      • an Error propagates by identity, preserving its type and cause;
      • a non-Error thrown value is normalized to an Error whose cause is the
        exact original value;
      • a body failure combined with teardown failure propagates as the complete
        aggregate produced by withInvocation();
      • later siblings do not execute unless an explicit collection boundary handles
        the failure.

      Failing an Effection operation is not an unconditional process crash. The
      structured scope unwinds and releases its resources, and an enclosing operation
      may still recover deliberately.

      Failure-handling middleware

      Add one public contextual component operation with this effective contract:

      exportinterfaceComponentFailure{readonlyname: string;readonlyposition?: SourcePosition;readonlyerror: Error;}interfaceComponentApi{handleFailure(failure: ComponentFailure): Operation<ErrorSegment>;}

      The final spelling may change only to avoid a collision with an existing
      repository convention; do not replace this with process-global hooks, events,
      or mutable registration metadata.

      The default implementation throws failure.error. The engine calls it only
      for an ordinary function-component invocation failure and only after
      withInvocation() has produced the complete body-and-teardown outcome.

      The standard collection middleware converts the failure into one component
      diagnostic, attributes the original failure as its cause, and reports it once
      through Component.raise. It is terminal: it handles rather than delegates.

      The following are classified before handleFailure and are never downgraded by
      the standard collector:

      • durability, stale-input, and replay-divergence failures;
      • a DocumentationError already selected by the caller's throwing policy;
      • the private content-failure transport that restores already-reported
        ErrorSegments;
      • established schema-validation diagnostics that already have a structured
        document representation.

      Component.raise remains the single observation point for an ErrorSegment.
      handleFailure handles an operation failure; it does not replace or duplicate
      error observation.

      collectFailures(fn)

      Export this public decorator from @executablemd/core:

      exportfunctioncollectFailures<TextendsFunctionComponent>(component: T): T;

      Usage:

      exportdefaultcollectFailures(function*WebForm(props){// body, requested content, retained work, and teardown are inside the// collection boundary});

      The decorator records the exact function identity and returns that same
      function object with its type preserved. It does not create a delegating
      generator and does not catch inside the component body.

      When the engine invokes a marked function, it installs the standard collection
      middleware outside the entire withInvocation() call. This placement is
      required: middleware installed from inside the component would disappear
      before invocation teardown could fail. Installing outside also makes the
      handler available to nested components and projected content whose scopes are
      created by that invocation.

      The marker is function identity, never component name. A repository component
      with the same name as a marked registered component does not inherit its
      collection behavior.

      <CollectFailures>

      Add <CollectFailures> as a reserved engine-owned control construct. It accepts
      no props and expands its content in the caller's ordinary expansion frame while
      the same standard collection middleware is installed. It preserves structured
      segments rather than rendering content to a string.

      <CollectFailures>
      <MayFail />
      <StillRuns />
      </CollectFailures>

      Both forms establish the same dynamic scope: the nearest collection boundary
      handles ordinary failures from the enclosed invocation tree.

      Collection converts ordinary failures into ErrorSegments; it does not change
      the ambient ErrorPolicy. The caller still settles the segment normally:

      • under collect (the root and <Output>), the diagnostic renders and later
        work continues;
      • under throw (documentation), the reported segment becomes a
        DocumentationError and execution stops.

      Do not add a second "already handled" bit or make collection silently override
      an enclosing throwing policy.

      Structured-concurrency invariants

      • withInvocation() and its ordered teardown remain the component lifetime
        boundary.
      • Failure handling runs after projected content, the component body, and
        retained invocation resources have all stopped in the established order.
      • A collector never reports success before teardown completes.
      • If body and teardown both fail, collection reports one diagnostic whose
        cause is the complete aggregate, not only its first member.
      • Cancellation is not converted into a document diagnostic.
      • A collector does not retry an invocation.

      Required cleanup

      Remove the mechanisms that exist only because collection is currently the
      default:

      • abortOrdinaryComponentFailures() and
        abortsOrdinaryComponentFailures();
      • the function-identity abort weak set;
      • componentAbort(), its error-identity weak set, and the aborted-failure arm
        of fatalCause();
      • hasOrdinaryFailure() and its ordinary-leaf traversal;
      • the agent fatally() wrappers and explicit componentAbort() calls;
      • the special decoration of AgentProvider;
      • tests and specification text that describe ordinary collection with fatality
        as an opt-in exception.

      Agent components simply throw when their existing contracts say the document
      must stop. They remain fail-fast because that is now the default. Prompt
      failures that are deliberately recorded and returned remain unchanged.

      Keep and update rather than remove:

      • withInvocation() and InvocationTeardownError;
      • durability-failure recognition and its precedence;
      • AmbientErrorPolicy, settle(), and DocumentationError;
      • Component.raise and exactly-once observation;
      • cause attribution for a failure deliberately converted to an ErrorSegment;
      • ContentError, tryContent(), and the private content transport;
      • consumer-boundary settlement between a component and its caller.

      Put the public decorator and its private exact-function marker in a focused
      module such as src/component-failures.ts; do not grow errors.ts with the
      inverse of the machinery being removed.

      Behavior matrix and tests

      Add discriminating tests for all of the following:

      1. An unmarked repository TypeScript component throws an Error: execution is
        Err with the exact object, later siblings do not run, and no diagnostic is
        rendered.
      2. An unmarked registered component has the same behavior.
      3. An unmarked component's teardown throws only after its body returns: teardown
        finishes and the exact teardown failure propagates.
      4. Body and teardown both fail: the complete ordered aggregate propagates.
      5. A non-Error throw is normalized with the exact thrown value in cause.
      6. collectFailures(fn) converts an ordinary body failure into exactly one
        observed ErrorSegment under collect, preserves the original failure as
        cause, and later siblings run.
      7. The decorator collects a teardown-only failure after teardown completes.
      8. The decorator collects the complete body-plus-teardown aggregate as one
        diagnostic with the aggregate in cause.
      9. <CollectFailures> handles a direct child failure and continues to a later
        child under collect.
      10. Both forms reach failures in nested components and in content projected
        through content() without duplicate handling or observation.
      11. The nearest nested collection boundary handles once.
      12. Under an ambient throwing policy, both forms report once and the resulting
        DocumentationError still stops execution.
      13. Neither form collects durability/replay divergence.
      14. Uncaught ContentError still restores the original segments without
        reporting them again; explicit content recovery remains recovery.
      15. Input- and return-schema behavior remains in its existing structured
        diagnostic channel.
      16. A repository component named like a marked registered component remains
        unmarked and fail-fast.
      17. Existing Agent, observation, capture, projection, invocation-failure, and
        replay suites remain green after obsolete marker cases are removed.

      Tests must distinguish successful operation completion containing a rendered
      diagnostic from failed operation completion; checking output text alone is not
      sufficient.

      Specification updates

      Update the executable MDX and component API specifications in present tense:

      • ordinary function-component operation failures propagate by default;
      • error collection is an explicit dynamic boundary;
      • handleFailure runs after complete invocation teardown;
      • Component.raise observation and operation-failure handling are distinct;
      • <CollectFailures> is reserved engine syntax;
      • collectFailures(fn) is keyed by exact function identity;
      • collection remains subordinate to the caller's ambient settlement policy;
      • durability failures and cancellation are not collectable diagnostics.

      Update #202's behavior-preservation account where it describes the private
      agent abort marker. This issue deliberately replaces that implementation while
      preserving the user-visible fail-fast behavior of Agent components.

      Sequencing

      1. Land this issue as one focused core semantic-refactor PR based on current
        main. Do not stack it on 🐛 Report a <Testing> boundary only once its body has finished #247.
      2. Keep 🐛 Report a <Testing> boundary only once its body has finished #247 open. After this issue lands, rebase 🐛 Report a <Testing> boundary only once its body has finished #247 and replace TB1's custom
        Component.raise throwing middleware with a real ordinary child component
        failure. Assert exact failure identity, absence of later output, and no
        testing boundary observation or journal entry. The ordering fix then lands
        on the new default.
      3. Continue Add a bundled local WebForm component for schema-backed user input #195 WebForm transport/server/browser work in parallel. Its public
        registered <WebForm> integration waits for this issue and chooses
        collectFailures(WebForm) only if the component contract explicitly intends
        recoverable rendered diagnostics. Do not inherit collection accidentally.
      4. Resume the remaining Register function components and migrate legacy component handlers #202 migrations after the semantic-refactor PR lands.
        Each migrated component is fail-fast unless its approved behavior requires
        an explicit collection boundary.
      5. Provider-neutral <Elicit> (Add provider-neutral Elicit through the core Context API #197) remains after the WebForm provider
        contract and the corrected Testing/error semantics are ready.

      Non-goals

      Verification

      deno task fmt
      deno task lint
      deno task check
      deno task test
      deno task check:jsr
      pnpm exec tsc --project tsconfig.node.json --noEmit
      deno task build
      ./dist/xmd test packages/core/src --raw
      git diff --check

      All hosted checks must be green. A recurring failure from the independent TD8
      daemon-liveness defect is tracked in #248 and must not be fixed in this PR.

      Acceptance criteria

      • Ordinary function-component body and teardown failures propagate by default
        after complete cleanup.
      • Component.handleFailure is the one contextual handling seam for ordinary
        invocation failures.
      • <CollectFailures> and collectFailures(fn) install the same standard
        collection middleware only within their explicit dynamic scopes.
      • Collected failures are observed once, retain the complete original failure as
        cause, and remain subject to the caller's ambient settlement policy.
      • Durability failures, cancellation, content transport, and established schema
        diagnostics retain their distinct semantics.
      • The inverse abort-marker machinery and agent-specific fatal wrappers are gone.
      • Specifications and discriminating tests describe the simplified model.

      Activity

      Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

      Metadata

      Metadata

      Assignees

      No one assigned

        Labels

        No labels
        No labels

        Projects

        No projects

          Milestone

          No milestone

          Relationships

          None yet

          Development

          No branches or pull requests

          Issue actions

          , '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

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

          Description

          @taras

          Purpose

          Make ordinary function-component failures follow Effection's normal fail-fast
          semantics by default. Continuing after such a failure becomes an explicit,
          scope-local choice expressed by <CollectFailures> or collectFailures(fn).

          This replaces the current inverse model, where the function-component boundary
          turns ordinary body and teardown failures into ErrorSegments unless the
          function or failure carries an internal abort marker.

          Product contract

          Default behavior

          An ordinary failure from a function component fails its current Effection
          operation by default:

          • the complete component invocation finishes teardown before the failure leaves
            the boundary;
          • an Error propagates by identity, preserving its type and cause;
          • a non-Error thrown value is normalized to an Error whose cause is the
            exact original value;
          • a body failure combined with teardown failure propagates as the complete
            aggregate produced by withInvocation();
          • later siblings do not execute unless an explicit collection boundary handles
            the failure.

          Failing an Effection operation is not an unconditional process crash. The
          structured scope unwinds and releases its resources, and an enclosing operation
          may still recover deliberately.

          Failure-handling middleware

          Add one public contextual component operation with this effective contract:

          exportinterfaceComponentFailure{readonlyname: string;readonlyposition?: SourcePosition;readonlyerror: Error;}interfaceComponentApi{handleFailure(failure: ComponentFailure): Operation<ErrorSegment>;}

          The final spelling may change only to avoid a collision with an existing
          repository convention; do not replace this with process-global hooks, events,
          or mutable registration metadata.

          The default implementation throws failure.error. The engine calls it only
          for an ordinary function-component invocation failure and only after
          withInvocation() has produced the complete body-and-teardown outcome.

          The standard collection middleware converts the failure into one component
          diagnostic, attributes the original failure as its cause, and reports it once
          through Component.raise. It is terminal: it handles rather than delegates.

          The following are classified before handleFailure and are never downgraded by
          the standard collector:

          • durability, stale-input, and replay-divergence failures;
          • a DocumentationError already selected by the caller's throwing policy;
          • the private content-failure transport that restores already-reported
            ErrorSegments;
          • established schema-validation diagnostics that already have a structured
            document representation.

          Component.raise remains the single observation point for an ErrorSegment.
          handleFailure handles an operation failure; it does not replace or duplicate
          error observation.

          collectFailures(fn)

          Export this public decorator from @executablemd/core:

          exportfunctioncollectFailures<TextendsFunctionComponent>(component: T): T;

          Usage:

          exportdefaultcollectFailures(function*WebForm(props){// body, requested content, retained work, and teardown are inside the// collection boundary});

          The decorator records the exact function identity and returns that same
          function object with its type preserved. It does not create a delegating
          generator and does not catch inside the component body.

          When the engine invokes a marked function, it installs the standard collection
          middleware outside the entire withInvocation() call. This placement is
          required: middleware installed from inside the component would disappear
          before invocation teardown could fail. Installing outside also makes the
          handler available to nested components and projected content whose scopes are
          created by that invocation.

          The marker is function identity, never component name. A repository component
          with the same name as a marked registered component does not inherit its
          collection behavior.

          <CollectFailures>

          Add <CollectFailures> as a reserved engine-owned control construct. It accepts
          no props and expands its content in the caller's ordinary expansion frame while
          the same standard collection middleware is installed. It preserves structured
          segments rather than rendering content to a string.

          <CollectFailures>
          <MayFail />
          <StillRuns />
          </CollectFailures>

          Both forms establish the same dynamic scope: the nearest collection boundary
          handles ordinary failures from the enclosed invocation tree.

          Collection converts ordinary failures into ErrorSegments; it does not change
          the ambient ErrorPolicy. The caller still settles the segment normally:

          • under collect (the root and <Output>), the diagnostic renders and later
            work continues;
          • under throw (documentation), the reported segment becomes a
            DocumentationError and execution stops.

          Do not add a second "already handled" bit or make collection silently override
          an enclosing throwing policy.

          Structured-concurrency invariants

          • withInvocation() and its ordered teardown remain the component lifetime
            boundary.
          • Failure handling runs after projected content, the component body, and
            retained invocation resources have all stopped in the established order.
          • A collector never reports success before teardown completes.
          • If body and teardown both fail, collection reports one diagnostic whose
            cause is the complete aggregate, not only its first member.
          • Cancellation is not converted into a document diagnostic.
          • A collector does not retry an invocation.

          Required cleanup

          Remove the mechanisms that exist only because collection is currently the
          default:

          • abortOrdinaryComponentFailures() and
            abortsOrdinaryComponentFailures();
          • the function-identity abort weak set;
          • componentAbort(), its error-identity weak set, and the aborted-failure arm
            of fatalCause();
          • hasOrdinaryFailure() and its ordinary-leaf traversal;
          • the agent fatally() wrappers and explicit componentAbort() calls;
          • the special decoration of AgentProvider;
          • tests and specification text that describe ordinary collection with fatality
            as an opt-in exception.

          Agent components simply throw when their existing contracts say the document
          must stop. They remain fail-fast because that is now the default. Prompt
          failures that are deliberately recorded and returned remain unchanged.

          Keep and update rather than remove:

          • withInvocation() and InvocationTeardownError;
          • durability-failure recognition and its precedence;
          • AmbientErrorPolicy, settle(), and DocumentationError;
          • Component.raise and exactly-once observation;
          • cause attribution for a failure deliberately converted to an ErrorSegment;
          • ContentError, tryContent(), and the private content transport;
          • consumer-boundary settlement between a component and its caller.

          Put the public decorator and its private exact-function marker in a focused
          module such as src/component-failures.ts; do not grow errors.ts with the
          inverse of the machinery being removed.

          Behavior matrix and tests

          Add discriminating tests for all of the following:

          1. An unmarked repository TypeScript component throws an Error: execution is
            Err with the exact object, later siblings do not run, and no diagnostic is
            rendered.
          2. An unmarked registered component has the same behavior.
          3. An unmarked component's teardown throws only after its body returns: teardown
            finishes and the exact teardown failure propagates.
          4. Body and teardown both fail: the complete ordered aggregate propagates.
          5. A non-Error throw is normalized with the exact thrown value in cause.
          6. collectFailures(fn) converts an ordinary body failure into exactly one
            observed ErrorSegment under collect, preserves the original failure as
            cause, and later siblings run.
          7. The decorator collects a teardown-only failure after teardown completes.
          8. The decorator collects the complete body-plus-teardown aggregate as one
            diagnostic with the aggregate in cause.
          9. <CollectFailures> handles a direct child failure and continues to a later
            child under collect.
          10. Both forms reach failures in nested components and in content projected
            through content() without duplicate handling or observation.
          11. The nearest nested collection boundary handles once.
          12. Under an ambient throwing policy, both forms report once and the resulting
            DocumentationError still stops execution.
          13. Neither form collects durability/replay divergence.
          14. Uncaught ContentError still restores the original segments without
            reporting them again; explicit content recovery remains recovery.
          15. Input- and return-schema behavior remains in its existing structured
            diagnostic channel.
          16. A repository component named like a marked registered component remains
            unmarked and fail-fast.
          17. Existing Agent, observation, capture, projection, invocation-failure, and
            replay suites remain green after obsolete marker cases are removed.

          Tests must distinguish successful operation completion containing a rendered
          diagnostic from failed operation completion; checking output text alone is not
          sufficient.

          Specification updates

          Update the executable MDX and component API specifications in present tense:

          • ordinary function-component operation failures propagate by default;
          • error collection is an explicit dynamic boundary;
          • handleFailure runs after complete invocation teardown;
          • Component.raise observation and operation-failure handling are distinct;
          • <CollectFailures> is reserved engine syntax;
          • collectFailures(fn) is keyed by exact function identity;
          • collection remains subordinate to the caller's ambient settlement policy;
          • durability failures and cancellation are not collectable diagnostics.

          Update #202's behavior-preservation account where it describes the private
          agent abort marker. This issue deliberately replaces that implementation while
          preserving the user-visible fail-fast behavior of Agent components.

          Sequencing

          1. Land this issue as one focused core semantic-refactor PR based on current
            main. Do not stack it on 🐛 Report a <Testing> boundary only once its body has finished #247.
          2. Keep 🐛 Report a <Testing> boundary only once its body has finished #247 open. After this issue lands, rebase 🐛 Report a <Testing> boundary only once its body has finished #247 and replace TB1's custom
            Component.raise throwing middleware with a real ordinary child component
            failure. Assert exact failure identity, absence of later output, and no
            testing boundary observation or journal entry. The ordering fix then lands
            on the new default.
          3. Continue Add a bundled local WebForm component for schema-backed user input #195 WebForm transport/server/browser work in parallel. Its public
            registered <WebForm> integration waits for this issue and chooses
            collectFailures(WebForm) only if the component contract explicitly intends
            recoverable rendered diagnostics. Do not inherit collection accidentally.
          4. Resume the remaining Register function components and migrate legacy component handlers #202 migrations after the semantic-refactor PR lands.
            Each migrated component is fail-fast unless its approved behavior requires
            an explicit collection boundary.
          5. Provider-neutral <Elicit> (Add provider-neutral Elicit through the core Context API #197) remains after the WebForm provider
            contract and the corrected Testing/error semantics are ready.

          Non-goals

          Verification

          deno task fmt
          deno task lint
          deno task check
          deno task test
          deno task check:jsr
          pnpm exec tsc --project tsconfig.node.json --noEmit
          deno task build
          ./dist/xmd test packages/core/src --raw
          git diff --check

          All hosted checks must be green. A recurring failure from the independent TD8
          daemon-liveness defect is tracked in #248 and must not be fixed in this PR.

          Acceptance criteria

          • Ordinary function-component body and teardown failures propagate by default
            after complete cleanup.
          • Component.handleFailure is the one contextual handling seam for ordinary
            invocation failures.
          • <CollectFailures> and collectFailures(fn) install the same standard
            collection middleware only within their explicit dynamic scopes.
          • Collected failures are observed once, retain the complete original failure as
            cause, and remain subject to the caller's ambient settlement policy.
          • Durability failures, cancellation, content transport, and established schema
            diagnostics retain their distinct semantics.
          • The inverse abort-marker machinery and agent-specific fatal wrappers are gone.
          • Specifications and discriminating tests describe the simplified model.

          Activity

          Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

          Metadata

          Metadata

          Assignees

          No one assigned

            Labels

            No labels
            No labels

            Projects

            No projects

              Milestone

              No milestone

              Relationships

              None yet

              Development

              No branches or pull requests

              Issue actions

              , '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 > 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

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

              Description

              @taras

              Purpose

              Make ordinary function-component failures follow Effection's normal fail-fast
              semantics by default. Continuing after such a failure becomes an explicit,
              scope-local choice expressed by <CollectFailures> or collectFailures(fn).

              This replaces the current inverse model, where the function-component boundary
              turns ordinary body and teardown failures into ErrorSegments unless the
              function or failure carries an internal abort marker.

              Product contract

              Default behavior

              An ordinary failure from a function component fails its current Effection
              operation by default:

              • the complete component invocation finishes teardown before the failure leaves
                the boundary;
              • an Error propagates by identity, preserving its type and cause;
              • a non-Error thrown value is normalized to an Error whose cause is the
                exact original value;
              • a body failure combined with teardown failure propagates as the complete
                aggregate produced by withInvocation();
              • later siblings do not execute unless an explicit collection boundary handles
                the failure.

              Failing an Effection operation is not an unconditional process crash. The
              structured scope unwinds and releases its resources, and an enclosing operation
              may still recover deliberately.

              Failure-handling middleware

              Add one public contextual component operation with this effective contract:

              exportinterfaceComponentFailure{readonlyname: string;readonlyposition?: SourcePosition;readonlyerror: Error;}interfaceComponentApi{handleFailure(failure: ComponentFailure): Operation<ErrorSegment>;}

              The final spelling may change only to avoid a collision with an existing
              repository convention; do not replace this with process-global hooks, events,
              or mutable registration metadata.

              The default implementation throws failure.error. The engine calls it only
              for an ordinary function-component invocation failure and only after
              withInvocation() has produced the complete body-and-teardown outcome.

              The standard collection middleware converts the failure into one component
              diagnostic, attributes the original failure as its cause, and reports it once
              through Component.raise. It is terminal: it handles rather than delegates.

              The following are classified before handleFailure and are never downgraded by
              the standard collector:

              • durability, stale-input, and replay-divergence failures;
              • a DocumentationError already selected by the caller's throwing policy;
              • the private content-failure transport that restores already-reported
                ErrorSegments;
              • established schema-validation diagnostics that already have a structured
                document representation.

              Component.raise remains the single observation point for an ErrorSegment.
              handleFailure handles an operation failure; it does not replace or duplicate
              error observation.

              collectFailures(fn)

              Export this public decorator from @executablemd/core:

              exportfunctioncollectFailures<TextendsFunctionComponent>(component: T): T;

              Usage:

              exportdefaultcollectFailures(function*WebForm(props){// body, requested content, retained work, and teardown are inside the// collection boundary});

              The decorator records the exact function identity and returns that same
              function object with its type preserved. It does not create a delegating
              generator and does not catch inside the component body.

              When the engine invokes a marked function, it installs the standard collection
              middleware outside the entire withInvocation() call. This placement is
              required: middleware installed from inside the component would disappear
              before invocation teardown could fail. Installing outside also makes the
              handler available to nested components and projected content whose scopes are
              created by that invocation.

              The marker is function identity, never component name. A repository component
              with the same name as a marked registered component does not inherit its
              collection behavior.

              <CollectFailures>

              Add <CollectFailures> as a reserved engine-owned control construct. It accepts
              no props and expands its content in the caller's ordinary expansion frame while
              the same standard collection middleware is installed. It preserves structured
              segments rather than rendering content to a string.

              <CollectFailures>
              <MayFail />
              <StillRuns />
              </CollectFailures>

              Both forms establish the same dynamic scope: the nearest collection boundary
              handles ordinary failures from the enclosed invocation tree.

              Collection converts ordinary failures into ErrorSegments; it does not change
              the ambient ErrorPolicy. The caller still settles the segment normally:

              • under collect (the root and <Output>), the diagnostic renders and later
                work continues;
              • under throw (documentation), the reported segment becomes a
                DocumentationError and execution stops.

              Do not add a second "already handled" bit or make collection silently override
              an enclosing throwing policy.

              Structured-concurrency invariants

              • withInvocation() and its ordered teardown remain the component lifetime
                boundary.
              • Failure handling runs after projected content, the component body, and
                retained invocation resources have all stopped in the established order.
              • A collector never reports success before teardown completes.
              • If body and teardown both fail, collection reports one diagnostic whose
                cause is the complete aggregate, not only its first member.
              • Cancellation is not converted into a document diagnostic.
              • A collector does not retry an invocation.

              Required cleanup

              Remove the mechanisms that exist only because collection is currently the
              default:

              • abortOrdinaryComponentFailures() and
                abortsOrdinaryComponentFailures();
              • the function-identity abort weak set;
              • componentAbort(), its error-identity weak set, and the aborted-failure arm
                of fatalCause();
              • hasOrdinaryFailure() and its ordinary-leaf traversal;
              • the agent fatally() wrappers and explicit componentAbort() calls;
              • the special decoration of AgentProvider;
              • tests and specification text that describe ordinary collection with fatality
                as an opt-in exception.

              Agent components simply throw when their existing contracts say the document
              must stop. They remain fail-fast because that is now the default. Prompt
              failures that are deliberately recorded and returned remain unchanged.

              Keep and update rather than remove:

              • withInvocation() and InvocationTeardownError;
              • durability-failure recognition and its precedence;
              • AmbientErrorPolicy, settle(), and DocumentationError;
              • Component.raise and exactly-once observation;
              • cause attribution for a failure deliberately converted to an ErrorSegment;
              • ContentError, tryContent(), and the private content transport;
              • consumer-boundary settlement between a component and its caller.

              Put the public decorator and its private exact-function marker in a focused
              module such as src/component-failures.ts; do not grow errors.ts with the
              inverse of the machinery being removed.

              Behavior matrix and tests

              Add discriminating tests for all of the following:

              1. An unmarked repository TypeScript component throws an Error: execution is
                Err with the exact object, later siblings do not run, and no diagnostic is
                rendered.
              2. An unmarked registered component has the same behavior.
              3. An unmarked component's teardown throws only after its body returns: teardown
                finishes and the exact teardown failure propagates.
              4. Body and teardown both fail: the complete ordered aggregate propagates.
              5. A non-Error throw is normalized with the exact thrown value in cause.
              6. collectFailures(fn) converts an ordinary body failure into exactly one
                observed ErrorSegment under collect, preserves the original failure as
                cause, and later siblings run.
              7. The decorator collects a teardown-only failure after teardown completes.
              8. The decorator collects the complete body-plus-teardown aggregate as one
                diagnostic with the aggregate in cause.
              9. <CollectFailures> handles a direct child failure and continues to a later
                child under collect.
              10. Both forms reach failures in nested components and in content projected
                through content() without duplicate handling or observation.
              11. The nearest nested collection boundary handles once.
              12. Under an ambient throwing policy, both forms report once and the resulting
                DocumentationError still stops execution.
              13. Neither form collects durability/replay divergence.
              14. Uncaught ContentError still restores the original segments without
                reporting them again; explicit content recovery remains recovery.
              15. Input- and return-schema behavior remains in its existing structured
                diagnostic channel.
              16. A repository component named like a marked registered component remains
                unmarked and fail-fast.
              17. Existing Agent, observation, capture, projection, invocation-failure, and
                replay suites remain green after obsolete marker cases are removed.

              Tests must distinguish successful operation completion containing a rendered
              diagnostic from failed operation completion; checking output text alone is not
              sufficient.

              Specification updates

              Update the executable MDX and component API specifications in present tense:

              • ordinary function-component operation failures propagate by default;
              • error collection is an explicit dynamic boundary;
              • handleFailure runs after complete invocation teardown;
              • Component.raise observation and operation-failure handling are distinct;
              • <CollectFailures> is reserved engine syntax;
              • collectFailures(fn) is keyed by exact function identity;
              • collection remains subordinate to the caller's ambient settlement policy;
              • durability failures and cancellation are not collectable diagnostics.

              Update #202's behavior-preservation account where it describes the private
              agent abort marker. This issue deliberately replaces that implementation while
              preserving the user-visible fail-fast behavior of Agent components.

              Sequencing

              1. Land this issue as one focused core semantic-refactor PR based on current
                main. Do not stack it on 🐛 Report a <Testing> boundary only once its body has finished #247.
              2. Keep 🐛 Report a <Testing> boundary only once its body has finished #247 open. After this issue lands, rebase 🐛 Report a <Testing> boundary only once its body has finished #247 and replace TB1's custom
                Component.raise throwing middleware with a real ordinary child component
                failure. Assert exact failure identity, absence of later output, and no
                testing boundary observation or journal entry. The ordering fix then lands
                on the new default.
              3. Continue Add a bundled local WebForm component for schema-backed user input #195 WebForm transport/server/browser work in parallel. Its public
                registered <WebForm> integration waits for this issue and chooses
                collectFailures(WebForm) only if the component contract explicitly intends
                recoverable rendered diagnostics. Do not inherit collection accidentally.
              4. Resume the remaining Register function components and migrate legacy component handlers #202 migrations after the semantic-refactor PR lands.
                Each migrated component is fail-fast unless its approved behavior requires
                an explicit collection boundary.
              5. Provider-neutral <Elicit> (Add provider-neutral Elicit through the core Context API #197) remains after the WebForm provider
                contract and the corrected Testing/error semantics are ready.

              Non-goals

              Verification

              deno task fmt
              deno task lint
              deno task check
              deno task test
              deno task check:jsr
              pnpm exec tsc --project tsconfig.node.json --noEmit
              deno task build
              ./dist/xmd test packages/core/src --raw
              git diff --check

              All hosted checks must be green. A recurring failure from the independent TD8
              daemon-liveness defect is tracked in #248 and must not be fixed in this PR.

              Acceptance criteria

              • Ordinary function-component body and teardown failures propagate by default
                after complete cleanup.
              • Component.handleFailure is the one contextual handling seam for ordinary
                invocation failures.
              • <CollectFailures> and collectFailures(fn) install the same standard
                collection middleware only within their explicit dynamic scopes.
              • Collected failures are observed once, retain the complete original failure as
                cause, and remain subject to the caller's ambient settlement policy.
              • Durability failures, cancellation, content transport, and established schema
                diagnostics retain their distinct semantics.
              • The inverse abort-marker machinery and agent-specific fatal wrappers are gone.
              • Specifications and discriminating tests describe the simplified model.

              Activity

              Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

              Metadata

              Metadata

              Assignees

              No one assigned

                Labels

                No labels
                No labels

                Projects

                No projects

                  Milestone

                  No milestone

                  Relationships

                  None yet

                  Development

                  No branches or pull requests

                  Issue actions

                  , '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

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

                  Description

                  @taras

                  Purpose

                  Make ordinary function-component failures follow Effection's normal fail-fast
                  semantics by default. Continuing after such a failure becomes an explicit,
                  scope-local choice expressed by <CollectFailures> or collectFailures(fn).

                  This replaces the current inverse model, where the function-component boundary
                  turns ordinary body and teardown failures into ErrorSegments unless the
                  function or failure carries an internal abort marker.

                  Product contract

                  Default behavior

                  An ordinary failure from a function component fails its current Effection
                  operation by default:

                  • the complete component invocation finishes teardown before the failure leaves
                    the boundary;
                  • an Error propagates by identity, preserving its type and cause;
                  • a non-Error thrown value is normalized to an Error whose cause is the
                    exact original value;
                  • a body failure combined with teardown failure propagates as the complete
                    aggregate produced by withInvocation();
                  • later siblings do not execute unless an explicit collection boundary handles
                    the failure.

                  Failing an Effection operation is not an unconditional process crash. The
                  structured scope unwinds and releases its resources, and an enclosing operation
                  may still recover deliberately.

                  Failure-handling middleware

                  Add one public contextual component operation with this effective contract:

                  exportinterfaceComponentFailure{readonlyname: string;readonlyposition?: SourcePosition;readonlyerror: Error;}interfaceComponentApi{handleFailure(failure: ComponentFailure): Operation<ErrorSegment>;}

                  The final spelling may change only to avoid a collision with an existing
                  repository convention; do not replace this with process-global hooks, events,
                  or mutable registration metadata.

                  The default implementation throws failure.error. The engine calls it only
                  for an ordinary function-component invocation failure and only after
                  withInvocation() has produced the complete body-and-teardown outcome.

                  The standard collection middleware converts the failure into one component
                  diagnostic, attributes the original failure as its cause, and reports it once
                  through Component.raise. It is terminal: it handles rather than delegates.

                  The following are classified before handleFailure and are never downgraded by
                  the standard collector:

                  • durability, stale-input, and replay-divergence failures;
                  • a DocumentationError already selected by the caller's throwing policy;
                  • the private content-failure transport that restores already-reported
                    ErrorSegments;
                  • established schema-validation diagnostics that already have a structured
                    document representation.

                  Component.raise remains the single observation point for an ErrorSegment.
                  handleFailure handles an operation failure; it does not replace or duplicate
                  error observation.

                  collectFailures(fn)

                  Export this public decorator from @executablemd/core:

                  exportfunctioncollectFailures<TextendsFunctionComponent>(component: T): T;

                  Usage:

                  exportdefaultcollectFailures(function*WebForm(props){// body, requested content, retained work, and teardown are inside the// collection boundary});

                  The decorator records the exact function identity and returns that same
                  function object with its type preserved. It does not create a delegating
                  generator and does not catch inside the component body.

                  When the engine invokes a marked function, it installs the standard collection
                  middleware outside the entire withInvocation() call. This placement is
                  required: middleware installed from inside the component would disappear
                  before invocation teardown could fail. Installing outside also makes the
                  handler available to nested components and projected content whose scopes are
                  created by that invocation.

                  The marker is function identity, never component name. A repository component
                  with the same name as a marked registered component does not inherit its
                  collection behavior.

                  <CollectFailures>

                  Add <CollectFailures> as a reserved engine-owned control construct. It accepts
                  no props and expands its content in the caller's ordinary expansion frame while
                  the same standard collection middleware is installed. It preserves structured
                  segments rather than rendering content to a string.

                  <CollectFailures>
                  <MayFail />
                  <StillRuns />
                  </CollectFailures>

                  Both forms establish the same dynamic scope: the nearest collection boundary
                  handles ordinary failures from the enclosed invocation tree.

                  Collection converts ordinary failures into ErrorSegments; it does not change
                  the ambient ErrorPolicy. The caller still settles the segment normally:

                  • under collect (the root and <Output>), the diagnostic renders and later
                    work continues;
                  • under throw (documentation), the reported segment becomes a
                    DocumentationError and execution stops.

                  Do not add a second "already handled" bit or make collection silently override
                  an enclosing throwing policy.

                  Structured-concurrency invariants

                  • withInvocation() and its ordered teardown remain the component lifetime
                    boundary.
                  • Failure handling runs after projected content, the component body, and
                    retained invocation resources have all stopped in the established order.
                  • A collector never reports success before teardown completes.
                  • If body and teardown both fail, collection reports one diagnostic whose
                    cause is the complete aggregate, not only its first member.
                  • Cancellation is not converted into a document diagnostic.
                  • A collector does not retry an invocation.

                  Required cleanup

                  Remove the mechanisms that exist only because collection is currently the
                  default:

                  • abortOrdinaryComponentFailures() and
                    abortsOrdinaryComponentFailures();
                  • the function-identity abort weak set;
                  • componentAbort(), its error-identity weak set, and the aborted-failure arm
                    of fatalCause();
                  • hasOrdinaryFailure() and its ordinary-leaf traversal;
                  • the agent fatally() wrappers and explicit componentAbort() calls;
                  • the special decoration of AgentProvider;
                  • tests and specification text that describe ordinary collection with fatality
                    as an opt-in exception.

                  Agent components simply throw when their existing contracts say the document
                  must stop. They remain fail-fast because that is now the default. Prompt
                  failures that are deliberately recorded and returned remain unchanged.

                  Keep and update rather than remove:

                  • withInvocation() and InvocationTeardownError;
                  • durability-failure recognition and its precedence;
                  • AmbientErrorPolicy, settle(), and DocumentationError;
                  • Component.raise and exactly-once observation;
                  • cause attribution for a failure deliberately converted to an ErrorSegment;
                  • ContentError, tryContent(), and the private content transport;
                  • consumer-boundary settlement between a component and its caller.

                  Put the public decorator and its private exact-function marker in a focused
                  module such as src/component-failures.ts; do not grow errors.ts with the
                  inverse of the machinery being removed.

                  Behavior matrix and tests

                  Add discriminating tests for all of the following:

                  1. An unmarked repository TypeScript component throws an Error: execution is
                    Err with the exact object, later siblings do not run, and no diagnostic is
                    rendered.
                  2. An unmarked registered component has the same behavior.
                  3. An unmarked component's teardown throws only after its body returns: teardown
                    finishes and the exact teardown failure propagates.
                  4. Body and teardown both fail: the complete ordered aggregate propagates.
                  5. A non-Error throw is normalized with the exact thrown value in cause.
                  6. collectFailures(fn) converts an ordinary body failure into exactly one
                    observed ErrorSegment under collect, preserves the original failure as
                    cause, and later siblings run.
                  7. The decorator collects a teardown-only failure after teardown completes.
                  8. The decorator collects the complete body-plus-teardown aggregate as one
                    diagnostic with the aggregate in cause.
                  9. <CollectFailures> handles a direct child failure and continues to a later
                    child under collect.
                  10. Both forms reach failures in nested components and in content projected
                    through content() without duplicate handling or observation.
                  11. The nearest nested collection boundary handles once.
                  12. Under an ambient throwing policy, both forms report once and the resulting
                    DocumentationError still stops execution.
                  13. Neither form collects durability/replay divergence.
                  14. Uncaught ContentError still restores the original segments without
                    reporting them again; explicit content recovery remains recovery.
                  15. Input- and return-schema behavior remains in its existing structured
                    diagnostic channel.
                  16. A repository component named like a marked registered component remains
                    unmarked and fail-fast.
                  17. Existing Agent, observation, capture, projection, invocation-failure, and
                    replay suites remain green after obsolete marker cases are removed.

                  Tests must distinguish successful operation completion containing a rendered
                  diagnostic from failed operation completion; checking output text alone is not
                  sufficient.

                  Specification updates

                  Update the executable MDX and component API specifications in present tense:

                  • ordinary function-component operation failures propagate by default;
                  • error collection is an explicit dynamic boundary;
                  • handleFailure runs after complete invocation teardown;
                  • Component.raise observation and operation-failure handling are distinct;
                  • <CollectFailures> is reserved engine syntax;
                  • collectFailures(fn) is keyed by exact function identity;
                  • collection remains subordinate to the caller's ambient settlement policy;
                  • durability failures and cancellation are not collectable diagnostics.

                  Update #202's behavior-preservation account where it describes the private
                  agent abort marker. This issue deliberately replaces that implementation while
                  preserving the user-visible fail-fast behavior of Agent components.

                  Sequencing

                  1. Land this issue as one focused core semantic-refactor PR based on current
                    main. Do not stack it on 🐛 Report a <Testing> boundary only once its body has finished #247.
                  2. Keep 🐛 Report a <Testing> boundary only once its body has finished #247 open. After this issue lands, rebase 🐛 Report a <Testing> boundary only once its body has finished #247 and replace TB1's custom
                    Component.raise throwing middleware with a real ordinary child component
                    failure. Assert exact failure identity, absence of later output, and no
                    testing boundary observation or journal entry. The ordering fix then lands
                    on the new default.
                  3. Continue Add a bundled local WebForm component for schema-backed user input #195 WebForm transport/server/browser work in parallel. Its public
                    registered <WebForm> integration waits for this issue and chooses
                    collectFailures(WebForm) only if the component contract explicitly intends
                    recoverable rendered diagnostics. Do not inherit collection accidentally.
                  4. Resume the remaining Register function components and migrate legacy component handlers #202 migrations after the semantic-refactor PR lands.
                    Each migrated component is fail-fast unless its approved behavior requires
                    an explicit collection boundary.
                  5. Provider-neutral <Elicit> (Add provider-neutral Elicit through the core Context API #197) remains after the WebForm provider
                    contract and the corrected Testing/error semantics are ready.

                  Non-goals

                  Verification

                  deno task fmt
                  deno task lint
                  deno task check
                  deno task test
                  deno task check:jsr
                  pnpm exec tsc --project tsconfig.node.json --noEmit
                  deno task build
                  ./dist/xmd test packages/core/src --raw
                  git diff --check

                  All hosted checks must be green. A recurring failure from the independent TD8
                  daemon-liveness defect is tracked in #248 and must not be fixed in this PR.

                  Acceptance criteria

                  • Ordinary function-component body and teardown failures propagate by default
                    after complete cleanup.
                  • Component.handleFailure is the one contextual handling seam for ordinary
                    invocation failures.
                  • <CollectFailures> and collectFailures(fn) install the same standard
                    collection middleware only within their explicit dynamic scopes.
                  • Collected failures are observed once, retain the complete original failure as
                    cause, and remain subject to the caller's ambient settlement policy.
                  • Durability failures, cancellation, content transport, and established schema
                    diagnostics retain their distinct semantics.
                  • The inverse abort-marker machinery and agent-specific fatal wrappers are gone.
                  • Specifications and discriminating tests describe the simplified model.

                  Activity

                  Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

                  Metadata

                  Metadata

                  Assignees

                  No one assigned

                    Labels

                    No labels
                    No labels

                    Projects

                    No projects

                      Milestone

                      No milestone

                      Relationships

                      None yet

                      Development

                      No branches or pull requests

                      Issue actions

                      , '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

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

                      Description

                      @taras

                      Purpose

                      Make ordinary function-component failures follow Effection's normal fail-fast
                      semantics by default. Continuing after such a failure becomes an explicit,
                      scope-local choice expressed by <CollectFailures> or collectFailures(fn).

                      This replaces the current inverse model, where the function-component boundary
                      turns ordinary body and teardown failures into ErrorSegments unless the
                      function or failure carries an internal abort marker.

                      Product contract

                      Default behavior

                      An ordinary failure from a function component fails its current Effection
                      operation by default:

                      • the complete component invocation finishes teardown before the failure leaves
                        the boundary;
                      • an Error propagates by identity, preserving its type and cause;
                      • a non-Error thrown value is normalized to an Error whose cause is the
                        exact original value;
                      • a body failure combined with teardown failure propagates as the complete
                        aggregate produced by withInvocation();
                      • later siblings do not execute unless an explicit collection boundary handles
                        the failure.

                      Failing an Effection operation is not an unconditional process crash. The
                      structured scope unwinds and releases its resources, and an enclosing operation
                      may still recover deliberately.

                      Failure-handling middleware

                      Add one public contextual component operation with this effective contract:

                      exportinterfaceComponentFailure{readonlyname: string;readonlyposition?: SourcePosition;readonlyerror: Error;}interfaceComponentApi{handleFailure(failure: ComponentFailure): Operation<ErrorSegment>;}

                      The final spelling may change only to avoid a collision with an existing
                      repository convention; do not replace this with process-global hooks, events,
                      or mutable registration metadata.

                      The default implementation throws failure.error. The engine calls it only
                      for an ordinary function-component invocation failure and only after
                      withInvocation() has produced the complete body-and-teardown outcome.

                      The standard collection middleware converts the failure into one component
                      diagnostic, attributes the original failure as its cause, and reports it once
                      through Component.raise. It is terminal: it handles rather than delegates.

                      The following are classified before handleFailure and are never downgraded by
                      the standard collector:

                      • durability, stale-input, and replay-divergence failures;
                      • a DocumentationError already selected by the caller's throwing policy;
                      • the private content-failure transport that restores already-reported
                        ErrorSegments;
                      • established schema-validation diagnostics that already have a structured
                        document representation.

                      Component.raise remains the single observation point for an ErrorSegment.
                      handleFailure handles an operation failure; it does not replace or duplicate
                      error observation.

                      collectFailures(fn)

                      Export this public decorator from @executablemd/core:

                      exportfunctioncollectFailures<TextendsFunctionComponent>(component: T): T;

                      Usage:

                      exportdefaultcollectFailures(function*WebForm(props){// body, requested content, retained work, and teardown are inside the// collection boundary});

                      The decorator records the exact function identity and returns that same
                      function object with its type preserved. It does not create a delegating
                      generator and does not catch inside the component body.

                      When the engine invokes a marked function, it installs the standard collection
                      middleware outside the entire withInvocation() call. This placement is
                      required: middleware installed from inside the component would disappear
                      before invocation teardown could fail. Installing outside also makes the
                      handler available to nested components and projected content whose scopes are
                      created by that invocation.

                      The marker is function identity, never component name. A repository component
                      with the same name as a marked registered component does not inherit its
                      collection behavior.

                      <CollectFailures>

                      Add <CollectFailures> as a reserved engine-owned control construct. It accepts
                      no props and expands its content in the caller's ordinary expansion frame while
                      the same standard collection middleware is installed. It preserves structured
                      segments rather than rendering content to a string.

                      <CollectFailures>
                      <MayFail />
                      <StillRuns />
                      </CollectFailures>

                      Both forms establish the same dynamic scope: the nearest collection boundary
                      handles ordinary failures from the enclosed invocation tree.

                      Collection converts ordinary failures into ErrorSegments; it does not change
                      the ambient ErrorPolicy. The caller still settles the segment normally:

                      • under collect (the root and <Output>), the diagnostic renders and later
                        work continues;
                      • under throw (documentation), the reported segment becomes a
                        DocumentationError and execution stops.

                      Do not add a second "already handled" bit or make collection silently override
                      an enclosing throwing policy.

                      Structured-concurrency invariants

                      • withInvocation() and its ordered teardown remain the component lifetime
                        boundary.
                      • Failure handling runs after projected content, the component body, and
                        retained invocation resources have all stopped in the established order.
                      • A collector never reports success before teardown completes.
                      • If body and teardown both fail, collection reports one diagnostic whose
                        cause is the complete aggregate, not only its first member.
                      • Cancellation is not converted into a document diagnostic.
                      • A collector does not retry an invocation.

                      Required cleanup

                      Remove the mechanisms that exist only because collection is currently the
                      default:

                      • abortOrdinaryComponentFailures() and
                        abortsOrdinaryComponentFailures();
                      • the function-identity abort weak set;
                      • componentAbort(), its error-identity weak set, and the aborted-failure arm
                        of fatalCause();
                      • hasOrdinaryFailure() and its ordinary-leaf traversal;
                      • the agent fatally() wrappers and explicit componentAbort() calls;
                      • the special decoration of AgentProvider;
                      • tests and specification text that describe ordinary collection with fatality
                        as an opt-in exception.

                      Agent components simply throw when their existing contracts say the document
                      must stop. They remain fail-fast because that is now the default. Prompt
                      failures that are deliberately recorded and returned remain unchanged.

                      Keep and update rather than remove:

                      • withInvocation() and InvocationTeardownError;
                      • durability-failure recognition and its precedence;
                      • AmbientErrorPolicy, settle(), and DocumentationError;
                      • Component.raise and exactly-once observation;
                      • cause attribution for a failure deliberately converted to an ErrorSegment;
                      • ContentError, tryContent(), and the private content transport;
                      • consumer-boundary settlement between a component and its caller.

                      Put the public decorator and its private exact-function marker in a focused
                      module such as src/component-failures.ts; do not grow errors.ts with the
                      inverse of the machinery being removed.

                      Behavior matrix and tests

                      Add discriminating tests for all of the following:

                      1. An unmarked repository TypeScript component throws an Error: execution is
                        Err with the exact object, later siblings do not run, and no diagnostic is
                        rendered.
                      2. An unmarked registered component has the same behavior.
                      3. An unmarked component's teardown throws only after its body returns: teardown
                        finishes and the exact teardown failure propagates.
                      4. Body and teardown both fail: the complete ordered aggregate propagates.
                      5. A non-Error throw is normalized with the exact thrown value in cause.
                      6. collectFailures(fn) converts an ordinary body failure into exactly one
                        observed ErrorSegment under collect, preserves the original failure as
                        cause, and later siblings run.
                      7. The decorator collects a teardown-only failure after teardown completes.
                      8. The decorator collects the complete body-plus-teardown aggregate as one
                        diagnostic with the aggregate in cause.
                      9. <CollectFailures> handles a direct child failure and continues to a later
                        child under collect.
                      10. Both forms reach failures in nested components and in content projected
                        through content() without duplicate handling or observation.
                      11. The nearest nested collection boundary handles once.
                      12. Under an ambient throwing policy, both forms report once and the resulting
                        DocumentationError still stops execution.
                      13. Neither form collects durability/replay divergence.
                      14. Uncaught ContentError still restores the original segments without
                        reporting them again; explicit content recovery remains recovery.
                      15. Input- and return-schema behavior remains in its existing structured
                        diagnostic channel.
                      16. A repository component named like a marked registered component remains
                        unmarked and fail-fast.
                      17. Existing Agent, observation, capture, projection, invocation-failure, and
                        replay suites remain green after obsolete marker cases are removed.

                      Tests must distinguish successful operation completion containing a rendered
                      diagnostic from failed operation completion; checking output text alone is not
                      sufficient.

                      Specification updates

                      Update the executable MDX and component API specifications in present tense:

                      • ordinary function-component operation failures propagate by default;
                      • error collection is an explicit dynamic boundary;
                      • handleFailure runs after complete invocation teardown;
                      • Component.raise observation and operation-failure handling are distinct;
                      • <CollectFailures> is reserved engine syntax;
                      • collectFailures(fn) is keyed by exact function identity;
                      • collection remains subordinate to the caller's ambient settlement policy;
                      • durability failures and cancellation are not collectable diagnostics.

                      Update #202's behavior-preservation account where it describes the private
                      agent abort marker. This issue deliberately replaces that implementation while
                      preserving the user-visible fail-fast behavior of Agent components.

                      Sequencing

                      1. Land this issue as one focused core semantic-refactor PR based on current
                        main. Do not stack it on 🐛 Report a <Testing> boundary only once its body has finished #247.
                      2. Keep 🐛 Report a <Testing> boundary only once its body has finished #247 open. After this issue lands, rebase 🐛 Report a <Testing> boundary only once its body has finished #247 and replace TB1's custom
                        Component.raise throwing middleware with a real ordinary child component
                        failure. Assert exact failure identity, absence of later output, and no
                        testing boundary observation or journal entry. The ordering fix then lands
                        on the new default.
                      3. Continue Add a bundled local WebForm component for schema-backed user input #195 WebForm transport/server/browser work in parallel. Its public
                        registered <WebForm> integration waits for this issue and chooses
                        collectFailures(WebForm) only if the component contract explicitly intends
                        recoverable rendered diagnostics. Do not inherit collection accidentally.
                      4. Resume the remaining Register function components and migrate legacy component handlers #202 migrations after the semantic-refactor PR lands.
                        Each migrated component is fail-fast unless its approved behavior requires
                        an explicit collection boundary.
                      5. Provider-neutral <Elicit> (Add provider-neutral Elicit through the core Context API #197) remains after the WebForm provider
                        contract and the corrected Testing/error semantics are ready.

                      Non-goals

                      Verification

                      deno task fmt
                      deno task lint
                      deno task check
                      deno task test
                      deno task check:jsr
                      pnpm exec tsc --project tsconfig.node.json --noEmit
                      deno task build
                      ./dist/xmd test packages/core/src --raw
                      git diff --check

                      All hosted checks must be green. A recurring failure from the independent TD8
                      daemon-liveness defect is tracked in #248 and must not be fixed in this PR.

                      Acceptance criteria

                      • Ordinary function-component body and teardown failures propagate by default
                        after complete cleanup.
                      • Component.handleFailure is the one contextual handling seam for ordinary
                        invocation failures.
                      • <CollectFailures> and collectFailures(fn) install the same standard
                        collection middleware only within their explicit dynamic scopes.
                      • Collected failures are observed once, retain the complete original failure as
                        cause, and remain subject to the caller's ambient settlement policy.
                      • Durability failures, cancellation, content transport, and established schema
                        diagnostics retain their distinct semantics.
                      • The inverse abort-marker machinery and agent-specific fatal wrappers are gone.
                      • Specifications and discriminating tests describe the simplified model.

                      Activity

                      Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

                      Metadata

                      Metadata

                      Assignees

                      No one assigned

                        Labels

                        No labels
                        No labels

                        Projects

                        No projects

                          Milestone

                          No milestone

                          Relationships

                          None yet

                          Development

                          No branches or pull requests

                          Issue actions

                          , '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

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

                          Description

                          @taras

                          Purpose

                          Make ordinary function-component failures follow Effection's normal fail-fast
                          semantics by default. Continuing after such a failure becomes an explicit,
                          scope-local choice expressed by <CollectFailures> or collectFailures(fn).

                          This replaces the current inverse model, where the function-component boundary
                          turns ordinary body and teardown failures into ErrorSegments unless the
                          function or failure carries an internal abort marker.

                          Product contract

                          Default behavior

                          An ordinary failure from a function component fails its current Effection
                          operation by default:

                          • the complete component invocation finishes teardown before the failure leaves
                            the boundary;
                          • an Error propagates by identity, preserving its type and cause;
                          • a non-Error thrown value is normalized to an Error whose cause is the
                            exact original value;
                          • a body failure combined with teardown failure propagates as the complete
                            aggregate produced by withInvocation();
                          • later siblings do not execute unless an explicit collection boundary handles
                            the failure.

                          Failing an Effection operation is not an unconditional process crash. The
                          structured scope unwinds and releases its resources, and an enclosing operation
                          may still recover deliberately.

                          Failure-handling middleware

                          Add one public contextual component operation with this effective contract:

                          exportinterfaceComponentFailure{readonlyname: string;readonlyposition?: SourcePosition;readonlyerror: Error;}interfaceComponentApi{handleFailure(failure: ComponentFailure): Operation<ErrorSegment>;}

                          The final spelling may change only to avoid a collision with an existing
                          repository convention; do not replace this with process-global hooks, events,
                          or mutable registration metadata.

                          The default implementation throws failure.error. The engine calls it only
                          for an ordinary function-component invocation failure and only after
                          withInvocation() has produced the complete body-and-teardown outcome.

                          The standard collection middleware converts the failure into one component
                          diagnostic, attributes the original failure as its cause, and reports it once
                          through Component.raise. It is terminal: it handles rather than delegates.

                          The following are classified before handleFailure and are never downgraded by
                          the standard collector:

                          • durability, stale-input, and replay-divergence failures;
                          • a DocumentationError already selected by the caller's throwing policy;
                          • the private content-failure transport that restores already-reported
                            ErrorSegments;
                          • established schema-validation diagnostics that already have a structured
                            document representation.

                          Component.raise remains the single observation point for an ErrorSegment.
                          handleFailure handles an operation failure; it does not replace or duplicate
                          error observation.

                          collectFailures(fn)

                          Export this public decorator from @executablemd/core:

                          exportfunctioncollectFailures<TextendsFunctionComponent>(component: T): T;

                          Usage:

                          exportdefaultcollectFailures(function*WebForm(props){// body, requested content, retained work, and teardown are inside the// collection boundary});

                          The decorator records the exact function identity and returns that same
                          function object with its type preserved. It does not create a delegating
                          generator and does not catch inside the component body.

                          When the engine invokes a marked function, it installs the standard collection
                          middleware outside the entire withInvocation() call. This placement is
                          required: middleware installed from inside the component would disappear
                          before invocation teardown could fail. Installing outside also makes the
                          handler available to nested components and projected content whose scopes are
                          created by that invocation.

                          The marker is function identity, never component name. A repository component
                          with the same name as a marked registered component does not inherit its
                          collection behavior.

                          <CollectFailures>

                          Add <CollectFailures> as a reserved engine-owned control construct. It accepts
                          no props and expands its content in the caller's ordinary expansion frame while
                          the same standard collection middleware is installed. It preserves structured
                          segments rather than rendering content to a string.

                          <CollectFailures>
                          <MayFail />
                          <StillRuns />
                          </CollectFailures>

                          Both forms establish the same dynamic scope: the nearest collection boundary
                          handles ordinary failures from the enclosed invocation tree.

                          Collection converts ordinary failures into ErrorSegments; it does not change
                          the ambient ErrorPolicy. The caller still settles the segment normally:

                          • under collect (the root and <Output>), the diagnostic renders and later
                            work continues;
                          • under throw (documentation), the reported segment becomes a
                            DocumentationError and execution stops.

                          Do not add a second "already handled" bit or make collection silently override
                          an enclosing throwing policy.

                          Structured-concurrency invariants

                          • withInvocation() and its ordered teardown remain the component lifetime
                            boundary.
                          • Failure handling runs after projected content, the component body, and
                            retained invocation resources have all stopped in the established order.
                          • A collector never reports success before teardown completes.
                          • If body and teardown both fail, collection reports one diagnostic whose
                            cause is the complete aggregate, not only its first member.
                          • Cancellation is not converted into a document diagnostic.
                          • A collector does not retry an invocation.

                          Required cleanup

                          Remove the mechanisms that exist only because collection is currently the
                          default:

                          • abortOrdinaryComponentFailures() and
                            abortsOrdinaryComponentFailures();
                          • the function-identity abort weak set;
                          • componentAbort(), its error-identity weak set, and the aborted-failure arm
                            of fatalCause();
                          • hasOrdinaryFailure() and its ordinary-leaf traversal;
                          • the agent fatally() wrappers and explicit componentAbort() calls;
                          • the special decoration of AgentProvider;
                          • tests and specification text that describe ordinary collection with fatality
                            as an opt-in exception.

                          Agent components simply throw when their existing contracts say the document
                          must stop. They remain fail-fast because that is now the default. Prompt
                          failures that are deliberately recorded and returned remain unchanged.

                          Keep and update rather than remove:

                          • withInvocation() and InvocationTeardownError;
                          • durability-failure recognition and its precedence;
                          • AmbientErrorPolicy, settle(), and DocumentationError;
                          • Component.raise and exactly-once observation;
                          • cause attribution for a failure deliberately converted to an ErrorSegment;
                          • ContentError, tryContent(), and the private content transport;
                          • consumer-boundary settlement between a component and its caller.

                          Put the public decorator and its private exact-function marker in a focused
                          module such as src/component-failures.ts; do not grow errors.ts with the
                          inverse of the machinery being removed.

                          Behavior matrix and tests

                          Add discriminating tests for all of the following:

                          1. An unmarked repository TypeScript component throws an Error: execution is
                            Err with the exact object, later siblings do not run, and no diagnostic is
                            rendered.
                          2. An unmarked registered component has the same behavior.
                          3. An unmarked component's teardown throws only after its body returns: teardown
                            finishes and the exact teardown failure propagates.
                          4. Body and teardown both fail: the complete ordered aggregate propagates.
                          5. A non-Error throw is normalized with the exact thrown value in cause.
                          6. collectFailures(fn) converts an ordinary body failure into exactly one
                            observed ErrorSegment under collect, preserves the original failure as
                            cause, and later siblings run.
                          7. The decorator collects a teardown-only failure after teardown completes.
                          8. The decorator collects the complete body-plus-teardown aggregate as one
                            diagnostic with the aggregate in cause.
                          9. <CollectFailures> handles a direct child failure and continues to a later
                            child under collect.
                          10. Both forms reach failures in nested components and in content projected
                            through content() without duplicate handling or observation.
                          11. The nearest nested collection boundary handles once.
                          12. Under an ambient throwing policy, both forms report once and the resulting
                            DocumentationError still stops execution.
                          13. Neither form collects durability/replay divergence.
                          14. Uncaught ContentError still restores the original segments without
                            reporting them again; explicit content recovery remains recovery.
                          15. Input- and return-schema behavior remains in its existing structured
                            diagnostic channel.
                          16. A repository component named like a marked registered component remains
                            unmarked and fail-fast.
                          17. Existing Agent, observation, capture, projection, invocation-failure, and
                            replay suites remain green after obsolete marker cases are removed.

                          Tests must distinguish successful operation completion containing a rendered
                          diagnostic from failed operation completion; checking output text alone is not
                          sufficient.

                          Specification updates

                          Update the executable MDX and component API specifications in present tense:

                          • ordinary function-component operation failures propagate by default;
                          • error collection is an explicit dynamic boundary;
                          • handleFailure runs after complete invocation teardown;
                          • Component.raise observation and operation-failure handling are distinct;
                          • <CollectFailures> is reserved engine syntax;
                          • collectFailures(fn) is keyed by exact function identity;
                          • collection remains subordinate to the caller's ambient settlement policy;
                          • durability failures and cancellation are not collectable diagnostics.

                          Update #202's behavior-preservation account where it describes the private
                          agent abort marker. This issue deliberately replaces that implementation while
                          preserving the user-visible fail-fast behavior of Agent components.

                          Sequencing

                          1. Land this issue as one focused core semantic-refactor PR based on current
                            main. Do not stack it on 🐛 Report a <Testing> boundary only once its body has finished #247.
                          2. Keep 🐛 Report a <Testing> boundary only once its body has finished #247 open. After this issue lands, rebase 🐛 Report a <Testing> boundary only once its body has finished #247 and replace TB1's custom
                            Component.raise throwing middleware with a real ordinary child component
                            failure. Assert exact failure identity, absence of later output, and no
                            testing boundary observation or journal entry. The ordering fix then lands
                            on the new default.
                          3. Continue Add a bundled local WebForm component for schema-backed user input #195 WebForm transport/server/browser work in parallel. Its public
                            registered <WebForm> integration waits for this issue and chooses
                            collectFailures(WebForm) only if the component contract explicitly intends
                            recoverable rendered diagnostics. Do not inherit collection accidentally.
                          4. Resume the remaining Register function components and migrate legacy component handlers #202 migrations after the semantic-refactor PR lands.
                            Each migrated component is fail-fast unless its approved behavior requires
                            an explicit collection boundary.
                          5. Provider-neutral <Elicit> (Add provider-neutral Elicit through the core Context API #197) remains after the WebForm provider
                            contract and the corrected Testing/error semantics are ready.

                          Non-goals

                          Verification

                          deno task fmt
                          deno task lint
                          deno task check
                          deno task test
                          deno task check:jsr
                          pnpm exec tsc --project tsconfig.node.json --noEmit
                          deno task build
                          ./dist/xmd test packages/core/src --raw
                          git diff --check

                          All hosted checks must be green. A recurring failure from the independent TD8
                          daemon-liveness defect is tracked in #248 and must not be fixed in this PR.

                          Acceptance criteria

                          • Ordinary function-component body and teardown failures propagate by default
                            after complete cleanup.
                          • Component.handleFailure is the one contextual handling seam for ordinary
                            invocation failures.
                          • <CollectFailures> and collectFailures(fn) install the same standard
                            collection middleware only within their explicit dynamic scopes.
                          • Collected failures are observed once, retain the complete original failure as
                            cause, and remain subject to the caller's ambient settlement policy.
                          • Durability failures, cancellation, content transport, and established schema
                            diagnostics retain their distinct semantics.
                          • The inverse abort-marker machinery and agent-specific fatal wrappers are gone.
                          • Specifications and discriminating tests describe the simplified model.

                          Activity

                          Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

                          Metadata

                          Metadata

                          Assignees

                          No one assigned

                            Labels

                            No labels
                            No labels

                            Projects

                            No projects

                              Milestone

                              No milestone

                              Relationships

                              None yet

                              Development

                              No branches or pull requests

                              Issue actions

                              , '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

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

                              Description

                              @taras

                              Purpose

                              Make ordinary function-component failures follow Effection's normal fail-fast
                              semantics by default. Continuing after such a failure becomes an explicit,
                              scope-local choice expressed by <CollectFailures> or collectFailures(fn).

                              This replaces the current inverse model, where the function-component boundary
                              turns ordinary body and teardown failures into ErrorSegments unless the
                              function or failure carries an internal abort marker.

                              Product contract

                              Default behavior

                              An ordinary failure from a function component fails its current Effection
                              operation by default:

                              • the complete component invocation finishes teardown before the failure leaves
                                the boundary;
                              • an Error propagates by identity, preserving its type and cause;
                              • a non-Error thrown value is normalized to an Error whose cause is the
                                exact original value;
                              • a body failure combined with teardown failure propagates as the complete
                                aggregate produced by withInvocation();
                              • later siblings do not execute unless an explicit collection boundary handles
                                the failure.

                              Failing an Effection operation is not an unconditional process crash. The
                              structured scope unwinds and releases its resources, and an enclosing operation
                              may still recover deliberately.

                              Failure-handling middleware

                              Add one public contextual component operation with this effective contract:

                              exportinterfaceComponentFailure{readonlyname: string;readonlyposition?: SourcePosition;readonlyerror: Error;}interfaceComponentApi{handleFailure(failure: ComponentFailure): Operation<ErrorSegment>;}

                              The final spelling may change only to avoid a collision with an existing
                              repository convention; do not replace this with process-global hooks, events,
                              or mutable registration metadata.

                              The default implementation throws failure.error. The engine calls it only
                              for an ordinary function-component invocation failure and only after
                              withInvocation() has produced the complete body-and-teardown outcome.

                              The standard collection middleware converts the failure into one component
                              diagnostic, attributes the original failure as its cause, and reports it once
                              through Component.raise. It is terminal: it handles rather than delegates.

                              The following are classified before handleFailure and are never downgraded by
                              the standard collector:

                              • durability, stale-input, and replay-divergence failures;
                              • a DocumentationError already selected by the caller's throwing policy;
                              • the private content-failure transport that restores already-reported
                                ErrorSegments;
                              • established schema-validation diagnostics that already have a structured
                                document representation.

                              Component.raise remains the single observation point for an ErrorSegment.
                              handleFailure handles an operation failure; it does not replace or duplicate
                              error observation.

                              collectFailures(fn)

                              Export this public decorator from @executablemd/core:

                              exportfunctioncollectFailures<TextendsFunctionComponent>(component: T): T;

                              Usage:

                              exportdefaultcollectFailures(function*WebForm(props){// body, requested content, retained work, and teardown are inside the// collection boundary});

                              The decorator records the exact function identity and returns that same
                              function object with its type preserved. It does not create a delegating
                              generator and does not catch inside the component body.

                              When the engine invokes a marked function, it installs the standard collection
                              middleware outside the entire withInvocation() call. This placement is
                              required: middleware installed from inside the component would disappear
                              before invocation teardown could fail. Installing outside also makes the
                              handler available to nested components and projected content whose scopes are
                              created by that invocation.

                              The marker is function identity, never component name. A repository component
                              with the same name as a marked registered component does not inherit its
                              collection behavior.

                              <CollectFailures>

                              Add <CollectFailures> as a reserved engine-owned control construct. It accepts
                              no props and expands its content in the caller's ordinary expansion frame while
                              the same standard collection middleware is installed. It preserves structured
                              segments rather than rendering content to a string.

                              <CollectFailures>
                              <MayFail />
                              <StillRuns />
                              </CollectFailures>

                              Both forms establish the same dynamic scope: the nearest collection boundary
                              handles ordinary failures from the enclosed invocation tree.

                              Collection converts ordinary failures into ErrorSegments; it does not change
                              the ambient ErrorPolicy. The caller still settles the segment normally:

                              • under collect (the root and <Output>), the diagnostic renders and later
                                work continues;
                              • under throw (documentation), the reported segment becomes a
                                DocumentationError and execution stops.

                              Do not add a second "already handled" bit or make collection silently override
                              an enclosing throwing policy.

                              Structured-concurrency invariants

                              • withInvocation() and its ordered teardown remain the component lifetime
                                boundary.
                              • Failure handling runs after projected content, the component body, and
                                retained invocation resources have all stopped in the established order.
                              • A collector never reports success before teardown completes.
                              • If body and teardown both fail, collection reports one diagnostic whose
                                cause is the complete aggregate, not only its first member.
                              • Cancellation is not converted into a document diagnostic.
                              • A collector does not retry an invocation.

                              Required cleanup

                              Remove the mechanisms that exist only because collection is currently the
                              default:

                              • abortOrdinaryComponentFailures() and
                                abortsOrdinaryComponentFailures();
                              • the function-identity abort weak set;
                              • componentAbort(), its error-identity weak set, and the aborted-failure arm
                                of fatalCause();
                              • hasOrdinaryFailure() and its ordinary-leaf traversal;
                              • the agent fatally() wrappers and explicit componentAbort() calls;
                              • the special decoration of AgentProvider;
                              • tests and specification text that describe ordinary collection with fatality
                                as an opt-in exception.

                              Agent components simply throw when their existing contracts say the document
                              must stop. They remain fail-fast because that is now the default. Prompt
                              failures that are deliberately recorded and returned remain unchanged.

                              Keep and update rather than remove:

                              • withInvocation() and InvocationTeardownError;
                              • durability-failure recognition and its precedence;
                              • AmbientErrorPolicy, settle(), and DocumentationError;
                              • Component.raise and exactly-once observation;
                              • cause attribution for a failure deliberately converted to an ErrorSegment;
                              • ContentError, tryContent(), and the private content transport;
                              • consumer-boundary settlement between a component and its caller.

                              Put the public decorator and its private exact-function marker in a focused
                              module such as src/component-failures.ts; do not grow errors.ts with the
                              inverse of the machinery being removed.

                              Behavior matrix and tests

                              Add discriminating tests for all of the following:

                              1. An unmarked repository TypeScript component throws an Error: execution is
                                Err with the exact object, later siblings do not run, and no diagnostic is
                                rendered.
                              2. An unmarked registered component has the same behavior.
                              3. An unmarked component's teardown throws only after its body returns: teardown
                                finishes and the exact teardown failure propagates.
                              4. Body and teardown both fail: the complete ordered aggregate propagates.
                              5. A non-Error throw is normalized with the exact thrown value in cause.
                              6. collectFailures(fn) converts an ordinary body failure into exactly one
                                observed ErrorSegment under collect, preserves the original failure as
                                cause, and later siblings run.
                              7. The decorator collects a teardown-only failure after teardown completes.
                              8. The decorator collects the complete body-plus-teardown aggregate as one
                                diagnostic with the aggregate in cause.
                              9. <CollectFailures> handles a direct child failure and continues to a later
                                child under collect.
                              10. Both forms reach failures in nested components and in content projected
                                through content() without duplicate handling or observation.
                              11. The nearest nested collection boundary handles once.
                              12. Under an ambient throwing policy, both forms report once and the resulting
                                DocumentationError still stops execution.
                              13. Neither form collects durability/replay divergence.
                              14. Uncaught ContentError still restores the original segments without
                                reporting them again; explicit content recovery remains recovery.
                              15. Input- and return-schema behavior remains in its existing structured
                                diagnostic channel.
                              16. A repository component named like a marked registered component remains
                                unmarked and fail-fast.
                              17. Existing Agent, observation, capture, projection, invocation-failure, and
                                replay suites remain green after obsolete marker cases are removed.

                              Tests must distinguish successful operation completion containing a rendered
                              diagnostic from failed operation completion; checking output text alone is not
                              sufficient.

                              Specification updates

                              Update the executable MDX and component API specifications in present tense:

                              • ordinary function-component operation failures propagate by default;
                              • error collection is an explicit dynamic boundary;
                              • handleFailure runs after complete invocation teardown;
                              • Component.raise observation and operation-failure handling are distinct;
                              • <CollectFailures> is reserved engine syntax;
                              • collectFailures(fn) is keyed by exact function identity;
                              • collection remains subordinate to the caller's ambient settlement policy;
                              • durability failures and cancellation are not collectable diagnostics.

                              Update #202's behavior-preservation account where it describes the private
                              agent abort marker. This issue deliberately replaces that implementation while
                              preserving the user-visible fail-fast behavior of Agent components.

                              Sequencing

                              1. Land this issue as one focused core semantic-refactor PR based on current
                                main. Do not stack it on 🐛 Report a <Testing> boundary only once its body has finished #247.
                              2. Keep 🐛 Report a <Testing> boundary only once its body has finished #247 open. After this issue lands, rebase 🐛 Report a <Testing> boundary only once its body has finished #247 and replace TB1's custom
                                Component.raise throwing middleware with a real ordinary child component
                                failure. Assert exact failure identity, absence of later output, and no
                                testing boundary observation or journal entry. The ordering fix then lands
                                on the new default.
                              3. Continue Add a bundled local WebForm component for schema-backed user input #195 WebForm transport/server/browser work in parallel. Its public
                                registered <WebForm> integration waits for this issue and chooses
                                collectFailures(WebForm) only if the component contract explicitly intends
                                recoverable rendered diagnostics. Do not inherit collection accidentally.
                              4. Resume the remaining Register function components and migrate legacy component handlers #202 migrations after the semantic-refactor PR lands.
                                Each migrated component is fail-fast unless its approved behavior requires
                                an explicit collection boundary.
                              5. Provider-neutral <Elicit> (Add provider-neutral Elicit through the core Context API #197) remains after the WebForm provider
                                contract and the corrected Testing/error semantics are ready.

                              Non-goals

                              Verification

                              deno task fmt
                              deno task lint
                              deno task check
                              deno task test
                              deno task check:jsr
                              pnpm exec tsc --project tsconfig.node.json --noEmit
                              deno task build
                              ./dist/xmd test packages/core/src --raw
                              git diff --check

                              All hosted checks must be green. A recurring failure from the independent TD8
                              daemon-liveness defect is tracked in #248 and must not be fixed in this PR.

                              Acceptance criteria

                              • Ordinary function-component body and teardown failures propagate by default
                                after complete cleanup.
                              • Component.handleFailure is the one contextual handling seam for ordinary
                                invocation failures.
                              • <CollectFailures> and collectFailures(fn) install the same standard
                                collection middleware only within their explicit dynamic scopes.
                              • Collected failures are observed once, retain the complete original failure as
                                cause, and remain subject to the caller's ambient settlement policy.
                              • Durability failures, cancellation, content transport, and established schema
                                diagnostics retain their distinct semantics.
                              • The inverse abort-marker machinery and agent-specific fatal wrappers are gone.
                              • Specifications and discriminating tests describe the simplified model.

                              Activity

                              Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

                              Metadata

                              Metadata

                              Assignees

                              No one assigned

                                Labels

                                No labels
                                No labels

                                Projects

                                No projects

                                  Milestone

                                  No milestone

                                  Relationships

                                  None yet

                                  Development

                                  No branches or pull requests

                                  Issue actions