Two-Phase Architecture
OriginTS separates planning from execution into two distinct phases. This separation enables inspection, serialization, and reuse of execution plans without coupling them to runtime logic.
Build phase
Section titled “Build phase”The build phase constructs an immutable execution plan. This phase performs no I/O — it only defines what will happen.
Planner.in(input) → define input source .mapIn(transform) → apply input transform .emit((out, $) => …) → produce output .mapOut($ => …) → structurally transform output .compile() → Plan<Out>Planner
Section titled “Planner”A mutable, single-use builder that constructs execution plans through a fluent API. Define inputs, apply transforms, specify output construction, then compile to an immutable plan.
import { Planner, load } from '@origints/core'
const plan = new Planner() .in(load({ name: 'Alice', age: 30 })) .emit((out, $) => out .add('name', $.get('name').string()) .add('age', $.get('age').number()) ) .compile()An immutable execution plan containing the AST (abstract syntax tree) of operations and runtime injectors. Plans can be inspected, serialized, or executed multiple times.
Run phase
Section titled “Run phase”The run phase executes the plan against actual data sources. It records lineage at every step and returns either a success value or structured failures — never thrown exceptions.
import { run } from '@origints/core'
const result = await run(plan)
if (result.ok) { console.log(result.value)} else { for (const failure of result.failures) { console.error(`[${failure.kind}] ${failure.message}`) }}Input types
Section titled “Input types”| Type | Factory | Description |
|---|---|---|
| Direct | load(data) | Inline data with no I/O |
| File | loadFile(path) | Load a single file as a byte stream |
| Files | loadFiles(glob) | Load multiple files matching a pattern |
All file inputs produce ReadableStream<Uint8Array>. Decoding (JSON parsing, text decoding, etc.) must be explicit through transforms.
Transform system
Section titled “Transform system”Transforms are split into declarative specs and runtime implementations:
- TransformAst — Pure data describing a transform (namespace, name, arguments). JSON-serializable.
- TransformImpl — Runtime executor that performs the actual transformation.
- TransformRegistry — Maps declarative specs to implementations at execution time.
This separation allows plans to exist independently of their execution logic.
import { Planner, loadFile, parseJson } from '@origints/core'
const plan = new Planner() .in(loadFile('data.json')) .mapIn(parseJson()) // TransformAst — recorded in the plan .emit((out, $) => out .add('name', $.get('name').string()) ) .compile()Merging plans
Section titled “Merging plans”Multiple plans can be combined:
// Flat merge — combines outputsconst combined = Planner.merge(configPlan, usersPlan)// Result type: { host: string } & { users: string[] }
// Named merge — nests under keysconst nested = Planner.mergeAs({ config: configPlan, users: usersPlan })// Result type: { config: { host: string }, users: { users: string[] } }Design invariants
Section titled “Design invariants”These properties are non-negotiable:
- Plans are immutable after compilation
- Planner is mutable and single-use
- Inputs are pure specifications
- All decoding is explicit
- No implicit tolerance
- Failures abort execution
- Lineage is always available
- AST is declarative data
- Execution is registry-driven