@executablemd/testing 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/esm/{testing/npm/src/src → src}/assertions.js +15 -24
  2. package/esm/{testing/npm/src/src → src}/handlers.js +140 -10
  3. package/esm/{testing/npm/src/src → src}/vocabulary.js +3 -0
  4. package/package.json +3 -3
  5. package/types/src/assertions.d.ts +11 -1
  6. package/types/src/handlers.d.ts +1 -0
  7. package/esm/core/mod.js +0 -55
  8. package/esm/core/src/api.js +0 -31
  9. package/esm/core/src/collect.js +0 -24
  10. package/esm/core/src/component-api.js +0 -47
  11. package/esm/core/src/content-context.js +0 -23
  12. package/esm/core/src/deno-compiler.js +0 -51
  13. package/esm/core/src/errors.js +0 -13
  14. package/esm/core/src/eval-context.js +0 -13
  15. package/esm/core/src/eval-handler.js +0 -79
  16. package/esm/core/src/eval-interpolate.js +0 -63
  17. package/esm/core/src/eval-transform.js +0 -378
  18. package/esm/core/src/execute.js +0 -378
  19. package/esm/core/src/expand.js +0 -988
  20. package/esm/core/src/frontmatter.js +0 -98
  21. package/esm/core/src/heal.js +0 -29
  22. package/esm/core/src/interpolate.js +0 -39
  23. package/esm/core/src/modifiers/daemon.js +0 -60
  24. package/esm/core/src/modifiers/persist.js +0 -35
  25. package/esm/core/src/modifiers/timeout.js +0 -55
  26. package/esm/core/src/modifiers.js +0 -113
  27. package/esm/core/src/output/normalize.js +0 -33
  28. package/esm/core/src/output/terminal.js +0 -20
  29. package/esm/core/src/render.js +0 -30
  30. package/esm/core/src/replay-stream.js +0 -58
  31. package/esm/core/src/sample-api.js +0 -45
  32. package/esm/core/src/scanner.js +0 -632
  33. package/esm/core/src/temp-file-compiler.js +0 -68
  34. package/esm/core/src/types.js +0 -7
  35. package/esm/core/src/validate.js +0 -82
  36. package/esm/durable-streams/combinators.js +0 -227
  37. package/esm/durable-streams/context.js +0 -13
  38. package/esm/durable-streams/divergence.js +0 -56
  39. package/esm/durable-streams/each.js +0 -193
  40. package/esm/durable-streams/effect.js +0 -273
  41. package/esm/durable-streams/ephemeral.js +0 -80
  42. package/esm/durable-streams/errors.js +0 -92
  43. package/esm/durable-streams/http-stream.js +0 -207
  44. package/esm/durable-streams/mod.js +0 -34
  45. package/esm/durable-streams/operations.js +0 -88
  46. package/esm/durable-streams/replay-guard.js +0 -87
  47. package/esm/durable-streams/replay-index.js +0 -132
  48. package/esm/durable-streams/run.js +0 -137
  49. package/esm/durable-streams/serialize.js +0 -65
  50. package/esm/durable-streams/stream.js +0 -56
  51. package/esm/durable-streams/types.js +0 -10
  52. package/esm/runtime/apis.js +0 -237
  53. package/esm/runtime/find-free-port.js +0 -33
  54. package/esm/runtime/mod.js +0 -20
  55. /package/esm/{testing/npm/src/_dnt.polyfills.js → _dnt.polyfills.js} +0 -0
  56. /package/esm/{testing/npm/src/_dnt.shims.js → _dnt.shims.js} +0 -0
  57. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/almost_equals.js +0 -0
  58. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/array_includes.js +0 -0
  59. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/assert.js +0 -0
  60. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/assertion_error.js +0 -0
  61. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/equal.js +0 -0
  62. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/equals.js +0 -0
  63. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/exists.js +0 -0
  64. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/fail.js +0 -0
  65. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/false.js +0 -0
  66. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/greater.js +0 -0
  67. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/greater_or_equal.js +0 -0
  68. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/instance_of.js +0 -0
  69. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/is_error.js +0 -0
  70. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/less.js +0 -0
  71. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/less_or_equal.js +0 -0
  72. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/match.js +0 -0
  73. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/mod.js +0 -0
  74. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/not_equals.js +0 -0
  75. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/not_instance_of.js +0 -0
  76. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/not_match.js +0 -0
  77. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/not_strict_equals.js +0 -0
  78. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/object_match.js +0 -0
  79. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/rejects.js +0 -0
  80. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/strict_equals.js +0 -0
  81. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/string_includes.js +0 -0
  82. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/throws.js +0 -0
  83. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/unimplemented.js +0 -0
  84. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/unreachable.js +0 -0
  85. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/internal/1.0.14/build_message.js +0 -0
  86. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/internal/1.0.14/diff.js +0 -0
  87. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/internal/1.0.14/diff_str.js +0 -0
  88. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/internal/1.0.14/format.js +0 -0
  89. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/internal/1.0.14/styles.js +0 -0
  90. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/internal/1.0.14/types.js +0 -0
  91. /package/esm/{testing/npm/src/mod.js → mod.js} +0 -0
  92. /package/esm/{testing/npm/src/src → src}/journal.js +0 -0
  93. /package/esm/{testing/npm/src/src → src}/test-api.js +0 -0
  94. /package/esm/{testing/npm/src/src → src}/use-testing.js +0 -0
