Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 56 additions & 1 deletion architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,14 @@ Existing documents and code get aligned to this section retroactively.
| middleware | applied by the lexical structure, used by runtime execution |
| workflow run | a workflow being carried out with its progress and outcome recorded durably; document executions perform its work, while ongoing effects remain scoped to the document execution in which they run |
| document execution | one evaluation of a root document initiated through `execute()`, producing one output stream and one completion result while reading and appending a durable journal; its ongoing effects belong to the Effection scope in which the evaluation runs |
| workflow definition | what a workflow run is a run of: a versioned descriptor naming an immutable object — its format and object ID — together with the repository-relative path of the root document inside it. A repository locator is not part of it, and it is distinct from every Repository created inside the run's Workspace |
| workflow definition | what a workflow run is a run of: a versioned descriptor naming an immutable object — its format and object ID — together with the repository-relative path of the root document inside it, and the exact canonical document target when one is selected. A repository locator is not part of it, and it is distinct from every Repository created inside the run's Workspace |
| retrieval metadata | replaceable, credential-free information about where a workflow definition can be fetched from now; it takes no part in run identity and is reauthorized by the host before use |
| stop reason | why a workflow run or a document execution stopped: a categorical host code, or a reference to an already-filtered journal event |
| run ID | an opaque stable public identifier generated by the host or selected by an authorized caller; it associates the run's durable records and effects, remains unchanged for the life of the run, and has no semantics beyond equality and lifecycle addressing |
| definition base | the Git revision supplied to choose a workflow definition's pinned commit |
| Repository base | the optional Git revision from which one named Workspace Repository initializes its primary checkout |
| pinned commit | the commit obtained by resolving a base once; it remains the workflow run's starting repository state even as the run creates descendant commits |
| document target | an addressable static heading in a root document's own Markdown flow, named by the canonical path of heading labels that reaches it; selecting one executes the preamble, each ancestor's own content, and that heading's complete subtree |
| expansion | one logical evaluation of an authored executable element within a document execution |
| expansion ID | a deterministic identifier for one logical expansion; restoring or retrying that expansion preserves the ID, while a distinct evaluation requested by the document receives another |
| Git capability | the contextual interface through which workflow infrastructure queries the Git repository associated with the current working directory |
Expand Down Expand Up @@ -533,6 +534,51 @@ interpreter; graceful Worker shutdown alone is insufficient. Worker Shell
exposes no native executable or host PATH and is not described as POSIX or
native Bash.

## Document targets

A root document addresses its own sections. The outline is discovered from the
document's static Markdown alone: only headings in the root flow are targets,
and a heading is addressable only when its text is statically rendered — a
heading generated inside a component, or one carrying an interpolation, has no
stable address, and neither does anything beneath it. Discovery therefore
parses a copy in which the scanner's top-level component spans are blanked,
because a Markdown parser reading raw XMD cannot tell a component's children
from the root flow.

Selection resolves exactly once, before the document expands and before any
authored effect runs. A selector may glob, but it must name exactly one catalog
entry: naming none and naming several are both failures, and two sections that
canonicalize to the same path stay two entries so the ambiguity is reported
rather than resolved arbitrarily.

The selector and the target it resolves to are different things, and only one of
them is identity.

**The exact canonical target is definition identity.** It is what ran, so it is
what a document execution records durably, what a targeted workflow definition
carries, and what a resumed run is checked against.

**A caller's glob is non-authoritative invocation metadata.** It describes what
a caller asked for — two callers may spell one request differently — and it
never substitutes for the exact target: it does not occupy the recorded
exact-target field, it never enters a workflow definition, and it is never
re-resolved against a newer checkout to decide what a resumed run means. A glob
is retained in exactly one place, a failed selection's structural record, and
only so that an ordinary failed execution can be reproduced.

A resumed run re-resolves the current selector against the *recorded* content
and refuses to continue unless the outcome is the one recorded. A failed
selection is an outcome too, and is recorded and compared as one — otherwise a
journal left by a selector that matched nothing would answer a later request for
a section that does exist.

A recorded selection is a closed protocol, and a record that does not satisfy it
is refused rather than delegated. "This event is not the root import" and "the
root import, malformed" are different answers: one continues, the other fails
before the recorded terminal result can be reused, without executing authored
work or appending history. The record carries the content it was taken from, so
the selection is verified against that content rather than merely parsed.

## Expansion identity

Core describes the executable element currently being expanded:
Expand Down Expand Up @@ -569,6 +615,14 @@ JavaScript object identity.
document execution receives expansion identity without installing workflow
middleware.

Selecting a document target does not disturb any of this. Projection retains
the original source ranges and scans each one under its own origin, so a
retained element keeps the offset and line it was authored at, and with them
its expansion ID. Two runs of the same document under different targets
therefore agree on the ID of every element they both retain, and may share IDs
without sharing effects — run identity and workflow-definition identity are
what tell those runs apart.

