Skip to content

feat: component-declared output via <Output> (#88)#89

Merged
taras merged 9 commits into
mainfrom
worktree-component-output
Jul 18, 2026
Merged

feat: component-declared output via <Output> (#88)#89
taras merged 9 commits into
mainfrom
worktree-component-output

Conversation

@taras

@taras taras commented Jul 18, 2026

Copy link
Copy Markdown
Owner

Motivation

Markdown components render their entire body (spec §6.2), so documentation
prose leaks into consumers — e.g. ReleaseSpecWarning.md's heading, notes, and
file list land in the posted PR comment that renders <ReleaseSpecWarning />.
Existing workarounds are all lossy or semantically wrong: <Capture> hides docs
in an unread binding, <Capture select> flattens markdown (drops > prefixes)
and falls back to full content on no match, and eval return/output() forces
content 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, nested
components run) but never renders. Absent <Output>, the whole body renders —
fully backward compatible.

Three mechanisms:

  • Structural preflight (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 (including
    inside <Show when={false}>, passed to a content-discarding component, or
    nested) is aggregated into one diagnostic, and an invalid component runs no
    side effects.
  • Provenance chunking (buildBody/expandBody) — output policy is decided
    from the definition body before <Content /> substitution, so a
    caller-projected <Output> can neither activate nor alter the callee's
    policy. <Content /> inside <Output> projects one level in. Multiple
    regions concatenate; as= captures only selected output.
  • Contextual error policy (raise() middleware) — documentation fails
    fast (first error throws to be handled above); output regions and
    no-<Output> bodies keep today's <!-- ERROR --> comments. A
    consumer-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 through
expansion) 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 / ComponentApi plus direct operations — importComponent,
    applyModifiers, raise, env, evalScope, codeBlock, persistent,
    content — exported from core/mod.ts; useCodeBlock()/useContent()
    remain as ergonomic aliases.
  • Runtime implementations install as scope-local middleware at min
    (document import/modifier providers before durableRun, per-component
    state inside scoped()); instrumentation and overrides wrap at max.
    Nested providers override ancestors without leaking into siblings — this
    is also how Output's collecting raise shadows documentation's throwing
    one.
  • Expansion signatures are context-free (no ctx parameter). Calls from
    Workflow-typed code bridge with ephemeral(); journal shape is unchanged
    (replay/journal tests + a dedicated journal-shape test).
  • Spec: new §5.5 documents the public contract and observable scoping;
    raw-key sections restated behaviorally; decisions.md DEC-012 records the
    architectural rationale (why the Api replaces dependency threading, why
    min/max).

Scope confirmation

  • All changed files relate to the stated purpose
  • No drive-by refactors or "while I'm here" cleanups
  • No formatting changes mixed with functional changes

New abstractions (if any)

  • Component / ComponentApi (core/src/component-api.ts) — the public
    contextual-operations surface; consumed across expand.ts,
    run-document.ts, modifiers.ts, eval-handler.ts, and the modifier
    factories.
  • DocumentationError (core/src/error-policy.ts) — internal throwable so
    generic 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)

  • None.

New tests (if any)

  • core/tests/expand.test.ts — region selection, documentation execution
    order, 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, and
    consumer-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-buffer
    emits 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 by
    preceding documentation eval, while the component's docs are suppressed.
  • core/tests/component-api.test.ts — operation defaults, missing-provider
    diagnostics, scoped max override wrapping + removal on scope exit, nested
    min precedence without sibling leakage, persist-flag scoping, content
    dispatch; plus a journal-shape integration test in
    core/tests/run-document.test.ts (import/exec identities unchanged through
    context-api dispatch).

Verification

  • deno task lint — 0 errors, format clean
  • deno check core/mod.ts / deno task check / tsc --project tsconfig.node.json — no errors
  • deno test --no-check --allow-all core/tests/ durable-streams/tests/
    72 passed (613 steps) / 0 failed
  • git diff --check — clean

@github-actions

github-actions Bot commented Jul 18, 2026

Copy link
Copy Markdown

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.

Slop

Static Analysis

✅ Oxlint found no issues.

Correctness

No 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
taras force-pushed the worktree-component-output branch from ae24f66 to 152a5e5 Compare July 18, 2026 19:46
taras added 3 commits July 18, 2026 15:52
- 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.
taras added 5 commits July 18, 2026 18:14
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.
@taras
taras merged commit 9c5ab10 into main Jul 18, 2026
7 checks passed
@taras
taras deleted the worktree-component-output branch July 18, 2026 23:30
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.

Component-declared output: render only a designated region, keep the rest as documentation

1 participant