omkit 0.0.14 → 0.0.15

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.
@@ -15,8 +15,10 @@ export declare class AssertionError extends Error {
15
15
  /**
16
16
  * Build an {@link Assert} bound to one action (or the spin body): `name` is its
17
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.
18
+ * logger to append to, and `onResult` is invoked once per call with the pass
19
+ * boolean (and, on failure, an {@link AssertionError} to record into the verdict) —
20
+ * so the host can tally passes/failures and record faults in one place. The factory
21
+ * is file-agnostic — it only appends `level: "assert"` entries; the dedicated
22
+ * `assertions.log` is a downstream projection of that stream.
21
23
  */
22
- export declare function createAssert(name: string, logs: Logger, onFail: (error: AssertionError) => void): Assert;
24
+ export declare function createAssert(name: string, logs: Logger, onResult: (pass: boolean, error?: AssertionError) => void): Assert;
@@ -8,19 +8,19 @@ export class AssertionError extends Error {
8
8
  /**
9
9
  * Build an {@link Assert} bound to one action (or the spin body): `name` is its
10
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.
11
+ * logger to append to, and `onResult` is invoked once per call with the pass
12
+ * boolean (and, on failure, an {@link AssertionError} to record into the verdict) —
13
+ * so the host can tally passes/failures and record faults in one place. The factory
14
+ * is file-agnostic — it only appends `level: "assert"` entries; the dedicated
15
+ * `assertions.log` is a downstream projection of that stream.
14
16
  */
15
- export function createAssert(name, logs, onFail) {
17
+ export function createAssert(name, logs, onResult) {
16
18
  return (value, what) => {
17
19
  const pass = value;
18
20
  const glyph = pass ? "✓" : "✗";
19
21
  const message = `${name} · ${glyph} ${what}`;
20
22
  logs.append({ source: "assert", level: "assert", message });
21
- if (!pass) {
22
- onFail(new AssertionError(`${name}: ${what}`));
23
- }
23
+ onResult(pass, pass ? undefined : new AssertionError(`${name}: ${what}`));
24
24
  return pass;
25
25
  };
26
26
  }
@@ -41,6 +41,8 @@ export declare class SpinHost {
41
41
  /** The current lifecycle state — see {@link SpinState}. */
42
42
  get state(): SpinState;
43
43
  private readonly failures;
44
+ private assertionsPassed;
45
+ private assertionsFailed;
44
46
  private readonly inFlight;
45
47
  private readonly launched;
46
48
  private readonly streaming;
@@ -78,6 +78,9 @@ export class SpinHost {
78
78
  }
79
79
  // Genuine failures (not teardown cascade). Frozen into the verdict at close.
80
80
  failures = [];
81
+ // Running tally of every assertion this run made, for the run.log summary footer.
82
+ assertionsPassed = 0;
83
+ assertionsFailed = 0;
81
84
  // Every launched action's promise lives here until it settles; the drive loop
82
85
  // empties this set. Daemons keep it non-empty until the controller aborts.
83
86
  inFlight = new Set();
@@ -240,15 +243,20 @@ export class SpinHost {
240
243
  this.nodInto(child, path);
241
244
  return child;
242
245
  };
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);
246
+ // A boolean check bound to this action's path: it logs a `⊨` milestone, tallies
247
+ // the result for the run.log summary, and on failure records a verdict failure
248
+ // the same way an action's own failure does — skipped during teardown (cascade,
249
+ // not a fault). Uses `this.logs` (not `scoped`) the same way `record` below does:
250
+ // the path is already embedded in the message via `name`, so routing through
251
+ // `scoped` would double-prefix the `source` field for a nodded child.
252
+ const assert = createAssert(path, this.logs, (pass, error) => {
253
+ if (pass)
254
+ this.assertionsPassed++;
255
+ else {
256
+ this.assertionsFailed++;
257
+ if (!this.controller.signal.aborted && error)
258
+ this.recordFailure(path, error);
259
+ }
252
260
  });
253
261
  this.narrate(`launch ${path}`);
254
262
  // The action logs into this run's shared store (entries carry their `source`;
@@ -318,14 +326,14 @@ export class SpinHost {
318
326
  // before `writeLog` streams it into the final collapsed run.log.
319
327
  this.logs.close();
320
328
  await this.streaming;
321
- const file = await this.output.runLog.write();
329
+ // Hand the assertion tally to `write`, which bubbles a summary + the
330
+ // `assertions.log` link into the run.log footer (only when there were any).
331
+ const file = await this.output.runLog.write({
332
+ passed: this.assertionsPassed,
333
+ failed: this.assertionsFailed,
334
+ });
322
335
  // Direct to the terminal — the file's already written, so this line isn't in it.
323
336
  this.term(LogRenderer.marker("run", `log → ${file}`));
324
- // If this run logged any assertions, link the dedicated file beside the main one
325
- // (only exists when there was at least one assertion). Terminal-only, like `log →`.
326
- const assertionsFile = this.output.runLog.assertionsLog;
327
- if (assertionsFile)
328
- this.term(LogRenderer.marker("assert", `assertions → ${assertionsFile}`));
329
337
  const result = { ok: this.failures.length === 0, failures: this.failures };
330
338
  // One process = one run = one verdict; reflect pass/fail in the exit code so
331
339
  // shells and CI see it without parsing the log. Only set it on failure, so a
@@ -40,8 +40,15 @@ export declare class RunLog {
40
40
  *
41
41
  * Call only after {@link stream} has closed (the run's log is ended), so `run.jsonl`
42
42
  * is complete and no longer being written.
43
+ *
44
+ * When this run logged any assertions, `summary` bubbles a footer into `run.log`
45
+ * (only) — the pass/fail tally and a link to `assertions.log` — so results surface
46
+ * in the persistent log rather than the live process output.
43
47
  */
44
- write(): Promise<string>;
48
+ write(summary?: {
49
+ passed: number;
50
+ failed: number;
51
+ }): Promise<string>;
45
52
  /** A compact header block for the top of run.log — legend + run identity. */
46
53
  private header;
47
54
  }
@@ -86,8 +86,12 @@ export class RunLog {
86
86
  *
87
87
  * Call only after {@link stream} has closed (the run's log is ended), so `run.jsonl`
88
88
  * is complete and no longer being written.
89
+ *
90
+ * When this run logged any assertions, `summary` bubbles a footer into `run.log`
91
+ * (only) — the pass/fail tally and a link to `assertions.log` — so results surface
92
+ * in the persistent log rather than the live process output.
89
93
  */
90
- async write() {
94
+ async write(summary) {
91
95
  await this.folder.ensure();
92
96
  const jsonlFile = this.folder.resolve("run.jsonl");
93
97
  const file = this.folder.resolve("run.log");
@@ -126,6 +130,12 @@ export class RunLog {
126
130
  }
127
131
  for (const item of renderer.end())
128
132
  emit(item);
133
+ // Bubble the assertion summary + the dedicated-log link into the run.log footer —
134
+ // only when this run logged assertions (`assertionsPath` is set on the first one).
135
+ if (this.assertionsPath && summary) {
136
+ out.write(LogRenderer.marker("assert", `assertions · ${summary.passed} passed · ${summary.failed} failed`) + "\n");
137
+ out.write(LogRenderer.marker("assert", `assertions → ${this.assertionsPath}`) + "\n");
138
+ }
129
139
  await closeStream(out); // flush + close run.log before reporting it written
130
140
  await Promise.all(writes); // ensure every off-loaded run is on disk
131
141
  return file;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omkit",
3
- "version": "0.0.14",
3
+ "version": "0.0.15",
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",