## Two layers

Error handling has two layers:
Expand Down Expand Up @@ -890,6 +944,7 @@ Status is measured against main.
| `<PrintErrors>` / `printErrors(fn)` | prints failures | built on main |
| `<Output>` region `output` mode | an undecided error fails the document execution | built on main |
| `Expansion` / `getExpansion()` | describes the current logical element expansion | built on main |
| document targets | catalogs a root document's addressable static headings, resolves one selector to one exact target, and projects the document to it before expansion | built on the #412 stack; `xmd targets`, targeted `xmd run`, and the targeted workflow definition are unbuilt |
| `useWorkflow()` / `getWorkflowRun()` | associates one document execution with a workflow run | built on main |
| `Git.revParse()` | verifies and resolves one Git revision expression contextually | built on main |
| workflow run storage | creates or compatibly finds one run by public run ID, retains its identity, state, document executions and filtered journal, and validates immutable Workspace roots through one provider-owned connection entry | built on the #365 stack; Workspace effect publication is unbuilt |
Expand Down
21 changes: 19 additions & 2 deletions packages/core/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,25 @@ export type {
ExecutionApi,
DocumentExecution,
} from "./src/execute.ts";
export { INLINE_SOURCE_PATH, inlineSource, rootSourcePath } from "./src/root-source.ts";
export type { InlineRootDocument, RootDocumentSource } from "./src/root-source.ts";
export {
fileSource,
formatDocumentReference,
INLINE_SOURCE_PATH,
inlineSource,
rootSourcePath,
} from "./src/root-source.ts";
export type {
FileRootDocument,
InlineRootDocument,
RootDocumentSource,
} from "./src/root-source.ts";
export {
asDocumentTargetError,
DocumentTargetError,
isDocumentTargetError,
parseDocumentTargetFailure,
} from "./src/document-targets.ts";
export type { DocumentTargetErrorKind, DocumentTargetFailure } from "./src/document-targets.ts";
export { inspectComponent, inspectDocument } from "./src/inspect.ts";
export type {
ComponentInfo,
Expand Down
210 changes: 176 additions & 34 deletions packages/core/src/definition.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import type { Operation } from "effection";
import type { ComponentDefinition } from "./types.ts";
import { Ok } from "effection";
import type { Operation, Result } from "effection";
import type { ComponentDefinition, Segment } from "./types.ts";
import { parseFrontmatter } from "./frontmatter.ts";
import { compilePropsSchema, compileReturnsSchema } from "./validate.ts";
import { scanSegments } from "./scanner.ts";
import { scanComponentSpans, scanSegments } from "./scanner.ts";
import { findTarget, outlineDocument, retainedRanges, selectTarget } from "./document-targets.ts";
import type { DocumentOutline } from "./document-targets.ts";

import matter from "gray-matter";

Expand All @@ -15,56 +18,195 @@ export function isFunctionComponentPath(path: string): boolean {
return path.endsWith(".ts");
}

/** A document's frontmatter data, its markdown body, and where the body sits. */
interface ParsedSource {
data: Record<string, unknown>;
content: string;
baseOffset: number;
baseLine: number;
}

/**
* Parse markdown source into a component definition. Execution and
* inspection share this so their frontmatter and schema behavior cannot
* drift: both compile the props and return schemas, so a malformed schema
* fails the same way whether the document runs or is only described.
* Split frontmatter from the markdown body without reading either.
*
* The markdown body is a verbatim suffix of the raw file, so the body start
* is computed by length — never by content search, which could false-match
* body text repeated inside frontmatter. The invariant check turns any
* gray-matter normalization surprise into a loud error instead of silently
* wrong source positions.
*/
export function* parseMarkdownDefinition(
name: string,
path: string,
content: string,
): Operation<ComponentDefinition> {
function parseSource(path: string, content: string): ParsedSource {
const parsed = matter(content);
const { meta, props, returns } = parseFrontmatter(parsed.data);
yield* compilePropsSchema(props);
if (returns !== undefined) {
yield* compileReturnsSchema(returns);
}
// The markdown body is a verbatim suffix of the raw file, so the body start
// is computed by length — never by content search, which could false-match
// body text repeated inside frontmatter. The invariant check turns any
// gray-matter normalization surprise into a loud error instead of silently
// wrong source positions.
const bodyStart = content.length - parsed.content.length;
if (content.slice(bodyStart) !== parsed.content) {
const baseOffset = content.length - parsed.content.length;
if (content.slice(baseOffset) !== parsed.content) {
throw new Error(`frontmatter parse did not preserve the markdown body verbatim: ${path}`);
}
let baseLine = 1;
for (let i = 0; i < bodyStart; i++) {
for (let i = 0; i < baseOffset; i++) {
if (content[i] === "\n") {
baseLine++;
}
}
const bodySegments = scanSegments(parsed.content, {
path,
baseOffset: bodyStart,
baseLine,
});
return { data: parsed.data, content: parsed.content, baseOffset, baseLine };
}

/** The static heading structure a document's body offers as targets. */
function documentOutline(path: string, content: string): DocumentOutline {
const body = parseSource(path, content).content;
return outlineDocument(body, scanComponentSpans(body));
}

/**
* The exact canonical target a selector names in this document's content, or
* the failure describing why it names none.
*
* Synchronous and free of effects, so the resolution that decides *what* runs
* happens before anything runs — including inside the durable operation that
* records the root, and inside a replay guard reading recorded content. The
* outcome comes back rather than being thrown because both of those callers
* record it as data before anyone reports it.
*/
export function resolveDocumentTarget(
path: string,
content: string,
selector: string,
): Result<string> {
const found = findTarget(documentOutline(path, content), selector);
return found.ok ? Ok(found.value.target) : found;
}

interface CompiledFrontmatter {
meta: Record<string, unknown>;
props: ComponentDefinition["props"];
returns: ComponentDefinition["returns"];
}

function* compileFrontmatter(data: Record<string, unknown>): Operation<CompiledFrontmatter> {
const { meta, props, returns } = parseFrontmatter(data);
yield* compilePropsSchema(props);
if (returns !== undefined) {
yield* compileReturnsSchema(returns);
}
return { meta, props, returns };
}

function buildDefinition(
name: string,
path: string,
frontmatter: CompiledFrontmatter,
bodySegments: Segment[],
): ComponentDefinition {
// `returns` stays absent in text mode: absence is what distinguishes a text
// component from one that explicitly declares a string return.
const definition: ComponentDefinition = {
kind: "markdown",
name,
path,
meta,
props,
meta: frontmatter.meta,
props: frontmatter.props,
bodySegments,
};
if (returns !== undefined) {
definition.returns = returns;
if (frontmatter.returns !== undefined) {
definition.returns = frontmatter.returns;
}
return definition;
}

/**
* Parse markdown source into a component definition. Execution and
* inspection share this so their frontmatter and schema behavior cannot
* drift: both compile the props and return schemas, so a malformed schema
* fails the same way whether the document runs or is only described.
*/
export function* parseMarkdownDefinition(
name: string,
path: string,
content: string,
): Operation<ComponentDefinition> {
const body = parseSource(path, content);
const frontmatter = yield* compileFrontmatter(body.data);
return buildDefinition(
name,
path,
frontmatter,
scanSegments(body.content, { path, baseOffset: body.baseOffset, baseLine: body.baseLine }),
);
}

/** A root document as parsed: what it declares, and what it addresses. */
export interface ParsedRootDocument {
definition: ComponentDefinition;
/** Canonical encoded target fragments in document order, duplicates kept. */
targets: readonly string[];
/** The exact canonical target selected, when one was requested. */
target?: string;
}

/**
* Parse a root document, projecting it to one target when a selector asks for
* one.
*
* Selection happens here, before any segment exists, so a selector that names
* nothing or names several sections fails with nothing expanded. Without a
* selector the whole body is scanned exactly as an ordinary markdown component
* is.
*
* A projection scans each retained range on its own, with the origin that range
* has in the original file, rather than scanning a concatenated string. Skipped
* source therefore cannot renumber what follows it: a retained element keeps
* the offset and line it was authored at, and with them its expansion ID.
*/
export function* parseRootMarkdownDefinition(
name: string,
path: string,
content: string,
selector?: string,
): Operation<ParsedRootDocument> {
const body = parseSource(path, content);
const frontmatter = yield* compileFrontmatter(body.data);
const outline = outlineDocument(body.content, scanComponentSpans(body.content));

if (selector === undefined) {
const bodySegments = scanSegments(body.content, {
path,
baseOffset: body.baseOffset,
baseLine: body.baseLine,
});
return {
definition: buildDefinition(name, path, frontmatter, bodySegments),
targets: outline.targets,
};
}

const entry = selectTarget(outline, selector);
const newlines = newlineCounts(body.content);
const bodySegments: Segment[] = [];
for (const range of retainedRanges(outline, entry)) {
bodySegments.push(
...scanSegments(body.content.slice(range.start, range.end), {
path,
baseOffset: body.baseOffset + range.start,
baseLine: body.baseLine + newlines[range.start]!,
}),
);
}
return {
definition: buildDefinition(name, path, frontmatter, bodySegments),
targets: outline.targets,
target: entry.target,
};
}

/** How many newlines precede each offset, so a retained range knows its line. */
function newlineCounts(body: string): number[] {
const counts = new Array<number>(body.length + 1);
let seen = 0;
for (let i = 0; i < body.length; i++) {
counts[i] = seen;
if (body[i] === "\n") {
seen++;
}
}
counts[body.length] = seen;
return counts;
}
Loading
Loading