trellis
Version:
Agentic State Engine — event-sourced causal graph with branching, decision traces, and realtime sync for AI-native applications
189 lines • 7.45 kB
TypeScript
/**
* TML v0 — Trellis Markup Language runtime.
*
* Typed source; transpiled to JS on the fly by the dashboard server for the
* browser (`/tml-runtime.js`), and imported directly by vitest under node.
*
* Spec: docs/specs/tml-v0.md. Core (Store / parse / evaluate) is DOM-free so it
* unit-tests under node; only `mount` / `applyBindings` touch the DOM.
*
* @module trellis/ui
*/
import { EAVStore } from '../core/store/eav-store.js';
import type { VcsOp } from '../vcs/types.js';
export type ResultRow = Record<string, unknown>;
export interface RefHandle {
id: string;
read(): unknown;
write(value: unknown): void;
}
export interface TmlDriver {
query(q: string): Promise<ResultRow[]>;
op(action: string, args: Record<string, unknown>): Promise<void>;
live(q: string, cb: (rows?: ResultRow[]) => void): () => void;
ref(id: string): RefHandle;
}
export interface TmlBinding {
query?: string;
each?: {
var: string;
collection: string;
} | null;
live?: boolean;
ref?: string;
op?: {
action: string;
args: {
name: string;
expr: string;
}[];
} | null;
if?: string;
text?: string;
attrs: Record<string, string>;
}
/** Outcome of walking a dotted field path. See `resolvePath`. */
export interface PathResult {
/** false = a key along the path was absent from its container. */
ok: boolean;
/** Identical to `getPath`'s return value. */
value: unknown;
}
export interface TmlDiagnostic {
code: 'unresolved-path' | 'malformed-each' | 'malformed-op' | 'each-without-query';
/** The attribute the diagnostic came from, e.g. `tml-text`. */
attr: string;
/** The offending expression, verbatim. */
expr: string;
/** Context, e.g. the scope's top-level keys. Never contains row values. */
detail?: string;
}
export interface MountOptions {
onDiagnostic?: (d: TmlDiagnostic) => void;
/** Throw on the first diagnostic instead of reporting and continuing. */
strict?: boolean;
}
/**
* Resolve a dotted field path, distinguishing "key absent" from "key present
* and undefined". `value` matches `getPath` exactly; `ok` is the new signal.
*/
export declare function resolvePath(obj: unknown, path: string): PathResult;
/** Resolve a dotted field path against an object, e.g. `lane.id`. */
export declare function getPath(obj: unknown, path: string): unknown;
/**
* Resolve a TML expression against a scope.
* Single field path / literal -> raw value (preserves booleans/numbers).
* Multiple parts joined by `+` -> concatenated string.
*/
export declare function resolveExpr(expr: string, scope: Record<string, unknown>): unknown;
/**
* The field paths an expression reads, ignoring quoted literals. Shares
* `splitPlus` with `resolveExpr` so both agree on what a literal is.
*/
export declare function expressionPaths(expr: string): string[];
/**
* Truthiness for `tml-if` only. Empty arrays and empty strings are falsy
* (unlike bare JS), so `tml-if="issue.laneIds"` hides when `laneIds === []`.
* Does not affect `tml-text` / `tml-attr-*` via `resolveExpr`.
*/
export declare function isTmlTruthy(v: unknown): boolean;
/** Parse a flat attribute map into a structured TML binding. */
export declare function parseTmlAttrs(attrs: Record<string, string | null>): TmlBinding;
/**
* Evaluate a minimal TQL `find ?e where type = 'X' [and …]` against a
* LanesSnapshot. Supports parenthesized OR groups and negated OR groups:
* `and (status = 'backlog' or status = 'queue')`
* `and not (status = 'in_progress' or status = 'paused' or status = 'closed')`
*/
export declare function evaluateQuery(query: string, snapshot: unknown): ResultRow[];
export declare class Store {
snapshot: unknown;
private _subs;
seed(s: unknown): void;
mutate(fn: (s: unknown) => void): void;
subscribe(cb: (rows?: ResultRow[]) => void): () => void;
private _notify;
}
export declare class WebDriver implements TmlDriver {
base: string;
store: Store;
private _refs;
constructor(opts?: {
baseUrl?: string;
});
seed(snapshot: unknown): void;
query(q: string): Promise<ResultRow[]>;
live(_q: string, cb: (rows?: ResultRow[]) => void): () => void;
op(action: string, args: Record<string, unknown>): Promise<void>;
ref(id: string): RefHandle;
/** Seed from snapshot, then subscribe to the SSE op stream. Returns the EventSource. */
connect(opts: {
snapshotUrl: string;
streamUrl: string;
}): Promise<unknown>;
}
/**
* A TML driver that IS a peer.
*
* `WebDriver` renders from server-built snapshots, which means the server has to
* anticipate every projection the page wants — the page cannot ask a question
* nobody planned for. That is not local-first (TRL-108 Finding 2), and it forced
* `evaluateQuery`: a regex approximation of TQL over two hardcoded collections.
* Two implementations of one query language.
*
* This applies the op stream to a real `EAVStore` via `decompose` and answers
* with the real `QueryEngine`. So TML gets whatever TQL can express — traversal,
* links, arbitrary types — instead of `Lane` and `Issue`.
*
* Cost of the whole thing (store + decompose + QueryEngine + hash verify) is
* ~5.7 KB gzipped, measured. `decompose` only became bundleable once its
* accidental `fs`/`path` imports were removed.
*
* Not the default yet: SPEC-v1.1 (TRL-110) owns whether peers materialize, and
* `WebDriver` remains for consumers that genuinely want a thin client.
*/
export declare class PeerDriver implements TmlDriver {
base: string;
store: EAVStore;
private _engine;
private _subs;
private _seen;
private _refs;
constructor(opts?: {
baseUrl?: string;
});
/**
* Apply one op. Idempotent by hash: the stream replays on reconnect, and a
* peer that double-applied would double-project.
*/
applyOp(op: VcsOp): void;
/** Apply a batch, then notify once — a per-op notify would re-render per op. */
applyOps(ops: VcsOp[]): void;
query(q: string): Promise<ResultRow[]>;
live(_q: string, cb: () => void): () => void;
op(action: string, args: Record<string, unknown>): Promise<void>;
ref(id: string): RefHandle;
/** Subscribe to the op stream and materialize it. Returns the EventSource. */
connect(opts: {
streamUrl: string;
}): unknown;
}
/**
* Resolve text/attr/if/op bindings on `el` and its children against `scope`.
* Returns false if the element was removed by a falsy `tml-if` (caller skips it).
*/
export declare function applyBindings(el: any, scope: Record<string, unknown>, driver: TmlDriver): boolean;
/**
* Static tier: malformed attributes, detectable without data. Runs at mount on
* `el` and its descendants.
*/
export declare function checkStatic(el: any, report: (d: TmlDiagnostic) => void): void;
/**
* Shape tier: field paths that do not resolve against a real result row. Runs
* once, against the first row — a binding well-formed for row 0 is well-formed
* for row N.
*/
export declare function checkShape(el: any, scope: Record<string, unknown>, report: (d: TmlDiagnostic) => void): void;
/** Mount all `tml-query` subtrees under `root` against `driver`. */
export declare function mount(root: any, driver: TmlDriver, opts?: MountOptions): void;
//# sourceMappingURL=tml-runtime.d.ts.map