Skip to content
Merged
27 changes: 25 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,34 @@ Do not commit if any check fails. Fix the issue first, then re-run all three.
language in specifications or source code.
6. Parse to infer type; Do not type cast with `as`.
7. Do not use braceless `if` statements.
8. Keep the release spec current — changes to the release configuration
require changes to specs/release-process-spec.md to match.
9. Prefer stateless generators - use a function when calling a function that
returns an operation; Do not do this function*(arg) { return yield* generator(arg) }

## PR Process

1. Use .github/pull_request_template.md
2. After PR is open, monitor PR for
2. After PR is open, monitor PR for
1. CI failures
2. Comments with feedback
3. Integrate changes feedback appears
3. Integrate changes feedback appears

## Agent Roles

If you're an Opus model, you're an Implementor agent.
If you're a GPT model, you're an Planner agent.
If you're a Fabel model, you're a Problem solver agent.

### Implementor agent

Writes code following Code Rules.

### Planner agent

##### When reviewing Implementor agent's plans**

**User will ask you**: Review <subject>; verdict; prompt on failure.
**Respond by:**
* Interviewing user to resolve ambiguity; do not ask the Implementor agent to make decisions.
* Writing a feedback prompt that user will handoff to the Implementor agent
25 changes: 20 additions & 5 deletions core/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,17 +39,32 @@ export type {
ModifierMiddleware,
CodeBlockWorkflow,
} from "./src/modifiers.ts";
export { useCodeBlock, CodeBlockCtx } from "./src/modifiers.ts";
export { useCodeBlock } from "./src/modifiers.ts";

// ---------------------------------------------------------------------------
// Component Api — contextual operations for component expansion
// ---------------------------------------------------------------------------

export type { ComponentApi } from "./src/component-api.ts";
export {
Component,
importComponent,
applyModifiers,
raise,
env,
evalScope,
codeBlock,
persistent,
content,
} from "./src/component-api.ts";

// ---------------------------------------------------------------------------
// Eval system (generator eval blocks)
// ---------------------------------------------------------------------------

export type { EvalEnv } from "./src/eval-env.ts";
export { EvalEnvCtx, EvalScopeCtx } from "./src/eval-env.ts";
export type { EvalEnv } from "./src/types.ts";

export type { EvalContext } from "./src/eval-context.ts";
export { EvalCtxKey, createEvalContext, compileBlock } from "./src/eval-context.ts";
export { compileBlock } from "./src/eval-context.ts";

// ---------------------------------------------------------------------------
// executable.md Globals (for generated eval modules)
Expand Down
93 changes: 93 additions & 0 deletions core/src/component-api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/**
* Component Api — contextual operations for component expansion.
*
* One public Api replaces the former dependency container (ExpansionContext)
* and raw Effection context keys. Context-dependent behavior is installed as
* scope-local middleware via `Component.around(...)`:
*
* - Runtime implementations (document import, modifier execution, component
* state) install at `{ at: "min" }`. Middleware installed in a nested scope
* runs before inherited middleware, so a component that installs its own
* `env` shadows its ancestors without leaking into siblings — install
* inside `scoped()` for automatic removal.
* - Caller instrumentation and overrides wrap at the default `"max"`.
*/

import { createApi } from "@effectionx/context-api";
import type { Operation } from "effection";
import type { EvalScope } from "@effectionx/scope-eval";
import type {
CodeBlockContext,
CodeBlockResult,
ComponentDefinition,
ErrorSegment,
EvalEnv,
FunctionComponentDefinition,
Modifier,
} from "./types.ts";

export interface ComponentApi {
/** `"__root__"` imports the root document. */
importComponent(name: string): Operation<ComponentDefinition | FunctionComponentDefinition>;
applyModifiers(modifiers: Modifier[], block: CodeBlockContext): Operation<CodeBlockResult>;
/**
* Report an ErrorSegment under the ambient error policy. The default
* returns the segment for rendering; suppressed-documentation scopes
* install middleware that throws instead (spec §6.9).
*/
raise(error: ErrorSegment): Operation<ErrorSegment>;
env: EvalEnv | undefined;
evalScope: EvalScope | undefined;
codeBlock(): Operation<CodeBlockContext>;
/** Whether the current block runs with persistent resource lifetime. */
persistent: boolean;
/** Render the invoking component's children (optionally a named slot). */
content(slot?: string): Operation<string>;
}

export const Component = createApi<ComponentApi>("Component", {
// deno-lint-ignore require-yield
*importComponent(name: string): Operation<ComponentDefinition | FunctionComponentDefinition> {
throw new Error(
`Component.importComponent("${name}") has no provider. Install one with ` +
`Component.around({ importComponent }, { at: "min" }) before expansion.`,
);
},
// deno-lint-ignore require-yield
*applyModifiers(_modifiers: Modifier[], block: CodeBlockContext): Operation<CodeBlockResult> {
throw new Error(
`Component.applyModifiers() has no provider for block "${block.blockId}". Install one ` +
`with Component.around({ applyModifiers }, { at: "min" }) before expansion.`,
);
},
// deno-lint-ignore require-yield
*raise(error: ErrorSegment): Operation<ErrorSegment> {
return error;
},
env: undefined,
evalScope: undefined,
// deno-lint-ignore require-yield
*codeBlock(): Operation<CodeBlockContext> {
throw new Error(
"Component.codeBlock() has no provider: no code block is executing in this scope.",
);
},
persistent: false,
// deno-lint-ignore require-yield
*content(_slot?: string): Operation<string> {
throw new Error(
"Component.content() has no provider: not inside a function component invocation.",
);
},
});

