Skip to content

💥 feat: make <Output> fail fast and rename CollectFailures to CaptureErrors - #312

Closed
taras wants to merge 6 commits into
mainfrom
feat/issue-309-output-fail-fast
Closed

💥 feat: make <Output> fail fast and rename CollectFailures to CaptureErrors#312
taras wants to merge 6 commits into
mainfrom
feat/issue-309-output-fail-fast

Conversation

@taras

@taras taras commented Aug 3, 2026

Copy link
Copy Markdown
Owner

Closes #309. Builds on #310.

What changed

<Output> is fail-fast. A region renders what it produced before a failure, then the execution fails; no later sibling, region, documentation block, or effect begins. Continuing is asked for explicitly and stays scope-local:

<Output>
  <CaptureErrors>
    ... work whose errors should render without aborting ...
  </CaptureErrors>
</Output>

Three settlement policies replace two (packages/core/src/errors.ts):

Policy Installed at Ordinary diagnostic Captured diagnostic
collect the default; roots/bodies with no <Output> renders, continues renders
output every <Output> region, root and component alike throws renders, region continues
throw documentation, value roots throws throws — hidden, nothing to render into

settle() is the only place that branches on the policy, so this is one function's decision.

Capture is remembered by identity. useFailures() installs a raise middleware that marks every segment raised beneath the boundary — including the structural ones the region reports itself, not just failures translated through handleFailure — on the original object, with no clone and no second observation. That mark is what lets a captured diagnostic cross an invocation boundary into a fail-fast caller and still render.

