omkit 0.0.14 → 0.1.0

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.
Files changed (110) hide show
  1. package/README.md +110 -97
  2. package/dist/actions/chrome-page.d.ts +12 -29
  3. package/dist/actions/chrome-page.js +19 -32
  4. package/dist/actions/command.d.ts +8 -41
  5. package/dist/actions/command.js +15 -82
  6. package/dist/actions/healthcheck.d.ts +3 -11
  7. package/dist/actions/healthcheck.js +8 -22
  8. package/dist/actions/index.d.ts +6 -8
  9. package/dist/actions/index.js +4 -6
  10. package/dist/actions/prompt.d.ts +5 -19
  11. package/dist/actions/prompt.js +10 -31
  12. package/dist/actions/tail-log.d.ts +7 -21
  13. package/dist/actions/tail-log.js +13 -34
  14. package/dist/actions/until-log.d.ts +4 -7
  15. package/dist/actions/until-log.js +3 -3
  16. package/dist/actions/watch-dir.d.ts +5 -19
  17. package/dist/actions/watch-dir.js +13 -28
  18. package/dist/actions/watch.d.ts +4 -12
  19. package/dist/actions/watch.js +9 -16
  20. package/dist/core/ActionRun.d.ts +97 -0
  21. package/dist/core/ActionRun.js +266 -0
  22. package/dist/core/ExecutionTree.d.ts +88 -0
  23. package/dist/core/ExecutionTree.js +320 -0
  24. package/dist/core/action.d.ts +6 -0
  25. package/dist/core/action.js +72 -0
  26. package/dist/core/context.d.ts +9 -0
  27. package/dist/core/context.js +8 -0
  28. package/dist/core/om.d.ts +9 -0
  29. package/dist/core/om.js +25 -0
  30. package/dist/core/step.d.ts +9 -0
  31. package/dist/core/step.js +18 -0
  32. package/dist/core/types.d.ts +128 -0
  33. package/dist/foundation/ActionRef.d.ts +18 -0
  34. package/dist/foundation/ActionRef.js +5 -0
  35. package/dist/{utils → foundation}/AsyncQueue.d.ts +2 -4
  36. package/dist/{utils → foundation}/AsyncQueue.js +6 -19
  37. package/dist/foundation/CancelledError.d.ts +10 -0
  38. package/dist/foundation/CancelledError.js +13 -0
  39. package/dist/foundation/Deferred.d.ts +8 -0
  40. package/dist/foundation/Deferred.js +10 -0
  41. package/dist/foundation/LogEntry.d.ts +29 -0
  42. package/dist/{orchestration/events → foundation}/events.d.ts +2 -11
  43. package/dist/{orchestration/events → foundation}/events.js +1 -7
  44. package/dist/{utils → foundation}/format.d.ts +3 -4
  45. package/dist/{utils → foundation}/format.js +12 -4
  46. package/dist/foundation/fsSafe.d.ts +7 -0
  47. package/dist/foundation/fsSafe.js +7 -0
  48. package/dist/foundation/ids.d.ts +9 -0
  49. package/dist/foundation/ids.js +10 -0
  50. package/dist/index.d.ts +11 -22
  51. package/dist/index.js +10 -30
  52. package/dist/output/LiveRenderer.d.ts +13 -0
  53. package/dist/output/LiveRenderer.js +56 -0
  54. package/dist/output/console/ConsoleCapture.d.ts +20 -0
  55. package/dist/output/console/ConsoleCapture.js +47 -0
  56. package/dist/output/folder/RunFolder.d.ts +11 -22
  57. package/dist/output/folder/RunFolder.js +18 -36
  58. package/dist/output/log/LogStore.d.ts +22 -0
  59. package/dist/output/log/LogStore.js +56 -0
  60. package/dist/output/log/RawStream.d.ts +11 -0
  61. package/dist/output/log/RawStream.js +18 -0
  62. package/dist/output/snapshot/SnapshotStore.d.ts +6 -43
  63. package/dist/output/snapshot/SnapshotStore.js +12 -52
  64. package/dist/output/writers/NodeLogWriter.d.ts +8 -0
  65. package/dist/output/writers/NodeLogWriter.js +36 -0
  66. package/dist/output/writers/ResultWriter.d.ts +3 -0
  67. package/dist/output/writers/ResultWriter.js +5 -0
  68. package/dist/output/writers/RollupWriter.d.ts +9 -0
  69. package/dist/output/writers/RollupWriter.js +29 -0
  70. package/dist/output/writers/entryLine.d.ts +10 -0
  71. package/dist/output/writers/entryLine.js +39 -0
  72. package/dist/output/writers/views.d.ts +38 -0
  73. package/dist/output/writers/views.js +6 -0
  74. package/dist/system/fs/FolderCache.d.ts +6 -34
  75. package/dist/system/fs/FolderCache.js +7 -38
  76. package/dist/system/proc.d.ts +51 -0
  77. package/dist/system/proc.js +148 -0
  78. package/dist/system/procRegistry.d.ts +52 -0
  79. package/dist/system/procRegistry.js +117 -0
  80. package/package.json +3 -1
  81. package/dist/orchestration/action/action.d.ts +0 -23
  82. package/dist/orchestration/action/action.js +0 -256
  83. package/dist/orchestration/action/types.d.ts +0 -182
  84. package/dist/orchestration/assert.d.ts +0 -22
  85. package/dist/orchestration/assert.js +0 -26
  86. package/dist/orchestration/spin/SpinHost.d.ts +0 -109
  87. package/dist/orchestration/spin/SpinHost.js +0 -337
  88. package/dist/orchestration/spin/spin.d.ts +0 -24
  89. package/dist/orchestration/spin/spin.js +0 -57
  90. package/dist/orchestration/spin/types.d.ts +0 -63
  91. package/dist/output/Output.d.ts +0 -24
  92. package/dist/output/Output.js +0 -27
  93. package/dist/output/index.d.ts +0 -11
  94. package/dist/output/index.js +0 -11
  95. package/dist/output/log/LogRenderer.d.ts +0 -73
  96. package/dist/output/log/LogRenderer.js +0 -133
  97. package/dist/output/log/LogsCollector.d.ts +0 -79
  98. package/dist/output/log/LogsCollector.js +0 -141
  99. package/dist/output/log/RunLog.d.ts +0 -47
  100. package/dist/output/log/RunLog.js +0 -142
  101. package/dist/output/log/console-capture.d.ts +0 -24
  102. package/dist/output/log/console-capture.js +0 -53
  103. package/dist/system/fs/streams.d.ts +0 -11
  104. package/dist/system/fs/streams.js +0 -14
  105. package/dist/system/process/process.d.ts +0 -35
  106. package/dist/system/process/process.js +0 -104
  107. package/dist/utils/serialize.d.ts +0 -2
  108. package/dist/utils/serialize.js +0 -12
  109. /package/dist/{orchestration/action → core}/types.js +0 -0
  110. /package/dist/{orchestration/spin/types.js → foundation/LogEntry.js} +0 -0
