@prismatic-io/lux 0.0.2-preview.3 → 0.0.2-preview.5
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/lib/answerers/terminal/index.js +1 -1
- package/lib/answerers/terminal/index.js.map +1 -1
- package/lib/assertions/authoring.d.ts +2 -2
- package/lib/assertions/authoring.d.ts.map +1 -1
- package/lib/assertions/authoring.js.map +1 -1
- package/lib/assertions/core/helpers.d.ts +2 -7
- package/lib/assertions/core/helpers.d.ts.map +1 -1
- package/lib/assertions/core/helpers.js +2 -6
- package/lib/assertions/core/helpers.js.map +1 -1
- package/lib/assertions/core/index.d.ts.map +1 -1
- package/lib/assertions/core/index.js +2 -1
- package/lib/assertions/core/index.js.map +1 -1
- package/lib/assertions/core/resource-checks.d.ts +19 -0
- package/lib/assertions/core/resource-checks.d.ts.map +1 -1
- package/lib/assertions/core/resource-checks.js +32 -0
- package/lib/assertions/core/resource-checks.js.map +1 -1
- package/lib/cli/program.d.ts.map +1 -1
- package/lib/cli/program.js +12 -67
- package/lib/cli/program.js.map +1 -1
- package/lib/cli/render/reporter.d.ts.map +1 -1
- package/lib/cli/render/reporter.js +16 -3
- package/lib/cli/render/reporter.js.map +1 -1
- package/lib/cli/run-options.d.ts +1 -0
- package/lib/cli/run-options.d.ts.map +1 -1
- package/lib/cli/run-options.js.map +1 -1
- package/lib/cli/view.d.ts +13 -0
- package/lib/cli/view.d.ts.map +1 -0
- package/lib/cli/view.js +126 -0
- package/lib/cli/view.js.map +1 -0
- package/lib/core/action-events.d.ts +29 -0
- package/lib/core/action-events.d.ts.map +1 -0
- package/lib/core/action-events.js +51 -0
- package/lib/core/action-events.js.map +1 -0
- package/lib/core/artifact-evidence.d.ts +8 -0
- package/lib/core/artifact-evidence.d.ts.map +1 -1
- package/lib/core/artifact-evidence.js +36 -4
- package/lib/core/artifact-evidence.js.map +1 -1
- package/lib/core/assertion.d.ts +12 -0
- package/lib/core/assertion.d.ts.map +1 -1
- package/lib/core/assertion.js +13 -0
- package/lib/core/assertion.js.map +1 -1
- package/lib/core/driver-metrics.d.ts +98 -0
- package/lib/core/driver-metrics.d.ts.map +1 -0
- package/lib/core/driver-metrics.js +36 -0
- package/lib/core/driver-metrics.js.map +1 -0
- package/lib/core/events.d.ts +2 -0
- package/lib/core/events.d.ts.map +1 -1
- package/lib/core/events.js +3 -2
- package/lib/core/events.js.map +1 -1
- package/lib/core/experiment.d.ts +19 -0
- package/lib/core/experiment.d.ts.map +1 -1
- package/lib/core/experiment.js +2 -0
- package/lib/core/experiment.js.map +1 -1
- package/lib/core/index.d.ts +2 -0
- package/lib/core/index.d.ts.map +1 -1
- package/lib/core/index.js +2 -0
- package/lib/core/index.js.map +1 -1
- package/lib/core/run.d.ts +51 -0
- package/lib/core/run.d.ts.map +1 -1
- package/lib/core/run.js +6 -0
- package/lib/core/run.js.map +1 -1
- package/lib/core/wire.d.ts +1 -0
- package/lib/core/wire.d.ts.map +1 -1
- package/lib/core/wire.js +5 -1
- package/lib/core/wire.js.map +1 -1
- package/lib/index.d.ts +3 -2
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +3 -2
- package/lib/index.js.map +1 -1
- package/lib/orchestrator/compare.d.ts +9 -1
- package/lib/orchestrator/compare.d.ts.map +1 -1
- package/lib/orchestrator/compare.js +8 -0
- package/lib/orchestrator/compare.js.map +1 -1
- package/lib/orchestrator/config.d.ts.map +1 -1
- package/lib/orchestrator/config.js +0 -2
- package/lib/orchestrator/config.js.map +1 -1
- package/lib/orchestrator/experiment-contracts.d.ts +19 -0
- package/lib/orchestrator/experiment-contracts.d.ts.map +1 -1
- package/lib/orchestrator/experiment-evaluation.d.ts.map +1 -1
- package/lib/orchestrator/experiment-evaluation.js +17 -0
- package/lib/orchestrator/experiment-evaluation.js.map +1 -1
- package/lib/orchestrator/experiment-report.d.ts +19 -0
- package/lib/orchestrator/experiment-report.d.ts.map +1 -1
- package/lib/orchestrator/experiment-runs.d.ts +38 -0
- package/lib/orchestrator/experiment-runs.d.ts.map +1 -1
- package/lib/orchestrator/grader-experiment.d.ts +12 -0
- package/lib/orchestrator/grader-experiment.d.ts.map +1 -1
- package/lib/orchestrator/grading.d.ts +2 -1
- package/lib/orchestrator/grading.d.ts.map +1 -1
- package/lib/orchestrator/grading.js +16 -4
- package/lib/orchestrator/grading.js.map +1 -1
- package/lib/orchestrator/index.d.ts +1 -1
- package/lib/orchestrator/index.d.ts.map +1 -1
- package/lib/orchestrator/index.js +1 -1
- package/lib/orchestrator/index.js.map +1 -1
- package/lib/orchestrator/list-runs.d.ts +6 -1
- package/lib/orchestrator/list-runs.d.ts.map +1 -1
- package/lib/orchestrator/list-runs.js +15 -1
- package/lib/orchestrator/list-runs.js.map +1 -1
- package/lib/orchestrator/orchestrator.d.ts +3 -0
- package/lib/orchestrator/orchestrator.d.ts.map +1 -1
- package/lib/orchestrator/orchestrator.js +3 -0
- package/lib/orchestrator/orchestrator.js.map +1 -1
- package/lib/orchestrator/regrade.d.ts +6 -0
- package/lib/orchestrator/regrade.d.ts.map +1 -1
- package/lib/orchestrator/run-execution.d.ts.map +1 -1
- package/lib/orchestrator/run-execution.js +13 -3
- package/lib/orchestrator/run-execution.js.map +1 -1
- package/lib/orchestrator/suite-compare.d.ts +34 -0
- package/lib/orchestrator/suite-compare.d.ts.map +1 -1
- package/lib/orchestrator/suite-summary.d.ts +68 -0
- package/lib/orchestrator/suite-summary.d.ts.map +1 -1
- package/lib/orchestrator/suite-summary.js +22 -7
- package/lib/orchestrator/suite-summary.js.map +1 -1
- package/lib/previewer/artifacts.d.ts +25 -0
- package/lib/previewer/artifacts.d.ts.map +1 -0
- package/lib/previewer/artifacts.js +180 -0
- package/lib/previewer/artifacts.js.map +1 -0
- package/lib/previewer/server.d.ts +104 -0
- package/lib/previewer/server.d.ts.map +1 -0
- package/lib/previewer/server.js +541 -0
- package/lib/previewer/server.js.map +1 -0
- package/package.json +2 -2
- package/skills/lux/SKILL.md +7 -3
- package/skills/lux/references/cli.md +25 -2
- package/skills/lux/references/eval-authoring.md +158 -4
- package/skills/lux-answerer/SKILL.md +1 -1
- package/src/answerers/terminal/index.ts +1 -1
- package/src/assertions/authoring.ts +2 -0
- package/src/assertions/core/helpers.ts +3 -6
- package/src/assertions/core/index.ts +2 -0
- package/src/assertions/core/resource-checks.ts +35 -0
- package/src/cli/program.ts +14 -93
- package/src/cli/render/reporter.ts +18 -3
- package/src/cli/run-options.ts +1 -0
- package/src/cli/view.ts +168 -0
- package/src/core/action-events.ts +59 -0
- package/src/core/artifact-evidence.ts +47 -4
- package/src/core/assertion.ts +15 -0
- package/src/core/driver-metrics.ts +51 -0
- package/src/core/events.ts +3 -2
- package/src/core/experiment.ts +2 -0
- package/src/core/index.ts +2 -0
- package/src/core/run.ts +6 -0
- package/src/core/wire.ts +5 -1
- package/src/index.ts +25 -5
- package/src/orchestrator/compare.ts +12 -0
- package/src/orchestrator/config.ts +0 -2
- package/src/orchestrator/experiment-evaluation.ts +23 -0
- package/src/orchestrator/grading.ts +21 -2
- package/src/orchestrator/index.ts +1 -0
- package/src/orchestrator/list-runs.ts +25 -0
- package/src/orchestrator/orchestrator.ts +6 -0
- package/src/orchestrator/run-execution.ts +14 -1
- package/src/orchestrator/suite-summary.ts +36 -8
- package/src/previewer/artifacts.ts +221 -0
- package/src/previewer/server.ts +763 -0
- package/viewer/app.css +2022 -0
- package/viewer/app.js +601 -0
- package/viewer/artifact-view.js +48 -0
- package/viewer/charts.js +86 -0
- package/viewer/detail.js +1108 -0
- package/viewer/format.js +129 -0
- package/viewer/index.html +151 -0
- package/lib/answerers/scripted/index.d.ts +0 -8
- package/lib/answerers/scripted/index.d.ts.map +0 -1
- package/lib/answerers/scripted/index.js +0 -163
- package/lib/answerers/scripted/index.js.map +0 -1
- package/src/answerers/scripted/index.ts +0 -186
package/viewer/format.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
export const escapeHtml = (value) =>
|
|
2
|
+
String(value ?? "").replace(
|
|
3
|
+
/[&<>"']/g,
|
|
4
|
+
(character) =>
|
|
5
|
+
({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[character],
|
|
6
|
+
);
|
|
7
|
+
|
|
8
|
+
export const percentage = (value) =>
|
|
9
|
+
typeof value === "number" ? `${Math.round(value * 100)}%` : "—";
|
|
10
|
+
|
|
11
|
+
export const signedPercentage = (value) => {
|
|
12
|
+
if (typeof value !== "number") return "—";
|
|
13
|
+
const rounded = Math.round(value * 1_000) / 10;
|
|
14
|
+
return `${rounded > 0 ? "+" : ""}${rounded}%`;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const duration = (milliseconds) => {
|
|
18
|
+
if (typeof milliseconds !== "number") return "—";
|
|
19
|
+
if (milliseconds < 1_000) return `${Math.round(milliseconds)}ms`;
|
|
20
|
+
if (milliseconds < 60_000) return `${(milliseconds / 1_000).toFixed(1)}s`;
|
|
21
|
+
return `${Math.floor(milliseconds / 60_000)}m ${Math.round((milliseconds % 60_000) / 1_000)}s`;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const signedDuration = (milliseconds) => {
|
|
25
|
+
if (typeof milliseconds !== "number") return "—";
|
|
26
|
+
return `${milliseconds > 0 ? "+" : ""}${duration(milliseconds)}`;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const compactNumber = (value) =>
|
|
30
|
+
new Intl.NumberFormat(undefined, { notation: "compact", maximumFractionDigits: 1 }).format(
|
|
31
|
+
value ?? 0,
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
export const relativeTime = (value) => {
|
|
35
|
+
if (!value) return "unknown";
|
|
36
|
+
const elapsed = Date.now() - Date.parse(value);
|
|
37
|
+
if (!Number.isFinite(elapsed)) return "unknown";
|
|
38
|
+
const minutes = Math.floor(elapsed / 60_000);
|
|
39
|
+
if (minutes < 1) return "now";
|
|
40
|
+
if (minutes < 60) return `${minutes}m`;
|
|
41
|
+
const hours = Math.floor(minutes / 60);
|
|
42
|
+
if (hours < 24) return `${hours}h`;
|
|
43
|
+
return `${Math.floor(hours / 24)}d`;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const fullDate = (value) => {
|
|
47
|
+
if (!value) return "—";
|
|
48
|
+
const date = new Date(value);
|
|
49
|
+
return Number.isNaN(date.valueOf())
|
|
50
|
+
? "—"
|
|
51
|
+
: new Intl.DateTimeFormat(undefined, {
|
|
52
|
+
month: "short",
|
|
53
|
+
day: "numeric",
|
|
54
|
+
hour: "numeric",
|
|
55
|
+
minute: "2-digit",
|
|
56
|
+
}).format(date);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export const statusLabel = (status) =>
|
|
60
|
+
({
|
|
61
|
+
passed: "Passed",
|
|
62
|
+
failed: "Failed",
|
|
63
|
+
running: "Running",
|
|
64
|
+
aborted: "Aborted",
|
|
65
|
+
error: "Error",
|
|
66
|
+
promote: "Promote",
|
|
67
|
+
retain: "Retain",
|
|
68
|
+
reject: "Reject",
|
|
69
|
+
incomplete: "Incomplete",
|
|
70
|
+
})[status] ?? "Ungraded";
|
|
71
|
+
|
|
72
|
+
export const badge = (status) =>
|
|
73
|
+
`<span class="status-badge ${escapeHtml(status)}">${escapeHtml(statusLabel(status))}</span>`;
|
|
74
|
+
|
|
75
|
+
export const totalTokens = (usage = {}) =>
|
|
76
|
+
(usage.input ?? 0) + (usage.output ?? 0) + (usage.cacheRead ?? 0) + (usage.cacheCreation ?? 0);
|
|
77
|
+
|
|
78
|
+
export const toneForDelta = (value, lowerIsBetter = false) => {
|
|
79
|
+
if (!value) return "metric-neutral";
|
|
80
|
+
const better = lowerIsBetter ? value < 0 : value > 0;
|
|
81
|
+
return better ? "metric-good" : "metric-bad";
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export const finite = (value, digits = 2) =>
|
|
85
|
+
typeof value === "number" && Number.isFinite(value)
|
|
86
|
+
? value.toLocaleString(undefined, { maximumFractionDigits: digits })
|
|
87
|
+
: "—";
|
|
88
|
+
|
|
89
|
+
const bytes = (value) => {
|
|
90
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return "—";
|
|
91
|
+
if (Math.abs(value) < 1_024) return `${finite(value)} B`;
|
|
92
|
+
if (Math.abs(value) < 1_048_576) return `${finite(value / 1_024)} KiB`;
|
|
93
|
+
return `${finite(value / 1_048_576)} MiB`;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export const metricValue = (value, unit = "number") => {
|
|
97
|
+
if (unit === "milliseconds") return duration(value);
|
|
98
|
+
if (unit === "bytes") return bytes(value);
|
|
99
|
+
if (unit === "tokens") return typeof value === "number" ? `${compactNumber(value)} tokens` : "—";
|
|
100
|
+
if (unit === "usd") return typeof value === "number" ? `$${finite(value, 4)}` : "—";
|
|
101
|
+
if (unit === "ratio" || unit === "score") return percentage(value);
|
|
102
|
+
if (unit === "percent") return typeof value === "number" ? `${finite(value)}%` : "—";
|
|
103
|
+
return finite(value);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export const metricName = (name, label) => {
|
|
107
|
+
if (label) return label;
|
|
108
|
+
const tail =
|
|
109
|
+
String(name)
|
|
110
|
+
.replace(/^driver\./, "")
|
|
111
|
+
.split(".")
|
|
112
|
+
.at(-1) ?? String(name);
|
|
113
|
+
return tail
|
|
114
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
115
|
+
.replace(/[-_]+/g, " ")
|
|
116
|
+
.replace(/^./, (character) => character.toUpperCase());
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
export const detailLoading = (label) =>
|
|
120
|
+
`<div class="loading-card"><span></span><span></span><span></span><p>${escapeHtml(label)}</p></div>`;
|
|
121
|
+
|
|
122
|
+
export const detailError = (message) => `<div class="error-card">${escapeHtml(message)}</div>`;
|
|
123
|
+
|
|
124
|
+
export const detailEmpty = () => `
|
|
125
|
+
<div class="inspector-empty">
|
|
126
|
+
<span class="empty-glyph" aria-hidden="true">↗</span>
|
|
127
|
+
<h2>Select evidence to inspect</h2>
|
|
128
|
+
<p>Scores, assertions, traces, spend, and artifacts meet here.</p>
|
|
129
|
+
</div>`;
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<meta name="color-scheme" content="dark" />
|
|
7
|
+
<meta name="theme-color" content="#0b0d0f" />
|
|
8
|
+
<meta
|
|
9
|
+
name="description"
|
|
10
|
+
content="A local review cockpit for Lux evaluation runs and experiments."
|
|
11
|
+
/>
|
|
12
|
+
<title>Lux Review</title>
|
|
13
|
+
<link rel="stylesheet" href="/assets/app.css" />
|
|
14
|
+
<script src="/assets/app.js" type="module"></script>
|
|
15
|
+
</head>
|
|
16
|
+
<body>
|
|
17
|
+
<div class="app-shell">
|
|
18
|
+
<aside class="sidebar">
|
|
19
|
+
<a class="brand" href="/" aria-label="Lux Review home">
|
|
20
|
+
<span class="brand-mark" aria-hidden="true">LX</span>
|
|
21
|
+
<span>
|
|
22
|
+
<strong>LUX</strong>
|
|
23
|
+
<small>REVIEW</small>
|
|
24
|
+
</span>
|
|
25
|
+
</a>
|
|
26
|
+
|
|
27
|
+
<nav class="primary-nav" aria-label="Evidence type">
|
|
28
|
+
<button class="nav-item is-active" type="button" data-tab="runs">
|
|
29
|
+
<span class="nav-glyph" aria-hidden="true">R</span>
|
|
30
|
+
<span>Runs</span>
|
|
31
|
+
<span class="nav-count" data-count="runs">0</span>
|
|
32
|
+
</button>
|
|
33
|
+
<button class="nav-item" type="button" data-tab="suites">
|
|
34
|
+
<span class="nav-glyph" aria-hidden="true">S</span>
|
|
35
|
+
<span>Suites</span>
|
|
36
|
+
<span class="nav-count" data-count="suites">0</span>
|
|
37
|
+
</button>
|
|
38
|
+
<button class="nav-item" type="button" data-tab="experiments">
|
|
39
|
+
<span class="nav-glyph" aria-hidden="true">E</span>
|
|
40
|
+
<span>Experiments</span>
|
|
41
|
+
<span class="nav-count" data-count="experiments">0</span>
|
|
42
|
+
</button>
|
|
43
|
+
</nav>
|
|
44
|
+
|
|
45
|
+
<div class="sidebar-note">
|
|
46
|
+
<span class="pulse-dot" aria-hidden="true"></span>
|
|
47
|
+
<span>
|
|
48
|
+
<strong>Local evidence</strong>
|
|
49
|
+
<small>Read-only · auto-refreshing</small>
|
|
50
|
+
</span>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<div class="shortcut-card">
|
|
54
|
+
<p>Shortcuts</p>
|
|
55
|
+
<dl>
|
|
56
|
+
<div class="shortcut-row"><dt><kbd>/</kbd></dt><dd>Search</dd></div>
|
|
57
|
+
<div class="shortcut-row"><dt><kbd>J</kbd> <kbd>K</kbd></dt><dd>Move</dd></div>
|
|
58
|
+
<div class="shortcut-row"><dt><kbd>R</kbd></dt><dd>Refresh</dd></div>
|
|
59
|
+
</dl>
|
|
60
|
+
</div>
|
|
61
|
+
</aside>
|
|
62
|
+
|
|
63
|
+
<main>
|
|
64
|
+
<header class="topbar">
|
|
65
|
+
<div>
|
|
66
|
+
<span class="eyebrow">Evaluation evidence</span>
|
|
67
|
+
<h1>Evidence, at a glance.</h1>
|
|
68
|
+
</div>
|
|
69
|
+
<div class="topbar-actions">
|
|
70
|
+
<label class="search">
|
|
71
|
+
<span aria-hidden="true">⌕</span>
|
|
72
|
+
<input
|
|
73
|
+
id="search-input"
|
|
74
|
+
type="search"
|
|
75
|
+
placeholder="Search cases, drivers, campaigns…"
|
|
76
|
+
autocomplete="off"
|
|
77
|
+
/>
|
|
78
|
+
<kbd>/</kbd>
|
|
79
|
+
</label>
|
|
80
|
+
<button class="icon-button" id="refresh-button" type="button" aria-label="Refresh data">
|
|
81
|
+
↻
|
|
82
|
+
</button>
|
|
83
|
+
</div>
|
|
84
|
+
</header>
|
|
85
|
+
|
|
86
|
+
<section class="metrics" aria-label="Evidence summary">
|
|
87
|
+
<article>
|
|
88
|
+
<span>Graded pass rate</span>
|
|
89
|
+
<strong id="metric-pass-rate">—</strong>
|
|
90
|
+
<small id="metric-pass-note">No graded runs</small>
|
|
91
|
+
</article>
|
|
92
|
+
<article>
|
|
93
|
+
<span>Needs attention</span>
|
|
94
|
+
<strong id="metric-failures">0</strong>
|
|
95
|
+
<small>Failed or errored runs</small>
|
|
96
|
+
</article>
|
|
97
|
+
<article>
|
|
98
|
+
<span>Experiment activity</span>
|
|
99
|
+
<strong id="metric-experiments">0</strong>
|
|
100
|
+
<small id="metric-experiment-note">No campaigns yet</small>
|
|
101
|
+
</article>
|
|
102
|
+
<article>
|
|
103
|
+
<span>Last evidence</span>
|
|
104
|
+
<strong id="metric-recency">—</strong>
|
|
105
|
+
<small id="metric-refresh">Waiting for local data</small>
|
|
106
|
+
</article>
|
|
107
|
+
</section>
|
|
108
|
+
|
|
109
|
+
<section class="workspace">
|
|
110
|
+
<div class="collection-pane">
|
|
111
|
+
<div class="collection-toolbar">
|
|
112
|
+
<div>
|
|
113
|
+
<span class="eyebrow" id="collection-eyebrow">Individual evidence</span>
|
|
114
|
+
<h2 id="collection-title">Runs</h2>
|
|
115
|
+
</div>
|
|
116
|
+
<div class="collection-actions">
|
|
117
|
+
<span class="result-count" id="result-count">0 records</span>
|
|
118
|
+
<button class="compare-button" id="compare-button" type="button">Compare</button>
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
<div class="compare-tray" id="compare-tray" hidden></div>
|
|
122
|
+
<fieldset
|
|
123
|
+
class="status-filters"
|
|
124
|
+
id="status-filters"
|
|
125
|
+
aria-label="Filter by status"
|
|
126
|
+
></fieldset>
|
|
127
|
+
<div class="collection-list" id="collection-list" aria-live="polite">
|
|
128
|
+
<div class="loading-card">
|
|
129
|
+
<span></span><span></span><span></span>
|
|
130
|
+
<p>Reading Lux evidence…</p>
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
|
|
135
|
+
<aside class="inspector" id="inspector" aria-label="Evidence detail">
|
|
136
|
+
<div class="inspector-empty">
|
|
137
|
+
<span class="empty-glyph" aria-hidden="true">↗</span>
|
|
138
|
+
<h2>Select evidence to inspect</h2>
|
|
139
|
+
<p>Scores, assertions, traces, spend, and artifacts meet here.</p>
|
|
140
|
+
</div>
|
|
141
|
+
</aside>
|
|
142
|
+
</section>
|
|
143
|
+
|
|
144
|
+
<footer>
|
|
145
|
+
<span id="runs-root">Local Lux run store</span>
|
|
146
|
+
<span id="connection-state"><i></i> Connected</span>
|
|
147
|
+
</footer>
|
|
148
|
+
</main>
|
|
149
|
+
</div>
|
|
150
|
+
</body>
|
|
151
|
+
</html>
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import { type AnswererPlugin } from "../../core/index.js";
|
|
3
|
-
export declare const ScriptedAnswererConfigSchema: z.ZodObject<{
|
|
4
|
-
fixturePath: z.ZodString;
|
|
5
|
-
}, z.core.$strict>;
|
|
6
|
-
export type ScriptedAnswererConfig = z.infer<typeof ScriptedAnswererConfigSchema>;
|
|
7
|
-
export declare const scriptedAnswerer: AnswererPlugin<ScriptedAnswererConfig>;
|
|
8
|
-
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/answerers/scripted/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAGL,KAAK,cAAc,EAIpB,MAAM,qBAAqB,CAAC;AA8B7B,eAAO,MAAM,4BAA4B;;kBAAiD,CAAC;AAE3F,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,4BAA4B,CAAC,CAAC;AA0IlF,eAAO,MAAM,gBAAgB,EAAE,cAAc,CAAC,sBAAsB,CAInE,CAAC"}
|
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
import { readFile } from "node:fs/promises";
|
|
2
|
-
import { resolve } from "node:path";
|
|
3
|
-
import { load as loadYaml } from "js-yaml";
|
|
4
|
-
import { z } from "zod";
|
|
5
|
-
import { formatZodIssues, InterruptKindSchema, } from "../../core/index.js";
|
|
6
|
-
import { approveAnswer } from "../shared.js";
|
|
7
|
-
const MatchSchema = z
|
|
8
|
-
.object({
|
|
9
|
-
kind: InterruptKindSchema.optional(),
|
|
10
|
-
id: z.string().optional(),
|
|
11
|
-
question_contains: z.string().optional(),
|
|
12
|
-
question_matches: z.string().optional(),
|
|
13
|
-
request_contains: z.string().optional(),
|
|
14
|
-
})
|
|
15
|
-
.strict();
|
|
16
|
-
const AnsweredAskSchema = z.object({ answer: z.string() }).strict();
|
|
17
|
-
const AnsweredApproveSchema = z
|
|
18
|
-
.object({ approved: z.boolean(), reason: z.string().optional() })
|
|
19
|
-
.strict();
|
|
20
|
-
const AnsweredCustomSchema = z.object({ payload: z.unknown() }).strict();
|
|
21
|
-
const RuleSchema = z.union([
|
|
22
|
-
z.object({ match: MatchSchema, answer: z.string() }).strict(),
|
|
23
|
-
z.object({ match: MatchSchema, approved: z.boolean(), reason: z.string().optional() }).strict(),
|
|
24
|
-
z.object({ match: MatchSchema, payload: z.unknown() }).strict(),
|
|
25
|
-
z.object({ default: AnsweredAskSchema }).strict(),
|
|
26
|
-
z.object({ default: AnsweredApproveSchema }).strict(),
|
|
27
|
-
z.object({ default: AnsweredCustomSchema }).strict(),
|
|
28
|
-
]);
|
|
29
|
-
const FixtureSchema = z.array(RuleSchema);
|
|
30
|
-
export const ScriptedAnswererConfigSchema = z.object({ fixturePath: z.string() }).strict();
|
|
31
|
-
const ruleLabel = (path, index) => `rule ${index + 1} of ${path}`;
|
|
32
|
-
const bodyKind = (rule) => {
|
|
33
|
-
const body = "default" in rule ? rule.default : rule;
|
|
34
|
-
if ("answer" in body)
|
|
35
|
-
return "ask";
|
|
36
|
-
if ("approved" in body)
|
|
37
|
-
return "approve";
|
|
38
|
-
return "custom";
|
|
39
|
-
};
|
|
40
|
-
const matchableKind = (m) => {
|
|
41
|
-
if (m.kind)
|
|
42
|
-
return m.kind;
|
|
43
|
-
if (m.question_contains !== undefined || m.question_matches !== undefined)
|
|
44
|
-
return "ask";
|
|
45
|
-
if (m.request_contains !== undefined)
|
|
46
|
-
return "approve";
|
|
47
|
-
return undefined;
|
|
48
|
-
};
|
|
49
|
-
const compileQuestionPattern = (rule, path, index) => {
|
|
50
|
-
if (!("match" in rule) || rule.match.question_matches === undefined)
|
|
51
|
-
return undefined;
|
|
52
|
-
try {
|
|
53
|
-
return new RegExp(rule.match.question_matches);
|
|
54
|
-
}
|
|
55
|
-
catch (err) {
|
|
56
|
-
throw new Error(`scripted: invalid question_matches in ${ruleLabel(path, index)}: ${err instanceof Error ? err.message : String(err)}`);
|
|
57
|
-
}
|
|
58
|
-
};
|
|
59
|
-
// The rule's match fields and answer body are independently valid shapes, so
|
|
60
|
-
// their interrupt kinds still need a semantic cross-check.
|
|
61
|
-
const prepareRule = (rule, path, index) => {
|
|
62
|
-
if ("match" in rule) {
|
|
63
|
-
const wanted = matchableKind(rule.match);
|
|
64
|
-
const answered = bodyKind(rule);
|
|
65
|
-
if (wanted !== undefined && wanted !== answered) {
|
|
66
|
-
throw new Error(`scripted: ${ruleLabel(path, index)} matches ${wanted} interrupts but its body answers ${answered}`);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
return { rule, questionPattern: compileQuestionPattern(rule, path, index) };
|
|
70
|
-
};
|
|
71
|
-
const matches = ({ rule, questionPattern }, interrupt) => {
|
|
72
|
-
if (!("match" in rule))
|
|
73
|
-
return false;
|
|
74
|
-
const m = rule.match;
|
|
75
|
-
if (m.kind && m.kind !== interrupt.kind)
|
|
76
|
-
return false;
|
|
77
|
-
if (m.id && m.id !== interrupt.id)
|
|
78
|
-
return false;
|
|
79
|
-
if (m.question_contains) {
|
|
80
|
-
if (interrupt.kind !== "ask")
|
|
81
|
-
return false;
|
|
82
|
-
if (!interrupt.question.includes(m.question_contains))
|
|
83
|
-
return false;
|
|
84
|
-
}
|
|
85
|
-
if (questionPattern) {
|
|
86
|
-
if (interrupt.kind !== "ask")
|
|
87
|
-
return false;
|
|
88
|
-
if (!questionPattern.test(interrupt.question))
|
|
89
|
-
return false;
|
|
90
|
-
}
|
|
91
|
-
if (m.request_contains) {
|
|
92
|
-
if (interrupt.kind !== "approve")
|
|
93
|
-
return false;
|
|
94
|
-
if (!interrupt.request.includes(m.request_contains))
|
|
95
|
-
return false;
|
|
96
|
-
}
|
|
97
|
-
return true;
|
|
98
|
-
};
|
|
99
|
-
// A fixture may carry one default per interrupt kind; the shape of the
|
|
100
|
-
// default's body is what says which kind it can answer.
|
|
101
|
-
const defaultAnswersKind = (rule, kind) => {
|
|
102
|
-
if (!("default" in rule))
|
|
103
|
-
return false;
|
|
104
|
-
const body = rule.default;
|
|
105
|
-
if (kind === "ask")
|
|
106
|
-
return "answer" in body;
|
|
107
|
-
if (kind === "approve")
|
|
108
|
-
return "approved" in body;
|
|
109
|
-
return "payload" in body;
|
|
110
|
-
};
|
|
111
|
-
const buildAnswer = (rule, interrupt) => {
|
|
112
|
-
const body = "default" in rule ? rule.default : rule;
|
|
113
|
-
if (interrupt.kind === "ask") {
|
|
114
|
-
if (!("answer" in body)) {
|
|
115
|
-
throw new Error(`scripted: rule for ask interrupt missing 'answer' (id=${interrupt.id})`);
|
|
116
|
-
}
|
|
117
|
-
return { kind: "ask", id: interrupt.id, text: body.answer };
|
|
118
|
-
}
|
|
119
|
-
if (interrupt.kind === "approve") {
|
|
120
|
-
if (!("approved" in body)) {
|
|
121
|
-
throw new Error(`scripted: rule for approve interrupt missing 'approved' (id=${interrupt.id})`);
|
|
122
|
-
}
|
|
123
|
-
return approveAnswer(interrupt.id, body.approved, body.reason);
|
|
124
|
-
}
|
|
125
|
-
if (!("payload" in body)) {
|
|
126
|
-
throw new Error(`scripted: rule for custom interrupt missing 'payload' (id=${interrupt.id})`);
|
|
127
|
-
}
|
|
128
|
-
return { kind: "custom", id: interrupt.id, payload: body.payload };
|
|
129
|
-
};
|
|
130
|
-
class ScriptedAnswererImpl {
|
|
131
|
-
rules = [];
|
|
132
|
-
config;
|
|
133
|
-
constructor(config) {
|
|
134
|
-
this.config = config;
|
|
135
|
-
}
|
|
136
|
-
async prepare() {
|
|
137
|
-
const path = resolve(this.config.fixturePath);
|
|
138
|
-
const raw = loadYaml(await readFile(path, "utf-8"));
|
|
139
|
-
const parsed = FixtureSchema.safeParse(raw);
|
|
140
|
-
if (!parsed.success) {
|
|
141
|
-
throw new Error(`scripted: invalid fixture at ${path}: ${formatZodIssues(parsed.error)}`);
|
|
142
|
-
}
|
|
143
|
-
this.rules = parsed.data.map((rule, index) => prepareRule(rule, path, index));
|
|
144
|
-
}
|
|
145
|
-
async answer(interrupt) {
|
|
146
|
-
const matched = this.rules.find((r) => matches(r, interrupt));
|
|
147
|
-
if (matched) {
|
|
148
|
-
return buildAnswer(matched.rule, interrupt);
|
|
149
|
-
}
|
|
150
|
-
const fallback = this.rules.find((r) => defaultAnswersKind(r.rule, interrupt.kind));
|
|
151
|
-
if (fallback) {
|
|
152
|
-
return buildAnswer(fallback.rule, interrupt);
|
|
153
|
-
}
|
|
154
|
-
throw new Error(`scripted: no rule matched interrupt (kind=${interrupt.kind}, id=${interrupt.id}) and no default provided`);
|
|
155
|
-
}
|
|
156
|
-
async close() { }
|
|
157
|
-
}
|
|
158
|
-
export const scriptedAnswerer = {
|
|
159
|
-
name: "scripted",
|
|
160
|
-
configSchema: ScriptedAnswererConfigSchema,
|
|
161
|
-
create: (config) => new ScriptedAnswererImpl(config),
|
|
162
|
-
};
|
|
163
|
-
//# sourceMappingURL=index.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/answerers/scripted/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
import { readFile } from "node:fs/promises";
|
|
2
|
-
import { resolve } from "node:path";
|
|
3
|
-
import { load as loadYaml } from "js-yaml";
|
|
4
|
-
import { z } from "zod";
|
|
5
|
-
import {
|
|
6
|
-
type Answer,
|
|
7
|
-
type Answerer,
|
|
8
|
-
type AnswererPlugin,
|
|
9
|
-
formatZodIssues,
|
|
10
|
-
type Interrupt,
|
|
11
|
-
InterruptKindSchema,
|
|
12
|
-
} from "../../core/index.js";
|
|
13
|
-
import { approveAnswer } from "../shared.js";
|
|
14
|
-
|
|
15
|
-
const MatchSchema = z
|
|
16
|
-
.object({
|
|
17
|
-
kind: InterruptKindSchema.optional(),
|
|
18
|
-
id: z.string().optional(),
|
|
19
|
-
question_contains: z.string().optional(),
|
|
20
|
-
question_matches: z.string().optional(),
|
|
21
|
-
request_contains: z.string().optional(),
|
|
22
|
-
})
|
|
23
|
-
.strict();
|
|
24
|
-
|
|
25
|
-
const AnsweredAskSchema = z.object({ answer: z.string() }).strict();
|
|
26
|
-
const AnsweredApproveSchema = z
|
|
27
|
-
.object({ approved: z.boolean(), reason: z.string().optional() })
|
|
28
|
-
.strict();
|
|
29
|
-
const AnsweredCustomSchema = z.object({ payload: z.unknown() }).strict();
|
|
30
|
-
|
|
31
|
-
const RuleSchema = z.union([
|
|
32
|
-
z.object({ match: MatchSchema, answer: z.string() }).strict(),
|
|
33
|
-
z.object({ match: MatchSchema, approved: z.boolean(), reason: z.string().optional() }).strict(),
|
|
34
|
-
z.object({ match: MatchSchema, payload: z.unknown() }).strict(),
|
|
35
|
-
z.object({ default: AnsweredAskSchema }).strict(),
|
|
36
|
-
z.object({ default: AnsweredApproveSchema }).strict(),
|
|
37
|
-
z.object({ default: AnsweredCustomSchema }).strict(),
|
|
38
|
-
]);
|
|
39
|
-
|
|
40
|
-
const FixtureSchema = z.array(RuleSchema);
|
|
41
|
-
|
|
42
|
-
export const ScriptedAnswererConfigSchema = z.object({ fixturePath: z.string() }).strict();
|
|
43
|
-
|
|
44
|
-
export type ScriptedAnswererConfig = z.infer<typeof ScriptedAnswererConfigSchema>;
|
|
45
|
-
|
|
46
|
-
type Rule = z.infer<typeof RuleSchema>;
|
|
47
|
-
|
|
48
|
-
/** A rule vetted at load time: kind/body cross-checked, regex pre-compiled. */
|
|
49
|
-
type PreparedRule = { rule: Rule; questionPattern: RegExp | undefined };
|
|
50
|
-
|
|
51
|
-
const ruleLabel = (path: string, index: number): string => `rule ${index + 1} of ${path}`;
|
|
52
|
-
|
|
53
|
-
const bodyKind = (rule: Rule): Interrupt["kind"] => {
|
|
54
|
-
const body = "default" in rule ? rule.default : rule;
|
|
55
|
-
if ("answer" in body) return "ask";
|
|
56
|
-
if ("approved" in body) return "approve";
|
|
57
|
-
return "custom";
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
const matchableKind = (m: z.infer<typeof MatchSchema>): Interrupt["kind"] | undefined => {
|
|
61
|
-
if (m.kind) return m.kind;
|
|
62
|
-
if (m.question_contains !== undefined || m.question_matches !== undefined) return "ask";
|
|
63
|
-
if (m.request_contains !== undefined) return "approve";
|
|
64
|
-
return undefined;
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
const compileQuestionPattern = (rule: Rule, path: string, index: number): RegExp | undefined => {
|
|
68
|
-
if (!("match" in rule) || rule.match.question_matches === undefined) return undefined;
|
|
69
|
-
try {
|
|
70
|
-
return new RegExp(rule.match.question_matches);
|
|
71
|
-
} catch (err) {
|
|
72
|
-
throw new Error(
|
|
73
|
-
`scripted: invalid question_matches in ${ruleLabel(path, index)}: ${
|
|
74
|
-
err instanceof Error ? err.message : String(err)
|
|
75
|
-
}`,
|
|
76
|
-
);
|
|
77
|
-
}
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
// The rule's match fields and answer body are independently valid shapes, so
|
|
81
|
-
// their interrupt kinds still need a semantic cross-check.
|
|
82
|
-
const prepareRule = (rule: Rule, path: string, index: number): PreparedRule => {
|
|
83
|
-
if ("match" in rule) {
|
|
84
|
-
const wanted = matchableKind(rule.match);
|
|
85
|
-
const answered = bodyKind(rule);
|
|
86
|
-
if (wanted !== undefined && wanted !== answered) {
|
|
87
|
-
throw new Error(
|
|
88
|
-
`scripted: ${ruleLabel(path, index)} matches ${wanted} interrupts but its body answers ${answered}`,
|
|
89
|
-
);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
return { rule, questionPattern: compileQuestionPattern(rule, path, index) };
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
const matches = ({ rule, questionPattern }: PreparedRule, interrupt: Interrupt): boolean => {
|
|
96
|
-
if (!("match" in rule)) return false;
|
|
97
|
-
const m = rule.match;
|
|
98
|
-
if (m.kind && m.kind !== interrupt.kind) return false;
|
|
99
|
-
if (m.id && m.id !== interrupt.id) return false;
|
|
100
|
-
if (m.question_contains) {
|
|
101
|
-
if (interrupt.kind !== "ask") return false;
|
|
102
|
-
if (!interrupt.question.includes(m.question_contains)) return false;
|
|
103
|
-
}
|
|
104
|
-
if (questionPattern) {
|
|
105
|
-
if (interrupt.kind !== "ask") return false;
|
|
106
|
-
if (!questionPattern.test(interrupt.question)) return false;
|
|
107
|
-
}
|
|
108
|
-
if (m.request_contains) {
|
|
109
|
-
if (interrupt.kind !== "approve") return false;
|
|
110
|
-
if (!interrupt.request.includes(m.request_contains)) return false;
|
|
111
|
-
}
|
|
112
|
-
return true;
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
// A fixture may carry one default per interrupt kind; the shape of the
|
|
116
|
-
// default's body is what says which kind it can answer.
|
|
117
|
-
const defaultAnswersKind = (rule: Rule, kind: Interrupt["kind"]): boolean => {
|
|
118
|
-
if (!("default" in rule)) return false;
|
|
119
|
-
const body = rule.default;
|
|
120
|
-
if (kind === "ask") return "answer" in body;
|
|
121
|
-
if (kind === "approve") return "approved" in body;
|
|
122
|
-
return "payload" in body;
|
|
123
|
-
};
|
|
124
|
-
|
|
125
|
-
const buildAnswer = (rule: Rule, interrupt: Interrupt): Answer => {
|
|
126
|
-
const body = "default" in rule ? rule.default : rule;
|
|
127
|
-
if (interrupt.kind === "ask") {
|
|
128
|
-
if (!("answer" in body)) {
|
|
129
|
-
throw new Error(`scripted: rule for ask interrupt missing 'answer' (id=${interrupt.id})`);
|
|
130
|
-
}
|
|
131
|
-
return { kind: "ask", id: interrupt.id, text: body.answer };
|
|
132
|
-
}
|
|
133
|
-
if (interrupt.kind === "approve") {
|
|
134
|
-
if (!("approved" in body)) {
|
|
135
|
-
throw new Error(
|
|
136
|
-
`scripted: rule for approve interrupt missing 'approved' (id=${interrupt.id})`,
|
|
137
|
-
);
|
|
138
|
-
}
|
|
139
|
-
return approveAnswer(interrupt.id, body.approved, body.reason);
|
|
140
|
-
}
|
|
141
|
-
if (!("payload" in body)) {
|
|
142
|
-
throw new Error(`scripted: rule for custom interrupt missing 'payload' (id=${interrupt.id})`);
|
|
143
|
-
}
|
|
144
|
-
return { kind: "custom", id: interrupt.id, payload: body.payload };
|
|
145
|
-
};
|
|
146
|
-
|
|
147
|
-
class ScriptedAnswererImpl implements Answerer {
|
|
148
|
-
private rules: PreparedRule[] = [];
|
|
149
|
-
private readonly config: ScriptedAnswererConfig;
|
|
150
|
-
|
|
151
|
-
constructor(config: ScriptedAnswererConfig) {
|
|
152
|
-
this.config = config;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
async prepare(): Promise<void> {
|
|
156
|
-
const path = resolve(this.config.fixturePath);
|
|
157
|
-
const raw = loadYaml(await readFile(path, "utf-8"));
|
|
158
|
-
const parsed = FixtureSchema.safeParse(raw);
|
|
159
|
-
if (!parsed.success) {
|
|
160
|
-
throw new Error(`scripted: invalid fixture at ${path}: ${formatZodIssues(parsed.error)}`);
|
|
161
|
-
}
|
|
162
|
-
this.rules = parsed.data.map((rule, index) => prepareRule(rule, path, index));
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
async answer(interrupt: Interrupt): Promise<Answer> {
|
|
166
|
-
const matched = this.rules.find((r) => matches(r, interrupt));
|
|
167
|
-
if (matched) {
|
|
168
|
-
return buildAnswer(matched.rule, interrupt);
|
|
169
|
-
}
|
|
170
|
-
const fallback = this.rules.find((r) => defaultAnswersKind(r.rule, interrupt.kind));
|
|
171
|
-
if (fallback) {
|
|
172
|
-
return buildAnswer(fallback.rule, interrupt);
|
|
173
|
-
}
|
|
174
|
-
throw new Error(
|
|
175
|
-
`scripted: no rule matched interrupt (kind=${interrupt.kind}, id=${interrupt.id}) and no default provided`,
|
|
176
|
-
);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
async close(): Promise<void> {}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
export const scriptedAnswerer: AnswererPlugin<ScriptedAnswererConfig> = {
|
|
183
|
-
name: "scripted",
|
|
184
|
-
configSchema: ScriptedAnswererConfigSchema,
|
|
185
|
-
create: (config) => new ScriptedAnswererImpl(config),
|
|
186
|
-
};
|