export const {
importComponent,
applyModifiers,
raise,
env,
evalScope,
codeBlock,
persistent,
content,
} = Component.operations;
58 changes: 11 additions & 47 deletions core/src/content-context.ts
Original file line number Diff line number Diff line change
@@ -1,62 +1,26 @@
/**
* Content context for function components.
*
* Provides `useContent()` — the function component equivalent of
* `<Content />` in markdown components. The expansion engine sets
* the content handle on the Effection scope before calling the
* function component. Components that need rendered children call
* `yield* useContent()` to retrieve them.
*
* Named slots are supported: `yield* useContent("header")` returns
* the content for the "header" slot, matching `<Content slot="header" />`
* in markdown components.
* useContent() — the function component equivalent of `<Content />` in
* markdown components. Ergonomic alias for the Component `content()`
* operation; the expansion engine installs the slot-rendering middleware
* around each function component invocation.
*/

import { createContext } from "effection";
import type { Operation } from "effection";
import type { Segment } from "./types.ts";

/**
* Handle providing access to a component's children content.
* Set on the Effection scope by the expansion engine.
*/
export interface ContentHandle {
/** Render the default slot (children without a slot prop). */
renderDefault: () => Operation<string>;
/** Render a named slot. Returns empty string if the slot has no content. */
renderSlot: (name: string) => Operation<string>;
/** Raw child segments (all slots combined, for inspection). */
segments: Segment[];
}
import { content } from "./component-api.ts";

/**
* Effection context key for the content handle.
*/
export const ContentCtx = createContext<ContentHandle>("content");

/**
* Render children content from the parent scope.
*
* The function component equivalent of `<Content />` in markdown components.
* Render children content from the invoking component.
*
* @param slotName - Optional slot name. If provided, renders only the
* content assigned to that slot. If omitted, renders the default slot
* (children without a `slot` prop).
* @returns The rendered content as a string.
* content assigned to that slot (matching `<Content slot="name" />`).
* If omitted, renders the default slot.
*
* @example
* ```ts
* // Render default content (equivalent to <Content /> in .md)
* const content = yield* useContent();
*
* // Render named slot (equivalent to <Content slot="header" /> in .md)
* const body = yield* useContent();
* const header = yield* useContent("header");
* ```
*/
export function* useContent(slotName?: string): Operation<string> {
const handle = yield* ContentCtx.expect();
if (slotName !== undefined) {
return yield* handle.renderSlot(slotName);
}
return yield* handle.renderDefault();
export function useContent(slotName?: string): Operation<string> {
return content(slotName);
}
16 changes: 16 additions & 0 deletions core/src/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { ErrorSegment } from "./types.ts";

/**
* Thrown by suppressed-documentation raise middleware (spec §6.9). Generic
* catches in the engine rethrow it instead of converting it into an
* ErrorSegment, so documentation fail-fast is never swallowed.
*/
export class DocumentationError extends Error {
readonly segment: ErrorSegment;

constructor(segment: ErrorSegment) {
super(segment.message);
this.name = "DocumentationError";
this.segment = segment;
}
}
39 changes: 3 additions & 36 deletions core/src/eval-context.ts
Original file line number Diff line number Diff line change
@@ -1,52 +1,19 @@
/**
* Eval block compilation context (spec §5).
* Eval block compilation (spec §5).
*/

import { createContext as createEffectionContext } from "effection";
import type { Operation } from "effection";
import { compile as runtimeCompile } from "@executablemd/runtime";

// ---------------------------------------------------------------------------
// EvalContext — lightweight context for the eval system
// ---------------------------------------------------------------------------

/**
* Eval context for document runs.
*
* In the Deno model, there is no VM context — eval blocks are compiled
* into data: URI modules that import their dependencies. The EvalContext
* exists as a marker that the eval system has been initialized.
*/
export interface EvalContext {
/** Placeholder for future per-document eval configuration. */
initialized: true;
}

/**
* Effection context key for the eval context.
*/
export const EvalCtxKey = createEffectionContext<EvalContext>("evalContext");

/**
* Create an eval context for a document run.
*
* In the Deno model, this is lightweight — no VM sandbox creation.
* The eval context is set on the Effection scope so eval blocks
* can verify the eval system is initialized.
*/
export function createEvalContext(_globals: Record<string, unknown> = {}): EvalContext {
return { initialized: true };
}

/**
* Compile transformed source code into a generator function.
*
* Delegates to `@executablemd/runtime` so platform-specific
* compilation can be provided via API.Compiler middleware.
*/
export function* compileBlock(
export function compileBlock(
transformedBodyCode: string,
userImports: string[],
): Operation<(env: Record<string, unknown>) => Generator<unknown, unknown, unknown>> {
return yield* runtimeCompile(transformedBodyCode, { imports: userImports });
return runtimeCompile(transformedBodyCode, { imports: userImports });
}
58 changes: 0 additions & 58 deletions core/src/eval-env.ts

This file was deleted.

Loading
Loading