Output ownership. Preserving partial output meant saying who owns rendered segments: expansion writes into the accumulator its caller gave it (expandSegments' existing collect parameter, now owner), and a call site producing a binding, a value, or a string owns a private one. A failing <Capture as> or as= invocation therefore never promotes content the document was not going to render — asserted by OFF9, not assumed.

A failed document is a determined durable outcome. documentWorkflow catches an ordinary DocumentationError outside yield* scopedExpansion, so teardown and any aggregate wrapper are complete first, and returns {status: "err", output, error}. The root closes ok around it, so replay restores both halves without re-entering the workflow. durabilityFailure() is consulted first and still escapes; a failure before entering or after returning from durableRun still produces no close. A live run resolves the error it actually caught (sidecar keyed on the returned outcome); a replayed run reports the reconstruction the recorded fields describe — AggregateError when errors is present, Error otherwise. The replayed value is parsed field by field, never cast.

Rename (breaking)

<CollectFailures><CaptureErrors>, collectFailures()captureErrors(), collectsFailures()capturesErrors(), useFailureCollection()useFailures(), plus the diagnostic and structural helper names and the reservation entry. No aliases<CollectFailures> now resolves like any unknown component. The sweep for both old names returns nothing outside git history. Commit 1 is the rename alone, with a green gate, so it reviews separately from the semantics.

Tests

New packages/core/tests/output-fail-fast.test.ts (Tier OFF, 14 tests). Verified against 193da3c4 in a throwaway worktree, and the split matched the plan exactly:

Mutations run and confirmed: omitting the captured mark reddens OFF5d; treating output as throw reddens the capture tests. A third mutation — a policy downgrade inside the capture boundary — turned out to redden nothing, so that code was removed rather than kept unguarded.

Existing tests changed, and why

Each is #309's settled contract, not a test bent to fit:

Test Change
expand.test.ts "keeps errors inside an <Output> region as comments" split: ordinary → fails; captured → still a comment
expand.test.ts child/content errors inside parent <Output> moved under <CaptureErrors>, plus a new uncaptured-fails counterpart
execute.test.ts "emits no partial output…" inverted: the selection is emitted, and the run fails
eval-policy.test.ts O24, O31 moved under <CaptureErrors>; O29 restated to fail-fast
loop.test.ts LOOP44, LOOP46 root close is ok around the failed outcome

O29 also surfaced a pre-existing limit worth knowing: <Content /> is only claimed at a region's top level, so it cannot be nested inside <CaptureErrors> — the fixture stays unwrapped and asserts fail-fast.

Spec

§6.9 now distinguishes four situations (collecting root, fail-fast region, fail-fast documentation, captured-inside-region) with a table, and states the capture mark; §6.8.1 covers the marking; §10 gains 10.2.1, the root-close outcome contract with the exact persisted error fields; C43, E12 and decision 68 rewritten; Tier OFF rows added.

Verification

Runtimes asserted first (Deno 2.9.1, Node v22.23.2, Bun 1.3.14), one deno task setup, nothing reinstalled after:

Gate Result
deno task fmt / lint clean / 0 errors
deno task check pass
deno task test 328 passed, 0 failed
deno task check:jsr Success
tsc --project tsconfig.node.json pass
pnpm test:node 0 fail
bun run test:bun 2228 pass, 0 fail
Bun entrypoint smoke pass
deno task build + ./dist/xmd test packages/core/src --raw pass
./dist/xmd test smoke-test/README.md (CI's component dirs) 74/74
git diff --summary / --check no mode entry / clean

taras added 2 commits August 2, 2026 23:44
The construct names an explicit boundary where a document asks to carry on
past a failure. "Collect" described the mechanism; "capture errors" says
what an author is asking for, and the paired vocabulary now matches:
<CaptureErrors>, captureErrors(fn), capturesErrors(), useFailures().

The old tag and export are gone rather than aliased — <CollectFailures>
now resolves like any other unknown component.

Mechanical: no behavior changes.
`<Output>` collected failures and carried on. That is the wrong default for
an operational document: a failed preview could still reach a later
<Elicit> or a destructive publish step, and the document rendered as though
the stage had worked.

A region is now fail-fast. What it produced before the failure is kept and
emitted — usually the explanation an operator needs — and then the execution
fails. No later sibling, region, documentation block, or effect begins.

Continuing is asked for explicitly, and stays scope-local:

    <Output>
      <CaptureErrors>
        ... work whose errors should render without aborting ...
      </CaptureErrors>
    </Output>

Three settlement policies replace two. `collect` renders and continues, and
is still what a root without <Output> does. `output` throws an ordinary
diagnostic and returns one an explicit capture boundary handled. `throw`
ends the execution whatever the diagnostic is, because documentation is
hidden and a captured diagnostic there has nothing to render into.
`useFailures()` marks the segments raised beneath a boundary — by identity,
never a copy — which is how a captured diagnostic crosses an invocation
boundary into a fail-fast caller and still renders.

Preserving partial output required saying who owns rendered segments.
Expansion writes into the accumulator its caller gave it; a call site that
produces a binding, a value, or a string owns a private one instead, so a
failing `<Capture as>` or `as=` invocation never promotes content the
document was not going to render.

A failed document is now a determined durable outcome rather than an
escape: the root closes `ok` around `{status: "err", output, error}`, so a
replay restores the same partial output and the same failure without
re-entering the workflow. Durability failures still escape, and a failure
before entering or after returning from durableRun still produces no close.
A live run resolves the error it actually caught — same object, type, cause
and aggregate members — while a replayed run reports the reconstruction the
recorded fields describe.

Closes #309

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Found 1 redundant comment. Inline suggestions to remove them below.

Comment thread packages/core/src/expand.ts Outdated

return expanded;
// A rendering invocation already wrote into the caller's owner, so there is
// nothing left to hand back — its consumer settles what is now in place.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// nothing left to hand back — its consumer settles what is now in place.

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown

PR #312: 💥 feat: make fail fast and rename CollectFailures to CaptureErrors

37 files, +2307 / -333

Scope

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

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

🟡 37 files changed. Are all changes related?

🟡 PR mixes config and source changes.

Structural

🟡 Type declarations with no consumers: ExpandSegments.
Symbol Declared at Refs in diff Why flagged
ExpandSegments packages/core/src/answers.ts:92 1 referenced ≤1× within the added diff (pre-existing usages not counted)

Slop

  • packages/core/src/components/parse-schema.ts:99// long as this call.
  • packages/core/src/execute.ts:355// nowhere useful, and what this records is what this failure was given.
  • packages/core/src/execute.ts:559// selected; every streaming root has already emitted streamed.
  • packages/core/src/execute.ts:593// still leaves this frame holding what the regions rendered before it.
  • packages/core/src/execute.ts:654// an earlier segment went out.
  • packages/core/src/execute.ts:667// streaming loop emitted together with that tail.
  • packages/core/src/expand.ts:276// the region itself and the prefix is already where the document needs it.
  • packages/core/src/expand.ts:310// hand back; one that kept its own returns what it rendered.
  • packages/core/src/validate.ts:86// below run on every call precisely so that question cannot arise.

Static Analysis

✅ Oxlint found no issues.

Correctness

No extraneous code patterns detected.

…e journal

Three module-lifetime habits and two unfinished conversions, from review.

**No module-scoped registries.** `capturesErrors` and the captured-diagnostic
mark were WeakSets beside the objects they described: one table per process,
shared by every run, invisible to any scope. Both are now brands on the
object itself, under `Symbol.for` and non-enumerable — the component
function carries its own answer, and a captured diagnostic carries the
decision that was made about it, so the state lives and dies with the value
and stays out of rendering and serialization.

`local/no-module-scoped-weakset` makes that a rule rather than a habit. It
reports module-lifetime `new WeakSet()` — declared, exported, assigned
later, or held inside another module-scoped value — and accepts one created
inside a function or an operation. Confirmed reddening both declarations
before they were removed.

**Every visible producer writes into the region.** The selected `<If>`
branch, `<Loop>` iterations, a rendering `<Each>`, an answered `<Answers>`
body, and projected `<Content />` each built a private array and lost it
when nested expansion threw. They now write into the owner they are given
and return only what the caller must still append. The atomic paths are
unchanged and stay unchanged on purpose: `<Capture>`, `as`, string
projection, documentation, and value production keep private buffers that
are never merged, so a failure cannot promote content the document was not
going to render.

**The journal is parsed, not coerced.** A recorded failure with a
non-string `source` or `cause`, a non-list `errors`, or a member missing its
message is refused rather than silently read as absent — reporting a failure
that disagrees with the one recorded is worse than refusing to report. The
presence contract is back to what it describes: an absent own cause stays
absent, an own `cause` of `undefined` records `"undefined"`, and an
inherited one is not an own one (`Object.hasOwn`).

The live sidecar is now taken rather than read, so a caller replaying the
very same outcome object gets the journal's account like any other replay.

Coverage: one prefix-survives and one exact-count case per visible producer,
one isolation case per atomic path, seven malformed-journal cases, the
persisted-field and reconstruction contract, and live identity through the
cause chain. Five mutations confirmed discriminating — clone before the
sidecar lookup, omit the capture mark, settle `output` like `throw`, share
an owner at an `as` boundary, and append a shared write twice.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

: { source: documentation.segment.source }),
},
// An own property, not an inherited one: every Error inherits `cause` from
// nowhere useful, and what this records is what this failure was given.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// nowhere useful, and what this records is what this failure was given.

}
return [...options.errors, ...result.segments];
// A projection that wrote into the caller's region has nothing left to
// hand back; one that kept its own returns what it rendered.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// hand back; one that kept its own returns what it rendered.

