feat: component-declared output via <Output> (#88)#89
Merged
Conversation
PR #89: feat: component-declared output via (#88)30 files, +2205 / -1422 Scope🔴 PR has 3627 lines changed. Split into focused PRs. 🟡 3627 lines changed. PRs under 400 receive more thorough review. 🟡 30 files changed. Are all changes related? 🟡 PR mixes config and source changes. Structural✅ No structural bloat detected. SlopStatic Analysis✅ Oxlint found no issues. CorrectnessNo extraneous code patterns detected. |
Components render their entire body, so documentation prose leaks into consumers. A component (or root) now declares its rendered region with a top-level <Output> boundary; everything outside is documentation that executes for side effects but never renders. - error-policy.ts: contextual ErrorPolicyCtx (collect/throw), raise, and DocumentationError for documentation fail-fast. - expand.ts: source-AST structural preflight (validateOutputPlacement), provenance-based buildBody/expandBody, misplaced-<Output> defensive branch, and consumer-boundary re-raise of transported errors. - run-document.ts: root buffers when it declares <Output> (emit once after success, none on failure, none when empty); streams otherwise. - spec §5.4/§6.1/§6.2 pseudocode, new §6.9, decision row 68, test-plan rows. - Tests: expand + run-document coverage; smoke Show.md/OutputDemo.md.
taras
force-pushed
the
worktree-component-output
branch
from
July 18, 2026 19:46
ae24f66 to
152a5e5
Compare
- spec §6.9: use a four-backtick outer markdown fence so the inner ```ts eval block renders inside one code block. - expand.ts: drop JSDoc/comments that merely restate names/types/control flow (per AGENTS.md rule 4); keep the provenance and transported-error rationale.
Revert the source-shaped additions to the documentWorkflow, expandSegments, and expandComponent pseudocode blocks, and rewrite §6.9 to state observable behavior without private symbols (validateOutputPlacement, buildBody, expandBody, ErrorPolicyCtx, raise, consume, DocumentationError) or internal control flow. Keep the authoring example, the normative prose (placement, definition-owned structure, execution order, error behavior, root buffering, consistency, multiple regions, as= capture, empty-tag parity), and the C32–C43 / E11–E13 conformance cases.
The documentWorkflow (§5.4), segment-expansion (§6.1), and component-expansion (§6.2) pseudocode blocks were implementation snapshots. Remove them and state the externally significant behavior as normative prose: streaming vs. buffered root emission, per-segment rewriting and error rendering, and cycle/depth guards + placement-before-execution + as= capture. Cross-reference the dedicated sections (§6.3 slots, §6.5 prop validation, §6.9 <Output>) instead of duplicating them. Keep the authoring example and the C32–C43 / E11–E13 cases.
One public Component context API (backed by @effectionx/context-api)
replaces ExpansionContext and the raw operational context keys.
Context-dependent behavior installs as scope-local middleware; nested
providers override ancestors without leaking into siblings.
- component-api.ts: Component/ComponentApi plus direct operations
(importComponent, applyModifiers, raise, env, evalScope, codeBlock,
persistent, content) with missing-provider defaults.
- expand.ts: ctx parameter removed from all expansion functions;
error policy becomes raise() middleware (documentation throws,
Output installs a collecting provider that shadows it); consumer
boundaries raise transported errors unconditionally.
- run-document.ts: document providers install at min before
durableRun; root imports via importComponent("__root__"); journal
shape unchanged (verified by replay/journal tests).
- modifiers/persist/daemon/eval-handler/content-context: read state
through the operations; useCodeBlock/useContent stay as aliases.
- mod.ts: export the new public surface; drop CodeBlockCtx,
EvalEnvCtx, EvalScopeCtx, EvalCtxKey, createEvalContext.
- tests: makeCtx/recordingCtx replaced with Component middleware
helpers; new component-api.test.ts (defaults, missing-provider
diagnostics, scoped overrides, nested precedence, persist flag,
content dispatch) and a journal-shape integration test.
- specs: new §5.5 Component Api contract + observable scoping;
raw-key sections restated behaviorally; DEC-012 records the
architectural decision.
… terminal CI typechecks the test files (deno task check + tsc); the persist test's terminal is CodeBlockWorkflow-typed, so the contextual read crosses the Workflow/Operation seam through ephemeral(), per the bridging rule.
- spec: replace the evalFactory, daemonFactory, and runDocument provider-wiring implementation snapshots with observable-behavior prose (§5.5 carries the public contract; DEC-012 the rationale). - tests: nested evalScope providers override ancestors without sibling leakage; function component renders default + named slot through useContent(slot); journal-shape test also asserts the eval entry identity (eval:eval:root:N). - drop the new `as unknown as Operation` assertion in the persist test (plain yield* typechecks). - types.ts: projectedEnv doc refers to the contextual env() operation. - revert unrelated deno.lock exact-version aliases. - rule 9 (AGENTS.md): delegating generator wrappers become plain functions returning the operation (useContent, provideEnv, provideEvalScope, test helpers).
Address the plannotator inline annotations: - env, evalScope, and persistent become Component value operations — read without invocation (yield* env); providers are plain arrows returning the value (env: () => componentEnv). codeBlock/content stay functions (throwing defaults / slot argument). - error-policy.ts renamed to errors.ts with the header trimmed; EvalEnv moves into types.ts and eval-env.ts is deleted. - compileBlock and the renderChildren/render closures become stateless (rule 9); the dead DocumentationError guard on the import catch is removed; the new section separators in expand.ts are dropped; the pre-existing daemon.test cast is removed (plain yield* typechecks). - min placement for providers and evalScope's home on Component stay as-is per review discussion (innermost-min-first is what lets nested components shadow ancestors; max remains free for wrapping middleware). - spec: §5.5 documents the value-operation form; §4.3/§4.4 and the persist sections use the value notation; file table updated.
The implementation-shaped block duplicated source and carried stale details (sampleHandler, runtime, Resolve wiring, useBuiltinModifiers). State the actual options (docPath, stream, componentDirs?, modifiers?), the DocumentExecution contract (yield for full output, output stream of emitted chunks with the full output as close value), the scope-local runtime providers, and success/failure behavior. The §8.2 usage example stays — it demonstrates the public API.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
Markdown components render their entire body (spec §6.2), so documentation
prose leaks into consumers — e.g.
ReleaseSpecWarning.md's heading, notes, andfile list land in the posted PR comment that renders
<ReleaseSpecWarning />.Existing workarounds are all lossy or semantically wrong:
<Capture>hides docsin an unread binding,
<Capture select>flattens markdown (drops>prefixes)and falls back to full content on no match, and eval
return/output()forcescontent into JS strings. Closes #88.
Approach
A component (or root document) declares which region of its body renders using a
top-level
<Output>…</Output>boundary. Everything outside is documentation:it still executes (eval/exec run,
<Capture>populates bindings, nestedcomponents run) but never renders. Absent
<Output>, the whole body renders —fully backward compatible.
Three mechanisms:
validateOutputPlacement) — validates<Output>placement against the component's own source AST before any body executes.
Only a direct top-level
<Output>is valid; every misplaced one (includinginside
<Show when={false}>, passed to a content-discarding component, ornested) is aggregated into one diagnostic, and an invalid component runs no
side effects.
buildBody/expandBody) — output policy is decidedfrom the definition body before
<Content />substitution, so acaller-projected
<Output>can neither activate nor alter the callee'spolicy.
<Content />inside<Output>projects one level in. Multipleregions concatenate;
as=captures only selected output.raise()middleware) — documentation failsfast (first error throws to be handled above); output regions and
no-
<Output>bodies keep today's<!-- ERROR -->comments. Aconsumer-boundary re-raise handles errors transported out of a child's own
Output into a suppressed-documentation caller.
Roots declaring top-level
<Output>buffer completely (emit once after success,nothing on failure, nothing when empty); roots without it keep per-segment
streaming.
ComponentApi (follow-up correction, same PR)
Per review,
ExpansionContext(the dependency container threaded throughexpansion) and the raw operational context keys (
EvalEnvCtx,EvalScopeCtx,CodeBlockCtx,PersistFlagCtx,EvalCtxKey,ContentCtx,ErrorPolicyCtx)are replaced by one public Component context Api backed by
@effectionx/context-api:Component/ComponentApiplus direct operations —importComponent,applyModifiers,raise,env,evalScope,codeBlock,persistent,content— exported fromcore/mod.ts;useCodeBlock()/useContent()remain as ergonomic aliases.
min(document import/modifier providers before
durableRun, per-componentstate inside
scoped()); instrumentation and overrides wrap atmax.Nested providers override ancestors without leaking into siblings — this
is also how Output's collecting
raiseshadows documentation's throwingone.
ctxparameter). Calls fromWorkflow-typed code bridge with
ephemeral(); journal shape is unchanged(replay/journal tests + a dedicated journal-shape test).
raw-key sections restated behaviorally;
decisions.mdDEC-012 records thearchitectural rationale (why the Api replaces dependency threading, why
min/max).Scope confirmation
New abstractions (if any)
Component/ComponentApi(core/src/component-api.ts) — the publiccontextual-operations surface; consumed across
expand.ts,run-document.ts,modifiers.ts,eval-handler.ts, and the modifierfactories.
DocumentationError(core/src/error-policy.ts) — internal throwable sogeneric catches rethrow documentation failures instead of swallowing them.
<Output>boundary tag — handled like the existing<Capture>/<Content>built-ins; no scanner change.
New dependencies (if any)
New tests (if any)
core/tests/expand.test.ts— region selection, documentation executionorder, binding dependency, multiple regions, markdown/admonition
preservation, empty-tag parity, prop rejection,
<Content />projection,structural preflight (aggregate diagnostic, no side effects,
<Show when={false}>, content-discarding component), documentation fail-fast, andconsumer-boundary re-raise (throw from docs / comment inside Output /
as=).core/tests/run-document.test.ts— root/component consistency, buffering,no-partial-output-on-failure, streaming without
<Output>, empty-bufferemits nothing, replay, misplaced root
<Output>.core/tests/smoke.test.ts+smoke-test/Show.md/smoke-test/OutputDemo.md— end-to-end:
<Show>inside<Output>renders using a binding computed bypreceding documentation eval, while the component's docs are suppressed.
core/tests/component-api.test.ts— operation defaults, missing-providerdiagnostics, scoped
maxoverride wrapping + removal on scope exit, nestedminprecedence without sibling leakage, persist-flag scoping, contentdispatch; plus a journal-shape integration test in
core/tests/run-document.test.ts(import/exec identities unchanged throughcontext-api dispatch).
Verification
deno task lint— 0 errors, format cleandeno check core/mod.ts/deno task check/tsc --project tsconfig.node.json— no errorsdeno test --no-check --allow-all core/tests/ durable-streams/tests/—72 passed (613 steps) / 0 failed
git diff --check— clean