@@ -1,3 +1,4 @@
1
+ /** A single-consumer async queue: `push` feeds it, `for await` drains it, `close` ends it. */
1
2
  export class AsyncQueue {
2
3
  items = [];
3
4
  resolvers = [];
@@ -7,35 +8,23 @@ export class AsyncQueue {
7
8
  return;
8
9
  const resolver = this.resolvers.shift();
9
10
  if (resolver) {
10
- resolver({
11
- done: false,
12
- value: item,
13
- });
11
+ resolver({ done: false, value: item });
14
12
  return;
15
13
  }
16
14
  this.items.push(item);
17
15
  }
18
- /**
19
- * Ends the queue: any parked reader resolves `{ done: true }` immediately, and
20
- * once already-buffered items are drained, future reads do too. Idempotent.
21
- */
16
+ /** Ends the queue: parked readers resolve `{ done: true }`, drained reads too. Idempotent. */
22
17
  close() {
23
18
  if (this.closed)
24
19
  return;
25
20
  this.closed = true;
26
- // Unblock everyone parked — nothing more is coming.
27
21
  for (const resolve of this.resolvers.splice(0))
28
22
  resolve({ done: true, value: undefined });
29
23
  }
30
24
  next() {
31
25
  const item = this.items.shift();
32
- if (item !== undefined) {
33
- return Promise.resolve({
34
- done: false,
35
- value: item,
36
- });
37
- }
38
- // Drained and closed → the iteration is over.
26
+ if (item !== undefined)
27
+ return Promise.resolve({ done: false, value: item });
39
28
  if (this.closed)
40
29
  return Promise.resolve({ done: true, value: undefined });
41
30
  return new Promise((resolve) => {
@@ -43,8 +32,6 @@ export class AsyncQueue {
43
32
  });
44
33
  }
45
34
  [Symbol.asyncIterator]() {
46
- return {
47
- next: () => this.next(),
48
- };
35
+ return { next: () => this.next() };
49
36
  }
50
37
  }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * The error a cancelled node reports — its `.ref` rejects with it and its `.result`
3
+ * resolves `{ ok: false, error: CancelledError }` — so an awaiter can tell cancellation
4
+ * from a genuine failure. A cancelled node is never a verdict failure.
5
+ */
6
+ export declare class CancelledError extends Error {
7
+ constructor(message?: string);
8
+ }
9
+ /** True if `error` is a {@link CancelledError}. */
10
+ export declare const isCancelled: (error: unknown) => error is CancelledError;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * The error a cancelled node reports — its `.ref` rejects with it and its `.result`
3
+ * resolves `{ ok: false, error: CancelledError }` — so an awaiter can tell cancellation
4
+ * from a genuine failure. A cancelled node is never a verdict failure.
5
+ */
6
+ export class CancelledError extends Error {
7
+ constructor(message = "cancelled") {
8
+ super(message);
9
+ this.name = "CancelledError";
10
+ }
11
+ }
12
+ /** True if `error` is a {@link CancelledError}. */
13
+ export const isCancelled = (error) => error instanceof CancelledError;
@@ -0,0 +1,8 @@
1
+ /** A promise paired with its settlers, for a set-once value (a handle, an Outcome, …). */
2
+ export interface Deferred<T> {
3
+ promise: Promise<T>;
4
+ resolve: (value: T) => void;
5
+ reject: (reason: unknown) => void;
6
+ }
7
+ /** Create a {@link Deferred}: a fresh promise with its `resolve`/`reject` captured. */
8
+ export declare function defer<T>(): Deferred<T>;
@@ -0,0 +1,10 @@
1
+ /** Create a {@link Deferred}: a fresh promise with its `resolve`/`reject` captured. */
2
+ export function defer() {
3
+ let resolve;
4
+ let reject;
5
+ const promise = new Promise((res, rej) => {
6
+ resolve = res;
7
+ reject = rej;
8
+ });
9
+ return { promise, resolve, reject };
10
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * One row on the run's timeline, and the read-only log view. Plain data shapes in
3
+ * foundation so every layer (output writers, core, actions) can name them without
4
+ * depending on the output layer. Attribution is lightweight — `nodeId`/`path`, not
5
+ * the full ActionRef — so name/tags aren't repeated per row.
6
+ */
7
+ export interface LogEntry {
8
+ /** The run-global monotonic sequence — the cross-file merge/ordering key. */
9
+ readonly sequence: number;
10
+ /** Epoch millis when appended. */
11
+ readonly ts: number;
12
+ /** The owning node's id (attribution key). */
13
+ readonly nodeId: string;
14
+ /** The owning node's execution path (`main/chromePage_9f3c`). */
15
+ readonly path: string;
16
+ /** `info` | `error` | `event` | `assert` | `snapshot` | `tag` | `child` | `run`. */
17
+ readonly level: string;
18
+ /** Finer origin within the node (proc name, `event`, `run`, …). */
19
+ readonly source: string;
20
+ readonly message: string;
21
+ }
22
+ /** What an appender supplies; the store stamps `sequence` and `ts`. */
23
+ export type LogInput = Omit<LogEntry, "sequence" | "ts">;
24
+ /** The read-only view handed to actions — they can observe the log, never write it. */
25
+ export interface ReadableLog {
26
+ subscribe(options?: {
27
+ replay?: boolean;
28
+ }): AsyncIterable<LogEntry>;
29
+ }
@@ -12,20 +12,11 @@ type EmitArgs<P> = [P] extends [void] ? [] : [payload: P];
12
12
  * immediately (with `isSnapshot: true`) before receiving live emits.
13
13
  */
14
14
  export interface Emitter<Events extends object> {
15
- /** Emit `key`'s payload to current listeners and retain it as the snapshot. */
16
15
  emit<K extends keyof Events>(key: K, ...args: EmitArgs<Events[K]>): void;
17
- /** Subscribe to `key`. Fires immediately with the snapshot if one exists. Returns an unsubscribe. */
18
16
  listen<K extends keyof Events>(key: K, handler: EventHandler<Events[K]>): () => void;
19
- /** Like {@link Emitter.listen} but auto-unsubscribes after the first delivery (snapshot counts). */
20
17
  listenOnce<K extends keyof Events>(key: K, handler: EventHandler<Events[K]>): () => void;
21
- /**
22
- * Observe **every** live emit on this bus, whatever the key — the cross-cutting
23
- * hook the bus itself stays out of. Used by the action engine to log each emit
24
- * onto the run timeline (through the action's scoped logger), so the bus is pure
25
- * pub/sub and logging is layered on top. Fires only for live emits, not snapshot
26
- * replays. Returns an unsubscribe.
27
- */
18
+ /** Observe every live emit, whatever the key — the cross-cutting hook (e.g. logging). */
28
19
  onAny(handler: (key: keyof Events, payload: unknown) => void): () => void;
29
20
  }
30
- export declare const events: <Events extends object>(_namespace?: string) => Emitter<Events>;
21
+ export declare const events: <Events extends object>() => Emitter<Events>;
31
22
  export {};
@@ -1,19 +1,15 @@
1
- export const events = (_namespace) => {
1
+ export const events = () => {
2
2
  const handlers = new Map();
3
3
  const snapshots = new Map();
4
4
  const anyHandlers = new Set();
5
5
  const emit = (key, ...args) => {
6
6
  const payload = args[0];
7
7
  snapshots.set(key, payload);
8
- // Notify the cross-cutting observers first (they log the emit onto the
9
- // timeline), then the keyed handlers — the bus does no logging of its own.
10
- // Copy so an observer unsubscribing mid-emit doesn't disturb iteration.
11
8
  for (const handler of [...anyHandlers])
12
9
  handler(key, payload);
13
10
  const set = handlers.get(key);
14
11
  if (!set)
15
12
  return;
16
- // Copy so a handler unsubscribing (or listenOnce) mid-emit doesn't disturb iteration.
17
13
  for (const handler of [...set])
18
14
  handler(payload, { isSnapshot: false });
19
15
  };
@@ -24,7 +20,6 @@ export const events = (_namespace) => {
24
20
  handlers.set(key, set);
25
21
  }
26
22
  set.add(handler);
27
- // Snapshot on subscribe, if one has ever been emitted for this key.
28
23
  if (snapshots.has(key))
29
24
  handler(snapshots.get(key), { isSnapshot: true });
30
25
  return () => {
@@ -32,7 +27,6 @@ export const events = (_namespace) => {
32
27
  };
33
28
  };
34
29
  const listenOnce = (key, handler) => {
35
- // A retained snapshot satisfies "once" immediately — deliver and we're done.
36
30
  if (snapshots.has(key)) {
37
31
  handler(snapshots.get(key), { isSnapshot: true });
38
32
  return () => { };
@@ -2,8 +2,7 @@
2
2
  export declare const pad: (n: number, width?: number) => string;
3
3
  /** A date as `YYYY-MM-DD`. */
4
4
  export declare const ymd: (d: Date) => string;
5
- /**
6
- * A clock time as `HH<sep>MM<sep>SS`. Default `:` for display; pass `-` for a
7
- * filesystem-safe form (folder names can't contain colons on every platform).
8
- */
5
+ /** A clock time as `HH<sep>MM<sep>SS` (`-` for filesystem-safe folder names). */
9
6
  export declare const hms: (d: Date, sep?: string) => string;
7
+ /** Render a value for the timeline: strings inline, everything else as JSON. */
8
+ export declare const renderValue: (v: unknown) => string;
@@ -2,8 +2,16 @@
2
2
  export const pad = (n, width = 2) => String(n).padStart(width, "0");
3
3
  /** A date as `YYYY-MM-DD`. */
4
4
  export const ymd = (d) => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
5
- /**
6
- * A clock time as `HH<sep>MM<sep>SS`. Default `:` for display; pass `-` for a
7
- * filesystem-safe form (folder names can't contain colons on every platform).
8
- */
5
+ /** A clock time as `HH<sep>MM<sep>SS` (`-` for filesystem-safe folder names). */
9
6
  export const hms = (d, sep = ":") => `${pad(d.getHours())}${sep}${pad(d.getMinutes())}${sep}${pad(d.getSeconds())}`;
7
+ /** Render a value for the timeline: strings inline, everything else as JSON. */
8
+ export const renderValue = (v) => {
9
+ if (typeof v === "string")
10
+ return v;
11
+ try {
12
+ return JSON.stringify(v);
13
+ }
14
+ catch {
15
+ return String(v);
16
+ }
17
+ };
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Make one path segment safe to use as a filename on any OS. Windows is the
3
+ * strictest — it forbids `<>:"/\|?*` (a `:` in an action name like
4
+ * `TSKB:root:watch:docs` would otherwise crash the log writer). We keep it simple:
5
+ * collapse every run of non-`[A-Za-z0-9._-]` characters to a single `-`.
6
+ */
7
+ export declare const fsSafe: (segment: string) => string;
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Make one path segment safe to use as a filename on any OS. Windows is the
3
+ * strictest — it forbids `<>:"/\|?*` (a `:` in an action name like
4
+ * `TSKB:root:watch:docs` would otherwise crash the log writer). We keep it simple:
5
+ * collapse every run of non-`[A-Za-z0-9._-]` characters to a single `-`.
6
+ */
7
+ export const fsSafe = (segment) => segment.replace(/[^a-zA-Z0-9._-]+/g, "-");
@@ -0,0 +1,9 @@
1
+ /** A fresh v4 uuid for a node. */
2
+ export declare const newUuid: () => string;
3
+ /** The short form used in ids/paths — first 8 hex of the uuid. */
4
+ export declare const shortId: (uuid: string) => string;
5
+ /**
6
+ * A node's id: `${name}_${shortId}` → `chromePage_9f3c`. The root passes its own
7
+ * plain name (`main`) and skips this.
8
+ */
9
+ export declare const makeId: (name: string, uuid: string) => string;
@@ -0,0 +1,10 @@
1
+ import { randomUUID } from "node:crypto";
2
+ /** A fresh v4 uuid for a node. */
3
+ export const newUuid = () => randomUUID();
4
+ /** The short form used in ids/paths — first 8 hex of the uuid. */
5
+ export const shortId = (uuid) => uuid.slice(0, 8);
6
+ /**
7
+ * A node's id: `${name}_${shortId}` → `chromePage_9f3c`. The root passes its own
8
+ * plain name (`main`) and skips this.
9
+ */
10
+ export const makeId = (name, uuid) => `${name}_${shortId(uuid)}`;
package/dist/index.d.ts CHANGED
@@ -1,25 +1,14 @@
1
1
  /**
2
- * tswm — the operational/runtime layer to tskb's knowledge layer.
2
+ * omkit (refactored core) — the ExecutionTree model.
3
3
  *
4
- * The core engine: define typed {@link action}s, launch them under one
5
- * {@link run}, and share a single append-only log. Reusable actions built on
6
- * this engine live at the `tswm/actions` entry point.
7
- *
8
- * This is a curated public surface — internal helpers (the async queue, the log
9
- * renderer, marker prefixing) are intentionally not exported.
4
+ * A curated public surface. `om(body)` runs a linear orchestration as the root of
5
+ * one run; `action(...)` defines typed units you launch with `.exec()`;
6
+ * `step(name, fn)` is the inline anonymous action. The run's introspectable output
7
+ * is the on-disk `result.json` / `raw.jsonl` / `.log` files — the ExecutionTree
8
+ * engine itself is internal and intentionally not exported.
10
9
  */
11
- export { action } from "./orchestration/action/action.ts";
12
- export type { Action, ActionBuilder, ActionInstance, AnyActionInstance, ActionContext, SystemGlobal, SystemEvents, InstanceEvents, NoEvents, Awaitable, Outcome, } from "./orchestration/action/types.ts";
13
- export { AssertionError } from "./orchestration/assert.ts";
14
- export type { Assert } from "./orchestration/assert.ts";
15
- export { spin } from "./orchestration/spin/spin.ts";
16
- export type { Nod, SpinContext, SpinBody } from "./orchestration/spin/types.ts";
17
- export { events } from "./orchestration/events/events.ts";
18
- export type { Emitter, EventHandler, EventMeta } from "./orchestration/events/events.ts";
19
- export { createProc } from "./system/process/process.ts";
20
- export type { Proc } from "./system/process/process.ts";
21
- export { LogsCollector } from "./output/log/LogsCollector.ts";
22
- export type { Logger, LogEntry, LogInput, SubscribeOptions } from "./output/log/LogsCollector.ts";
23
- export { Output } from "./output/index.ts";
24
- export type { SnapshotRef } from "./output/index.ts";
25
- export { FolderCache } from "./system/fs/FolderCache.ts";
10
+ export { om } from "./core/om.ts";
11
+ export { action } from "./core/action.ts";
12
+ export { step } from "./core/step.ts";
13
+ export { CancelledError, isCancelled } from "./foundation/CancelledError.ts";
14
+ export type { Action, ActionInstance, ActionContext, OmContext, RunHandle, Outcome, } from "./core/types.ts";
package/dist/index.js CHANGED
@@ -1,33 +1,13 @@
1
1
  /**
2
- * tswm — the operational/runtime layer to tskb's knowledge layer.
2
+ * omkit (refactored core) — the ExecutionTree model.
3
3
  *
4
- * The core engine: define typed {@link action}s, launch them under one
5
- * {@link run}, and share a single append-only log. Reusable actions built on
6
- * this engine live at the `tswm/actions` entry point.
7
- *
8
- * This is a curated public surface — internal helpers (the async queue, the log
9
- * renderer, marker prefixing) are intentionally not exported.
4
+ * A curated public surface. `om(body)` runs a linear orchestration as the root of
5
+ * one run; `action(...)` defines typed units you launch with `.exec()`;
6
+ * `step(name, fn)` is the inline anonymous action. The run's introspectable output
7
+ * is the on-disk `result.json` / `raw.jsonl` / `.log` files — the ExecutionTree
8
+ * engine itself is internal and intentionally not exported.
10
9
  */
11
- // ── Actions ────────────────────────────────────────────────────────────────
12
- export { action } from "./orchestration/action/action.js";
13
- export { AssertionError } from "./orchestration/assert.js";
14
- // ── Spin (linear orchestration) ──────────────────────────────────────────────
15
- export { spin } from "./orchestration/spin/spin.js";
16
- // ── Events ─────────────────────────────────────────────────────────────────
17
- export { events } from "./orchestration/events/events.js";
18
- // ── Child processes ────────────────────────────────────────────────────────
19
- export { createProc } from "./system/process/process.js";
20
- // ── The log ────────────────────────────────────────────────────────────────
21
- export { LogsCollector } from "./output/log/LogsCollector.js";
22
- // ── Snapshots & artifacts ────────────────────────────────────────────────────
23
- // The output subsystem is owned per-run by the spin and reached through context —
24
- // `ctx.output` (folder / snapshots / runLog) inside an action, `ctx.snapshot` and
25
- // `ctx.artifactsFolder` inside a spin body. The classes are exported for typing and
26
- // advanced use; there is no process-global singleton or free-function form.
27
- export { Output } from "./output/index.js";
28
- // ── Folder cache ─────────────────────────────────────────────────────────────
29
- // Backs the `.withCache(...inputs)` method on instances (fingerprints an
30
- // action's input files/folders, not its outputs); exposed for scripts that want
31
- // to inspect or manually invalidate the cache — `FolderCache.fingerprint(...)`,
32
- // `FolderCache.dir`.
33
- export { FolderCache } from "./system/fs/FolderCache.js";
10
+ export { om } from "./core/om.js";
11
+ export { action } from "./core/action.js";
12
+ export { step } from "./core/step.js";
13
+ export { CancelledError, isCancelled } from "./foundation/CancelledError.js";
@@ -0,0 +1,13 @@
1
+ import type { LogStore } from "./log/LogStore.ts";
2
+ /**
3
+ * Renders a **curated** stream of milestones to the terminal, live — action
4
+ * launches, completions, and errors, plus events, asserts, and snapshots. The bulk
5
+ * output (console lines, proc stdout/stderr) is intentionally *not* shown live; it
6
+ * lives in the per-action `.log` files. Writes through the pre-patch terminal
7
+ * writer (so it can't feed back into console capture).
8
+ */
9
+ export declare class LiveRenderer {
10
+ private readonly write;
11
+ constructor(write: (line: string) => void);
12
+ run(store: LogStore): Promise<void>;
13
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Renders a **curated** stream of milestones to the terminal, live — action
3
+ * launches, completions, and errors, plus events, asserts, and snapshots. The bulk
4
+ * output (console lines, proc stdout/stderr) is intentionally *not* shown live; it
5
+ * lives in the per-action `.log` files. Writes through the pre-patch terminal
6
+ * writer (so it can't feed back into console capture).
7
+ */
8
+ export class LiveRenderer {
9
+ write;
10
+ constructor(write) {
11
+ this.write = write;
12
+ }
13
+ async run(store) {
14
+ for await (const entry of store.subscribe({ replay: true })) {
15
+ const line = format(entry);
16
+ if (line !== null)
17
+ this.write(line);
18
+ }
19
+ }
20
+ }
21
+ const firstLine = (message) => message.split("\n", 1)[0] ?? message;
22
+ /** ✓ ok · ⊘ cancelled (a clean stop) · ✗ failed. */
23
+ const doneIcon = (status) => status === "ok" ? "✓" : status === "cancelled" ? "⊘" : "✗";
24
+ /** Map a curated entry to a terminal line, or `null` to drop it from the live view. */
25
+ const format = (e) => {
26
+ // Action launch — bubbled onto the parent as `→ <childId> · <log>`.
27
+ if (e.level === "child" && e.source === "launch") {
28
+ const child = e.message.replace(/^→\s*/, "").split(" · ")[0];
29
+ return `▶ ${e.path}/${child}`;
30
+ }
31
+ // Lifecycle events: `launched` is already shown via the launch pointer above, so
32
+ // drop the duplicate; `done · <status>` renders with the status icon.
33
+ if (e.level === "event" && e.source === "lifecycle") {
34
+ if (e.message === "launched")
35
+ return null;
36
+ const status = e.message.replace(/^done · /, "");
37
+ return `${doneIcon(status)} ${e.path} · ${status}`;
38
+ }
39
+ // Run milestones (started / tearing down / finished).
40
+ if (e.level === "run")
41
+ return `· ${e.message}`;
42
+ // An action's *own* failure — not proc stderr (source = proc name), console.error,
43
+ // or a teardown cancellation (which is a clean stop, not an error).
44
+ if (e.level === "error" && e.source === "error") {
45
+ return `✗ ${e.path} · ${firstLine(e.message)}`;
46
+ }
47
+ // User-emitted events.
48
+ if (e.level === "event")
49
+ return `⚡ ${e.path} · ${e.message}`;
50
+ if (e.level === "assert")
51
+ return ` ${e.path} · ${e.message}`;
52
+ if (e.level === "snapshot")
53
+ return `📸 ${e.path} · ${e.message}`;
54
+ // Everything else (info / console / proc output / tag / bubbled child events) → files only.
55
+ return null;
56
+ };
@@ -0,0 +1,20 @@
1
+ import type { ActionRef } from "../../foundation/ActionRef.ts";
2
+ import type { LogStore } from "../log/LogStore.ts";
3
+ /** Resolves the action a console call should be attributed to (the ALS current node). */
4
+ export type AttributionProvider = () => ActionRef | undefined;
5
+ /**
6
+ * Folds every `console.*` call made during the run into the log, attributed to the
7
+ * action it was called in. Uses `patch-console`, which swaps the whole console
8
+ * surface (`log`/`table`/`trace`/`group`/…) over a captured `Console`, so native
9
+ * formatting is preserved. Level is stream-granular: stdout → `info`, stderr →
10
+ * `error`. The `AttributionProvider` (injected by `core`, wired to the ALS) keeps
11
+ * this layer ignorant of the execution tree.
12
+ */
13
+ export declare class ConsoleCapture {
14
+ private readonly store;
15
+ private readonly attribution;
16
+ private restore;
17
+ constructor(store: LogStore, attribution: AttributionProvider);
18
+ install(): void;
19
+ uninstall(): void;
20
+ }
@@ -0,0 +1,47 @@
1
+ import { Console as NodeConsole } from "node:console";
2
+ import patchConsole from "patch-console";
3
+ /**
4
+ * Folds every `console.*` call made during the run into the log, attributed to the
5
+ * action it was called in. Uses `patch-console`, which swaps the whole console
6
+ * surface (`log`/`table`/`trace`/`group`/…) over a captured `Console`, so native
7
+ * formatting is preserved. Level is stream-granular: stdout → `info`, stderr →
8
+ * `error`. The `AttributionProvider` (injected by `core`, wired to the ALS) keeps
9
+ * this layer ignorant of the execution tree.
10
+ */
11
+ export class ConsoleCapture {
12
+ store;
13
+ attribution;
14
+ restore;
15
+ constructor(store, attribution) {
16
+ this.store = store;
17
+ this.attribution = attribution;
18
+ }
19
+ install() {
20
+ // patch-console does `new console.Console(...)`; some runners (vitest) swap the
21
+ // global console for one without `.Console`, so fill it from node:console.
22
+ const globalConsole = console;
23
+ if (typeof globalConsole.Console !== "function")
24
+ globalConsole.Console = NodeConsole;
25
+ this.restore = patchConsole((stream, data) => {
26
+ const ref = this.attribution();
27
+ if (!ref)
28
+ return;
29
+ const level = stream === "stderr" ? "error" : "info";
30
+ for (const line of data.split("\n")) {
31
+ if (line.length === 0)
32
+ continue;
33
+ this.store.append({
34
+ nodeId: ref.id,
35
+ path: ref.path,
36
+ level,
37
+ source: "console",
38
+ message: line,
39
+ });
40
+ }
41
+ });
42
+ }
43
+ uninstall() {
44
+ this.restore?.();
45
+ this.restore = undefined;
46
+ }
47
+ }
@@ -1,29 +1,18 @@
1
1
  /**
2
- * Owns this process's single output directory — `logs/<name>/<date>/<time>/`, where
3
- * `run.log`, `run.jsonl`, and snapshots all live together.
4
- *
5
- * One folder per run, one run per process: the path is computed **once, lazily**, so
6
- * a snapshot taken mid-run lands in the same folder as the log that's flushed later.
7
- * The {@link SnapshotStore} and {@link RunLog} take a shared instance rather than
8
- * reaching for module globals, so "where does this run's output go" has one owner.
2
+ * Owns this run's single output directory — `logs/<name>/<date>/<time>/` — where
3
+ * `raw.jsonl`, the per-action `.log` files, rollups, and `result.json` all live.
4
+ * The path is computed once, lazily, and always returned absolute so links written
5
+ * into logs resolve regardless of cwd.
9
6
  */
10
7
  export declare class RunFolder {
11
8
  private dir;
12
- private made;
13
- private ensuredSync;
14
- /** The entry script's base name (the run's name), e.g. "tskb-build". */
9
+ private ensured;
10
+ /** The entry script's base name — the run's folder name. */
15
11
  name(): string;
16
- /** The run's output directory, `logs/<name>/<date>/<time>/` — computed once, lazily. */
12
+ /** The absolute run directory, computed once and created on first access. */
17
13
  path(): string;
18
- /** A path inside the run folder — `join(path(), name)`. */
19
- resolve(name: string): string;
20
- /** Create the folder, memoized — every caller awaits the same underlying mkdir. */
21
- ensure(): Promise<void>;
22
- /**
23
- * Absolute path to the run folder, created **synchronously** so a caller can write
24
- * into it in the same tick. Absolute so it's safe to hand to a child process with a
25
- * different cwd. This is the artifact drop for actions that want to write beside
26
- * `run.log` (one folder per run; one run per process).
27
- */
28
- artifacts(): string;
14
+ /** Absolute path to `name` inside the run folder; ensures the folder exists. */
15
+ file(name: string): string;
16
+ /** Create the run folder synchronously (idempotent). */
17
+ ensure(): void;
29
18
  }
@@ -1,56 +1,38 @@
1
1
  import { mkdirSync } from "node:fs";
2
- import { mkdir } from "node:fs/promises";
3
2
  import path from "node:path";
4
- import { ymd, hms } from "../../utils/format.js";
3
+ import { ymd, hms } from "../../foundation/format.js";
5
4
  /**
6
- * Owns this process's single output directory — `logs/<name>/<date>/<time>/`, where
7
- * `run.log`, `run.jsonl`, and snapshots all live together.
8
- *
9
- * One folder per run, one run per process: the path is computed **once, lazily**, so
10
- * a snapshot taken mid-run lands in the same folder as the log that's flushed later.
11
- * The {@link SnapshotStore} and {@link RunLog} take a shared instance rather than
12
- * reaching for module globals, so "where does this run's output go" has one owner.
5
+ * Owns this run's single output directory — `logs/<name>/<date>/<time>/` — where
6
+ * `raw.jsonl`, the per-action `.log` files, rollups, and `result.json` all live.
7
+ * The path is computed once, lazily, and always returned absolute so links written
8
+ * into logs resolve regardless of cwd.
13
9
  */
14
10
  export class RunFolder {
15
- // Computed once on first access and cached — every consumer sees the same folder.
16
11
  dir;
17
- // Memoized async mkdir; the promise is shared so concurrent callers await one op.
18
- made;
19
- // Whether the folder has been created synchronously (for `artifacts()`).
20
- ensuredSync = false;
21
- /** The entry script's base name (the run's name), e.g. "tskb-build". */
12
+ ensured = false;
13
+ /** The entry script's base name — the run's folder name. */
22
14
  name() {
23
15
  const entry = process.argv[1];
24
- return entry ? path.basename(entry, path.extname(entry)) : "pipeline";
16
+ return entry ? path.basename(entry, path.extname(entry)) : "om";
25
17
  }
26
- /** The run's output directory, `logs/<name>/<date>/<time>/` — computed once, lazily. */
18
+ /** The absolute run directory, computed once and created on first access. */
27
19
  path() {
28
20
  if (this.dir)
29
21
  return this.dir;
30
22
  const now = new Date();
31
- this.dir = path.join("logs", this.name(), ymd(now), hms(now, "-"));
23
+ this.dir = path.resolve("logs", this.name(), ymd(now), hms(now, "-"));
32
24
  return this.dir;
33
25
  }
34
- /** A path inside the run folder — `join(path(), name)`. */
35
- resolve(name) {
26
+ /** Absolute path to `name` inside the run folder; ensures the folder exists. */
27
+ file(name) {
28
+ this.ensure();
36
29
  return path.join(this.path(), name);
37
30
  }
38
- /** Create the folder, memoized — every caller awaits the same underlying mkdir. */
31
+ /** Create the run folder synchronously (idempotent). */
39
32
  ensure() {
40
- return (this.made ??= mkdir(this.path(), { recursive: true }).then(() => { }));
41
- }
42
- /**
43
- * Absolute path to the run folder, created **synchronously** so a caller can write
44
- * into it in the same tick. Absolute so it's safe to hand to a child process with a
45
- * different cwd. This is the artifact drop for actions that want to write beside
46
- * `run.log` (one folder per run; one run per process).
47
- */
48
- artifacts() {
49
- const abs = path.resolve(this.path());
50
- if (!this.ensuredSync) {
51
- mkdirSync(abs, { recursive: true });
52
- this.ensuredSync = true;
53
- }
54
- return abs;
33
+ if (this.ensured)
34
+ return;
35
+ mkdirSync(this.path(), { recursive: true });
36
+ this.ensured = true;
55
37
  }
56
38
  }
@@ -0,0 +1,22 @@
1
+ import type { LogEntry, LogInput, ReadableLog } from "../../foundation/LogEntry.ts";
2
+ /**
3
+ * The run's single append-only timeline. Owns the run-global `sequence`, keeps the
4
+ * full history (for replay + finalize projections), and fans live entries out to
5
+ * subscribers. It holds no presentation — headers/prefixes are applied by writers
6
+ * at read time. Actions receive it only as {@link ReadableLog}.
7
+ */
8
+ export declare class LogStore implements ReadableLog {
9
+ private seq;
10
+ private readonly history;
11
+ private readonly subscribers;
12
+ private ended;
13
+ /** Append one entry; stamps `sequence`/`ts`, retains it, fans it out. */
14
+ append(input: LogInput): LogEntry;
15
+ subscribe({ replay }?: {
16
+ replay?: boolean | undefined;
17
+ }): AsyncIterable<LogEntry>;
18
+ /** EOF for readers: end every live subscription and refuse new ones. Called once at finalize. */
19
+ close(): void;
20
+ /** The complete history — the source for finalize-time projections (per-node logs, rollups). */
21
+ entries(): readonly LogEntry[];
22
+ }