rulit 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +21 -0
- package/README.md +545 -0
- package/dist/cli/ui-template.hbs +313 -0
- package/dist/cli/ui.d.mts +9 -0
- package/dist/cli/ui.d.ts +9 -0
- package/dist/cli/ui.js +311 -0
- package/dist/cli/ui.js.map +1 -0
- package/dist/cli/ui.mjs +277 -0
- package/dist/cli/ui.mjs.map +1 -0
- package/dist/index.d.mts +745 -0
- package/dist/index.d.ts +745 -0
- package/dist/index.js +1357 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1322 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +65 -0
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
<html lang="en">
|
|
2
|
+
<head>
|
|
3
|
+
<meta charset="UTF-8" />
|
|
4
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
5
|
+
<meta name="color-scheme" content="dark" />
|
|
6
|
+
<title>Rulit Registry</title>
|
|
7
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
8
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
9
|
+
<link
|
|
10
|
+
href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600&family=Manrope:wght@400;500;600;700&display=swap"
|
|
11
|
+
rel="stylesheet"
|
|
12
|
+
/>
|
|
13
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
14
|
+
<script>
|
|
15
|
+
tailwind.config = { theme: { extend: { fontFamily: { display: ["Manrope", "ui-sans-serif",
|
|
16
|
+
"system-ui"], mono: ["JetBrains Mono", "ui-monospace", "SFMono-Regular", "Menlo",
|
|
17
|
+
"monospace"], }, }, }, };
|
|
18
|
+
</script>
|
|
19
|
+
<style>
|
|
20
|
+
.grid-surface {
|
|
21
|
+
background-image:
|
|
22
|
+
linear-gradient(rgba(255, 255, 255, 0.04) 1px, transparent 1px),
|
|
23
|
+
linear-gradient(90deg, rgba(255, 255, 255, 0.04) 1px, transparent 1px);
|
|
24
|
+
background-size: 48px 48px;
|
|
25
|
+
}
|
|
26
|
+
</style>
|
|
27
|
+
</head>
|
|
28
|
+
<body class="min-h-screen bg-neutral-950 text-neutral-100 font-display">
|
|
29
|
+
<div class="pointer-events-none fixed inset-0 -z-10 opacity-40 grid-surface"></div>
|
|
30
|
+
<div
|
|
31
|
+
class="pointer-events-none fixed inset-0 -z-10 bg-[radial-gradient(600px_circle_at_15%_10%,rgba(16,185,129,0.2),transparent_60%),radial-gradient(520px_circle_at_85%_0%,rgba(59,130,246,0.2),transparent_55%)]"
|
|
32
|
+
></div>
|
|
33
|
+
<header class="border-b border-neutral-800/80 px-6 py-6 sm:px-10">
|
|
34
|
+
<div class="mx-auto flex max-w-6xl flex-col gap-3">
|
|
35
|
+
<div class="flex items-center gap-3 text-xs uppercase tracking-[0.35em] text-neutral-400">
|
|
36
|
+
<span class="rounded-full border border-neutral-700/80 px-2 py-1">Rulit</span>
|
|
37
|
+
<span>Registry UI</span>
|
|
38
|
+
</div>
|
|
39
|
+
<div class="flex flex-col gap-2">
|
|
40
|
+
<h1 class="text-3xl font-semibold tracking-tight sm:text-4xl">Rulesets in memory</h1>
|
|
41
|
+
<p class="text-sm text-neutral-400">
|
|
42
|
+
Inspect graphs, trace executions, and verify rule decisions across your active rulesets.
|
|
43
|
+
</p>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
</header>
|
|
47
|
+
<section
|
|
48
|
+
class="mx-auto grid max-w-6xl gap-6 px-6 py-8 lg:grid-cols-[260px_1fr] lg:gap-8 lg:px-10"
|
|
49
|
+
>
|
|
50
|
+
<aside
|
|
51
|
+
class="h-fit rounded-2xl border border-neutral-800/80 bg-neutral-950/60 p-5 shadow-[0_20px_60px_rgba(0,0,0,0.4)] backdrop-blur sm:sticky sm:top-6"
|
|
52
|
+
>
|
|
53
|
+
<div class="flex items-center justify-between">
|
|
54
|
+
<h2
|
|
55
|
+
class="text-xs font-semibold uppercase tracking-[0.35em] text-neutral-400"
|
|
56
|
+
>Rulesets</h2>
|
|
57
|
+
<span
|
|
58
|
+
class="rounded-full border border-neutral-700/80 bg-neutral-950/80 px-2 py-0.5 text-[11px] text-neutral-300"
|
|
59
|
+
>
|
|
60
|
+
{{entries.length}}
|
|
61
|
+
</span>
|
|
62
|
+
</div>
|
|
63
|
+
<input
|
|
64
|
+
class="mt-4 w-full rounded-xl border border-neutral-800 bg-neutral-950/80 px-3 py-2 text-sm text-neutral-100 placeholder:text-neutral-600 focus:border-emerald-400 focus:outline-none"
|
|
65
|
+
id="search"
|
|
66
|
+
placeholder="Filter rulesets..."
|
|
67
|
+
/>
|
|
68
|
+
<ul class="mt-4 grid gap-2 text-sm" id="nav">
|
|
69
|
+
{{#if entries.length}}
|
|
70
|
+
{{#each entries}}
|
|
71
|
+
<li>
|
|
72
|
+
<a
|
|
73
|
+
href="#{{id}}"
|
|
74
|
+
class="block rounded-xl border border-transparent bg-neutral-950/40 px-3 py-3 transition hover:border-neutral-700 hover:bg-neutral-900/80"
|
|
75
|
+
>
|
|
76
|
+
<div class="font-semibold text-neutral-100">{{name}}</div>
|
|
77
|
+
<div class="text-xs text-neutral-500">{{createdAt}}</div>
|
|
78
|
+
</a>
|
|
79
|
+
</li>
|
|
80
|
+
{{/each}}
|
|
81
|
+
{{else}}
|
|
82
|
+
<li
|
|
83
|
+
class="rounded-xl border border-dashed border-neutral-800 p-3 text-xs text-neutral-500"
|
|
84
|
+
>
|
|
85
|
+
No rulesets found.
|
|
86
|
+
</li>
|
|
87
|
+
{{/if}}
|
|
88
|
+
</ul>
|
|
89
|
+
</aside>
|
|
90
|
+
<main class="grid gap-5" id="cards">
|
|
91
|
+
{{#if entries.length}}
|
|
92
|
+
{{#each entries}}
|
|
93
|
+
<section
|
|
94
|
+
id="{{id}}"
|
|
95
|
+
data-trace-card
|
|
96
|
+
class="rounded-2xl border border-neutral-800/80 bg-neutral-950/60 p-6 shadow-[0_20px_60px_rgba(0,0,0,0.4)]"
|
|
97
|
+
>
|
|
98
|
+
<header class="flex flex-wrap items-start justify-between gap-4">
|
|
99
|
+
<div>
|
|
100
|
+
<h2 class="text-xl font-semibold text-neutral-100">{{name}}</h2>
|
|
101
|
+
<div class="text-xs text-neutral-500">{{createdAt}}</div>
|
|
102
|
+
</div>
|
|
103
|
+
<div
|
|
104
|
+
class="rounded-full border border-emerald-500/30 bg-emerald-500/10 px-3 py-1 text-xs font-semibold text-emerald-200"
|
|
105
|
+
>
|
|
106
|
+
{{id}}
|
|
107
|
+
</div>
|
|
108
|
+
</header>
|
|
109
|
+
<div class="mt-5">
|
|
110
|
+
<button
|
|
111
|
+
type="button"
|
|
112
|
+
class="group relative w-full overflow-auto rounded-2xl border border-neutral-800 bg-neutral-950/80 p-0 text-left focus:outline-none focus:ring-2 focus:ring-emerald-400"
|
|
113
|
+
data-expand
|
|
114
|
+
data-mermaid="{{mermaidEncoded}}"
|
|
115
|
+
>
|
|
116
|
+
<span
|
|
117
|
+
class="pointer-events-none absolute right-3 top-3 rounded-full bg-neutral-900/80 px-2 py-1 text-[10px] font-semibold text-neutral-200 opacity-0 transition group-hover:opacity-100"
|
|
118
|
+
>
|
|
119
|
+
Expand
|
|
120
|
+
</span>
|
|
121
|
+
<pre
|
|
122
|
+
class="mermaid whitespace-pre-wrap px-4 py-5 font-mono text-[11px] leading-relaxed text-neutral-100"
|
|
123
|
+
>{{mermaid}}</pre>
|
|
124
|
+
</button>
|
|
125
|
+
</div>
|
|
126
|
+
<details class="mt-4 rounded-2xl border border-neutral-800 bg-neutral-950/40 p-4">
|
|
127
|
+
<summary class="cursor-pointer text-sm font-medium text-emerald-200">JSON graph</summary>
|
|
128
|
+
<pre
|
|
129
|
+
class="mt-3 overflow-auto rounded-lg bg-neutral-950/80 p-3 font-mono text-xs text-neutral-200"
|
|
130
|
+
>{{json}}</pre>
|
|
131
|
+
</details>
|
|
132
|
+
<details class="mt-6 border-t border-neutral-800 pt-5" open>
|
|
133
|
+
<summary class="flex items-center justify-between text-left">
|
|
134
|
+
<div class="text-xs uppercase tracking-[0.35em] text-neutral-400">Trace playback</div>
|
|
135
|
+
<div class="text-xs text-neutral-500">{{traces.length}} runs</div>
|
|
136
|
+
</summary>
|
|
137
|
+
<div class="mt-4">
|
|
138
|
+
<div class="mt-3 space-y-2">
|
|
139
|
+
{{#if traces.length}}
|
|
140
|
+
{{#each traces}}
|
|
141
|
+
<details
|
|
142
|
+
class="rounded-2xl border border-neutral-800 bg-neutral-950/60 p-4"
|
|
143
|
+
data-trace-row
|
|
144
|
+
data-trace="{{traceEncoded}}"
|
|
145
|
+
>
|
|
146
|
+
<summary
|
|
147
|
+
class="grid cursor-pointer grid-cols-[1fr_160px_160px_100px] items-center gap-3 text-xs text-neutral-200"
|
|
148
|
+
>
|
|
149
|
+
<div>
|
|
150
|
+
<div class="text-sm font-semibold text-neutral-100">{{id}}</div>
|
|
151
|
+
<div class="text-[11px] text-neutral-500">Trace run</div>
|
|
152
|
+
</div>
|
|
153
|
+
<div
|
|
154
|
+
class="text-center text-[11px] text-neutral-400"
|
|
155
|
+
>{{createdAt}}</div>
|
|
156
|
+
<div class="text-center text-[11px] text-neutral-400">{{firedCount}}
|
|
157
|
+
fired</div>
|
|
158
|
+
<div class="text-center text-[11px] text-neutral-400">{{matchedCount}}
|
|
159
|
+
matched</div>
|
|
160
|
+
</summary>
|
|
161
|
+
<div class="mt-4 space-y-4" data-trace-row-body></div>
|
|
162
|
+
</details>
|
|
163
|
+
{{/each}}
|
|
164
|
+
{{else}}
|
|
165
|
+
<div
|
|
166
|
+
class="w-full rounded-xl border border-dashed border-neutral-800 bg-neutral-950/40 p-4 text-xs text-neutral-500"
|
|
167
|
+
>
|
|
168
|
+
No traces recorded. Run an engine to capture a playback.
|
|
169
|
+
</div>
|
|
170
|
+
{{/if}}
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
</details>
|
|
174
|
+
</section>
|
|
175
|
+
{{/each}}
|
|
176
|
+
{{else}}
|
|
177
|
+
<p
|
|
178
|
+
class="rounded-xl border border-dashed border-neutral-800 p-6 text-sm text-neutral-500"
|
|
179
|
+
>
|
|
180
|
+
No rulesets found.
|
|
181
|
+
</p>
|
|
182
|
+
{{/if}}
|
|
183
|
+
</main>
|
|
184
|
+
</section>
|
|
185
|
+
<div
|
|
186
|
+
id="overlay"
|
|
187
|
+
class="fixed inset-0 z-50 hidden flex items-center justify-center bg-neutral-950/85 p-4"
|
|
188
|
+
aria-hidden="true"
|
|
189
|
+
>
|
|
190
|
+
<div
|
|
191
|
+
class="relative max-h-[92vh] w-full max-w-7xl overflow-auto rounded-2xl border border-neutral-800 bg-neutral-950 p-6 shadow-[0_30px_80px_rgba(0,0,0,0.55)]"
|
|
192
|
+
>
|
|
193
|
+
<div class="flex flex-wrap items-center justify-between gap-3">
|
|
194
|
+
<div class="text-xs uppercase tracking-[0.3em] text-neutral-500">Expanded graph</div>
|
|
195
|
+
<button
|
|
196
|
+
type="button"
|
|
197
|
+
id="overlay-close"
|
|
198
|
+
class="rounded-full border border-neutral-700 bg-neutral-950/80 px-3 py-1 text-xs font-semibold text-neutral-200 hover:border-emerald-400"
|
|
199
|
+
>
|
|
200
|
+
Close
|
|
201
|
+
</button>
|
|
202
|
+
</div>
|
|
203
|
+
<div
|
|
204
|
+
id="overlay-mermaid"
|
|
205
|
+
class="mt-4 rounded-2xl border border-neutral-800 bg-neutral-950/80 p-4"
|
|
206
|
+
></div>
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
209
|
+
<script type="module">
|
|
210
|
+
import mermaid from "https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs";
|
|
211
|
+
mermaid.initialize({ startOnLoad: true, theme: "dark" }); const search =
|
|
212
|
+
document.getElementById("search"); const nav = document.getElementById("nav"); const cards =
|
|
213
|
+
document.getElementById("cards"); const overlay = document.getElementById("overlay"); const
|
|
214
|
+
overlayClose = document.getElementById("overlay-close"); const overlayMermaid =
|
|
215
|
+
document.getElementById("overlay-mermaid"); const filter = () => { const value =
|
|
216
|
+
(search?.value || "").toLowerCase(); const links = nav?.querySelectorAll("a") || []; const
|
|
217
|
+
sections = cards?.querySelectorAll("section") || []; links.forEach((link) => { const text =
|
|
218
|
+
link.textContent?.toLowerCase() || ""; const show = text.includes(value);
|
|
219
|
+
link.parentElement.style.display = show ? "" : "none"; }); sections.forEach((section) => {
|
|
220
|
+
const text = section.textContent?.toLowerCase() || ""; section.style.display =
|
|
221
|
+
text.includes(value) ? "" : "none"; }); }; if (search) { search.addEventListener("input",
|
|
222
|
+
filter); } const showOverlayWithSvg = (svgMarkup) => { if (!overlay || !overlayMermaid) {
|
|
223
|
+
return; } overlayMermaid.innerHTML = svgMarkup; overlay.classList.remove("hidden");
|
|
224
|
+
overlay.setAttribute("aria-hidden", "false"); }; const showOverlayFromMermaid = async
|
|
225
|
+
(content) => { if (!overlay || !overlayMermaid) { return; } const { svg } = await
|
|
226
|
+
mermaid.render(`expanded-${Date.now()}`, content); showOverlayWithSvg(svg); };
|
|
227
|
+
document.querySelectorAll("[data-expand]").forEach((button) => {
|
|
228
|
+
button.addEventListener("click", () => { const svg = button.querySelector("svg"); if
|
|
229
|
+
(svg?.outerHTML) { showOverlayWithSvg(svg.outerHTML); return; } const content =
|
|
230
|
+
button.dataset.mermaid ? decodeURIComponent(button.dataset.mermaid) : ""; if (content) { void
|
|
231
|
+
showOverlayFromMermaid(content); } }); }); const hideOverlay = () => { if (!overlay) { return;
|
|
232
|
+
} overlay.classList.add("hidden"); overlay.setAttribute("aria-hidden", "true"); };
|
|
233
|
+
overlay?.addEventListener("click", (event) => { if (event.target === overlay) { hideOverlay();
|
|
234
|
+
} }); overlayClose?.addEventListener("click", hideOverlay); const formatValue = (value) => {
|
|
235
|
+
if (value === undefined) { return "—"; } if (typeof value === "string") { return `"${value}"`;
|
|
236
|
+
} try { return JSON.stringify(value); } catch { return String(value); } }; const formatFacts =
|
|
237
|
+
(value) => { if (value === undefined) { return "—"; } try { return JSON.stringify(value, null,
|
|
238
|
+
2); } catch { return String(value); } }; const statusClass = (result) => result ?
|
|
239
|
+
"border-emerald-500/40 text-emerald-200" : "border-rose-500/40 text-rose-200"; const
|
|
240
|
+
renderCondition = (container, condition, depth) => { const row =
|
|
241
|
+
document.createElement("div"); row.className = "rounded-xl border border-neutral-800
|
|
242
|
+
bg-neutral-950/70 px-3 py-2 text-xs text-neutral-200"; row.style.marginLeft = `${depth *
|
|
243
|
+
12}px`; const header = document.createElement("div"); header.className = "flex flex-wrap
|
|
244
|
+
items-center justify-between gap-2"; const title = document.createElement("div");
|
|
245
|
+
title.className = "font-semibold text-neutral-100"; title.textContent = condition.label ||
|
|
246
|
+
"condition"; const badge = document.createElement("span"); badge.className = `rounded-full
|
|
247
|
+
border px-2 py-0.5 text-[10px] uppercase tracking-[0.2em] ${statusClass( condition.result,
|
|
248
|
+
)}`; badge.textContent = condition.result ? "pass" : "fail"; header.appendChild(title);
|
|
249
|
+
header.appendChild(badge); row.appendChild(header); if (condition.reasonCode) { const reason =
|
|
250
|
+
document.createElement("div"); reason.className = "mt-1 text-[11px] text-neutral-400";
|
|
251
|
+
reason.textContent = `Reason: ${condition.reasonCode}`; row.appendChild(reason); } if
|
|
252
|
+
(condition.left !== undefined || condition.op || condition.right !== undefined) { const detail
|
|
253
|
+
= document.createElement("div"); detail.className = "mt-1 font-mono text-[11px]
|
|
254
|
+
text-neutral-300"; detail.textContent = `${formatValue(condition.left)} ${condition.op || ""}
|
|
255
|
+
${formatValue( condition.right, )}`.trim(); row.appendChild(detail); }
|
|
256
|
+
container.appendChild(row); if (condition.children && condition.children.length > 0) {
|
|
257
|
+
condition.children.forEach((child) => renderCondition(container, child, depth + 1)); } };
|
|
258
|
+
const renderRuleRow = (container, rule, index) => { const wrapper =
|
|
259
|
+
document.createElement("div"); wrapper.className = "rounded-xl border border-neutral-800
|
|
260
|
+
bg-neutral-950/70 p-3"; const header = document.createElement("div"); header.className = "flex
|
|
261
|
+
flex-wrap items-center justify-between gap-2"; const title = document.createElement("div");
|
|
262
|
+
title.className = "text-sm font-semibold text-neutral-100"; title.textContent = `${index + 1}.
|
|
263
|
+
${rule.ruleId}`; const status = document.createElement("span"); status.className =
|
|
264
|
+
"rounded-full border px-2 py-0.5 text-[10px] uppercase tracking-[0.2em]"; if
|
|
265
|
+
(rule.skippedReason) { status.textContent = `skipped (${rule.skippedReason})`;
|
|
266
|
+
status.classList.add("border-amber-500/40", "bg-amber-500/10", "text-amber-200"); } else if
|
|
267
|
+
(rule.matched) { status.textContent = "matched"; status.classList.add("border-emerald-500/40",
|
|
268
|
+
"bg-emerald-500/10", "text-emerald-200"); } else { status.textContent = "not matched";
|
|
269
|
+
status.classList.add("border-rose-500/40", "bg-rose-500/10", "text-rose-200"); }
|
|
270
|
+
header.appendChild(title); header.appendChild(status); wrapper.appendChild(header); const meta
|
|
271
|
+
= document.createElement("div"); meta.className = "mt-1 text-xs text-neutral-500"; const tags
|
|
272
|
+
= rule.meta?.tags?.length ? `Tags: ${rule.meta.tags.join(", ")}` : ""; const reason =
|
|
273
|
+
rule.meta?.reasonCode ? `Reason: ${rule.meta.reasonCode}` : ""; const duration =
|
|
274
|
+
rule.durationMs ? `${rule.durationMs}ms` : ""; const parts = [tags, reason,
|
|
275
|
+
duration].filter(Boolean); meta.textContent = parts.length ? parts.join(" • ") : "No
|
|
276
|
+
metadata"; wrapper.appendChild(meta); if (rule.conditions?.length) { const conditionsHeader =
|
|
277
|
+
document.createElement("div"); conditionsHeader.className = "mt-3 text-[10px] uppercase
|
|
278
|
+
tracking-[0.3em] text-neutral-400"; conditionsHeader.textContent = "Conditions";
|
|
279
|
+
wrapper.appendChild(conditionsHeader); const conditions = document.createElement("div");
|
|
280
|
+
conditions.className = "mt-2 space-y-2"; rule.conditions.forEach((condition) =>
|
|
281
|
+
renderCondition(conditions, condition, 0)); wrapper.appendChild(conditions); } if
|
|
282
|
+
(rule.notes?.length) { const notesHeader = document.createElement("div");
|
|
283
|
+
notesHeader.className = "mt-3 text-[10px] uppercase tracking-[0.3em] text-neutral-400";
|
|
284
|
+
notesHeader.textContent = "Notes"; wrapper.appendChild(notesHeader); const notes =
|
|
285
|
+
document.createElement("ul"); notes.className = "mt-2 space-y-1 text-xs text-neutral-300";
|
|
286
|
+
rule.notes.forEach((note) => { const li = document.createElement("li"); li.textContent = note;
|
|
287
|
+
notes.appendChild(li); }); wrapper.appendChild(notes); } container.appendChild(wrapper); };
|
|
288
|
+
const renderTraceRow = (row) => { const body = row.querySelector("[data-trace-row-body]"); if
|
|
289
|
+
(!body || body.childElementCount > 0) { return; } let trace = null; try { trace =
|
|
290
|
+
JSON.parse(decodeURIComponent(row.dataset.trace || "")); } catch { return; } const inputHeader
|
|
291
|
+
= document.createElement("div"); inputHeader.className = "flex items-center justify-between";
|
|
292
|
+
const inputTitle = document.createElement("div"); inputTitle.className = "text-xs uppercase
|
|
293
|
+
tracking-[0.3em] text-neutral-400"; inputTitle.textContent = "Input"; const copyBtn =
|
|
294
|
+
document.createElement("button"); copyBtn.type = "button"; copyBtn.className = "rounded-full
|
|
295
|
+
border border-neutral-700 bg-neutral-950/80 px-2 py-0.5 text-[11px] text-neutral-300
|
|
296
|
+
hover:border-emerald-400"; copyBtn.textContent = "Copy"; inputHeader.appendChild(inputTitle);
|
|
297
|
+
inputHeader.appendChild(copyBtn); const input = document.createElement("pre"); input.className
|
|
298
|
+
= "mt-2 overflow-auto rounded-lg border border-neutral-800 bg-neutral-950/80 p-3 font-mono
|
|
299
|
+
text-[11px] text-neutral-200 shadow-[0_0_0_1px_rgba(16,185,129,0.05)]"; input.textContent =
|
|
300
|
+
formatFacts(trace.facts); copyBtn.addEventListener("click", () => {
|
|
301
|
+
navigator.clipboard?.writeText(input.textContent || ""); }); body.appendChild(inputHeader);
|
|
302
|
+
body.appendChild(input); const rulesHeader = document.createElement("div");
|
|
303
|
+
rulesHeader.className = "text-xs uppercase tracking-[0.3em] text-neutral-400";
|
|
304
|
+
rulesHeader.textContent = "Rule evaluations"; body.appendChild(rulesHeader); const rulesList =
|
|
305
|
+
document.createElement("div"); rulesList.className = "space-y-3"; trace.trace.forEach((rule,
|
|
306
|
+
index) => renderRuleRow(rulesList, rule, index)); body.appendChild(rulesList); }; const
|
|
307
|
+
initTraceRows = (card) => { const rows = card.querySelectorAll("[data-trace-row]");
|
|
308
|
+
rows.forEach((row) => { row.addEventListener("toggle", () => { if (row.open) {
|
|
309
|
+
renderTraceRow(row); } }); }); };
|
|
310
|
+
document.querySelectorAll("[data-trace-card]").forEach((card) => { initTraceRows(card); });
|
|
311
|
+
</script>
|
|
312
|
+
</body>
|
|
313
|
+
</html>
|
package/dist/cli/ui.d.ts
ADDED
package/dist/cli/ui.js
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __export = (target, all) => {
|
|
10
|
+
for (var name in all)
|
|
11
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
12
|
+
};
|
|
13
|
+
var __copyProps = (to, from, except, desc) => {
|
|
14
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
15
|
+
for (let key of __getOwnPropNames(from))
|
|
16
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
17
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
18
|
+
}
|
|
19
|
+
return to;
|
|
20
|
+
};
|
|
21
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
22
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
23
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
24
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
25
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
26
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
27
|
+
mod
|
|
28
|
+
));
|
|
29
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
30
|
+
|
|
31
|
+
// src/cli/ui.ts
|
|
32
|
+
var ui_exports = {};
|
|
33
|
+
__export(ui_exports, {
|
|
34
|
+
buildHtml: () => buildHtml,
|
|
35
|
+
startServer: () => startServer
|
|
36
|
+
});
|
|
37
|
+
module.exports = __toCommonJS(ui_exports);
|
|
38
|
+
|
|
39
|
+
// node_modules/.pnpm/tsup@8.5.1_postcss@8.5.6_tsx@4.21.0_typescript@5.9.3/node_modules/tsup/assets/cjs_shims.js
|
|
40
|
+
var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.tagName.toUpperCase() === "SCRIPT" ? document.currentScript.src : new URL("main.js", document.baseURI).href;
|
|
41
|
+
var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
|
|
42
|
+
|
|
43
|
+
// src/cli/ui.ts
|
|
44
|
+
var import_node_fs = __toESM(require("fs"));
|
|
45
|
+
var import_node_http = __toESM(require("http"));
|
|
46
|
+
var import_node_path = __toESM(require("path"));
|
|
47
|
+
var import_node_url = require("url");
|
|
48
|
+
var import_handlebars = __toESM(require("handlebars"));
|
|
49
|
+
|
|
50
|
+
// src/registry.ts
|
|
51
|
+
var entries = /* @__PURE__ */ new Map();
|
|
52
|
+
var nameIndex = /* @__PURE__ */ new Map();
|
|
53
|
+
var counter = 0;
|
|
54
|
+
var traceCounter = 0;
|
|
55
|
+
var traceLimit = 25;
|
|
56
|
+
function makeId() {
|
|
57
|
+
return `ruleset-${counter++}`;
|
|
58
|
+
}
|
|
59
|
+
var registry = {
|
|
60
|
+
/**
|
|
61
|
+
* Register a ruleset in the global registry.
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* ```ts
|
|
65
|
+
* const rs = Rules.ruleset<Facts, Effects>("eligibility");
|
|
66
|
+
* Rules.registry.list();
|
|
67
|
+
* ```
|
|
68
|
+
*/
|
|
69
|
+
register(source, name) {
|
|
70
|
+
const id = makeId();
|
|
71
|
+
entries.set(id, { id, name, createdAt: Date.now(), source, traces: [] });
|
|
72
|
+
if (name) {
|
|
73
|
+
nameIndex.set(name, id);
|
|
74
|
+
}
|
|
75
|
+
return id;
|
|
76
|
+
},
|
|
77
|
+
/**
|
|
78
|
+
* List all registered rulesets.
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* ```ts
|
|
82
|
+
* const list = Rules.registry.list();
|
|
83
|
+
* ```
|
|
84
|
+
*/
|
|
85
|
+
list() {
|
|
86
|
+
return Array.from(entries.values()).map(({ id, name, createdAt }) => ({
|
|
87
|
+
id,
|
|
88
|
+
name,
|
|
89
|
+
createdAt
|
|
90
|
+
}));
|
|
91
|
+
},
|
|
92
|
+
/**
|
|
93
|
+
* Get a graph by id or ruleset name.
|
|
94
|
+
*
|
|
95
|
+
* @example
|
|
96
|
+
* ```ts
|
|
97
|
+
* const graph = Rules.registry.getGraph("eligibility");
|
|
98
|
+
* ```
|
|
99
|
+
*/
|
|
100
|
+
getGraph(idOrName) {
|
|
101
|
+
const entry = getEntry(idOrName);
|
|
102
|
+
return entry?.source.graph();
|
|
103
|
+
},
|
|
104
|
+
/**
|
|
105
|
+
* Get Mermaid output by id or ruleset name.
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
* ```ts
|
|
109
|
+
* const mermaid = Rules.registry.getMermaid("eligibility");
|
|
110
|
+
* ```
|
|
111
|
+
*/
|
|
112
|
+
getMermaid(idOrName) {
|
|
113
|
+
const entry = getEntry(idOrName);
|
|
114
|
+
return entry?.source.toMermaid();
|
|
115
|
+
},
|
|
116
|
+
/**
|
|
117
|
+
* Record a trace run for a ruleset. Keeps a rolling window of traces.
|
|
118
|
+
*
|
|
119
|
+
* @example
|
|
120
|
+
* ```ts
|
|
121
|
+
* Rules.registry.recordTrace("eligibility", trace, fired);
|
|
122
|
+
* ```
|
|
123
|
+
*/
|
|
124
|
+
recordTrace(idOrName, trace, fired, facts) {
|
|
125
|
+
const entry = getEntry(idOrName);
|
|
126
|
+
if (!entry) {
|
|
127
|
+
return void 0;
|
|
128
|
+
}
|
|
129
|
+
const run = {
|
|
130
|
+
id: `trace-${traceCounter++}`,
|
|
131
|
+
createdAt: Date.now(),
|
|
132
|
+
facts,
|
|
133
|
+
fired,
|
|
134
|
+
trace
|
|
135
|
+
};
|
|
136
|
+
entry.traces.push(run);
|
|
137
|
+
if (entry.traces.length > traceLimit) {
|
|
138
|
+
entry.traces.splice(0, entry.traces.length - traceLimit);
|
|
139
|
+
}
|
|
140
|
+
return run;
|
|
141
|
+
},
|
|
142
|
+
/**
|
|
143
|
+
* List trace runs for a ruleset.
|
|
144
|
+
*
|
|
145
|
+
* @example
|
|
146
|
+
* ```ts
|
|
147
|
+
* const traces = Rules.registry.listTraces("eligibility");
|
|
148
|
+
* ```
|
|
149
|
+
*/
|
|
150
|
+
listTraces(idOrName) {
|
|
151
|
+
const entry = getEntry(idOrName);
|
|
152
|
+
return entry ? [...entry.traces] : [];
|
|
153
|
+
},
|
|
154
|
+
/**
|
|
155
|
+
* Get a trace by id for a ruleset.
|
|
156
|
+
*
|
|
157
|
+
* @example
|
|
158
|
+
* ```ts
|
|
159
|
+
* const trace = Rules.registry.getTrace("eligibility", "trace-0");
|
|
160
|
+
* ```
|
|
161
|
+
*/
|
|
162
|
+
getTrace(idOrName, traceId) {
|
|
163
|
+
const entry = getEntry(idOrName);
|
|
164
|
+
return entry?.traces.find((run) => run.id === traceId);
|
|
165
|
+
},
|
|
166
|
+
/**
|
|
167
|
+
* Clear the registry (useful in tests).
|
|
168
|
+
*
|
|
169
|
+
* @example
|
|
170
|
+
* ```ts
|
|
171
|
+
* Rules.registry.clear();
|
|
172
|
+
* ```
|
|
173
|
+
*/
|
|
174
|
+
clear() {
|
|
175
|
+
entries.clear();
|
|
176
|
+
nameIndex.clear();
|
|
177
|
+
traceCounter = 0;
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
function getEntry(idOrName) {
|
|
181
|
+
const byId = entries.get(idOrName);
|
|
182
|
+
if (byId) {
|
|
183
|
+
return byId;
|
|
184
|
+
}
|
|
185
|
+
const id = nameIndex.get(idOrName);
|
|
186
|
+
return id ? entries.get(id) : void 0;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// src/cli/ui.ts
|
|
190
|
+
async function startServer(options) {
|
|
191
|
+
await loadModules(options.load, Date.now());
|
|
192
|
+
const server = import_node_http.default.createServer((req, res) => {
|
|
193
|
+
if (!req.url || req.url === "/") {
|
|
194
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
195
|
+
res.end(buildHtml());
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
res.writeHead(404);
|
|
199
|
+
res.end("Not found");
|
|
200
|
+
});
|
|
201
|
+
server.listen(options.port, () => {
|
|
202
|
+
console.log(`Rulit UI running at http://localhost:${options.port}`);
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
function buildHtml() {
|
|
206
|
+
const entries2 = registry.list();
|
|
207
|
+
const template = import_handlebars.default.compile(loadTemplate());
|
|
208
|
+
const view = {
|
|
209
|
+
entries: entries2.map((entry) => {
|
|
210
|
+
const mermaid = registry.getMermaid(entry.id) ?? "flowchart TD\n empty";
|
|
211
|
+
const graph = registry.getGraph(entry.id) ?? { nodes: [], edges: [] };
|
|
212
|
+
return {
|
|
213
|
+
id: entry.id,
|
|
214
|
+
name: entry.name ?? entry.id,
|
|
215
|
+
createdAt: new Date(entry.createdAt).toLocaleString(),
|
|
216
|
+
mermaid,
|
|
217
|
+
mermaidEncoded: encodeURIComponent(mermaid),
|
|
218
|
+
traces: registry.listTraces(entry.id).map((trace) => ({
|
|
219
|
+
id: trace.id,
|
|
220
|
+
createdAt: new Date(trace.createdAt).toLocaleString(),
|
|
221
|
+
firedCount: trace.fired.length,
|
|
222
|
+
matchedCount: trace.trace.filter((rule) => rule.matched).length,
|
|
223
|
+
traceEncoded: encodeURIComponent(JSON.stringify(trace))
|
|
224
|
+
})),
|
|
225
|
+
json: JSON.stringify(graph, null, 2)
|
|
226
|
+
};
|
|
227
|
+
})
|
|
228
|
+
};
|
|
229
|
+
return template(view);
|
|
230
|
+
}
|
|
231
|
+
function parseArgs(args) {
|
|
232
|
+
const options = {
|
|
233
|
+
port: 5173,
|
|
234
|
+
load: []
|
|
235
|
+
};
|
|
236
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
237
|
+
const value = args[i];
|
|
238
|
+
if (!value) {
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
if (value === "--port") {
|
|
242
|
+
const next = args[i + 1];
|
|
243
|
+
if (next) {
|
|
244
|
+
options.port = Number(next);
|
|
245
|
+
i += 1;
|
|
246
|
+
}
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
if (value === "--load") {
|
|
250
|
+
const next = args[i + 1];
|
|
251
|
+
if (next) {
|
|
252
|
+
options.load.push(next);
|
|
253
|
+
i += 1;
|
|
254
|
+
}
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
const envLoad = process.env.RULIT_UI_LOAD;
|
|
259
|
+
if (envLoad) {
|
|
260
|
+
options.load.push(
|
|
261
|
+
...envLoad.split(",").map((value) => value.trim()).filter(Boolean)
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
const envPort = process.env.RULIT_UI_PORT;
|
|
265
|
+
if (envPort) {
|
|
266
|
+
options.port = Number(envPort);
|
|
267
|
+
}
|
|
268
|
+
return options;
|
|
269
|
+
}
|
|
270
|
+
async function loadModules(paths, cacheBust) {
|
|
271
|
+
for (const modulePath of paths) {
|
|
272
|
+
const fullPath = import_node_path.default.resolve(process.cwd(), modulePath);
|
|
273
|
+
await import(`${(0, import_node_url.pathToFileURL)(fullPath).href}?t=${cacheBust}`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
var isMain = typeof importMetaUrl === "string" && importMetaUrl === (0, import_node_url.pathToFileURL)(process.argv[1] ?? "").href;
|
|
277
|
+
if (isMain) {
|
|
278
|
+
const options = parseArgs(process.argv.slice(2));
|
|
279
|
+
void startServer(options);
|
|
280
|
+
}
|
|
281
|
+
function loadTemplate() {
|
|
282
|
+
const templatePath = resolveTemplatePath();
|
|
283
|
+
if (!templatePath) {
|
|
284
|
+
throw new Error("UI template not found. Expected ui-template.hbs near cli ui.");
|
|
285
|
+
}
|
|
286
|
+
return import_node_fs.default.readFileSync(templatePath, "utf8");
|
|
287
|
+
}
|
|
288
|
+
function resolveTemplatePath() {
|
|
289
|
+
const candidates = [];
|
|
290
|
+
if (typeof importMetaUrl === "string") {
|
|
291
|
+
candidates.push(
|
|
292
|
+
new URL("./ui-template.hbs", importMetaUrl),
|
|
293
|
+
new URL("../../src/cli/ui-template.hbs", importMetaUrl)
|
|
294
|
+
);
|
|
295
|
+
} else {
|
|
296
|
+
candidates.push(import_node_path.default.join(__dirname, "ui-template.hbs"));
|
|
297
|
+
}
|
|
298
|
+
for (const candidate of candidates) {
|
|
299
|
+
const filePath = typeof candidate === "string" ? candidate : (0, import_node_url.fileURLToPath)(candidate);
|
|
300
|
+
if (import_node_fs.default.existsSync(filePath)) {
|
|
301
|
+
return filePath;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
307
|
+
0 && (module.exports = {
|
|
308
|
+
buildHtml,
|
|
309
|
+
startServer
|
|
310
|
+
});
|
|
311
|
+
//# sourceMappingURL=ui.js.map
|