@executablemd/testing 0.3.1 → 0.4.1
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.
- package/esm/{testing/npm/src/src → src}/assertions.js +15 -24
- package/esm/{testing/npm/src/src → src}/handlers.js +140 -10
- package/esm/{testing/npm/src/src → src}/vocabulary.js +3 -0
- package/package.json +3 -3
- package/types/src/assertions.d.ts +11 -1
- package/types/src/handlers.d.ts +1 -0
- package/esm/core/mod.js +0 -55
- package/esm/core/src/api.js +0 -31
- package/esm/core/src/collect.js +0 -24
- package/esm/core/src/component-api.js +0 -47
- package/esm/core/src/content-context.js +0 -23
- package/esm/core/src/deno-compiler.js +0 -51
- package/esm/core/src/errors.js +0 -13
- package/esm/core/src/eval-context.js +0 -13
- package/esm/core/src/eval-handler.js +0 -79
- package/esm/core/src/eval-interpolate.js +0 -63
- package/esm/core/src/eval-transform.js +0 -378
- package/esm/core/src/execute.js +0 -378
- package/esm/core/src/expand.js +0 -988
- package/esm/core/src/frontmatter.js +0 -98
- package/esm/core/src/heal.js +0 -29
- package/esm/core/src/interpolate.js +0 -39
- package/esm/core/src/modifiers/daemon.js +0 -60
- package/esm/core/src/modifiers/persist.js +0 -35
- package/esm/core/src/modifiers/timeout.js +0 -55
- package/esm/core/src/modifiers.js +0 -113
- package/esm/core/src/output/normalize.js +0 -33
- package/esm/core/src/output/terminal.js +0 -20
- package/esm/core/src/render.js +0 -30
- package/esm/core/src/replay-stream.js +0 -58
- package/esm/core/src/sample-api.js +0 -45
- package/esm/core/src/scanner.js +0 -632
- package/esm/core/src/temp-file-compiler.js +0 -68
- package/esm/core/src/types.js +0 -7
- package/esm/core/src/validate.js +0 -82
- package/esm/durable-streams/combinators.js +0 -227
- package/esm/durable-streams/context.js +0 -13
- package/esm/durable-streams/divergence.js +0 -56
- package/esm/durable-streams/each.js +0 -193
- package/esm/durable-streams/effect.js +0 -273
- package/esm/durable-streams/ephemeral.js +0 -80
- package/esm/durable-streams/errors.js +0 -92
- package/esm/durable-streams/http-stream.js +0 -207
- package/esm/durable-streams/mod.js +0 -34
- package/esm/durable-streams/operations.js +0 -88
- package/esm/durable-streams/replay-guard.js +0 -87
- package/esm/durable-streams/replay-index.js +0 -132
- package/esm/durable-streams/run.js +0 -137
- package/esm/durable-streams/serialize.js +0 -65
- package/esm/durable-streams/stream.js +0 -56
- package/esm/durable-streams/types.js +0 -10
- package/esm/runtime/apis.js +0 -237
- package/esm/runtime/find-free-port.js +0 -33
- package/esm/runtime/mod.js +0 -20
- /package/esm/{testing/npm/src/_dnt.polyfills.js → _dnt.polyfills.js} +0 -0
- /package/esm/{testing/npm/src/_dnt.shims.js → _dnt.shims.js} +0 -0
- /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/almost_equals.js +0 -0
- /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/array_includes.js +0 -0
- /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/assert.js +0 -0
- /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/assertion_error.js +0 -0
- /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/equal.js +0 -0
- /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/equals.js +0 -0
- /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/exists.js +0 -0
- /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/fail.js +0 -0
- /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/false.js +0 -0
- /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/greater.js +0 -0
- /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/greater_or_equal.js +0 -0
- /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/instance_of.js +0 -0
- /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/is_error.js +0 -0
- /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/less.js +0 -0
- /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/less_or_equal.js +0 -0
- /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/match.js +0 -0
- /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/mod.js +0 -0
- /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/not_equals.js +0 -0
- /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/not_instance_of.js +0 -0
- /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/not_match.js +0 -0
- /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/not_strict_equals.js +0 -0
- /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/object_match.js +0 -0
- /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/rejects.js +0 -0
- /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/strict_equals.js +0 -0
- /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/string_includes.js +0 -0
- /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/throws.js +0 -0
- /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/unimplemented.js +0 -0
- /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/unreachable.js +0 -0
- /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/internal/1.0.14/build_message.js +0 -0
- /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/internal/1.0.14/diff.js +0 -0
- /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/internal/1.0.14/diff_str.js +0 -0
- /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/internal/1.0.14/format.js +0 -0
- /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/internal/1.0.14/styles.js +0 -0
- /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/internal/1.0.14/types.js +0 -0
- /package/esm/{testing/npm/src/mod.js → mod.js} +0 -0
- /package/esm/{testing/npm/src/src → src}/journal.js +0 -0
- /package/esm/{testing/npm/src/src → src}/test-api.js +0 -0
- /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
|
-
}
|
package/esm/core/src/heal.js
DELETED
|
@@ -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
|
-
}
|
package/esm/core/src/render.js
DELETED
|
@@ -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
|
-
});
|