@@ -1,98 +0,0 @@
1
- /**
2
- * Frontmatter parsing for component definitions (spec §4.1).
3
- *
4
- * Extracts meta values and input definitions from YAML frontmatter.
5
- * Supports shorthand syntax (value-as-default) and full JSON Schema subset.
6
- */
7
- /**
8
- * Parse raw frontmatter object into meta and inputs.
9
- *
10
- * - `inputs` key: declared input interface
11
- * - Everything else (or `meta` key with typed definitions): component metadata
12
- */
13
- export function parseFrontmatter(raw) {
14
- const rawInputs = (raw["inputs"] ?? {});
15
- const inputs = {};
16
- // Reserved input names — consumed by the expansion engine, not
17
- // available as component inputs. See spec §6.3 (named slots).
18
- const RESERVED_INPUT_NAMES = new Set(["slot", "as"]);
19
- for (const [key, value] of Object.entries(rawInputs)) {
20
- if (RESERVED_INPUT_NAMES.has(key)) {
21
- throw new Error(`"${key}" is a reserved prop name and cannot be declared as a component input`);
22
- }
23
- inputs[key] = normalizeInputDef(value);
24
- }
25
- // Meta: everything except 'inputs'
26
- // If 'meta' key exists and contains typed definitions, resolve defaults
27
- const meta = {};
28
- if (raw["meta"] && typeof raw["meta"] === "object" && !Array.isArray(raw["meta"])) {
29
- for (const [key, value] of Object.entries(raw["meta"])) {
30
- if (isTypedDefinition(value)) {
31
- meta[key] = value["default"];
32
- }
33
- else {
34
- meta[key] = value;
35
- }
36
- }
37
- }
38
- else {
39
- for (const [key, value] of Object.entries(raw)) {
40
- if (key !== "inputs") {
41
- meta[key] = value;
42
- }
43
- }
44
- }
45
- return { meta, inputs };
46
- }
47
- /**
48
- * Convert shorthand or full definition to InputDefinition.
49
- */
50
- export function normalizeInputDef(value) {
51
- // Full definition: object with a 'type' key
52
- if (isTypedDefinition(value)) {
53
- const def = value;
54
- const hasDefault = "default" in def;
55
- return {
56
- type: def["type"] ?? "any",
57
- ...(hasDefault ? { default: def["default"] } : {}),
58
- required: def["required"] === true || (!hasDefault && def["required"] !== false),
59
- ...(def["enum"] ? { enum: def["enum"] } : {}),
60
- ...(def["description"] ? { description: def["description"] } : {}),
61
- };
62
- }
63
- // Shorthand: null means required with no default
64
- if (value === null) {
65
- return { type: "any", required: true };
66
- }
67
- // Shorthand: value is the default, type inferred
68
- return {
69
- type: inferType(value),
70
- default: value,
71
- required: false,
72
- };
73
- }
74
- /**
75
- * Check if a value is a typed definition object (has a `type` key).
76
- */
77
- export function isTypedDefinition(value) {
78
- return (typeof value === "object" &&
79
- value !== null &&
80
- !Array.isArray(value) &&
81
- "type" in value);
82
- }
83
- /**
84
- * Infer the InputDefinition type from a JavaScript value.
85
- */
86
- export function inferType(value) {
87
- if (typeof value === "string")
88
- return "string";
89
- if (typeof value === "number")
90
- return "number";
91
- if (typeof value === "boolean")
92
- return "boolean";
93
- if (Array.isArray(value))
94
- return "array";
95
- if (typeof value === "object" && value !== null)
96
- return "object";
97
- return "any";
98
- }
@@ -1,29 +0,0 @@
1
- /**
2
- * Markdown healing (spec §2.3).
3
- *
4
- * Closes unclosed markdown constructs (bold, italic, code spans, links, etc.)
5
- * at segment boundaries. Runs after the boundary scanner, before interpolation.
6
- *
7
- * Pure, synchronous, stateless — no journal entry, no Effection yield.
8
- */
9
- import remend from "remend";
10
- /**
11
- * Heal incomplete markdown constructs in a text segment.
12
- *
13
- * `htmlTags: false` — the boundary scanner owns JSX/HTML completeness;
14
- * remend owns markdown construct completeness.
15
- *
16
- * Remend strips a single trailing space (setext heading heuristic).
17
- * Text segments are inline fragments where trailing spaces are
18
- * significant word separators (e.g., `"Before "` before `<Content />`),
19
- * so we preserve and restore the trailing space when remend removes it.
20
- */
21
- export function healSegment(text) {
22
- const hadTrailingSpace = text.length > 0 && text.endsWith(" ");
23
- const healed = remend(text, { htmlTags: false });
24
- // Restore trailing space if remend stripped it (setext heading heuristic)
25
- if (hadTrailingSpace && healed.length > 0 && !healed.endsWith(" ")) {
26
- return healed + " ";
27
- }
28
- return healed;
29
- }
@@ -1,39 +0,0 @@
1
- /**
2
- * Frontmatter interpolation: {meta.key} and {props.key} (spec §5.4).
3
- *
4
- * Runtime operation — deterministic from inputs, no journal entry.
5
- */
6
- /**
7
- * Replace `{meta.key}` and `{props.key}` references in text.
8
- *
9
- * Rules:
10
- * - Nested access via dot notation: `{meta.config.retry.count}`
11
- * - Missing key → empty string (no error)
12
- * - Arrays → comma-joined: `{meta.tags}` → `"alpha, beta"`
13
- * - Escaped braces: `\{not interpolated\}` → literal `{not interpolated}`
14
- */
15
- export function interpolate(text, meta, props) {
16
- return text.replace(/\\?\{(meta|props)\.([^}]+)\}/g, (match, namespace, keyPath) => {
17
- // Escaped brace
18
- if (match.startsWith("\\")) {
19
- return match.slice(1);
20
- }
21
- const source = namespace === "meta"
22
- ? meta
23
- : props;
24
- const value = getNestedValue(source, keyPath);
25
- if (value === undefined || value === null)
26
- return "";
27
- if (Array.isArray(value))
28
- return value.join(", ");
29
- return String(value);
30
- });
31
- }
32
- /**
33
- * Access a nested value via dot-separated path.
34
- */
35
- export function getNestedValue(obj, path) {
36
- return path
37
- .split(".")
38
- .reduce((current, key) => current?.[key], obj);
39
- }
@@ -1,60 +0,0 @@
1
- /**
2
- * The `daemon` terminal modifier factory (spec §3).
3
- *
4
- * Forks a long-running subprocess into the component's eval scope.
5
- * The process is alive for the duration of component expansion and
6
- * killed when the component scope closes. Unlike `exec`, it produces
7
- * no journal entry and never waits for the process to exit.
8
- *
9
- * Detection rule: daemon blocks are written as `bash daemon exec` —
10
- * `exec` satisfies the §3.2 detection rule but is never invoked since
11
- * `daemon` is the outermost terminal modifier and ignores `next`.
12
- */
13
- import { ephemeral } from "@executablemd/durable-streams";
14
- import { daemon } from "@effectionx/process";
15
- import { useCodeBlock } from "../modifiers.ts";
16
- import { evalScope } from "../component-api.ts";
17
- // ---------------------------------------------------------------------------
18
- // daemonFactory — terminal modifier (spec §3.3)
19
- // ---------------------------------------------------------------------------
20
- /**
21
- * Terminal modifier factory for long-running background processes.
22
- *
23
- * Ignores `next` — this is the terminal handler (like `exec` and `eval`).
24
- * Reads code block metadata via useCodeBlock() and the eval scope via
25
- * the contextual `evalScope` value.
26
- *
27
- * The block's content (already interpolated with eval bindings by the
28
- * expansion engine) is used to build the subprocess command. The command
29
- * is forked into the eval scope via `evalScope.eval()` — the subprocess
30
- * lives for the duration of component expansion.
31
- *
32
- * If the process exits prematurely, `daemon()` throws `DaemonExitError`,
33
- * which propagates through the eval scope to the document expansion.
34
- *
35
- * Daemon blocks produce no rendered output and no journal entry.
36
- */
37
- export const daemonFactory = (_params) => (_args, _next) => (function* () {
38
- const ctx = yield* useCodeBlock();
39
- // Bridge from Workflow (durable) to Operation (ephemeral) —
40
- // daemon produces no journal entry, so all its effects are ephemeral.
41
- const launchDaemon = {
42
- *[Symbol.iterator]() {
43
- const scope = yield* evalScope;
44
- if (!scope) {
45
- throw new Error("daemon requires a component eval scope; none is in scope.");
46
- }
47
- // The block content is a raw shell command (e.g. the body of a
48
- // ```bash daemon exec``` block). Pass it directly to daemon()
49
- // with shell:true so @effectionx/process invokes the system
50
- // shell instead of splitting with shellwords — which would
51
- // mangle commands containing nested quotes.
52
- yield* scope.eval(function* () {
53
- yield* daemon(ctx.content, { shell: true });
54
- });
55
- },
56
- };
57
- yield* ephemeral(launchDaemon);
58
- // Control returns here immediately after the fork.
59
- return { output: "", exitCode: 0, stderr: "" };
60
- })();
@@ -1,35 +0,0 @@
1
- /**
2
- * The `persist` wrapping modifier factory (spec §7.1).
3
- *
4
- * Extends resource lifetime from block scope to the component's eval-scope.
5
- * By default, resources spawned inside a block's operation are scoped to
6
- * that block's execution — they are torn down when the block's generator
7
- * returns. With `persist`, the eval handler runs the compiled block inside
8
- * evalScope.eval(), which retains spawned resources in the persistent
9
- * EvalScope until the component finishes expanding.
10
- *
11
- * Implementation: persist installs the contextual `persistent` value middleware that
12
- * the eval handler reads. The eval handler then runs fn(env) inside
13
- * evalScope.eval() instead of directly. This avoids wrapping the entire
14
- * modifier chain (which includes durable effects that can't cross the
15
- * evalScope channel boundary).
16
- */
17
- import { scoped } from "effection";
18
- import { ephemeral } from "@executablemd/durable-streams";
19
- import { Component } from "../component-api.ts";
20
- // ---------------------------------------------------------------------------
21
- // persistFactory (spec §7.1)
22
- // ---------------------------------------------------------------------------
23
- /**
24
- * Wrapping modifier that marks a block for persistent resource lifetime.
25
- *
26
- * Makes the contextual `persistent` value answer true for the duration of the inner
27
- * chain. The eval handler reads this and routes the compiled block
28
- * execution through evalScope.eval() for resource retention.
29
- */
30
- export const persistFactory = (_params) => (_args, next) => (function* () {
31
- return yield* ephemeral(scoped(function* () {
32
- yield* Component.around({ persistent: () => true }, { at: "min" });
33
- return yield* next();
34
- }));
35
- })();
@@ -1,55 +0,0 @@
1
- /**
2
- * The `timeout` wrapping modifier factory (spec §7.3).
3
- *
4
- * Constrains a code block's execution time using timebox() from
5
- * @effectionx/timebox. If the block does not complete within the
6
- * specified duration, the factory throws an error.
7
- *
8
- * timebox() returns a Timeboxed<T> discriminated union — not a thrown
9
- * error. The factory checks .timeout and raises explicitly.
10
- */
11
- import { timebox } from "@effectionx/timebox";
12
- import { ephemeral } from "@executablemd/durable-streams";
13
- // ---------------------------------------------------------------------------
14
- // Duration parsing
15
- // ---------------------------------------------------------------------------
16
- /**
17
- * Parse a duration string into milliseconds.
18
- *
19
- * Supports:
20
- * - `500ms` → 500
21
- * - `30s` → 30000
22
- * - `2m` → 120000
23
- * - `500` → 500 (raw number, treated as ms)
24
- */
25
- export function parseDuration(s) {
26
- if (s.endsWith("ms"))
27
- return parseInt(s, 10);
28
- if (s.endsWith("m"))
29
- return parseInt(s, 10) * 60_000;
30
- if (s.endsWith("s"))
31
- return parseInt(s, 10) * 1_000;
32
- return parseInt(s, 10);
33
- }
34
- // ---------------------------------------------------------------------------
35
- // timeoutFactory (spec §7.3)
36
- // ---------------------------------------------------------------------------
37
- /**
38
- * Wrapping modifier that constrains block execution time.
39
- *
40
- * Usage: `timeout=30s eval` or `timeout=500ms eval`
41
- * Default: 30s if no params provided.
42
- *
43
- * The timebox() call is an Operation (yields Effect values), so it
44
- * must be bridged into the Workflow context via ephemeral(). The
45
- * inner next() call returns a CodeBlockWorkflow which is cast to
46
- * Operation for timebox compatibility.
47
- */
48
- export const timeoutFactory = (params) => (_args, next) => (function* () {
49
- const ms = parseDuration(params ?? "30s");
50
- const result = yield* ephemeral(timebox(ms, () => next()));
51
- if (result.timeout) {
52
- throw new Error(`eval block timed out after ${params ?? "30s"}`);
53
- }
54
- return result.value;
55
- })();
@@ -1,113 +0,0 @@
1
- /**
2
- * Modifier system — middleware chain for code block execution (spec §3.3).
3
- *
4
- * Each modifier in the info string is a middleware that wraps the next
5
- * handler in the chain. The rightmost modifier (exec/eval) is the terminal.
6
- *
7
- * Aligns with Effection v4.1's Middleware<TArgs, TReturn> pattern:
8
- * - CodeBlockContext is delivered via Effection Context (scope-inherited)
9
- * - Each modifier's params are captured in a factory closure
10
- * - The middleware signature is Middleware<[], CodeBlockWorkflow>
11
- */
12
- import { scoped } from "effection";
13
- import { ephemeral } from "@executablemd/durable-streams";
14
- import { combine } from "@effectionx/middleware";
15
- import { Component, codeBlock } from "./component-api.ts";
16
- /**
17
- * Read the current code block context.
18
- *
19
- * Ergonomic alias for the Component `codeBlock()` operation, bridged via
20
- * `ephemeral` so it can be `yield*`'d inside modifier handlers that run
21
- * within durable workflows.
22
- */
23
- export function useCodeBlock() {
24
- return ephemeral(codeBlock());
25
- }
26
- /**
27
- * Create a new modifier registry, optionally inheriting from a parent.
28
- */
29
- export function createModifierRegistry(parent) {
30
- const registry = new Map();
31
- if (parent) {
32
- for (const [name, factory] of parent) {
33
- registry.set(name, factory);
34
- }
35
- }
36
- return registry;
37
- }
38
- // ---------------------------------------------------------------------------
39
- // Chain composition (spec §3.3)
40
- // ---------------------------------------------------------------------------
41
- /**
42
- * Compose a modifier chain from the info string.
43
- *
44
- * Uses the reusable `combine()` primitive from `./middleware.ts` to
45
- * build the chain. Each modifier's factory is called with its parsed
46
- * params to produce a middleware, then all middlewares are combined.
47
- *
48
- * The chain is composed **left-to-right for wrapping order**: the
49
- * leftmost modifier is the outermost wrapper. `combine` handles the
50
- * right-to-left reduction internally.
51
- *
52
- * The `context` parameter is made available to handlers via scope-local
53
- * `Component.operations.codeBlock()` middleware, installed for the
54
- * duration of the chain execution and removed when its scope exits.
55
- */
56
- export function composeModifierChain(modifiers, context, registry) {
57
- // deno-lint-ignore require-yield
58
- const terminal = function* () {
59
- throw new Error("No terminal modifier (exec/eval) in chain");
60
- };
61
- // Build the middleware array — each factory is called with its params
62
- const middlewares = [];
63
- for (const mod of modifiers) {
64
- const factory = registry.get(mod.name);
65
- if (!factory) {
66
- const missingName = mod.name;
67
- // deno-lint-ignore require-yield
68
- return function* () {
69
- throw new Error(`Unknown modifier: ${missingName}`);
70
- };
71
- }
72
- middlewares.push(factory(mod.params));
73
- }
74
- // Combine all middlewares into a single middleware
75
- const composed = combine(middlewares);
76
- // Return a thunk that provides the code block contextually for the
77
- // duration of the chain, then runs the composed middleware.
78
- // The cast is safe because CodeBlockWorkflow yields DurableEffect
79
- // values which extend Effect — structurally compatible with Operation.
80
- return function* () {
81
- return yield* ephemeral(scoped(function* () {
82
- yield* Component.around({
83
- // deno-lint-ignore require-yield
84
- *codeBlock(_args, _next) {
85
- return context;
86
- },
87
- }, { at: "min" });
88
- return yield* composed([], terminal);
89
- }));
90
- };
91
- }
92
- // ---------------------------------------------------------------------------
93
- // Command construction (spec §3.6)
94
- // ---------------------------------------------------------------------------
95
- /**
96
- * Build the command array for executing a code block.
97
- */
98
- export function buildCommand(language, content) {
99
- switch (language.toLowerCase()) {
100
- case "bash":
101
- case "sh":
102
- return ["bash", "-c", content];
103
- case "python":
104
- case "py":
105
- return ["python", "-c", content];
106
- case "node":
107
- case "javascript":
108
- case "js":
109
- return ["node", "-e", content];
110
- default:
111
- return [language, "-c", content];
112
- }
113
- }
@@ -1,33 +0,0 @@
1
- /**
2
- * Whitespace normalization middleware (spec §9.4).
3
- *
4
- * Stateful middleware that tracks trailing newlines across output() calls.
5
- * Collapses doubled blank lines at segment boundaries without needing the
6
- * full document.
7
- *
8
- * Mutable closure state (trailingNewlines) is safe because the middleware
9
- * is scoped per useNormalizedOutput() call — one instance per document
10
- * run, not shared across concurrent scopes.
11
- */
12
- import { DocumentOutput } from "../api.ts";
13
- export function* useNormalizedOutput() {
14
- let trailingNewlines = 0;
15
- yield* DocumentOutput.around({
16
- *output([text], next) {
17
- let normalized = text;
18
- // Strip trailing whitespace on each line
19
- normalized = normalized.replace(/[ \t]+\n/g, "\n");
20
- // Collapse leading newlines if previous write already ended
21
- // with enough to form a blank line
22
- if (trailingNewlines >= 2) {
23
- normalized = normalized.replace(/^\n+/, "\n");
24
- }
25
- // Collapse runs of 3+ newlines within a single write
26
- normalized = normalized.replace(/\n{3,}/g, "\n\n");
27
- // Track trailing newlines for next call
28
- const match = normalized.match(/\n+$/);
29
- trailingNewlines = match ? match[0].length : 0;
30
- yield* next(normalized);
31
- },
32
- });
33
- }
@@ -1,20 +0,0 @@
1
- /**
2
- * Terminal ANSI formatting middleware (spec §9.5).
3
- *
4
- * Converts markdown text to ANSI-colored terminal text using marked-terminal.
5
- * Synchronous only — async: false, no promises.
6
- */
7
- import { Marked } from "marked";
8
- // @ts-ignore -- marked-terminal has no type declarations
9
- import { markedTerminal } from "marked-terminal";
10
- import { DocumentOutput } from "../api.ts";
11
- export function* useTerminalOutput() {
12
- // markedTerminal() returns a marked extension object ({ renderer, useNewRenderer })
13
- const marked = new Marked(markedTerminal());
14
- yield* DocumentOutput.around({
15
- *output([text], next) {
16
- const formatted = marked.parse(text, { async: false });
17
- yield* next(formatted);
18
- },
19
- });
20
- }
@@ -1,30 +0,0 @@
1
- /**
2
- * Segment rendering — converts expanded segments to output string (spec §9).
3
- */
4
- /**
5
- * Render an array of expanded segments into a markdown string.
6
- */
7
- export function renderSegments(segments) {
8
- return segments.map(renderSegment).join("");
9
- }
10
- /**
11
- * Render a single segment to its string representation.
12
- */
13
- export function renderSegment(segment) {
14
- switch (segment.type) {
15
- case "text":
16
- return segment.content;
17
- case "execOutput":
18
- return segment.result.stdout;
19
- case "error":
20
- return `<!-- ERROR: ${segment.message} -->`;
21
- case "component":
22
- // Unexpanded component (shouldn't appear after expansion)
23
- return `<!-- UNEXPANDED: <${segment.name} /> -->`;
24
- case "codeBlock":
25
- // Shouldn't appear after expansion (all executable blocks are processed)
26
- return `\`\`\`${segment.language}\n${segment.content}\n\`\`\``;
27
- default:
28
- return "";
29
- }
30
- }
@@ -1,58 +0,0 @@
1
- /**
2
- * Replay-safe stream — the document output transport (spec §9).
3
- *
4
- * An unbuffered channel drops values sent before a consumer subscribes. The
5
- * replay stream retains its full event history so late subscribers receive
6
- * every chunk and the close value, and multiple subscribers each get the
7
- * complete sequence exactly once.
8
- *
9
- * Race-free by construction: delivery appends to the history and forwards to
10
- * every registered subscriber queue in one synchronous step, and a new
11
- * subscription replays the history into its own queue and registers for live
12
- * events before yielding — no event can fall between replay and registration.
13
- */
14
- import { createQueue, ensure } from "effection";
15
- export function createReplayStream() {
16
- const history = [];
17
- const subscribers = new Set();
18
- let closed = false;
19
- function replay(queue, event) {
20
- if (event.done) {
21
- queue.close(event.value);
22
- }
23
- else {
24
- queue.add(event.value);
25
- }
26
- }
27
- function deliver(event) {
28
- if (closed) {
29
- return;
30
- }
31
- closed = event.done === true;
32
- history.push(event);
33
- for (const queue of subscribers) {
34
- replay(queue, event);
35
- }
36
- }
37
- return {
38
- // deno-lint-ignore require-yield
39
- *send(value) {
40
- deliver({ done: false, value });
41
- },
42
- // deno-lint-ignore require-yield
43
- *close(value) {
44
- deliver({ done: true, value });
45
- },
46
- *[Symbol.iterator]() {
47
- const queue = createQueue();
48
- for (const event of history) {
49
- replay(queue, event);
50
- }
51
- subscribers.add(queue);
52
- yield* ensure(() => {
53
- subscribers.delete(queue);
54
- });
55
- return queue;
56
- },
57
- };
58
- }
@@ -1,45 +0,0 @@
1
- /**
2
- * The Sample Api — Effection Api for LLM inference.
3
- *
4
- * Provider components (e.g., DeepInfraProvider, OllamaProvider) install
5
- * middleware via `yield* Sample.around(...)` to route sample calls to
6
- * their inference server.
7
- *
8
- * The `<Sample>` component calls `Sample.operations.sample()` to send
9
- * content to the LLM.
10
- *
11
- * The core handler throws by default — middleware must be installed
12
- * before any `<Sample>` component runs.
13
- */
14
- import { createApi } from "@effectionx/context-api";
15
- /**
16
- * The Sample Api instance.
17
- *
18
- * Usage in provider components (eval blocks):
19
- * ```js
20
- * const scope = yield* useScope();
21
- * yield* Sample.around({
22
- * *sample([context], next) {
23
- * if (context.model !== undefined && context.model !== model) {
24
- * return yield* next(context);
25
- * }
26
- * // ... call inference API ...
27
- * return result;
28
- * },
29
- * });
30
- * ```
31
- *
32
- * Usage in the Sample component:
33
- * ```js
34
- * const result = yield* Sample.operations.sample(context);
35
- * ```
36
- */
37
- export const Sample = createApi("Sample", {
38
- // deno-lint-ignore require-yield
39
- *sample(_context) {
40
- throw new Error("Sample Api requires provider middleware — " +
41
- "install a provider (e.g., DeepInfraProvider, OllamaProvider) or " +
42
- "yield* Sample.around({ *sample([ctx], next) { ... } }) " +
43
- "before using <Sample> components");
44
- },
45
- });