...(yield* expandCollectFailures(segment, parentMeta, parentProps, hideSet, counter)),
);
// It renders into this expansion's output, so it is handed the owner
// and writes there rather than handing segments back to be appended.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// and writes there rather than handing segments back to be appended.

// A rendering invocation wrote its body straight into this owner, so
// settling happens in place over what it added: the diagnostic is taken
// out first, so a policy that ends the execution does not leave it in
// the output the document rendered before failing.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// the output the document rendered before failing.

… stream

Two demonstrated contract violations, and the spec drift behind them.

**A capture boundary was deciding for its callees.** `useFailures()` marked
every diagnostic raised anywhere beneath it, so an enclosing boundary
flipped a nested component's `<Output>` region from fail-fast to
render-and-continue — running the very commands that region's author gated
behind the failure. Every built-in that captures its own failures (`<File>`,
`<Parse>`, `<SafeParse>`, `<Glob>`, `<TempDir>`) did it just by wrapping the
invocation, in a plain collecting root with no `<Output>` in sight.

A policy value cannot tell those apart: two frames can both be `output` and
still be different decisions, made by different authors about different
regions. So a decision now has identity — `AmbientPolicyFrame`, a fresh
object per `usePolicy()` — and a boundary marks only what is raised under
its own. A projection carries the caller's frame rather than opening one,
because it expands the caller's own text. What reaches the boundary from a
nested region is the invocation's failure, which it handles on its own
terms; a failure that region already settled stays outside that recovery,
like every already-settled documentation failure.

**The preserved tail never reached the stream.** It was left to the
completion path, which only emits for a run that streamed nothing at all —
so a document with any earlier output silently dropped exactly what the
preservation exists for, and only the close value carried it. It is emitted
in the workflow's catch now, before the failure is classified.

**§10.2.1 still described the null sentinels** the schema had already
dropped. It now describes absent keys, `Object.hasOwn` ownership, and the
refusal of malformed records; the Tier OFF table lists every test again, so
"identifiers match the file" is true.

