Skip to content

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.

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>

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.

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}`)
}
}
TypeFactoryDescription
Directload(data)Inline data with no I/O
FileloadFile(path)Load a single file as a byte stream
FilesloadFiles(glob)Load multiple files matching a pattern

All file inputs produce ReadableStream<Uint8Array>. Decoding (JSON parsing, text decoding, etc.) must be explicit through transforms.

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()

Multiple plans can be combined:

// Flat merge — combines outputs
const combined = Planner.merge(configPlan, usersPlan)
// Result type: { host: string } & { users: string[] }
// Named merge — nests under keys
const nested = Planner.mergeAs({ config: configPlan, users: usersPlan })
// Result type: { config: { host: string }, users: { users: string[] } }

These properties are non-negotiable:

  1. Plans are immutable after compilation
  2. Planner is mutable and single-use
  3. Inputs are pure specifications
  4. All decoding is explicit
  5. No implicit tolerance
  6. Failures abort execution
  7. Lineage is always available
  8. AST is declarative data
  9. Execution is registry-driven