omkit 0.0.12 → 0.0.13

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.
@@ -65,15 +65,15 @@ export const chromePage = action("Chrome Page")
65
65
  }
66
66
  page.on("console", (msg) => {
67
67
  logs.append({ source: label, level: "info", message: `console: ${msg.text()}` });
68
- emit("console", msg.text());
68
+ emit("console", `from: ${label}: ${msg.text()}`);
69
69
  });
70
70
  page.on("pageerror", (err) => {
71
71
  logs.append({ source: label, level: "error", message: err.message });
72
- emit("pageerror", err.message);
72
+ emit("pageerror", `from: ${label}: ${err.message}`);
73
73
  });
74
74
  page.on("framenavigated", (frame) => {
75
75
  if (frame === page.mainFrame())
76
- emit("navigated", frame.url());
76
+ emit("navigated", `${label}: ${frame.url()}`);
77
77
  });
78
78
  if (url) {
79
79
  await page.goto(url);
@@ -6,8 +6,6 @@ export interface CommandOptions {
6
6
  * node was executed from (process.cwd()). Default ".".
7
7
  */
8
8
  cwd?: string;
9
- /** Label the child's output is logged under. Defaults to the action's name. */
10
- label?: string;
11
9
  /**
12
10
  * Environment for the child. Pass the full set you want (e.g.
13
11
  * `{ ...process.env, NODE_ENV: "development" }`) — it's handed to the child
@@ -21,7 +21,7 @@ export function command(name, cmdOrFile, argsOrOptions, maybeOptions) {
21
21
  // against process.cwd() — exactly the "absolute from where node runs" rule.
22
22
  const cwd = path.resolve(options.cwd ?? ".");
23
23
  // proc sources the LogsCollector from async context and streams output into it.
24
- return proc(options.label ?? name, {
24
+ return proc(name, {
25
25
  cwd,
26
26
  ...(options.env ? { env: options.env } : {}),
27
27
  })(verbatim(cmdOrFile));
@@ -36,7 +36,7 @@ export function command(name, cmdOrFile, argsOrOptions, maybeOptions) {
36
36
  */
37
37
  const spawnDirect = (snapshots, logs, signal, name, file, args, options) => {
38
38
  const cwd = path.resolve(options.cwd ?? ".");
39
- const source = options.label ?? name;
39
+ const source = name;
40
40
  // windowsHide keeps a stray console window from flashing for the child.
41
41
  // env omitted -> spawn inherits the parent's process.env (Node's default).
42
42
  const child = spawn(file, args, { cwd, windowsHide: true, env: options.env });
package/dist/index.d.ts CHANGED
@@ -10,6 +10,8 @@
10
10
  */
11
11
  export { action } from "./orchestration/action/action.ts";
12
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";
13
15
  export { spin } from "./orchestration/spin/spin.ts";
14
16
  export type { Nod, SpinContext, SpinBody } from "./orchestration/spin/types.ts";
15
17
  export { events } from "./orchestration/events/events.ts";
package/dist/index.js CHANGED
@@ -10,6 +10,7 @@
10
10
  */
11
11
  // ── Actions ────────────────────────────────────────────────────────────────
12
12
  export { action } from "./orchestration/action/action.js";
13
+ export { AssertionError } from "./orchestration/assert.js";
13
14
  // ── Spin (linear orchestration) ──────────────────────────────────────────────
14
15
  export { spin } from "./orchestration/spin/spin.js";
15
16
  // ── Events ─────────────────────────────────────────────────────────────────
@@ -123,6 +123,7 @@ class ActionRun {
123
123
  signal: ctx.signal,
124
124
  nod: ctx.nod,
125
125
  output: ctx.output,
126
+ assert: ctx.assert,
126
127
  });
127
128
  // Re-throw the inner failure so the wrapper fails too (the framework re-wraps
128
129
  // it into this wrapper's own Outcome); only record on success.
@@ -2,6 +2,7 @@ import type { Logger } from "../../output/log/LogsCollector.ts";
2
2
  import type { Output } from "../../output/Output.ts";
3
3
  import type { Emitter, EventHandler } from "../events/events.ts";
4
4
  import type { Proc } from "../../system/process/process.ts";
5
+ import type { Assert } from "../assert.ts";
5
6
  /** An action that declares no events. */
6
7
  export type NoEvents = Record<never, never>;
7
8
  /** A value an action's `exec` may return: a result, or a promise of one. */
@@ -67,6 +68,12 @@ export interface SystemGlobal {
67
68
  * capture their own artifacts.
68
69
  */
69
70
  readonly output: Output;
71
+ /**
72
+ * Assert a boolean invariant for this action. Logs a `⊨` line every call; a
73
+ * mismatch is recorded into the run's verdict (never throws, never stops the
74
+ * run). Bound to this action's path for attribution.
75
+ */
76
+ readonly assert: Assert;
70
77
  }
71
78
  /** What an action's implementation receives: the system bag, plus `emit`, `attach`, and `proc`. */
72
79
  export interface ActionContext<Events extends object, Handle = void> extends SystemGlobal {
@@ -0,0 +1,22 @@
1
+ import type { Logger } from "../output/log/LogsCollector.ts";
2
+ /**
3
+ * An inline check injected into every action's `ctx` and the `spin` body. Passes
4
+ * when `value` is `true`. Every call is logged; a failure is recorded into the
5
+ * run's verdict — as data, not a throw — so the run keeps going and later
6
+ * assertions still fire. Returns the pass boolean so a body can branch on it.
7
+ *
8
+ * assert(page.isVisible(), "page loaded")
9
+ */
10
+ export type Assert = (value: boolean, what: string) => boolean;
11
+ /** Marks a failed assertion in the spin's verdict, distinct from an action's own error. */
12
+ export declare class AssertionError extends Error {
13
+ constructor(message: string);
14
+ }
15
+ /**
16
+ * Build an {@link Assert} bound to one action (or the spin body): `name` is its
17
+ * launch path (used for attribution in the log line and the error), `logs` is the
18
+ * logger to append to, and `onFail` records a genuine failure into the verdict. The
19
+ * factory is file-agnostic — it only appends `level: "assert"` entries; the
20
+ * dedicated `assertions.log` is a downstream projection of that stream.
21
+ */
22
+ export declare function createAssert(name: string, logs: Logger, onFail: (error: AssertionError) => void): Assert;
@@ -0,0 +1,26 @@
1
+ /** Marks a failed assertion in the spin's verdict, distinct from an action's own error. */
2
+ export class AssertionError extends Error {
3
+ constructor(message) {
4
+ super(message);
5
+ this.name = "AssertionError";
6
+ }
7
+ }
8
+ /**
9
+ * Build an {@link Assert} bound to one action (or the spin body): `name` is its
10
+ * launch path (used for attribution in the log line and the error), `logs` is the
11
+ * logger to append to, and `onFail` records a genuine failure into the verdict. The
12
+ * factory is file-agnostic — it only appends `level: "assert"` entries; the
13
+ * dedicated `assertions.log` is a downstream projection of that stream.
14
+ */
15
+ export function createAssert(name, logs, onFail) {
16
+ return (value, what) => {
17
+ const pass = value;
18
+ const glyph = pass ? "✓" : "✗";
19
+ const message = `${name} · ${glyph} ${what}`;
20
+ logs.append({ source: "assert", level: "assert", message });
21
+ if (!pass) {
22
+ onFail(new AssertionError(`${name}: ${what}`));
23
+ }
24
+ return pass;
25
+ };
26
+ }
@@ -4,6 +4,7 @@ import { LogsCollector, ScopedLogger } from "../../output/log/LogsCollector.js";
4
4
  import { LogRenderer } from "../../output/log/LogRenderer.js";
5
5
  import { actionScope, captureConsole } from "../../output/log/console-capture.js";
6
6
  import { Output } from "../../output/Output.js";
7
+ import { createAssert } from "../assert.js";
7
8
  /** The entry script, relative to where node was invoked (so the log says what produced it). */
8
9
  const scriptPath = () => {
9
10
  const entry = process.argv[1];
@@ -239,6 +240,16 @@ export class SpinHost {
239
240
  this.nodInto(child, path);
240
241
  return child;
241
242
  };
243
+ // A boolean check bound to this action's path: it logs a `⊨` milestone, and a
244
+ // mismatch records a verdict failure the same way an action's own failure does
245
+ // — skipped during teardown (cascade, not a fault). Uses `this.logs` (not
246
+ // `scoped`) the same way `record` below does: the path is already embedded in
247
+ // the message via `name`, so routing through `scoped` would double-prefix the
248
+ // `source` field for a nodded child.
249
+ const assert = createAssert(path, this.logs, (error) => {
250
+ if (!this.controller.signal.aborted)
251
+ this.recordFailure(path, error);
252
+ });
242
253
  this.narrate(`launch ${path}`);
243
254
  // The action logs into this run's shared store (entries carry their `source`;
244
255
  // grouping is applied at render time) and gets the spin's signal directly; the
@@ -259,7 +270,13 @@ export class SpinHost {
259
270
  this.recordFailure(path, error);
260
271
  };
261
272
  const promise = actionScope
262
- .run(path, () => instance.start({ logs: scoped, signal: this.controller.signal, nod, output: this.output }))
273
+ .run(path, () => instance.start({
274
+ logs: scoped,
275
+ signal: this.controller.signal,
276
+ nod,
277
+ output: this.output,
278
+ assert,
279
+ }))
263
280
  .then((outcome) => {
264
281
  if (!outcome.ok)
265
282
  record(outcome.error);
@@ -41,6 +41,7 @@ export function spin(body) {
41
41
  // the body from there rather than reaching for a module singleton.
42
42
  artifactsFolder: ctx.artifactsFolder,
43
43
  snapshot: (name, value) => ctx.output.snapshots.snapshot(name, value),
44
+ assert: ctx.assert,
44
45
  });
45
46
  });
46
47
  const mainSpinAction = hostAction();
@@ -1,5 +1,6 @@
1
1
  export type { Nod } from "../action/types.ts";
2
2
  import type { Nod } from "../action/types.ts";
3
+ import type { Assert } from "../assert.ts";
3
4
  /** What a {@link spin} body receives. */
4
5
  export interface SpinContext {
5
6
  /** Launch an action into the spin; returns the instance for awaiting/chaining. */
@@ -25,6 +26,12 @@ export interface SpinContext {
25
26
  * run-scoped counterpart to the action `ctx.output.snapshots`.
26
27
  */
27
28
  readonly snapshot: (name: string, value: unknown) => Promise<string>;
29
+ /**
30
+ * Assert a boolean invariant from the spin body. Same semantics as the action
31
+ * `ctx.assert`; body assertions are attributed to the host action path
32
+ * (`omkit:Spin`).
33
+ */
34
+ readonly assert: Assert;
28
35
  }
29
36
  /** The body of a {@link spin}: any (async) function — it isn't named or wrapped by you. */
30
37
  export type SpinBody = (ctx: SpinContext) => void | Promise<void>;
@@ -1,6 +1,6 @@
1
1
  import type { LogEntry } from "./LogsCollector.ts";
2
2
  /** The special "title" log lines that get a distinguishing prefix. */
3
- export type MarkerKind = "event" | "action" | "run" | "snapshot";
3
+ export type MarkerKind = "event" | "action" | "run" | "snapshot" | "assert";
4
4
  /** A run set aside for collapsing: its source and every output line, in order. */
5
5
  export interface CollapsedRun {
6
6
  readonly source: string;
@@ -3,6 +3,7 @@ const PREFIX = {
3
3
  action: "▸",
4
4
  run: "●",
5
5
  snapshot: "📎",
6
+ assert: "⊨",
6
7
  };
7
8
  /**
8
9
  * Levels that render as their own top-level milestone line — marker-prefixed and
@@ -13,6 +14,7 @@ const MILESTONE = {
13
14
  run: "run",
14
15
  event: "event",
15
16
  snapshot: "snapshot",
17
+ assert: "assert",
16
18
  };
17
19
  /** Only runs *longer than* this collapse, so a trigger always hides a meaningful amount. */
18
20
  const COLLAPSE_THRESHOLD = 15;
@@ -37,10 +37,18 @@ export class RunLog {
37
37
  const pretty = createWriteStream(this.folder.resolve("run.log"));
38
38
  const renderer = new LogRenderer();
39
39
  pretty.write(this.header() + "\n");
40
+ // The dedicated assertions file is a filtered projection of the same stream:
41
+ // opened lazily on the first `assert` entry (so runs with none produce no file),
42
+ // one `⊨ …` line each. Assertions still ride the main run.jsonl / run.log too.
43
+ let assertions;
40
44
  try {
41
45
  for await (const entry of entries) {
42
46
  jsonl.write(JSON.stringify(entry) + "\n");
43
47
  pretty.write(renderer.render(entry) + "\n");
48
+ if (entry.level === "assert") {
49
+ assertions ??= createWriteStream(this.folder.resolve("assertions.log"));
50
+ assertions.write(LogRenderer.marker("assert", entry.message) + "\n");
51
+ }
44
52
  }
45
53
  }
46
54
  finally {
@@ -49,7 +57,11 @@ export class RunLog {
49
57
  // this stream landed *after* write() truncated run.log, the OS would zero-fill the
50
58
  // gap — a multi-MB run of NUL bytes. `run` awaits this promise before calling
51
59
  // write(), so this writer is guaranteed done first (and run.jsonl is complete).
52
- await Promise.all([closeStream(jsonl), closeStream(pretty)]);
60
+ await Promise.all([
61
+ closeStream(jsonl),
62
+ closeStream(pretty),
63
+ ...(assertions ? [closeStream(assertions)] : []),
64
+ ]);
53
65
  }
54
66
  }
55
67
  /**
@@ -112,7 +124,7 @@ export class RunLog {
112
124
  const now = new Date();
113
125
  return [
114
126
  `─── ${this.folder.name()} · ${ymd(now)} ${hms(now)} ───`,
115
- `● run ⚡ event ▸ action 📎 snapshot ⇥ output`,
127
+ `● run ⚡ event ▸ action 📎 snapshot ⇥ output ⊨ assert`,
116
128
  "",
117
129
  ].join("\n");
118
130
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omkit",
3
- "version": "0.0.12",
3
+ "version": "0.0.13",
4
4
  "description": "A tiny runtime for developer workflows: typed actions that pass runtime capabilities and emit typed events, under one run and one shared log. The operational/runtime layer to tskb's knowledge layer.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",