Minor: `capturesErrors` asks `Object.hasOwn` rather than `in`, so a derived
object cannot inherit the brand; the live failure rides on the outcome under
a non-enumerable symbol instead of a module-scoped WeakMap; the lint rule
reports a static class field, which is module lifetime; and an old journal
with no recorded status now says so instead of reporting a generic shape
error.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

: { source: documentation.segment.source }),
},
// An own property, not an inherited one: every Error inherits `cause` from
// nowhere useful, and what this records is what this failure was given.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// nowhere useful, and what this records is what this failure was given.

}
return [...options.errors, ...result.segments];
// A projection that wrote into the caller's region has nothing left to
// hand back; one that kept its own returns what it rendered.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// hand back; one that kept its own returns what it rendered.

const out: Segment[] = [];
// A rendering loop writes into the caller's region as it goes, so a failure
// partway leaves the items it already produced behind. A captured one builds
// a value instead: its buffer is private and never becomes document output.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// a value instead: its buffer is private and never becomes document output.

* module-scoped ones, including exported and lazily-assigned declarations.
*/
// A class body is not a lifetime: a class declared at module scope holds its
// fields for the life of the module, which is exactly the shape this reports.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// fields for the life of the module, which is exactly the shape this reports.

State belongs to a scope that reclaims it. Three registries in this package
belonged to the process instead, and the previous commit made that worse by
moving one onto the objects it described — the same lifetime, spread across
more places, under a `Symbol.for` key anyone can forge.

Each is now created where a run begins and reclaimed with it:

- the diagnostics a run captured, and what expansion translated each segment
  from, are one `Diagnostics` record opened by `useDiagnostics()` and read
  through context. `markCaptured`, `isCaptured` and `attributeCause` became
  operations, and a `DocumentationError` is built by `documentationError()`,
  which reads the cause before constructing — a constructor cannot reach a
  scope. Expansion driven directly, with no execution around it, opens the
  registries for exactly its own lifetime.
- the failure a completion reports is left in a slot the run provides, filled
  by the workflow's catch and taken by the completion. A replayed run never
  enters the workflow, so the empty slot is the replay signal — no brand, no
  table, nothing to clear.
- the schema caches are gone. Ajv already memoizes by schema object, and the
  contract checks now run on every call, which is what the local caches were
  skipping.

`AmbientPolicyFrame`'s default was a module-scoped object shared by every run
— inert, but the banned shape, and the `?? {}` fallbacks around it would have
silently disabled marking if they ever fired. The default is now `undefined`,
matched by identity like any other frame, so no fallback is needed.

`captureErrors(fn)` keeps its brand on the function: it runs while a component
module is evaluated, outside any operation, and records what an author
declared about a definition rather than anything a run decided. The key is a
module-private `Symbol()` now, and the rule's docs draw that line.

The rule is `local/no-module-scoped-registry`: it covers `Map` and `Set`
alongside the weak kinds, and reports the tables a module creates empty and
fills later. A table built from its contents is a constant, an instance field
belongs to its object, and one handed straight to a call is not something the
module can accumulate into. Confirmed reporting all four converted
declarations before the conversions.

Also: the crossed OFF6b/OFF6c comments, and the one test left without an
identifier — the `"undefined"` cause — now has both a label and a spec row.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

const causes = (yield* RunDiagnostics.get())?.causes;
// Membership, not value: a component can throw `undefined`, and that is still
// the exact value this failure was translated from. Only a segment with no
// attribution has no own cause at all.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// attribution has no own cause at all.

: { source: documentation.segment.source }),
},
// An own property, not an inherited one: every Error inherits `cause` from
// nowhere useful, and what this records is what this failure was given.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// nowhere useful, and what this records is what this failure was given.

// rendered before stopping.
const rendered: Segment[] = [];
// rendered before stopping. When the caller owns a region, that array is
// the region itself and the prefix is already where the document needs it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// the region itself and the prefix is already where the document needs it.

}
return [...options.errors, ...result.segments];
// A projection that wrote into the caller's region has nothing left to
// hand back; one that kept its own returns what it rendered.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// hand back; one that kept its own returns what it rendered.

const out: Segment[] = [];
// A rendering loop writes into the caller's region as it goes, so a failure
// partway leaves the items it already produced behind. A captured one builds
// a value instead: its buffer is private and never becomes document output.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// a value instead: its buffer is private and never becomes document output.

* module-private `Symbol()` is how it is written.
*/
// A class body is not a lifetime: a class declared at module scope holds its
// fields for the life of the module, which is exactly the shape this reports.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// fields for the life of the module, which is exactly the shape this reports.

}

