omkit 0.8.10 → 0.8.11
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.
- package/README.md +1 -1
- package/dist/core/ActionRun.d.ts +3 -1
- package/dist/core/ActionRun.js +2 -0
- package/dist/core/ExecutionTree.d.ts +7 -0
- package/dist/core/ExecutionTree.js +27 -0
- package/dist/core/action.js +11 -2
- package/dist/core/om.js +3 -0
- package/dist/core/types.d.ts +13 -0
- package/dist/output/writers/ReportWriter.js +37 -0
- package/dist/output/writers/report/report.css +32 -4
- package/dist/output/writers/report/report.js +151 -40
- package/dist/output/writers/views.d.ts +10 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -185,7 +185,7 @@ flakyBackgroundJob().result.catch((e) => console.warn("job failed, carrying on",
|
|
|
185
185
|
|
|
186
186
|
Every action's output, events, asserts, snapshots, and lifecycle land on **one timeline** — streamed live to the terminal (curated milestones) and written to a per-run folder:
|
|
187
187
|
|
|
188
|
-
- `result.json` — the run's tree, per-node status, verdict, and curated artifacts.
|
|
188
|
+
- `result.json` — the run's tree, per-node status, verdict, and curated artifacts. Each node also carries what its definition declared about itself: the `.describe()` summary, the `.mcp()` exposure, and the `.args()` schema as JSON Schema.
|
|
189
189
|
- `raw.jsonl` — every entry, machine-readable.
|
|
190
190
|
- `main.log` + one `.log` per action — the human-readable timelines.
|
|
191
191
|
- `events.log` / `asserts.log` / `snapshots.log` / `artifacts.log` — cross-cutting rollups.
|
package/dist/core/ActionRun.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { ActionRef } from "../foundation/ActionRef.ts";
|
|
|
3
3
|
import type { LogStore } from "../output/log/LogStore.ts";
|
|
4
4
|
import type { SnapshotStore } from "../output/snapshot/SnapshotStore.ts";
|
|
5
5
|
import type { ArtifactStore } from "../output/artifact/ArtifactStore.ts";
|
|
6
|
-
import type { Activity, Exec, InstanceEvents, NodeStatus } from "./types.ts";
|
|
6
|
+
import type { ActionMeta, Activity, Exec, InstanceEvents, NodeStatus } from "./types.ts";
|
|
7
7
|
/** Everything a node needs to exist, minus its behavior. */
|
|
8
8
|
export interface NodeInit {
|
|
9
9
|
readonly store: LogStore;
|
|
@@ -45,6 +45,8 @@ export declare class ActionRun<Result = unknown, Events extends object = object,
|
|
|
45
45
|
readonly path: string;
|
|
46
46
|
readonly parentId: string | null;
|
|
47
47
|
readonly tags: string[];
|
|
48
|
+
/** What the definition said about itself (`.describe()`, `.mcp()`, `.args()`); for the record. */
|
|
49
|
+
meta: ActionMeta | undefined;
|
|
48
50
|
/** The captured args this node ran with (for the log header); set at run start. */
|
|
49
51
|
args: readonly unknown[];
|
|
50
52
|
readonly children: ActionRun[];
|
package/dist/core/ActionRun.js
CHANGED
|
@@ -18,6 +18,8 @@ export class ActionRun {
|
|
|
18
18
|
path;
|
|
19
19
|
parentId;
|
|
20
20
|
tags = [];
|
|
21
|
+
/** What the definition said about itself (`.describe()`, `.mcp()`, `.args()`); for the record. */
|
|
22
|
+
meta;
|
|
21
23
|
/** The captured args this node ran with (for the log header); set at run start. */
|
|
22
24
|
args = [];
|
|
23
25
|
children = [];
|
|
@@ -41,6 +41,7 @@ export declare class ExecutionTree {
|
|
|
41
41
|
private readonly snapshotStore;
|
|
42
42
|
private readonly artifactStore;
|
|
43
43
|
private readonly registry;
|
|
44
|
+
private readonly schemaCache;
|
|
44
45
|
private readonly unsettled;
|
|
45
46
|
private readonly originalLog;
|
|
46
47
|
private streaming;
|
|
@@ -101,6 +102,12 @@ export declare class ExecutionTree {
|
|
|
101
102
|
private printSummary;
|
|
102
103
|
private verdictOk;
|
|
103
104
|
private nodeView;
|
|
105
|
+
/**
|
|
106
|
+
* The node's self-description, projected for the record. The root's comes from the om
|
|
107
|
+
* builder; a child's from its action definition. The args schema is converted here, once per
|
|
108
|
+
* definition, so a step launched a thousand times converts its schema once.
|
|
109
|
+
*/
|
|
110
|
+
private metaView;
|
|
104
111
|
/** Test seam: the projected run view (same object written to result.json). */
|
|
105
112
|
runViewForTest(): RunView;
|
|
106
113
|
/** Test seam: the run's curated artifacts. */
|
|
@@ -61,6 +61,7 @@ export class ExecutionTree {
|
|
|
61
61
|
snapshotStore;
|
|
62
62
|
artifactStore = new ArtifactStore();
|
|
63
63
|
registry = [];
|
|
64
|
+
schemaCache = new Map();
|
|
64
65
|
unsettled = new Set();
|
|
65
66
|
originalLog = console.log.bind(console);
|
|
66
67
|
streaming;
|
|
@@ -137,6 +138,7 @@ export class ExecutionTree {
|
|
|
137
138
|
// into the parent's log, attributed to this child's id.
|
|
138
139
|
bubble: (message) => this.append(parent, "child", id, message),
|
|
139
140
|
});
|
|
141
|
+
node.meta = spec.meta;
|
|
140
142
|
parent.children.push(node);
|
|
141
143
|
this.registry.push(node);
|
|
142
144
|
// Launch reference — the parent's pointer to the child's own (absolute) log, plus where
|
|
@@ -386,9 +388,34 @@ export class ExecutionTree {
|
|
|
386
388
|
endedAt: node.endedAt,
|
|
387
389
|
duration: node.duration,
|
|
388
390
|
logFile: this.logFile(node.path),
|
|
391
|
+
...this.metaView(node),
|
|
389
392
|
children: node.children.map((child) => this.nodeView(child)),
|
|
390
393
|
};
|
|
391
394
|
}
|
|
395
|
+
/**
|
|
396
|
+
* The node's self-description, projected for the record. The root's comes from the om
|
|
397
|
+
* builder; a child's from its action definition. The args schema is converted here, once per
|
|
398
|
+
* definition, so a step launched a thousand times converts its schema once.
|
|
399
|
+
*/
|
|
400
|
+
metaView(node) {
|
|
401
|
+
const meta = node.parentId === null
|
|
402
|
+
? { description: this.description, mcp: this.mcp, describeArgs: node.meta?.describeArgs }
|
|
403
|
+
: node.meta;
|
|
404
|
+
if (!meta)
|
|
405
|
+
return {};
|
|
406
|
+
let inputSchema;
|
|
407
|
+
if (meta.describeArgs) {
|
|
408
|
+
if (!this.schemaCache.has(meta.describeArgs)) {
|
|
409
|
+
this.schemaCache.set(meta.describeArgs, meta.describeArgs().inputSchema);
|
|
410
|
+
}
|
|
411
|
+
inputSchema = this.schemaCache.get(meta.describeArgs);
|
|
412
|
+
}
|
|
413
|
+
return {
|
|
414
|
+
...(meta.description === undefined ? {} : { description: { ...meta.description } }),
|
|
415
|
+
...(meta.mcp === undefined ? {} : { mcp: { ...meta.mcp } }),
|
|
416
|
+
...(inputSchema === undefined ? {} : { inputSchema }),
|
|
417
|
+
};
|
|
418
|
+
}
|
|
392
419
|
/** Test seam: the projected run view (same object written to result.json). */
|
|
393
420
|
runViewForTest() {
|
|
394
421
|
return this.runView();
|
package/dist/core/action.js
CHANGED
|
@@ -49,9 +49,13 @@ class Builder {
|
|
|
49
49
|
// No `.args()` on this path, so there is nothing to describe — or to resolve against.
|
|
50
50
|
const describeArgs = () => describeSchema(undefined);
|
|
51
51
|
const parseArgs = (supplied) => supplied;
|
|
52
|
+
const meta = {
|
|
53
|
+
...(description === undefined ? {} : { description }),
|
|
54
|
+
...(mcp === undefined ? {} : { mcp }),
|
|
55
|
+
};
|
|
52
56
|
const exec = body;
|
|
53
57
|
const create = (...args) => {
|
|
54
|
-
const spec = { name, args, tags: [], body: exec, definedAt };
|
|
58
|
+
const spec = { name, args, tags: [], body: exec, definedAt, meta };
|
|
55
59
|
return ExecutionTree.require().launch(spec);
|
|
56
60
|
};
|
|
57
61
|
return Object.assign(create, {
|
|
@@ -102,6 +106,11 @@ class ArgsBuilder {
|
|
|
102
106
|
// that built it — rather than in whichever copy the discovery child is running.
|
|
103
107
|
const schema = this.schema;
|
|
104
108
|
const describeArgs = () => describeSchema(schema);
|
|
109
|
+
const meta = {
|
|
110
|
+
...(description === undefined ? {} : { description }),
|
|
111
|
+
...(mcp === undefined ? {} : { mcp }),
|
|
112
|
+
describeArgs,
|
|
113
|
+
};
|
|
105
114
|
// `safeParse` is what applies `.default()`s, so this is where an advertised default
|
|
106
115
|
// becomes the effective one. A refusal names the action and every failing field.
|
|
107
116
|
const parseArgs = (supplied) => {
|
|
@@ -112,7 +121,7 @@ class ArgsBuilder {
|
|
|
112
121
|
};
|
|
113
122
|
const exec = body;
|
|
114
123
|
const create = (...args) => {
|
|
115
|
-
const spec = { name, args, tags: [], body: exec, definedAt };
|
|
124
|
+
const spec = { name, args, tags: [], body: exec, definedAt, meta };
|
|
116
125
|
return ExecutionTree.require().launch(spec);
|
|
117
126
|
};
|
|
118
127
|
return Object.assign(create, {
|
package/dist/core/om.js
CHANGED
|
@@ -37,6 +37,9 @@ function launch(name, site, description, exposure, body, schema) {
|
|
|
37
37
|
}
|
|
38
38
|
const tree = new ExecutionTree(name, site, description, exposure);
|
|
39
39
|
ExecutionTree.current = tree;
|
|
40
|
+
// The root's description and exposure live on the tree; its schema rides on the node.
|
|
41
|
+
if (schema !== undefined)
|
|
42
|
+
tree.root.meta = { describeArgs: () => describeSchema(schema) };
|
|
40
43
|
const rootBody = (ctx) => body({
|
|
41
44
|
signal: ctx.signal,
|
|
42
45
|
logs: ctx.logs,
|
package/dist/core/types.d.ts
CHANGED
|
@@ -239,4 +239,17 @@ export interface LaunchSpec {
|
|
|
239
239
|
readonly body: Exec<object, unknown, unknown>;
|
|
240
240
|
/** `file:line` where the action was defined; carried onto the node for its log header. */
|
|
241
241
|
readonly definedAt: string | undefined;
|
|
242
|
+
/** What the definition declared about itself — carried onto the node for the run record. */
|
|
243
|
+
readonly meta?: ActionMeta;
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* A definition's self-description, as it travels onto each node it launches: what
|
|
247
|
+
* `.describe()` and `.mcp()` said, and the `.args()` schema as JSON Schema. Read only by the
|
|
248
|
+
* run record (`result.json`, `report.html`) — the runtime never branches on it.
|
|
249
|
+
*/
|
|
250
|
+
export interface ActionMeta {
|
|
251
|
+
readonly description?: OmDescription;
|
|
252
|
+
readonly mcp?: ResolvedMcpExposure;
|
|
253
|
+
/** Converts the declared args schema; called once, at finalize. */
|
|
254
|
+
readonly describeArgs?: () => DescribedArgs;
|
|
242
255
|
}
|
|
@@ -33,10 +33,13 @@ export async function writeReport(file, input) {
|
|
|
33
33
|
const assets = [];
|
|
34
34
|
const files = await collectFiles(input, assets);
|
|
35
35
|
const snapshots = await collectSnapshots(input);
|
|
36
|
+
const root = await projectRoot(input.folder);
|
|
36
37
|
const payload = {
|
|
37
38
|
version: 1,
|
|
38
39
|
name: input.name,
|
|
39
40
|
folder: input.folder,
|
|
41
|
+
projectRoot: root,
|
|
42
|
+
sources: definitionSources(input.run.root, root),
|
|
40
43
|
generatedAt: Date.now(),
|
|
41
44
|
run: input.run,
|
|
42
45
|
entries: input.entries,
|
|
@@ -183,6 +186,40 @@ function artifactTimes(entries) {
|
|
|
183
186
|
}
|
|
184
187
|
return out;
|
|
185
188
|
}
|
|
189
|
+
/**
|
|
190
|
+
* The project the run belongs to, for project-relative paths: the nearest ancestor of the run
|
|
191
|
+
* folder holding `.git`, else the folder `logs/` sits in (the root omkit was given), else cwd.
|
|
192
|
+
*/
|
|
193
|
+
async function projectRoot(folder) {
|
|
194
|
+
let dir = path.resolve(folder);
|
|
195
|
+
for (;;) {
|
|
196
|
+
try {
|
|
197
|
+
await stat(path.join(dir, ".git"));
|
|
198
|
+
return dir;
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
/* keep climbing */
|
|
202
|
+
}
|
|
203
|
+
const up = path.dirname(dir);
|
|
204
|
+
if (up === dir)
|
|
205
|
+
break;
|
|
206
|
+
dir = up;
|
|
207
|
+
}
|
|
208
|
+
return process.env.OMKIT_ROOT ? path.resolve(process.env.OMKIT_ROOT) : process.cwd();
|
|
209
|
+
}
|
|
210
|
+
/** Each distinct `definedAt` site → its file, relative to the project root, forward-slashed, no line. */
|
|
211
|
+
function definitionSources(root, projectDir) {
|
|
212
|
+
const out = {};
|
|
213
|
+
const visit = (n) => {
|
|
214
|
+
if (n.definedAt && !(n.definedAt in out)) {
|
|
215
|
+
const file = n.definedAt.replace(/:\d+(:\d+)?$/, "");
|
|
216
|
+
out[n.definedAt] = path.relative(projectDir, file).split(path.sep).join("/") || file;
|
|
217
|
+
}
|
|
218
|
+
n.children.forEach(visit);
|
|
219
|
+
};
|
|
220
|
+
visit(root);
|
|
221
|
+
return out;
|
|
222
|
+
}
|
|
186
223
|
async function walk(dir, depth = 0) {
|
|
187
224
|
if (depth > 4)
|
|
188
225
|
return [];
|
|
@@ -153,10 +153,13 @@ button { font: inherit; color: inherit; }
|
|
|
153
153
|
.guide::before { content: ""; position: absolute; left: 7px; top: 0; bottom: 0; border-left: 1px solid var(--line); }
|
|
154
154
|
|
|
155
155
|
.chart { position: relative; min-width: 0; }
|
|
156
|
-
.chart svg { display: block; }
|
|
157
|
-
.chart .bar-label { font-size: 12px; fill: var(--bar-text); font-weight: 500;
|
|
156
|
+
.chart svg { display: block; overflow: hidden; }
|
|
157
|
+
.chart .bar-label { font-size: 12px; fill: var(--bar-text); font-weight: 500; cursor: pointer; }
|
|
158
|
+
.chart .hit { fill: transparent; cursor: pointer; }
|
|
159
|
+
.chart .hit:hover + .bar { filter: brightness(1.15); }
|
|
158
160
|
.chart .bar-label.outside { fill: var(--text); }
|
|
159
|
-
.chart .mark-label { font-size: 11px; fill: var(--text-2);
|
|
161
|
+
.chart .mark-label { font-size: 11px; fill: var(--text-2); cursor: pointer; }
|
|
162
|
+
.chart .mark-label:hover { fill: var(--text); }
|
|
160
163
|
.chart .grid line { stroke: var(--line-soft); }
|
|
161
164
|
.chart .cursor { stroke: var(--text-2); stroke-dasharray: 3 3; pointer-events: none; }
|
|
162
165
|
.chart .link { fill: none; stroke: var(--text-3); stroke-width: 1.2; }
|
|
@@ -175,7 +178,7 @@ button { font: inherit; color: inherit; }
|
|
|
175
178
|
.flow-link { fill: none; stroke: var(--text-3); stroke-width: 1.3; }
|
|
176
179
|
.flame-rect { cursor: pointer; stroke: var(--panel); stroke-width: 1; }
|
|
177
180
|
.flame-rect.selected { stroke: var(--text); stroke-width: 2; }
|
|
178
|
-
.flame-text { font-size: 11px; fill: var(--bar-text);
|
|
181
|
+
.flame-text { font-size: 11px; fill: var(--bar-text); cursor: pointer; }
|
|
179
182
|
|
|
180
183
|
/* ── details ────────────────────────────────────────────── */
|
|
181
184
|
.details { display: grid; grid-template-rows: auto auto minmax(0, 1fr); overflow: hidden; }
|
|
@@ -252,6 +255,31 @@ table.list tr:hover td { background: var(--raise); }
|
|
|
252
255
|
}
|
|
253
256
|
.tip .tt { font-weight: 600; margin-bottom: 2px; }
|
|
254
257
|
.tip .ts { color: var(--text-2); }
|
|
258
|
+
.tip.wide { max-width: 420px; min-width: 260px; }
|
|
259
|
+
.tip .mono { font-family: var(--mono); font-size: 11px; overflow-wrap: anywhere; }
|
|
260
|
+
.tip .small { color: var(--text-2); }
|
|
261
|
+
.tip .badges { display: flex; flex-wrap: wrap; gap: 5px; margin: 6px 0; }
|
|
262
|
+
.tip .badge { display: inline-flex; align-items: center; gap: 5px; padding: 1px 8px; border-radius: 999px; background: var(--raise); font-size: 11px; color: var(--text-2); }
|
|
263
|
+
.tip .badge.mcp { background: var(--accent-soft); color: var(--accent); font-weight: 600; }
|
|
264
|
+
.tip .summary { margin: 4px 0 8px; line-height: 1.45; }
|
|
265
|
+
.tip .tkv { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 2px 10px; font-variant-numeric: tabular-nums; }
|
|
266
|
+
.tip .tkv .k, .tip .sec { color: var(--text-3); }
|
|
267
|
+
.tip .sec { margin: 8px 0 3px; font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em; }
|
|
268
|
+
.tip .sec.fail { color: var(--fail); }
|
|
269
|
+
.params { display: grid; gap: 3px; }
|
|
270
|
+
.param { font: 11px/1.4 var(--mono); }
|
|
271
|
+
.param .pn { color: var(--text); }
|
|
272
|
+
.param .pt { color: var(--accent); margin-left: 6px; }
|
|
273
|
+
.param .pd { color: var(--text-3); margin-left: 6px; }
|
|
274
|
+
.param .pdesc { font-family: var(--sans); color: var(--text-2); margin-left: 10px; }
|
|
275
|
+
.kv .v.defined { display: flex; align-items: center; gap: 6px; }
|
|
276
|
+
.kv .v.defined a { min-width: 0; overflow-wrap: anywhere; font-family: var(--mono); font-size: 12px; }
|
|
277
|
+
.icon-btn { flex: none; border: 1px solid var(--line); background: var(--panel-2); color: var(--text-2); border-radius: 6px; width: 24px; height: 22px; display: grid; place-items: center; cursor: pointer; padding: 0; }
|
|
278
|
+
.icon-btn:hover { color: var(--text); border-color: var(--accent); }
|
|
279
|
+
.icon-btn svg { width: 13px; height: 13px; }
|
|
280
|
+
.tip .hint { margin-top: 8px; color: var(--text-3); font-size: 11px; }
|
|
281
|
+
.tip kbd { font: 600 10px var(--mono); border: 1px solid var(--line); border-bottom-width: 2px; border-radius: 4px; padding: 0 4px; color: var(--text-2); }
|
|
282
|
+
.badge-inline { display: inline-block; padding: 0 7px; border-radius: 999px; background: var(--accent-soft); color: var(--accent); font-size: 11px; font-weight: 600; }
|
|
255
283
|
.tip img { display: block; max-width: 240px; max-height: 150px; border-radius: 4px; margin: 6px 0; border: 1px solid var(--line); }
|
|
256
284
|
.modal { position: fixed; inset: 0; background: rgba(5, 8, 18, 0.72); z-index: 60; display: grid; place-items: center; padding: 28px; }
|
|
257
285
|
.modal .box { background: var(--panel); border: 1px solid var(--line); border-radius: 12px; box-shadow: var(--shadow); max-width: min(1200px, 100%); max-height: 100%; display: grid; grid-template-rows: auto minmax(0, 1fr); overflow: hidden; }
|
|
@@ -393,6 +393,9 @@
|
|
|
393
393
|
select(n.id);
|
|
394
394
|
});
|
|
395
395
|
r.addEventListener("dblclick", () => focusNode(n));
|
|
396
|
+
r.addEventListener("mouseenter", (ev) => showTip(ev, nodeTip(n), true, 280));
|
|
397
|
+
r.addEventListener("mousemove", (ev) => moveTip(ev));
|
|
398
|
+
r.addEventListener("mouseleave", hideTip);
|
|
396
399
|
});
|
|
397
400
|
}
|
|
398
401
|
function treeRowHtml(n) {
|
|
@@ -488,6 +491,8 @@
|
|
|
488
491
|
.data(rowsNow, (n) => n.id)
|
|
489
492
|
.join((enter) => {
|
|
490
493
|
const r = enter.append("g").attr("class", "r");
|
|
494
|
+
// A transparent target spanning the bar *and* its label, so a 2px bar is still hoverable.
|
|
495
|
+
r.append("rect").attr("class", "hit").attr("y", BAR_Y - 4).attr("height", BAR_H + 8);
|
|
491
496
|
r.append("rect").attr("class", "bar").attr("rx", 5).attr("y", BAR_Y).attr("height", BAR_H);
|
|
492
497
|
r.append("text").attr("class", "bar-label").attr("y", BAR_Y + BAR_H / 2 + 4);
|
|
493
498
|
r.append("g").attr("class", "marks");
|
|
@@ -504,17 +509,7 @@
|
|
|
504
509
|
.attr("stroke-width", (n) => (n.status === "failed" ? 2 : null))
|
|
505
510
|
.attr("stroke-dasharray", (n) => (n.status === "cancelled" ? "4 3" : null))
|
|
506
511
|
.classed("selected", (n) => n.id === state.selected)
|
|
507
|
-
.
|
|
508
|
-
ev.stopPropagation();
|
|
509
|
-
select(n.id);
|
|
510
|
-
})
|
|
511
|
-
.on("dblclick", (ev, n) => {
|
|
512
|
-
ev.stopPropagation();
|
|
513
|
-
focusNode(n);
|
|
514
|
-
})
|
|
515
|
-
.on("mouseenter", (ev, n) => showTip(ev, nodeTip(n)))
|
|
516
|
-
.on("mousemove", (ev) => moveTip(ev))
|
|
517
|
-
.on("mouseleave", hideTip);
|
|
512
|
+
.call(bindNode);
|
|
518
513
|
|
|
519
514
|
g.select("text.bar-label").each(function (n) {
|
|
520
515
|
const t = d3.select(this);
|
|
@@ -524,10 +519,19 @@
|
|
|
524
519
|
const need = text.length * 6.6 + 14;
|
|
525
520
|
const inside = w >= need;
|
|
526
521
|
const clipX = Math.max(x0, 0);
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
522
|
+
const outside = !inside && w <= 60;
|
|
523
|
+
const shown = inside || outside ? text : truncate(text, (w - 14) / 6.6);
|
|
524
|
+
const tw = shown.length * 6.6;
|
|
525
|
+
// an outside label that would run off the chart hangs left of its bar instead
|
|
526
|
+
const flip = outside && x0 + Math.max(w, 3) + 6 + tw > W;
|
|
527
|
+
const lx = flip ? x0 - 6 : outside ? x0 + Math.max(w, 3) + 6 : Math.min(clipX + 8, x0 + w - 20);
|
|
528
|
+
t.text(shown).classed("outside", outside).attr("x", lx).attr("text-anchor", flip ? "end" : "start");
|
|
529
|
+
// the hit target covers bar + label, padded so a sliver of a bar is still easy to reach
|
|
530
|
+
const left = Math.min(x0, flip ? lx - tw : lx) - 6;
|
|
531
|
+
const right = Math.max(x0 + Math.max(w, 3), flip ? x0 : lx + tw) + 6;
|
|
532
|
+
d3.select(this.parentNode).select("rect.hit").attr("x", left).attr("width", Math.max(16, right - left));
|
|
530
533
|
});
|
|
534
|
+
g.selectAll("rect.hit, text.bar-label").call(bindNode);
|
|
531
535
|
|
|
532
536
|
g.select("g.marks").each(function (n) {
|
|
533
537
|
drawMarks(d3.select(this), n, x);
|
|
@@ -584,7 +588,14 @@
|
|
|
584
588
|
.attr("x", (l) => (flip(l) ? l.d.px + 4 : l.d.px - 4))
|
|
585
589
|
.attr("text-anchor", (l) => (flip(l) ? "end" : "start"))
|
|
586
590
|
.attr("y", BAR_Y + BAR_H + 13)
|
|
587
|
-
.text((l) => l.text)
|
|
591
|
+
.text((l) => l.text)
|
|
592
|
+
.on("click", (ev, l) => {
|
|
593
|
+
ev.stopPropagation();
|
|
594
|
+
openItem(l.d.m);
|
|
595
|
+
})
|
|
596
|
+
.on("mouseenter", (ev, l) => showTip(ev, markTip(l.d)))
|
|
597
|
+
.on("mousemove", (ev) => moveTip(ev))
|
|
598
|
+
.on("mouseleave", hideTip);
|
|
588
599
|
}
|
|
589
600
|
function worst(d) {
|
|
590
601
|
const all = [d.m, ...d.more].map((m) => m.kind);
|
|
@@ -669,21 +680,15 @@
|
|
|
669
680
|
.attr("fill", (n) => colorOf(n))
|
|
670
681
|
.attr("fill-opacity", (n) => (n.status === "cancelled" ? 0.55 : 0.92))
|
|
671
682
|
.classed("selected", (n) => n.id === state.selected)
|
|
672
|
-
.
|
|
673
|
-
.on("dblclick", (ev, n) => {
|
|
674
|
-
ev.stopPropagation();
|
|
675
|
-
focusNode(n);
|
|
676
|
-
})
|
|
677
|
-
.on("mouseenter", (ev, n) => showTip(ev, nodeTip(n)))
|
|
678
|
-
.on("mousemove", (ev) => moveTip(ev))
|
|
679
|
-
.on("mouseleave", hideTip);
|
|
683
|
+
.call(bindNode);
|
|
680
684
|
cells
|
|
681
685
|
.select("text")
|
|
682
686
|
.attr("x", (n) => Math.max(x(n.start), 0) + 5)
|
|
683
687
|
.text((n) => {
|
|
684
688
|
const w = x(n.end) - Math.max(x(n.start), 0);
|
|
685
689
|
return w < 34 ? "" : truncate(`${displayName(n)} ${fmtDur(n.duration)}`, (w - 10) / 6.2);
|
|
686
|
-
})
|
|
690
|
+
})
|
|
691
|
+
.call(bindNode);
|
|
687
692
|
}
|
|
688
693
|
|
|
689
694
|
// ── flow view ──
|
|
@@ -858,14 +863,18 @@
|
|
|
858
863
|
<div class="k">Duration</div><div class="v">${fmtDur(n.duration)}</div>
|
|
859
864
|
<div class="k">Start</div><div class="v">${fmtRel(n.start)} (${fmtClock(n.startAbs)})</div>
|
|
860
865
|
<div class="k">End</div><div class="v">${fmtRel(n.end)} (${fmtClock(n.endAbs)})</div>
|
|
866
|
+
${n.v.description ? `<div class="k">Summary</div><div class="v">${esc(n.v.description.summary)}</div>` : ""}
|
|
867
|
+
${n.v.mcp ? `<div class="k">MCP</div><div class="v"><span class="badge-inline">${esc(n.v.mcp.mode)}</span></div>` : ""}
|
|
861
868
|
<div class="k">Path</div><div class="v">${esc(n.v.path)}</div>
|
|
862
|
-
${n.v.definedAt ? `<div class="k">Defined at</div><div class="v"><a href="${esc(editorUrl(n.v.definedAt))}" title="${esc(n.v.definedAt)}">${esc(
|
|
869
|
+
${n.v.definedAt ? `<div class="k">Defined at</div><div class="v defined"><a href="${esc(editorUrl(n.v.definedAt))}" title="Open ${esc(n.v.definedAt)} in VS Code">${esc(sourceOf(n))}</a><button class="icon-btn" data-copy-source title="Copy relative path (C)">${svgIcon("copy")}</button></div>` : ""}
|
|
863
870
|
</div>
|
|
864
871
|
<div class="tabs">${tabs
|
|
865
872
|
.map(([k, l]) => `<button class="tab ${state.detailTab === k ? "active" : ""}" data-tab="${k}">${l}</button>`)
|
|
866
873
|
.join("")}</div>
|
|
867
874
|
<div class="tabbody">${detailTabHtml(n)}</div>
|
|
868
875
|
</div>`;
|
|
876
|
+
const copySrc = el.details.querySelector("[data-copy-source]");
|
|
877
|
+
if (copySrc) copySrc.addEventListener("click", () => copySource(n));
|
|
869
878
|
el.details.querySelector(".x").addEventListener("click", () => {
|
|
870
879
|
state.details = false;
|
|
871
880
|
el.main.classList.add("no-details");
|
|
@@ -883,7 +892,9 @@
|
|
|
883
892
|
function detailTabHtml(n) {
|
|
884
893
|
const tab = state.detailTab;
|
|
885
894
|
if (tab === "args") {
|
|
886
|
-
|
|
895
|
+
const params = schemaFields(n.v.inputSchema);
|
|
896
|
+
const schema = params.length ? `<div class="section-title">Schema</div><div class="params">${params.map(paramHtml).join("")}</div><div class="section-title">Values</div>` : "";
|
|
897
|
+
return schema + (n.v.args && n.v.args.length ? `<pre class="code">${esc(json(n.v.args.length === 1 ? n.v.args[0] : n.v.args))}</pre>` : '<div class="muted">No arguments.</div>');
|
|
887
898
|
}
|
|
888
899
|
if (tab === "events") {
|
|
889
900
|
const evs = n.entries.filter((i) => i.kind !== "info");
|
|
@@ -1115,11 +1126,50 @@
|
|
|
1115
1126
|
}
|
|
1116
1127
|
|
|
1117
1128
|
// ── tooltips ──
|
|
1129
|
+
/** Click selects, double-click focuses, hover explains — the same on every node mark. */
|
|
1130
|
+
function bindNode(sel) {
|
|
1131
|
+
sel
|
|
1132
|
+
.on("click", (ev, n) => {
|
|
1133
|
+
ev.stopPropagation();
|
|
1134
|
+
select(n.id);
|
|
1135
|
+
})
|
|
1136
|
+
.on("dblclick", (ev, n) => {
|
|
1137
|
+
ev.stopPropagation();
|
|
1138
|
+
focusNode(n);
|
|
1139
|
+
})
|
|
1140
|
+
.on("mouseenter", (ev, n) => showTip(ev, nodeTip(n), true))
|
|
1141
|
+
.on("mousemove", (ev) => moveTip(ev))
|
|
1142
|
+
.on("mouseleave", hideTip);
|
|
1143
|
+
}
|
|
1118
1144
|
function nodeTip(n) {
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1145
|
+
hovered = n; // the node `C` copies while its tooltip is up
|
|
1146
|
+
const v = n.v;
|
|
1147
|
+
const shown = displayName(n);
|
|
1148
|
+
const badges = [
|
|
1149
|
+
`<span class="badge"><span class="dot ${n.status}"></span>${esc(n.status)}</span>`,
|
|
1150
|
+
v.mcp ? `<span class="badge mcp" title="Exposed over MCP">MCP · ${esc(v.mcp.mode)}</span>` : "",
|
|
1151
|
+
n.depth === 0 ? '<span class="badge">om</span>' : "",
|
|
1152
|
+
].join("");
|
|
1153
|
+
const rows = [
|
|
1154
|
+
["Duration", fmtDur(n.duration)],
|
|
1155
|
+
["When", `${fmtRel(n.start)} → ${fmtRel(n.end)}`],
|
|
1156
|
+
v.tags.length ? ["Tags", esc(v.tags.join(" · "))] : null,
|
|
1157
|
+
v.definedAt ? ["Defined", `<span class="mono">${esc(sourceOf(n))}</span>`] : null,
|
|
1158
|
+
n.children.length ? ["Children", String(n.children.length)] : null,
|
|
1159
|
+
n.marks.length ? ["Events", String(n.marks.length)] : null,
|
|
1160
|
+
].filter(Boolean);
|
|
1161
|
+
const args = v.args && v.args.length ? truncate(oneLine(v.args.length === 1 ? v.args[0] : v.args), 220) : "";
|
|
1162
|
+
const params = schemaFields(v.inputSchema);
|
|
1163
|
+
const fails = model.run.failures.filter((f) => f.action === n.id || f.action === n.name || f.action === v.path);
|
|
1164
|
+
return `<div class="tt">${esc(shown)}</div>
|
|
1165
|
+
${shown !== n.name ? `<div class="ts mono">${esc(truncate(n.name, 140))}</div>` : ""}
|
|
1166
|
+
<div class="badges">${badges}</div>
|
|
1167
|
+
${v.description ? `<div class="summary">${esc(v.description.summary)}</div>` : '<div class="summary muted">No description.</div>'}
|
|
1168
|
+
<div class="tkv">${rows.map(([k, val]) => `<span class="k">${k}</span><span>${val}</span>`).join("")}</div>
|
|
1169
|
+
${params.length ? `<div class="sec">Args schema</div><div class="params">${params.map(paramHtml).join("")}</div>` : ""}
|
|
1170
|
+
${args ? `<div class="sec">Args</div><div class="mono small">${esc(args)}</div>` : ""}
|
|
1171
|
+
${v.definedAt ? '<div class="hint">Press <kbd>C</kbd> to copy the path</div>' : ""}
|
|
1172
|
+
${fails.length ? `<div class="sec fail">Failed</div><div class="mono small">${esc(truncate(String(fails[0].error).split(String.fromCharCode(10))[0], 200))}</div>` : ""}`;
|
|
1123
1173
|
}
|
|
1124
1174
|
function markTip(d) {
|
|
1125
1175
|
const m = d.m;
|
|
@@ -1129,12 +1179,34 @@
|
|
|
1129
1179
|
const extra = m.gallery && m.gallery.image ? `<div class="ts">${esc(m.gallery.f.rel)} · ${fmtSize(m.gallery.f.size)}</div>` : "";
|
|
1130
1180
|
return `<div class="tt">${esc(mk.label)} · ${fmtRel(m.t)}</div>${img}<div>${esc(truncate(m.kind === "snapshot" || m.kind === "artifact" ? m.label : m.e.message, 260))}</div>${extra}${more}`;
|
|
1131
1181
|
}
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1182
|
+
let tipTimer = 0;
|
|
1183
|
+
let hovered = null;
|
|
1184
|
+
/** The file where `n` is defined, relative to the project root — no line. */
|
|
1185
|
+
function sourceOf(n) {
|
|
1186
|
+
const site = n.v.definedAt;
|
|
1187
|
+
if (!site) return "";
|
|
1188
|
+
return (model.data.sources && model.data.sources[site]) || site.replace(/:\d+(:\d+)?$/, "");
|
|
1189
|
+
}
|
|
1190
|
+
function copySource(n) {
|
|
1191
|
+
const src = n && sourceOf(n);
|
|
1192
|
+
if (src) copy(src, `Copied ${src}`);
|
|
1193
|
+
}
|
|
1194
|
+
let lastEv = null;
|
|
1195
|
+
function showTip(ev, html, wide, delay) {
|
|
1196
|
+
clearTimeout(tipTimer);
|
|
1197
|
+
lastEv = ev;
|
|
1198
|
+
const open = () => {
|
|
1199
|
+
tip.innerHTML = html;
|
|
1200
|
+
tip.classList.toggle("wide", !!wide);
|
|
1201
|
+
tip.hidden = false;
|
|
1202
|
+
moveTip(lastEv);
|
|
1203
|
+
};
|
|
1204
|
+
if (delay && tip.hidden) tipTimer = setTimeout(open, delay);
|
|
1205
|
+
else open();
|
|
1136
1206
|
}
|
|
1137
1207
|
function moveTip(ev) {
|
|
1208
|
+
lastEv = ev;
|
|
1209
|
+
if (tip.hidden) return;
|
|
1138
1210
|
const pad = 14;
|
|
1139
1211
|
const r = tip.getBoundingClientRect();
|
|
1140
1212
|
let x = ev.clientX + pad;
|
|
@@ -1145,6 +1217,8 @@
|
|
|
1145
1217
|
tip.style.top = `${y}px`;
|
|
1146
1218
|
}
|
|
1147
1219
|
function hideTip() {
|
|
1220
|
+
hovered = null;
|
|
1221
|
+
clearTimeout(tipTimer);
|
|
1148
1222
|
tip.hidden = true;
|
|
1149
1223
|
}
|
|
1150
1224
|
|
|
@@ -1172,6 +1246,9 @@
|
|
|
1172
1246
|
if (e.key === "+" || e.key === "=") zoomBy(1.6);
|
|
1173
1247
|
else if (e.key === "-") zoomBy(1 / 1.6);
|
|
1174
1248
|
else if (e.key === "0") fit();
|
|
1249
|
+
else if ((e.key === "c" || e.key === "C") && !e.ctrlKey && !e.metaKey && !e.altKey) {
|
|
1250
|
+
copySource(hovered || model.byId.get(state.selected));
|
|
1251
|
+
}
|
|
1175
1252
|
else if (e.key === "ArrowDown" || e.key === "ArrowUp") {
|
|
1176
1253
|
const rows = visible();
|
|
1177
1254
|
const i = rows.findIndex((n) => n.id === state.selected);
|
|
@@ -1281,6 +1358,40 @@
|
|
|
1281
1358
|
c.opacity = a;
|
|
1282
1359
|
return c.formatRgb();
|
|
1283
1360
|
}
|
|
1361
|
+
function oneLine(v) {
|
|
1362
|
+
try {
|
|
1363
|
+
return typeof v === "string" ? v : JSON.stringify(v);
|
|
1364
|
+
} catch {
|
|
1365
|
+
return String(v);
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
/** Top-level fields of a JSON Schema object: name, type, required, default, description. */
|
|
1369
|
+
function schemaFields(schema) {
|
|
1370
|
+
if (!schema || typeof schema !== "object" || !schema.properties) return [];
|
|
1371
|
+
const req = new Set(schema.required || []);
|
|
1372
|
+
return Object.entries(schema.properties).map(([name, p]) => ({
|
|
1373
|
+
name,
|
|
1374
|
+
type: typeOf(p),
|
|
1375
|
+
required: req.has(name) && !(p && "default" in p),
|
|
1376
|
+
def: p && "default" in p ? oneLine(p.default) : undefined,
|
|
1377
|
+
desc: p && p.description,
|
|
1378
|
+
}));
|
|
1379
|
+
}
|
|
1380
|
+
function typeOf(p) {
|
|
1381
|
+
if (!p || typeof p !== "object") return "any";
|
|
1382
|
+
if (p.enum) return p.enum.map((e) => JSON.stringify(e)).join(" | ");
|
|
1383
|
+
if (p.const !== undefined) return JSON.stringify(p.const);
|
|
1384
|
+
if (Array.isArray(p.type)) return p.type.join(" | ");
|
|
1385
|
+
if (p.type === "array") return `${typeOf(p.items)}[]`;
|
|
1386
|
+
if (p.type) return p.type;
|
|
1387
|
+
if (p.anyOf || p.oneOf) return (p.anyOf || p.oneOf).map(typeOf).join(" | ");
|
|
1388
|
+
return "any";
|
|
1389
|
+
}
|
|
1390
|
+
function paramHtml(f) {
|
|
1391
|
+
return `<div class="param"><span class="pn">${esc(f.name)}${f.required ? "" : "?"}</span><span class="pt">${esc(truncate(f.type, 40))}</span>${
|
|
1392
|
+
f.def !== undefined ? `<span class="pd">= ${esc(truncate(f.def, 24))}</span>` : ""
|
|
1393
|
+
}${f.desc ? `<div class="pdesc">${esc(f.desc)}</div>` : ""}</div>`;
|
|
1394
|
+
}
|
|
1284
1395
|
function truncate(s, n) {
|
|
1285
1396
|
n = Math.max(1, Math.floor(n));
|
|
1286
1397
|
return s.length > n ? `${s.slice(0, Math.max(1, n - 1))}…` : s;
|
|
@@ -1306,19 +1417,19 @@
|
|
|
1306
1417
|
if (/build|compile|bundle/.test(s)) return "box";
|
|
1307
1418
|
return "play";
|
|
1308
1419
|
}
|
|
1309
|
-
function copy(text) {
|
|
1310
|
-
const done = () => toast("Copied");
|
|
1311
|
-
if (navigator.clipboard && window.isSecureContext) navigator.clipboard.writeText(text).then(done, () => fallbackCopy(text));
|
|
1312
|
-
else fallbackCopy(text);
|
|
1420
|
+
function copy(text, message) {
|
|
1421
|
+
const done = () => toast(message || "Copied");
|
|
1422
|
+
if (navigator.clipboard && window.isSecureContext) navigator.clipboard.writeText(text).then(done, () => fallbackCopy(text, message));
|
|
1423
|
+
else fallbackCopy(text, message);
|
|
1313
1424
|
}
|
|
1314
|
-
function fallbackCopy(text) {
|
|
1425
|
+
function fallbackCopy(text, message) {
|
|
1315
1426
|
const t = document.createElement("textarea");
|
|
1316
1427
|
t.value = text;
|
|
1317
1428
|
document.body.appendChild(t);
|
|
1318
1429
|
t.select();
|
|
1319
1430
|
try {
|
|
1320
1431
|
document.execCommand("copy");
|
|
1321
|
-
toast("Copied");
|
|
1432
|
+
toast(message || "Copied");
|
|
1322
1433
|
} catch {
|
|
1323
1434
|
toast(text);
|
|
1324
1435
|
}
|
|
@@ -19,6 +19,16 @@ export interface NodeView {
|
|
|
19
19
|
readonly duration: number;
|
|
20
20
|
/** Absolute path to this node's own `.log`. */
|
|
21
21
|
readonly logFile: string;
|
|
22
|
+
/** What the definition's `.describe()` said; absent when it said nothing. */
|
|
23
|
+
readonly description?: {
|
|
24
|
+
readonly summary: string;
|
|
25
|
+
};
|
|
26
|
+
/** The definition's `.mcp()` exposure; absent when it is not exposed. */
|
|
27
|
+
readonly mcp?: {
|
|
28
|
+
readonly mode: string;
|
|
29
|
+
};
|
|
30
|
+
/** The definition's `.args()` schema as JSON Schema; absent without one. */
|
|
31
|
+
readonly inputSchema?: unknown;
|
|
22
32
|
readonly children: readonly NodeView[];
|
|
23
33
|
}
|
|
24
34
|
/** A curated artifact, as projected into `result.json`. Data-only — no core import. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "omkit",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.11",
|
|
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",
|