Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
e3a755a
feat: make oven semver version first-class identity id@version (26072…
apresmoi Jul 21, 2026
240e3cd
feat: vendor per-project oven pins with opt-in upgrade (260721-001)
apresmoi Jul 21, 2026
60b4010
feat: path-based dashboard URL scheme with repoKey in path and back-c…
apresmoi Jul 21, 2026
a1016a8
feat: burnlist lens-switcher of contract-fit ovens (260721-001)
apresmoi Jul 21, 2026
1b4f18e
feat: oven catalog at /ovens with per-oven agent instructions and lan…
apresmoi Jul 21, 2026
3dd9fa9
feat: oven explainer page at /ovens/<id> with docs and sample-data de…
apresmoi Jul 21, 2026
9e00a05
docs: document oven versioning, adopt/upgrade pinning, and path-based…
apresmoi Jul 21, 2026
de8e9d6
docs: mirror oven versioning, adopt/upgrade pinning, and dashboard UR…
apresmoi Jul 21, 2026
18f7c05
fix: list oven adopt, upgrade, and fork verbs in the top-level help
apresmoi Jul 21, 2026
e9ddf3e
feat: share built-in oven runtime validation authority (260721-002)
apresmoi Jul 21, 2026
7d9835b
feat: dispatch oven data through runtime validation (260721-002)
apresmoi Jul 21, 2026
0f2ea32
feat: transact canonical oven data and bindings (260721-002)
apresmoi Jul 21, 2026
b13466b
feat: add validated oven set and use flows (260721-002)
apresmoi Jul 21, 2026
d9f8e22
docs: document built-in oven data shapes and starters (260721-002)
apresmoi Jul 21, 2026
4861cc5
docs: document oven use and set end to end (260721-002)
apresmoi Jul 21, 2026
1f81226
test: prove complete oven data flows (260721-002)
apresmoi Jul 21, 2026
419015d
fix: render repository-pinned ovens in live pages (260721-002)
apresmoi Jul 22, 2026
5c17fcd
fix: correct oven catalog actions and lens selection (260721-002)
apresmoi Jul 22, 2026
4a99607
fix: align custom oven validation with runtime (260721-002)
apresmoi Jul 22, 2026
037d79f
fix: harden vendored oven package access (260721-002)
apresmoi Jul 22, 2026
0091281
test: preserve bounded read coverage on Node 22 (260721-002)
apresmoi Jul 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion bin/burnlist.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ Usage:
burnlist differential-testing sdk
burnlist streaming-diff <ensure-feed|capture|url|hook> ...
burnlist hooks [install|uninstall|status] [--agent codex,claude] [--untracked] (bare defaults to status)
burnlist oven <list|view|bind|unbind|bindings|event|create|update> ...
burnlist oven <list|view|use|set|bind|unbind|bindings|event|create|update|adopt|upgrade|fork> ...
burnlist new [--repo <path>]
burnlist show <id>[#<item>] [--repo <path>]
burnlist ready <id> [--repo <path>]
Expand Down
18 changes: 12 additions & 6 deletions dashboard/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import { useMemo, useState } from "react";
import { ListChecks } from "lucide-react";
import { AppHeader, BurnlistTable, ChecklistOvenView, CustomOvenView, DashboardError, DifferentialTestingOvenPage, EmptyState, FILTERS, Filters, ModelLabPage, NewOvenPage, PerformanceTracingOvenPage, ProjectGroup, RunBurnPage, StreamingDiff, VisualParityPage } from "@components";
import { AppHeader, BurnlistTable, ChecklistOvenView, CustomOvenView, DashboardError, DifferentialTestingOvenPage, EmptyState, FILTERS, Filters, LensSwitcher, ModelLabPage, NewOvenPage, OvenCatalog, OvenDefinition, OvenExplainer, PerformanceTracingOvenPage, ProjectGroup, RunBurnPage, StreamingDiff, VisualParityPage } from "@components";
import { useDashboardData } from "@hooks";
import { currentSection, filterFromUrl, selectedBurnlist } from "@lib";
import { checklistOvenRepoKey, currentSection, filterFromUrl, ovenRepoKey, selectedBurnlist } from "@lib";
import type { Filter } from "@lib";
import { Button } from "@layout";

export function App() {
const section = currentSection();
const selected = useMemo(selectedBurnlist, [window.location.pathname, window.location.search]);
const repoKey = ovenRepoKey();
const [filter, setFilter] = useState(() => filterFromUrl(FILTERS));
const dashboardSection = section === "streaming-diff" ? "burnlists" : section;
const dashboardSection = ["landing", "burnlist", "streaming-diff"].includes(section) ? "burnlists" : section;
const { projects, progress, error, loading } = useDashboardData({ section: dashboardSection, selected });
const checklistRepoKey = checklistOvenRepoKey(progress, selected);
const visibleBurnlistCount = projects.reduce((total, project) => total + project.entries.filter((entry) => filter === "all" || entry.status === filter).length, 0);
const visibleProjectCount = projects.filter((project) => project.entries.some((entry) => filter === "all" || entry.status === filter)).length;

Expand All @@ -29,9 +32,9 @@ export function App() {
<div className="dashboard-app">
<AppHeader detail={progress} section={section} />
<main className="dashboard-main" data-layout={fullLayout ? "full" : "index"} data-section={section}>
{section === "differential-testing" ? <DifferentialTestingOvenPage /> : section === "model-lab" ? <ModelLabPage /> : section === "performance-tracing" ? <PerformanceTracingOvenPage /> : section === "streaming-diff" ? <StreamingDiff projects={projects} projectsLoading={loading} /> : section === "visual-parity" ? <VisualParityPage /> : section === "custom-oven" ? <CustomOvenView /> : section === "new-oven" ? <NewOvenPage /> : section === "run-burn" ? <RunBurnPage /> : selected ? (
{section === "differential-testing" ? <OvenDefinition id="differential-testing" repoKey={repoKey}>{(ir) => <DifferentialTestingOvenPage ir={ir} />}</OvenDefinition> : section === "model-lab" ? <OvenDefinition id="model-lab" repoKey={repoKey}>{(ir) => <ModelLabPage ir={ir} />}</OvenDefinition> : section === "performance-tracing" ? <OvenDefinition id="performance-tracing" repoKey={repoKey}>{(ir) => <PerformanceTracingOvenPage ir={ir} />}</OvenDefinition> : section === "streaming-diff" ? <OvenDefinition id="streaming-diff" repoKey={repoKey}>{(ir) => <StreamingDiff ir={ir} projects={projects} projectsLoading={loading} />}</OvenDefinition> : section === "visual-parity" ? <OvenDefinition id="visual-parity" repoKey={repoKey}>{(ir) => <VisualParityPage ir={ir} />}</OvenDefinition> : section === "custom-oven" ? <CustomOvenView /> : section === "new-oven" ? <NewOvenPage /> : section === "run-burn" ? <RunBurnPage /> : section === "ovens-catalog" ? <OvenCatalog /> : section === "oven-explainer" ? <OvenExplainer /> : selected ? (
error ? <DashboardError message={error} /> : loading && !progress ? <EmptyState title="Loading progress" detail="Reading the selected Burnlist." /> : progress ? (
<ChecklistOvenView data={progress} />
<><LensSwitcher /><OvenDefinition id="checklist" repoKey={checklistRepoKey}>{(ir) => <ChecklistOvenView data={progress} ir={ir} />}</OvenDefinition></>
) : <EmptyState title="Choose a Burnlist" detail="Select an item from the list to inspect its progress." icon={ListChecks} />
) : (
<section className="dashboard-index">
Expand All @@ -40,7 +43,10 @@ export function App() {
<h1 className="dashboard-index-title">Burnlists</h1>
<p className="dashboard-index-summary">{visibleBurnlistCount} Burnlists in {visibleProjectCount} {visibleProjectCount === 1 ? "project" : "projects"}</p>
</div>
<Filters filter={filter} onFilterChange={updateFilter} />
<div className="dashboard-index-actions">
<Button asChild size="sm" variant="outline"><a href="/ovens">Ovens</a></Button>
<Filters filter={filter} onFilterChange={updateFilter} />
</div>
</div>
{error ? <DashboardError message={error} /> : projects.length && visibleBurnlistCount ? (
<div className="dashboard-project-groups"><BurnlistTable showStatus={filter === "all"}>{projects.map((project) => <ProjectGroup filter={filter} key={project.canonicalRoot} project={project} />)}</BurnlistTable></div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { useEffect, useMemo } from "react";
import type { ResolvedOvenIr } from "@hooks";
import type { ChecklistProgressData } from "@lib";
import { adaptChecklist } from "@lib/checklist-adapter";
import { OvenRuntime } from "@/oven/runtime/OvenRuntime";
import ovenIr from "../../../../ovens/checklist/checklist.ir.json";
import "./ChecklistDashboard.css";

export function ChecklistOvenView({ data }: { data: ChecklistProgressData }) {
export function ChecklistOvenView({ data, ir }: { data: ChecklistProgressData; ir: ResolvedOvenIr }) {
const payload = useMemo(() => adaptChecklist(data), [data]);
useEffect(() => {
document.body.classList.add("driving-parity-view", "checklist-detail-view");
return () => document.body.classList.remove("driving-parity-view", "checklist-detail-view");
}, []);
return <OvenRuntime ir={ovenIr} payload={payload} />;
return <OvenRuntime ir={ir} payload={payload} />;
}
35 changes: 35 additions & 0 deletions dashboard/src/components/CopyButton/CopyButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { useEffect, useRef, useState } from "react";
import { Check, Copy } from "lucide-react";

export function CopyButton({ text }: { text: string }) {
const [isCopied, setIsCopied] = useState(false);
const resetTimer = useRef<ReturnType<typeof setTimeout>>();

useEffect(() => () => clearTimeout(resetTimer.current), []);

const copy = async () => {
const clipboard = typeof navigator === "undefined" ? undefined : navigator.clipboard;
if (!clipboard?.writeText) return;
try {
await clipboard.writeText(text);
setIsCopied(true);
clearTimeout(resetTimer.current);
resetTimer.current = setTimeout(() => setIsCopied(false), 1500);
} catch {
// Clipboard access may be unavailable outside a secure browser context.
}
};

return (
<button
aria-label={isCopied ? "Instructions copied" : "Copy instructions"}
className="copy-btn oven-catalog-copy-button"
onClick={() => void copy()}
title={isCopied ? "Copied" : "Copy instructions"}
type="button"
>
{isCopied ? <Check aria-hidden="true" /> : <Copy aria-hidden="true" />}
<span>{isCopied ? "Copied" : "Copy"}</span>
</button>
);
}
1 change: 1 addition & 0 deletions dashboard/src/components/CopyButton/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { CopyButton } from "./CopyButton";
22 changes: 17 additions & 5 deletions dashboard/src/components/CustomOvenView/CustomOvenView.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { useEffect, useState, type ComponentProps } from "react";
import { customOvenSelection } from "@lib";
import { adaptChecklist } from "@lib/checklist-adapter";
import type { ChecklistProgressData } from "@lib";
import { OvenRuntime } from "@/oven/runtime/OvenRuntime";
import { DashboardError } from "../DashboardError";
import { EmptyState } from "../EmptyState";
import { LensSwitcher } from "../LensSwitcher";

type OvenIr = ComponentProps<typeof OvenRuntime>["ir"];
type LoadedOven = { ir: OvenIr; payload: unknown };
Expand All @@ -11,6 +14,13 @@ function unwrapPayload(raw: unknown) {
return raw && typeof raw === "object" && "payload" in raw ? (raw as { payload: unknown }).payload : raw;
}

export function CustomOvenRuntime({ burnlistId, loaded }: { burnlistId?: string; loaded: LoadedOven }) {
if (burnlistId) {
return <><LensSwitcher /><OvenRuntime ir={{ ...loaded.ir, refreshSeconds: undefined }} payload={loaded.payload} /></>;
}
return <OvenRuntime ir={loaded.ir} initialPayload={loaded.payload} adapt={unwrapPayload} />;
}

export function CustomOvenView() {
const selection = customOvenSelection();
const [loaded, setLoaded] = useState<LoadedOven | null>(null);
Expand All @@ -30,23 +40,25 @@ export function CustomOvenView() {
try {
const [ovenResponse, dataResponse] = await Promise.all([
fetch(`/api/ovens/${selection.id}${query}`, { cache: "no-store", signal: controller.signal }),
fetch(`/api/oven-data/${selection.id}${query}`, { cache: "no-store", signal: controller.signal }),
fetch(selection.burnlistId
? `/api/progress?repoKey=${encodeURIComponent(selection.repoKey)}&id=${encodeURIComponent(selection.burnlistId)}`
: `/api/oven-data/${selection.id}${query}`, { cache: "no-store", signal: controller.signal }),
]);
if (!ovenResponse.ok) throw new Error(`Could not load Oven (${ovenResponse.status}).`);
if (!dataResponse.ok) throw new Error(`Could not load Oven data (${dataResponse.status}).`);
if (!dataResponse.ok) throw new Error(`Could not load ${selection.burnlistId ? "Burnlist progress" : "Oven data"} (${dataResponse.status}).`);
const [ovenBody, dataBody] = await Promise.all([ovenResponse.json(), dataResponse.json()]);
if (controller.signal.aborted) return;
setLoaded({ ir: ovenBody.oven.ir, payload: dataBody.payload });
setLoaded({ ir: ovenBody.oven.ir, payload: selection.burnlistId ? adaptChecklist(dataBody as ChecklistProgressData) : dataBody.payload });
} catch (cause) {
if (controller.signal.aborted) return;
setError(cause instanceof Error ? cause.message : "Could not load the custom Oven.");
}
};
void load();
return () => controller.abort();
}, [selection?.id, selection?.repoKey]);
}, [selection?.burnlistId, selection?.id, selection?.repoKey]);

if (error) return <DashboardError message={error} />;
if (!loaded) return <EmptyState title="Loading Oven" detail="Reading the Oven view and its bound data." />;
return <OvenRuntime ir={loaded.ir} payload={loaded.payload} adapt={unwrapPayload} />;
return <CustomOvenRuntime burnlistId={selection?.burnlistId} loaded={loaded} />;
}
34 changes: 23 additions & 11 deletions dashboard/src/components/CustomOvenView/custom-oven-render.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,26 @@ import assert from "node:assert/strict";
import { mkdtemp, rm } from "node:fs/promises";
import { join } from "node:path";
import test from "node:test";
import { createElement } from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { build } from "esbuild";
import { compileOven } from "../../../../src/ovens/dsl/oven-compile.mjs";

const runtimePath = new URL("../../oven/runtime/OvenRuntime.tsx", import.meta.url).pathname;
const componentPath = new URL("./CustomOvenView.tsx", import.meta.url).pathname;
const sourceDir = new URL("../../", import.meta.url).pathname;
const libPath = new URL("../../lib", import.meta.url).pathname;
const ovenPath = new URL("../../oven", import.meta.url).pathname;
const ovenSource = `<oven id="widget-oven" version="1" contract="checklist-progress@1" theme="checklist">
const ovenSource = `<oven id="widget-oven" version="0.1.0" contract="checklist-progress@1" theme="checklist">
<kpi-strip>
<kpi-item variant="current" heading="Widget" title="/widget/name" value="/widget/count"/>
</kpi-strip>
</oven>`;

test("a custom Oven runtime renders author-shaped data values", { timeout: 20_000 }, async () => {
test("custom Oven runtime modes preserve live standalone polling and controlled Burnlist data", { timeout: 20_000 }, async () => {
const outputDir = await mkdtemp(join(process.cwd(), ".custom-oven-render-test-"));
try {
const runtimeOutput = join(outputDir, "OvenRuntime.mjs");
const runtimeOutput = join(outputDir, "CustomOvenView.mjs");
await build({
entryPoints: [runtimePath],
entryPoints: [componentPath],
bundle: true,
format: "esm",
outfile: runtimeOutput,
Expand All @@ -32,17 +31,30 @@ test("a custom Oven runtime renders author-shaped data values", { timeout: 20_00
packages: "external",
target: "node18",
});
const { OvenRuntime } = await import(`${new URL(`file://${runtimeOutput}`).href}?test=${Date.now()}`);
const { CustomOvenRuntime } = await import(`${new URL(`file://${runtimeOutput}`).href}?test=${Date.now()}`);
const compiled = compileOven(ovenSource, { file: "widget-oven.oven" });
assert.equal(compiled.ok, true, compiled.ok ? "" : JSON.stringify(compiled.diagnostics));
if (!compiled.ok) return;

const markup = renderToStaticMarkup(createElement(OvenRuntime, {
ir: compiled.ir,
payload: { widget: { name: "Sprockets", count: 42 } },
}));
const payload = { widget: { name: "Sprockets", count: 42 } };
const ir = { ...compiled.ir, refreshSeconds: 7 };
const standalone = CustomOvenRuntime({ loaded: { ir, payload } });
assert.equal(standalone.props.ir, ir);
assert.equal(standalone.props.initialPayload, payload);
assert.equal("payload" in standalone.props, false);
assert.equal(standalone.props.ir.refreshSeconds, 7);
assert.equal(typeof standalone.props.adapt, "function");

const markup = renderToStaticMarkup(standalone);
assert.match(markup, /Sprockets/u);
assert.match(markup, />42</u);

const burnlist = CustomOvenRuntime({ burnlistId: "260722-001", loaded: { ir, payload } });
const controlled = burnlist.props.children[1];
assert.equal(controlled.props.payload, payload);
assert.equal("initialPayload" in controlled.props, false);
assert.equal(controlled.props.ir.refreshSeconds, undefined);
assert.equal(controlled.props.adapt, undefined);
} finally {
await rm(outputDir, { force: true, recursive: true });
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import assert from "node:assert/strict";
import { mkdtemp, readFile, rm } from "node:fs/promises";
import { dirname, join, resolve } from "node:path";
import { join } from "node:path";
import test from "node:test";
import { createElement } from "react";
import { renderToStaticMarkup } from "react-dom/server";
Expand All @@ -11,18 +11,12 @@ const componentPath = new URL("./DifferentialTestingOven.tsx", import.meta.url).
const sourcePath = new URL("../..", import.meta.url).pathname;
const libPath = new URL("../../lib", import.meta.url).pathname;
const ovenPath = new URL("../../oven", import.meta.url).pathname;
const ovenIrPlugin = {
name: "test-oven-ir",
setup(context) {
context.onResolve({ filter: /\.ir\.json$/ }, (args) => ({ namespace: "test-oven-ir", path: resolve(dirname(args.importer), args.path) }));
context.onLoad({ filter: /\.ir\.json$/, namespace: "test-oven-ir" }, async (args) => {
const sourcePath = args.path.replace(/\.ir\.json$/u, ".oven");
const compiled = compileOven(await readFile(sourcePath, "utf8"), { file: sourcePath });
if (!compiled.ok) throw new Error(JSON.stringify(compiled.diagnostics));
return { contents: JSON.stringify(compiled.ir), loader: "json" };
});
},
};
async function ovenIr(id) {
const path = new URL(`../../../../ovens/${id}/${id}.oven`, import.meta.url);
const compiled = compileOven(await readFile(path, "utf8"), { file: path.pathname });
if (!compiled.ok) throw new Error(JSON.stringify(compiled.diagnostics));
return compiled.ir;
}

test("live Differential Testing pages retain the scoped shell required by their dashboard CSS", async () => {
const outputDir = await mkdtemp(join(process.cwd(), ".differential-testing-oven-test-"));
Expand All @@ -37,12 +31,11 @@ test("live Differential Testing pages retain the scoped shell required by their
alias: { "@": sourcePath, "@lib": libPath, "@oven": ovenPath },
jsx: "automatic",
packages: "external",
plugins: [ovenIrPlugin],
target: "node18",
});
const { DifferentialTestingOvenPage, PerformanceTracingOvenPage } = await import(`${new URL(`file://${outputPath}`).href}?test=${Date.now()}`);
const differential = renderToStaticMarkup(createElement(DifferentialTestingOvenPage));
const performance = renderToStaticMarkup(createElement(PerformanceTracingOvenPage));
const differential = renderToStaticMarkup(createElement(DifferentialTestingOvenPage, { ir: await ovenIr("differential-testing") }));
const performance = renderToStaticMarkup(createElement(PerformanceTracingOvenPage, { ir: await ovenIr("performance-tracing") }));

assert.match(differential, /^<div class="shell driving-parity-view"><div class="empty">Loading…<\/div><\/div>$/u);
assert.match(performance, /^<div class="shell driving-parity-view performance-tracing-oven"><div class="empty">Loading…<\/div><\/div>$/u);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { useEffect, type ReactNode } from "react";
import type { ResolvedOvenIr } from "@hooks";
import { adaptPerformanceTracingReport } from "@lib";
import { adaptDifferentialTesting, type DifferentialTestingData } from "../../lib/differential-testing-adapter";
import { OvenRuntime } from "@/oven/runtime/OvenRuntime";
import { withDifferentialTestingEnvelope } from "@/oven/runtime/oven-payload-metadata";
import differentialTestingIr from "../../../../ovens/differential-testing/differential-testing.ir.json";
import performanceTracingIr from "../../../../ovens/performance-tracing/performance-tracing.ir.json";

type ResponseEnvelope = { payload: unknown; fieldPage?: unknown; frameDeltaMetrics?: unknown };

Expand Down Expand Up @@ -33,10 +32,10 @@ function DifferentialTestingShell({ children, performanceTracing = false }: { ch
return <div className={`shell driving-parity-view${performanceTracing ? " performance-tracing-oven" : ""}`}>{children}</div>;
}

export function DifferentialTestingOvenPage() {
return <DifferentialTestingShell><OvenRuntime ir={differentialTestingIr} adapt={dtAdapt} /></DifferentialTestingShell>;
export function DifferentialTestingOvenPage({ ir }: { ir: ResolvedOvenIr }) {
return <DifferentialTestingShell><OvenRuntime ir={ir} adapt={dtAdapt} /></DifferentialTestingShell>;
}

export function PerformanceTracingOvenPage() {
return <DifferentialTestingShell performanceTracing><OvenRuntime ir={performanceTracingIr} adapt={ptAdapt} /></DifferentialTestingShell>;
export function PerformanceTracingOvenPage({ ir }: { ir: ResolvedOvenIr }) {
return <DifferentialTestingShell performanceTracing><OvenRuntime ir={ir} adapt={ptAdapt} /></DifferentialTestingShell>;
}
20 changes: 20 additions & 0 deletions dashboard/src/components/LensSwitcher/LensSwitcher.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
.lens-switcher {
display: flex;
gap: 0.35rem;
margin: 0 0 1rem;
}

.lens-switcher-link {
border: 1px solid var(--border, #d1d5db);
border-radius: 999px;
color: inherit;
font-size: 0.8125rem;
padding: 0.35rem 0.65rem;
text-decoration: none;
}

.lens-switcher-link.is-active {
background: var(--accent, #2563eb);
border-color: var(--accent, #2563eb);
color: #fff;
}
Loading
Loading