// An instance field is initialized per instance, so its table belongs to
// that object's lifetime. A static one belongs to the module.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// that object's lifetime. A static one belongs to the module.


// Handed straight to a call — a context's empty default, a provider's
// initial registry — and never bound here. The module holds no reference,
// so nothing in it can accumulate into the table.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// so nothing in it can accumulate into the table.

Two things from review.

`handleFailure` was marking the diagnostic it built as well as the `raise`
middleware, on the theory that a provider's own call does not re-enter the
middleware it is part of. It does — the suite passes with the call removed,
and it passed before the call existed. Removed rather than left standing
without a test that goes red without it.

The Ajv instance in `parse-schema.ts` was the shape the deleted schema caches
were fronting: Ajv memoizes every compile in a table keyed by the schema
object, and a run brings fresh schema objects, so one compiler per process
accumulates one entry per schema per run. It belongs to the run now, created
where the run opens its other registries and reclaimed with them.
`compileParseSchema` and `prepareElicitation` became operations to read it;
expansion driven with no run around it compiles into a instance that lives
exactly as long as the call.

The Ajv in `validate.ts` is *not* converted. Reaching it means making
`validateProps` and `parseMarkdownDefinition` operations — both synchronous,
both public — which is a public-API change that belongs in its own PR. That,
and whether `captureErrors(fn)` should keep its brand on the function or move
capture-ness onto the per-run `ComponentDefinition`, are written into the
rule's docs as open rulings, so both are decisions somebody makes rather than
defaults the linter quietly settled.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

): Operation<ValidateFunction> {
const declaration = readSchema(componentName, schema);
// Without a run there is nothing to reclaim: the compiler lives exactly as
// long as this call.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// long as this call.

: { source: documentation.segment.source }),
},
// An own property, not an inherited one: every Error inherits `cause` from
// nowhere useful, and what this records is what this failure was given.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// nowhere useful, and what this records is what this failure was given.


// What the document rendered before it stopped, held outside the expansion
// scope so a failure still leaves it here (§6.9). The buffered root fills
// `selected`; every streaming root has already emitted `streamed`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// `selected`; every streaming root has already emitted `streamed`.

// so no partial output is produced.
// execute the whole body, then emit the selected regions once. The owner is
// allocated here rather than inside the expansion so that a failure partway
// still leaves this frame holding what the regions rendered before it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// still leaves this frame holding what the regions rendered before it.

// Emitted here, whatever the failure turns out to be: a consumer reading
// chunks is who the preservation is for, and the completion path only emits
// for a run that streamed nothing at all — which stops being true as soon as
// an earlier segment went out.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// an earlier segment went out.

throw error;
}
// Everything the document rendered: the buffered selection, or what the
// streaming loop emitted together with that tail.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// streaming loop emitted together with that tail.

// rendered before stopping.
const rendered: Segment[] = [];
// rendered before stopping. When the caller owns a region, that array is
// the region itself and the prefix is already where the document needs it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// the region itself and the prefix is already where the document needs it.

}
return [...options.errors, ...result.segments];
// A projection that wrote into the caller's region has nothing left to
// hand back; one that kept its own returns what it rendered.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// hand back; one that kept its own returns what it rendered.

// same schema costs one compile however often it is asked for. Nothing here
// keeps a table of its own: a cache beside Ajv's would also have to decide
// whose contract a cached validator was compiled under, and the contract checks
// below run on every call precisely so that question cannot arise.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// below run on every call precisely so that question cannot arise.

@taras

taras commented Aug 4, 2026

Copy link
Copy Markdown
Owner Author

Closing without merge — superseded by the architecture settled in architecture.md (#314) and the vocabulary alignment (#316/#317).

The model this PR converged on was simplified after it was written: control at raise now belongs to the error mode value alone, so the frame/marking apparatus this branch carries (AmbientPolicyFrame, markCaptured/isCaptured, consumer re-settlement) is not part of the target design. Rebasing was measured, not assumed: the branch is 7 commits behind main, 24 of its 33 files conflict, and its internal rename (<CaptureErrors>) diverges from the one that merged (<PrintErrors>), so every hunk would resolve twice.

The branch stays as a parts bin. Commit 0673f7d holds known-good implementations of the output error mode, partial-output ownership, the failed-run record with strict journal parsing, and the test corpus — the fresh semantics PR ports these through the registry vocabulary and implements <PrintErrors> by mode alone.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

💥 Make <Output> fail fast and rename failure capture to CaptureErrors

1 participant