feat(compound-engineering): artifact hub view, discovery, read routes (U3)

Add allowlist-based artifact discovery over the conventional CE locations
(STRATEGY.md, docs/ideation, docs/brainstorms, docs/plans, docs/solutions,
CONCEPTS.md), grouped by stage, with error entries for unreadable artifacts and
no reads outside the allowlist. Add list/read/preview routes (self-contained
sandboxed HTML, data-section markers) and the primary CompoundEngineeringView
with explicit empty/partial/error states, viewport-gated fetch, and a short-TTL
cache. Bind the view via registerBundledPluginViews + dashboard workspace dep
(the real binding; manifest componentPath is cosmetic).
This commit is contained in:
gsxdsm
2026-06-02 19:25:54 -07:00
parent 0189477d9d
commit 8bb459702c
17 changed files with 1470 additions and 14 deletions

View File

@@ -37,6 +37,18 @@ async function loadRoadmapView(): Promise<{ default: PluginViewComponent }> {
return { default: component as PluginViewComponent };
}
async function loadCompoundEngineeringView(): Promise<{ default: PluginViewComponent }> {
const moduleId = "@fusion-plugin-examples/compound-engineering/dashboard-view";
const exportName = "CompoundEngineeringDashboardView";
const mod = await import("@fusion-plugin-examples/compound-engineering/dashboard-view") as unknown as Record<string, ComponentType<{ context?: PluginDashboardViewContext }>>;
const component = mod[exportName];
if (!component) {
console.warn(`[plugin-views] Missing export ${exportName} from ${moduleId}`);
return { default: createMissingPluginView(moduleId, exportName) };
}
return { default: component as PluginViewComponent };
}
async function loadCliPrintingPressWizardView(): Promise<{ default: PluginViewComponent }> {
const moduleId = "@fusion-plugin-examples/cli-printing-press/dashboard-view";
const exportName = "CliPrintingPressWizardView";
@@ -85,6 +97,12 @@ export function registerBundledPluginViews(): void {
lazy(loadRoadmapView),
);
registerPluginView(
"fusion-plugin-compound-engineering",
"compound-engineering",
lazy(loadCompoundEngineeringView),
);
registerPluginView(
"fusion-plugin-cli-printing-press",
"wizard",

View File

@@ -90,6 +90,7 @@
"@codemirror/state": "^6.5.2",
"@codemirror/theme-one-dark": "^6.1.2",
"@codemirror/view": "^6.36.4",
"@fusion-plugin-examples/compound-engineering": "workspace:*",
"@fusion-plugin-examples/dependency-graph": "workspace:*",
"@fusion-plugin-examples/roadmap": "workspace:*",
"@fusion-plugin-examples/hermes-runtime": "workspace:*",

View File

@@ -0,0 +1,211 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import * as realFs from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
// Mock node:fs so we can observe/inject behaviour around readFileSync without
// relying on vi.spyOn (ESM namespace exports are not configurable). The hooks
// below default to passthrough and individual tests override them.
let readFileHook: ((path: realFs.PathOrFileDescriptor, original: typeof realFs.readFileSync, args: unknown[]) => unknown) | undefined;
vi.mock("node:fs", async (importOriginal) => {
const actual = await importOriginal<typeof realFs>();
return {
...actual,
readFileSync: (path: realFs.PathOrFileDescriptor, ...args: unknown[]) => {
if (readFileHook) return readFileHook(path, actual.readFileSync, args);
return (actual.readFileSync as (...a: unknown[]) => unknown)(path, ...args);
},
};
});
const { mkdtempSync, mkdirSync, writeFileSync, rmSync, utimesSync } = realFs;
const { discoverArtifacts, readArtifactById } = await import("../discovery.js");
function makeRepo(): string {
return mkdtempSync(join(tmpdir(), "ce-discovery-"));
}
describe("discoverArtifacts", () => {
let root: string;
afterEach(() => {
if (root) rmSync(root, { recursive: true, force: true });
readFileHook = undefined;
vi.restoreAllMocks();
});
it("returns grouped artifacts from a fixture repo tree (happy path)", () => {
root = makeRepo();
writeFileSync(join(root, "STRATEGY.md"), "# Strategy");
writeFileSync(join(root, "CONCEPTS.md"), "# Concepts");
mkdirSync(join(root, "docs/ideation"), { recursive: true });
writeFileSync(join(root, "docs/ideation/a.md"), "ideation a");
writeFileSync(join(root, "docs/ideation/b.md"), "ideation b");
mkdirSync(join(root, "docs/brainstorms"), { recursive: true });
writeFileSync(join(root, "docs/brainstorms/x.md"), "brainstorm x");
mkdirSync(join(root, "docs/plans"), { recursive: true });
writeFileSync(join(root, "docs/plans/plan1.md"), "plan 1");
mkdirSync(join(root, "docs/solutions"), { recursive: true });
writeFileSync(join(root, "docs/solutions/sol.md"), "solution");
const result = discoverArtifacts(root);
const byStage = Object.fromEntries(result.groups.map((g) => [g.stage, g]));
expect(result.totalArtifacts).toBe(7);
expect(result.totalErrors).toBe(0);
expect(byStage.strategy.entries).toHaveLength(1);
expect(byStage.concepts.entries).toHaveLength(1);
expect(byStage.ideation.entries).toHaveLength(2);
expect(byStage.brainstorm.entries).toHaveLength(1);
expect(byStage.plan.entries).toHaveLength(1);
expect(byStage.solution.entries).toHaveLength(1);
// Every group present is flagged present.
expect(byStage.ideation.present).toBe(true);
// All entries are artifacts in the happy path.
expect(result.groups.flatMap((g) => g.entries).every((e) => e.kind === "artifact")).toBe(true);
});
it("orders directory artifacts by updatedAt DESC", () => {
root = makeRepo();
mkdirSync(join(root, "docs/plans"), { recursive: true });
const older = join(root, "docs/plans/old.md");
const newer = join(root, "docs/plans/new.md");
writeFileSync(older, "old");
writeFileSync(newer, "new");
// Force deterministic mtimes: old < new.
const now = Date.now();
utimesSync(older, new Date(now - 10_000), new Date(now - 10_000));
utimesSync(newer, new Date(now), new Date(now));
const result = discoverArtifacts(root);
const plan = result.groups.find((g) => g.stage === "plan")!;
expect(plan.entries.map((e) => e.name)).toEqual(["new.md", "old.md"]);
});
it("reports a partial-discovery state: some categories present, others empty", () => {
root = makeRepo();
writeFileSync(join(root, "STRATEGY.md"), "# Strategy");
mkdirSync(join(root, "docs/plans"), { recursive: true });
writeFileSync(join(root, "docs/plans/p.md"), "plan");
// No ideation / brainstorms / solutions / CONCEPTS.
const result = discoverArtifacts(root);
const populated = result.groups.filter((g) => g.entries.length > 0);
const empty = result.groups.filter((g) => g.entries.length === 0);
expect(populated.map((g) => g.stage).sort()).toEqual(["plan", "strategy"]);
expect(empty.length).toBeGreaterThan(0);
// Empty groups are still present in the result so the hub can render them.
expect(result.groups).toHaveLength(6);
});
it("returns an all-empty result when nothing is present (first-run)", () => {
root = makeRepo();
const result = discoverArtifacts(root);
expect(result.totalArtifacts).toBe(0);
expect(result.totalErrors).toBe(0);
expect(result.groups.every((g) => g.entries.length === 0 && !g.present)).toBe(true);
});
it("represents an unreadable artifact as an error entry, not a crash or silent drop", () => {
root = makeRepo();
mkdirSync(join(root, "docs/plans"), { recursive: true });
const readable = join(root, "docs/plans/good.md");
writeFileSync(readable, "good");
// Simulate a malformed/unreadable artifact: the specific file throws on read.
readFileHook = (path, original, args) => {
if (typeof path === "string" && path.endsWith("good.md")) {
throw new Error("EIO: simulated read failure");
}
return (original as (...a: unknown[]) => unknown)(path, ...args);
};
const result = discoverArtifacts(root);
const plan = result.groups.find((g) => g.stage === "plan")!;
expect(plan.entries).toHaveLength(1);
const entry = plan.entries[0];
expect(entry.kind).toBe("error");
expect(entry.kind === "error" && entry.error).toContain("simulated read failure");
expect(result.totalErrors).toBe(1);
expect(result.totalArtifacts).toBe(0);
});
it("ignores unrelated files and does not read outside the conventional locations", () => {
root = makeRepo();
// Conventional artifact that SHOULD be read.
writeFileSync(join(root, "STRATEGY.md"), "# Strategy");
mkdirSync(join(root, "docs/ideation"), { recursive: true });
writeFileSync(join(root, "docs/ideation/keep.md"), "keep");
// Unrelated files that must NOT be read.
writeFileSync(join(root, "README.md"), "readme"); // root-level non-conventional .md
writeFileSync(join(root, "package.json"), "{}");
writeFileSync(join(root, "docs/ideation/notes.txt"), "non-md, ignore"); // non-.md in a scanned dir
mkdirSync(join(root, "secrets"), { recursive: true });
writeFileSync(join(root, "secrets/secret.md"), "TOP SECRET"); // outside the allowlist
mkdirSync(join(root, "docs/random"), { recursive: true });
writeFileSync(join(root, "docs/random/r.md"), "unrelated"); // docs subtree but not conventional
const opened: string[] = [];
readFileHook = (path, original, args) => {
if (typeof path === "string") opened.push(path);
return (original as (...a: unknown[]) => unknown)(path, ...args);
};
const result = discoverArtifacts(root);
// Only the two conventional artifacts were read.
expect(opened.some((p) => p.endsWith("STRATEGY.md"))).toBe(true);
expect(opened.some((p) => p.endsWith(join("ideation", "keep.md")))).toBe(true);
// Nothing outside the allowlist was opened.
expect(opened.some((p) => p.includes(`${join("secrets", "secret.md")}`))).toBe(false);
expect(opened.some((p) => p.endsWith("README.md"))).toBe(false);
expect(opened.some((p) => p.endsWith("package.json"))).toBe(false);
expect(opened.some((p) => p.endsWith("notes.txt"))).toBe(false);
expect(opened.some((p) => p.includes(join("random", "r.md")))).toBe(false);
expect(result.totalArtifacts).toBe(2);
});
});
describe("readArtifactById", () => {
let root: string;
beforeEach(() => {
root = makeRepo();
});
afterEach(() => {
rmSync(root, { recursive: true, force: true });
});
it("reads a conventional file artifact", () => {
writeFileSync(join(root, "STRATEGY.md"), "# Strategy body");
const res = readArtifactById(root, "strategy:STRATEGY.md");
expect(res).toBeDefined();
expect(res && "content" in res && res.content).toContain("Strategy body");
});
it("reads a directory artifact's immediate Markdown child", () => {
mkdirSync(join(root, "docs/plans"), { recursive: true });
writeFileSync(join(root, "docs/plans/p.md"), "plan body");
const res = readArtifactById(root, "plan:docs/plans/p.md");
expect(res && "content" in res && res.content).toBe("plan body");
});
it("refuses a forged id that escapes the conventional location", () => {
writeFileSync(join(root, "secrets.md"), "secret");
// Attempt to traverse out of docs/plans into the repo root.
expect(readArtifactById(root, "plan:../../secrets.md")).toBeUndefined();
// Wrong stage/path pairing for a file location.
expect(readArtifactById(root, "strategy:CONCEPTS.md")).toBeUndefined();
// Unknown stage.
expect(readArtifactById(root, "bogus:whatever.md")).toBeUndefined();
});
it("refuses a nested path under a directory location (non-immediate child)", () => {
mkdirSync(join(root, "docs/plans/sub"), { recursive: true });
writeFileSync(join(root, "docs/plans/sub/deep.md"), "deep");
expect(readArtifactById(root, "plan:docs/plans/sub/deep.md")).toBeUndefined();
});
});

View File

@@ -0,0 +1,364 @@
import { readdirSync, readFileSync, statSync } from "node:fs";
import { isAbsolute, join, relative, sep } from "node:path";
/**
* CE artifact discovery (U3).
*
* Scans a fixed allowlist of conventional CE artifact locations relative to a
* project root and returns artifacts grouped by stage. The allowlist is the
* ONLY filesystem surface this module touches — it never recurses outside a
* conventional location and never reads a file that does not live under one of
* them. An artifact that cannot be read or is malformed is represented as an
* `error` entry rather than crashing the scan or being silently dropped.
*
* Locations (per the plan): STRATEGY.md, docs/ideation/, docs/brainstorms/,
* docs/plans/, docs/solutions/, CONCEPTS.md.
*/
export type CeArtifactStage =
| "strategy"
| "ideation"
| "brainstorm"
| "plan"
| "solution"
| "concepts";
/** Whether a conventional location is a single file or a directory of files. */
type LocationKind = "file" | "directory";
interface ConventionalLocation {
stage: CeArtifactStage;
/** Human label for the stage group. */
label: string;
/** Project-root-relative path. */
path: string;
kind: LocationKind;
}
/**
* The conventional CE artifact locations. This is the discovery allowlist — the
* scanner reads ONLY these paths (and, for directories, their immediate `.md`
* children). Nothing outside this list is opened.
*/
export const CONVENTIONAL_LOCATIONS: readonly ConventionalLocation[] = [
{ stage: "strategy", label: "Strategy", path: "STRATEGY.md", kind: "file" },
{ stage: "ideation", label: "Ideation", path: "docs/ideation", kind: "directory" },
{ stage: "brainstorm", label: "Brainstorms", path: "docs/brainstorms", kind: "directory" },
{ stage: "plan", label: "Plans", path: "docs/plans", kind: "directory" },
{ stage: "solution", label: "Solutions", path: "docs/solutions", kind: "directory" },
{ stage: "concepts", label: "Concepts", path: "CONCEPTS.md", kind: "file" },
];
/** A discovered, readable artifact. */
export interface CeArtifact {
/** Stable id: `${stage}:${relativePath}`. Safe to use as a route param after encoding. */
id: string;
stage: CeArtifactStage;
/** Project-root-relative path with forward slashes. */
path: string;
/** Filename (basename). */
name: string;
/** Size in bytes. */
size: number;
/** Last-modified epoch ms — used for `(stage, updatedAt DESC)` ordering. */
updatedAt: number;
/** Discriminator. */
kind: "artifact";
}
/** An artifact location that exists but could not be read / was malformed. */
export interface CeArtifactError {
id: string;
stage: CeArtifactStage;
path: string;
name: string;
/** Discriminator. */
kind: "error";
/** Human-readable reason the artifact could not be surfaced. */
error: string;
}
export type CeArtifactEntry = CeArtifact | CeArtifactError;
/** Artifacts (and error entries) grouped by stage. */
export interface CeArtifactGroup {
stage: CeArtifactStage;
label: string;
/** True when the conventional location for this stage exists on disk. */
present: boolean;
/** Entries, ordered by `updatedAt DESC` (errors sort last, keyed by name). */
entries: CeArtifactEntry[];
}
export interface DiscoveryResult {
groups: CeArtifactGroup[];
/** Convenience flags for the hub's empty / partial states. */
totalArtifacts: number;
totalErrors: number;
}
const MAX_ARTIFACT_BYTES = 2_000_000;
function toPosix(p: string): string {
return p.split(sep).join("/");
}
/**
* Guard: a resolved path must stay within the project root AND under the
* specific conventional location it was discovered through. This is the
* concrete enforcement of "do not read outside the conventional locations".
*/
function isWithin(root: string, locationAbs: string, candidate: string): boolean {
const relToLocation = relative(locationAbs, candidate);
if (relToLocation.startsWith("..") || isAbsolute(relToLocation)) return false;
const relToRoot = relative(root, candidate);
if (relToRoot.startsWith("..") || isAbsolute(relToRoot)) return false;
return true;
}
function makeId(stage: CeArtifactStage, relPath: string): string {
return `${stage}:${relPath}`;
}
function readArtifactEntry(
stage: CeArtifactStage,
root: string,
locationAbs: string,
abs: string,
relPath: string,
): CeArtifactEntry {
const name = relPath.split("/").pop() ?? relPath;
// Defense in depth: refuse anything that escaped the conventional location.
if (!isWithin(root, locationAbs, abs)) {
return {
id: makeId(stage, relPath),
stage,
path: relPath,
name,
kind: "error",
error: "Path is outside its conventional location",
};
}
try {
const st = statSync(abs);
if (st.size > MAX_ARTIFACT_BYTES) {
return {
id: makeId(stage, relPath),
stage,
path: relPath,
name,
kind: "error",
error: `Artifact too large to read (${st.size} bytes)`,
};
}
// Read eagerly so a malformed/unreadable file is surfaced now as an error
// entry rather than crashing later at render time.
readFileSync(abs, "utf8");
return {
id: makeId(stage, relPath),
stage,
path: relPath,
name,
size: st.size,
updatedAt: st.mtimeMs,
kind: "artifact",
};
} catch (err) {
return {
id: makeId(stage, relPath),
stage,
path: relPath,
name,
kind: "error",
error: err instanceof Error ? err.message : String(err),
};
}
}
function sortEntries(entries: CeArtifactEntry[]): CeArtifactEntry[] {
// Composite ordering analogue: artifacts by updatedAt DESC; errors last,
// stable by name. (See docs/performance/dashboard-load.md — the persisted
// equivalent is a `(type, updatedAt DESC)` index.)
return [...entries].sort((a, b) => {
if (a.kind !== b.kind) return a.kind === "artifact" ? -1 : 1;
if (a.kind === "artifact" && b.kind === "artifact") return b.updatedAt - a.updatedAt;
return a.name.localeCompare(b.name);
});
}
function discoverLocation(root: string, loc: ConventionalLocation): CeArtifactGroup {
const locationAbs = join(root, loc.path);
const entries: CeArtifactEntry[] = [];
let present = false;
let st: ReturnType<typeof statSync> | undefined;
try {
st = statSync(locationAbs);
present = true;
} catch {
// Location simply does not exist — an empty (but valid) category.
return { stage: loc.stage, label: loc.label, present: false, entries: [] };
}
if (loc.kind === "file") {
if (st.isFile()) {
entries.push(readArtifactEntry(loc.stage, root, locationAbs, locationAbs, toPosix(loc.path)));
} else {
// A conventional file path that is actually a directory is malformed.
entries.push({
id: makeId(loc.stage, toPosix(loc.path)),
stage: loc.stage,
path: toPosix(loc.path),
name: loc.path.split("/").pop() ?? loc.path,
kind: "error",
error: "Expected a file at the conventional location but found a directory",
});
}
return { stage: loc.stage, label: loc.label, present, entries: sortEntries(entries) };
}
// Directory location: read ONLY immediate children, only Markdown files.
// Non-recursive on purpose — we never descend into unrelated subtrees.
let names: string[] = [];
try {
if (!st.isDirectory()) {
entries.push({
id: makeId(loc.stage, toPosix(loc.path)),
stage: loc.stage,
path: toPosix(loc.path),
name: loc.path.split("/").pop() ?? loc.path,
kind: "error",
error: "Expected a directory at the conventional location but found a file",
});
return { stage: loc.stage, label: loc.label, present, entries: sortEntries(entries) };
}
names = readdirSync(locationAbs);
} catch (err) {
entries.push({
id: makeId(loc.stage, toPosix(loc.path)),
stage: loc.stage,
path: toPosix(loc.path),
name: loc.path.split("/").pop() ?? loc.path,
kind: "error",
error: err instanceof Error ? err.message : String(err),
});
return { stage: loc.stage, label: loc.label, present, entries: sortEntries(entries) };
}
for (const childName of names) {
// Ignore unrelated files: only Markdown artifacts count. Dotfiles and any
// non-.md file are skipped outright (not read).
if (childName.startsWith(".")) continue;
if (!childName.toLowerCase().endsWith(".md")) continue;
const abs = join(locationAbs, childName);
const relPath = toPosix(join(loc.path, childName));
// Skip nested directories named *.md — only regular files are artifacts.
let childStat: ReturnType<typeof statSync>;
try {
childStat = statSync(abs);
} catch (err) {
entries.push({
id: makeId(loc.stage, relPath),
stage: loc.stage,
path: relPath,
name: childName,
kind: "error",
error: err instanceof Error ? err.message : String(err),
});
continue;
}
if (!childStat.isFile()) continue;
entries.push(readArtifactEntry(loc.stage, root, locationAbs, abs, relPath));
}
return { stage: loc.stage, label: loc.label, present, entries: sortEntries(entries) };
}
/**
* Discover CE artifacts under `projectRoot`, grouped by stage. Never throws for
* per-artifact problems — those become `error` entries. Always returns one
* group per conventional location (empty groups included so the hub can render
* a partial-discovery state).
*/
export function discoverArtifacts(projectRoot: string): DiscoveryResult {
const root = projectRoot;
const groups = CONVENTIONAL_LOCATIONS.map((loc) => discoverLocation(root, loc));
let totalArtifacts = 0;
let totalErrors = 0;
for (const g of groups) {
for (const e of g.entries) {
if (e.kind === "artifact") totalArtifacts += 1;
else totalErrors += 1;
}
}
return { groups, totalArtifacts, totalErrors };
}
/**
* Resolve a single artifact by its `stage:relativePath` id and return its raw
* content. Re-validates the path against the conventional-location allowlist so
* a forged id can never read an arbitrary file. Returns `undefined` if the id
* does not map to a known conventional location or the file is missing.
*/
export function readArtifactById(
projectRoot: string,
id: string,
): { artifact: CeArtifact; content: string } | { error: string } | undefined {
const sepIdx = id.indexOf(":");
if (sepIdx <= 0) return undefined;
const stage = id.slice(0, sepIdx) as CeArtifactStage;
const relPath = id.slice(sepIdx + 1);
const loc = CONVENTIONAL_LOCATIONS.find((l) => l.stage === stage);
if (!loc) return undefined;
const locationAbs = join(projectRoot, loc.path);
const abs = join(projectRoot, relPath);
// The requested path must live under the stage's conventional location.
// For file locations, the path must equal the location itself.
if (loc.kind === "file") {
if (toPosix(relPath) !== toPosix(loc.path)) return undefined;
} else if (!isWithin(projectRoot, locationAbs, abs)) {
return undefined;
}
// Directory artifacts must be immediate Markdown children.
if (loc.kind === "directory") {
const rel = relative(locationAbs, abs);
if (rel.includes(sep) || rel.startsWith("..") || !rel.toLowerCase().endsWith(".md")) {
return undefined;
}
}
let content: string;
let mtimeMs: number;
let size: number;
try {
const st = statSync(abs);
if (!st.isFile()) return { error: "Artifact is not a readable file" };
if (st.size > MAX_ARTIFACT_BYTES) return { error: `Artifact too large to read (${st.size} bytes)` };
mtimeMs = st.mtimeMs;
size = st.size;
} catch (err) {
// A missing file is "not found" (404), not a malformed-artifact error (422).
if ((err as NodeJS.ErrnoException)?.code === "ENOENT") return undefined;
return { error: err instanceof Error ? err.message : String(err) };
}
try {
content = readFileSync(abs, "utf8");
} catch (err) {
return { error: err instanceof Error ? err.message : String(err) };
}
const name = relPath.split("/").pop() ?? relPath;
return {
artifact: {
id,
stage,
path: toPosix(relPath),
name,
size,
updatedAt: mtimeMs,
kind: "artifact",
},
content,
};
}

View File

@@ -1,13 +1,12 @@
import type { PluginDashboardViewContext } from "@fusion/dashboard/app/plugins/types";
/**
* Placeholder dashboard surface for the Compound Engineering plugin.
* Dashboard surface entry for the Compound Engineering plugin (U3).
*
* U1 ships only a loadable scaffold. The real artifact hub and interactive
* ce-* session surface arrive in later units (U3+).
* Thin re-export of the real hub component (mirrors how reports splits
* `src/dashboard-view.tsx` from `src/dashboard/ReportsView.tsx`). The export
* name `CompoundEngineeringDashboardView` is the one `registerBundledPluginViews`
* imports — `componentPath` in the manifest is cosmetic; this binding is real.
*/
export function CompoundEngineeringDashboardView(_props: { context?: PluginDashboardViewContext }) {
return <div data-testid="compound-engineering-view">Compound Engineering</div>;
}
export default CompoundEngineeringDashboardView;
export {
CompoundEngineeringView as CompoundEngineeringDashboardView,
default,
} from "./dashboard/CompoundEngineeringView.js";

View File

@@ -0,0 +1,146 @@
.ce-view {
display: flex;
flex-direction: column;
gap: 1rem;
padding: 1rem 1.25rem;
height: 100%;
overflow: auto;
}
.ce-view-header {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 1rem;
}
.ce-view-header h2 {
margin: 0;
}
.ce-view-summary {
font-size: 0.8rem;
opacity: 0.7;
}
.ce-loading,
.ce-view-error {
padding: 0.75rem 1rem;
font-size: 0.85rem;
}
.ce-view-error {
color: var(--color-danger, #d23);
}
.ce-empty {
max-width: 36rem;
padding: 1.5rem;
display: flex;
flex-direction: column;
gap: 0.75rem;
align-items: flex-start;
}
.ce-empty h3 {
margin: 0;
}
.ce-empty-hint {
opacity: 0.7;
font-size: 0.85rem;
}
.ce-groups {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr));
gap: 1rem;
}
.ce-group {
border: 1px solid var(--color-border, rgba(128, 128, 128, 0.25));
border-radius: 8px;
padding: 0.75rem 1rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.ce-group[data-empty="true"] {
opacity: 0.65;
}
.ce-group-header {
display: flex;
align-items: center;
justify-content: space-between;
}
.ce-group-header h3 {
margin: 0;
font-size: 0.95rem;
}
.ce-group-count {
font-size: 0.75rem;
opacity: 0.6;
}
.ce-group-empty {
margin: 0;
font-size: 0.8rem;
opacity: 0.6;
}
.ce-artifact-list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 0.35rem;
}
.ce-artifact {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
font-size: 0.85rem;
}
.ce-artifact.is-selected {
font-weight: 600;
}
.ce-artifact-btn {
background: none;
border: none;
text-align: left;
cursor: pointer;
display: flex;
flex-direction: column;
color: inherit;
padding: 0.25rem 0;
flex: 1;
}
.ce-artifact-path {
font-size: 0.7rem;
opacity: 0.55;
}
.ce-artifact-error .ce-artifact-error-msg {
color: var(--color-danger, #d23);
font-size: 0.78rem;
}
.ce-artifact-error {
flex-direction: column;
align-items: flex-start;
gap: 0.15rem;
}
.ce-view[data-mobile="true"] .ce-groups {
grid-template-columns: 1fr;
}

View File

@@ -0,0 +1,190 @@
import "./CompoundEngineeringView.css";
import { useState } from "react";
import type { PluginDashboardViewContext } from "@fusion/dashboard/app/plugins/types";
import { useArtifacts } from "./hooks/useArtifacts.js";
import { useViewportMode } from "./hooks/useViewportMode.js";
import { getArtifactPreviewUrl } from "./hooks/api.js";
import type { CeArtifactEntry, CeArtifactGroup } from "../artifacts/discovery.js";
interface CompoundEngineeringViewProps {
context?: PluginDashboardViewContext;
/** Test seam: override the active project id without a host context. */
projectId?: string;
/** Test seam: force the viewport-gated fetch on/off. */
enabledOverride?: boolean;
}
function readProjectId(props: CompoundEngineeringViewProps): string | undefined {
if (props.projectId) return props.projectId;
const ctx = props.context as { projectId?: string } | undefined;
return ctx?.projectId;
}
/** First-run / empty state: no artifacts AND no errors anywhere. */
function EmptyState({ onStart }: { onStart: () => void }) {
return (
<div className="ce-empty card" data-testid="ce-empty-state">
<h3>Start your compounding pipeline</h3>
<p>
No compound-engineering artifacts found yet. Compound Engineering tracks the documents your
pipeline produces — strategy, ideation, brainstorms, plans, solutions, and concepts — as you
move through each stage.
</p>
<p className="ce-empty-hint">
Begin with a stage and its artifact will appear here, grouped and traceable.
</p>
<button type="button" className="btn btn-primary" data-testid="ce-start-action" onClick={onStart}>
Start a stage
</button>
</div>
);
}
function ArtifactRow({
entry,
projectId,
onSelect,
selected,
}: {
entry: CeArtifactEntry;
projectId?: string;
onSelect: (id: string) => void;
selected: boolean;
}) {
if (entry.kind === "error") {
return (
<li className="ce-artifact ce-artifact-error" data-testid="ce-artifact-error">
<span className="ce-artifact-name">{entry.name}</span>
<span className="ce-artifact-error-msg" role="alert">
Could not read: {entry.error}
</span>
<span className="ce-artifact-path">{entry.path}</span>
</li>
);
}
return (
<li className={`ce-artifact${selected ? " is-selected" : ""}`} data-testid="ce-artifact">
<button type="button" className="ce-artifact-btn" onClick={() => onSelect(entry.id)}>
<span className="ce-artifact-name">{entry.name}</span>
<span className="ce-artifact-path">{entry.path}</span>
</button>
<a
className="ce-artifact-open"
href={getArtifactPreviewUrl(entry.id, projectId)}
target="_blank"
rel="noreferrer"
>
Open
</a>
</li>
);
}
function StageGroup({
group,
projectId,
onSelect,
selectedId,
}: {
group: CeArtifactGroup;
projectId?: string;
onSelect: (id: string) => void;
selectedId?: string;
}) {
const empty = group.entries.length === 0;
return (
<section className="ce-group" data-testid="ce-group" data-stage={group.stage} data-empty={empty ? "true" : "false"}>
<header className="ce-group-header">
<h3>{group.label}</h3>
<span className="ce-group-count">{group.entries.length}</span>
</header>
{empty ? (
<p className="ce-group-empty" data-testid="ce-group-empty">
No {group.label.toLowerCase()} artifacts yet.
</p>
) : (
<ul className="ce-artifact-list">
{group.entries.map((entry) => (
<ArtifactRow
key={entry.id}
entry={entry}
projectId={projectId}
onSelect={onSelect}
selected={selectedId === entry.id}
/>
))}
</ul>
)}
</section>
);
}
export function CompoundEngineeringView(props: CompoundEngineeringViewProps) {
const projectId = readProjectId(props);
const { mobile, active } = useViewportMode();
const enabled = props.enabledOverride ?? active;
const { result, loading, error } = useArtifacts({ projectId, enabled });
const [selectedId, setSelectedId] = useState<string | undefined>();
const totalArtifacts = result?.totalArtifacts ?? 0;
const totalErrors = result?.totalErrors ?? 0;
const hasAnything = totalArtifacts > 0 || totalErrors > 0;
// Partial discovery: at least one category populated AND at least one empty.
const populatedGroups = result?.groups.filter((g) => g.entries.length > 0).length ?? 0;
const emptyGroups = result?.groups.filter((g) => g.entries.length === 0).length ?? 0;
const isPartial = populatedGroups > 0 && emptyGroups > 0;
const onStart = () => {
// Wiring to launch a stage session is U6. A placeholder affordance is fine
// here; it makes the first-run orientation actionable without coupling U3 to
// the session launcher.
props.context?.addToast?.("Stage launcher arrives with the CE flow renderer (U6).", "info");
};
return (
<div className="ce-view" data-testid="compound-engineering-view" data-mobile={mobile ? "true" : "false"}>
<div className="ce-view-header">
<h2>Compound Engineering</h2>
{hasAnything ? (
<span className="ce-view-summary" data-testid="ce-summary">
{totalArtifacts} artifact{totalArtifacts === 1 ? "" : "s"}
{totalErrors > 0 ? ` · ${totalErrors} unreadable` : ""}
{isPartial ? " · partial" : ""}
</span>
) : null}
</div>
{error ? (
<div className="ce-view-error card" role="alert" data-testid="ce-fetch-error">
Failed to load artifacts: {error}
</div>
) : null}
{loading && !result ? (
<div className="ce-loading" data-testid="ce-loading">
Discovering artifacts…
</div>
) : null}
{result && !hasAnything ? (
<EmptyState onStart={onStart} />
) : null}
{result && hasAnything ? (
<div className="ce-groups" data-partial={isPartial ? "true" : "false"}>
{result.groups.map((group) => (
<StageGroup
key={group.stage}
group={group}
projectId={projectId}
onSelect={setSelectedId}
selectedId={selectedId}
/>
))}
</div>
) : null}
</div>
);
}
export default CompoundEngineeringView;

View File

@@ -0,0 +1,111 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import type { DiscoveryResult } from "../../artifacts/discovery.js";
// Mock the network layer so the view renders from seeded discovery results.
const listArtifacts = vi.fn(async (): Promise<DiscoveryResult> => {
throw new Error("listArtifacts mock not configured");
});
vi.mock("../hooks/api.js", () => ({
listArtifacts: () => listArtifacts(),
getArtifactPreviewUrl: (id: string) => `/preview/${id}`,
}));
import { CompoundEngineeringView } from "../CompoundEngineeringView.js";
import { __test_clearArtifactsCache } from "../hooks/useArtifacts.js";
const ALL_STAGES: Array<{ stage: DiscoveryResult["groups"][number]["stage"]; label: string }> = [
{ stage: "strategy", label: "Strategy" },
{ stage: "ideation", label: "Ideation" },
{ stage: "brainstorm", label: "Brainstorms" },
{ stage: "plan", label: "Plans" },
{ stage: "solution", label: "Solutions" },
{ stage: "concepts", label: "Concepts" },
];
function makeResult(overrides: Partial<Record<DiscoveryResult["groups"][number]["stage"], DiscoveryResult["groups"][number]["entries"]>>): DiscoveryResult {
const groups = ALL_STAGES.map(({ stage, label }) => ({
stage,
label,
present: Boolean(overrides[stage]?.length),
entries: overrides[stage] ?? [],
}));
let totalArtifacts = 0;
let totalErrors = 0;
for (const g of groups) {
for (const e of g.entries) {
if (e.kind === "artifact") totalArtifacts += 1;
else totalErrors += 1;
}
}
return { groups, totalArtifacts, totalErrors };
}
describe("CompoundEngineeringView", () => {
beforeEach(() => {
__test_clearArtifactsCache();
listArtifacts.mockReset();
});
afterEach(() => vi.clearAllMocks());
it("renders the empty / first-run state with an orientation + start action", async () => {
listArtifacts.mockResolvedValue(makeResult({}));
render(<CompoundEngineeringView projectId="p1" enabledOverride />);
await screen.findByTestId("ce-empty-state");
expect(screen.getByText(/Start your compounding pipeline/i)).toBeInTheDocument();
const start = screen.getByTestId("ce-start-action");
expect(start).toBeInTheDocument();
// Start affordance is wired to a placeholder (toast); clicking does not throw.
fireEvent.click(start);
});
it("renders the partial-discovery state (some categories present, others empty)", async () => {
listArtifacts.mockResolvedValue(
makeResult({
strategy: [
{ kind: "artifact", id: "strategy:STRATEGY.md", stage: "strategy", path: "STRATEGY.md", name: "STRATEGY.md", size: 10, updatedAt: 1 },
],
plan: [
{ kind: "artifact", id: "plan:docs/plans/p.md", stage: "plan", path: "docs/plans/p.md", name: "p.md", size: 5, updatedAt: 2 },
],
}),
);
render(<CompoundEngineeringView projectId="p1" enabledOverride />);
await screen.findByTestId("ce-summary");
// Partial flag surfaces in the summary and on the groups container.
expect(screen.getByTestId("ce-summary").textContent).toMatch(/partial/i);
const groups = screen.getByTestId("ce-summary").closest(".ce-view")!.querySelector(".ce-groups");
expect(groups?.getAttribute("data-partial")).toBe("true");
// Populated groups render artifacts; empty ones render an empty hint.
expect(screen.getAllByTestId("ce-artifact")).toHaveLength(2);
expect(screen.getAllByTestId("ce-group-empty").length).toBeGreaterThan(0);
});
it("renders an error entry for an unreadable artifact (not a crash or silent drop)", async () => {
listArtifacts.mockResolvedValue(
makeResult({
plan: [
{ kind: "error", id: "plan:docs/plans/bad.md", stage: "plan", path: "docs/plans/bad.md", name: "bad.md", error: "EIO: simulated read failure" },
],
}),
);
render(<CompoundEngineeringView projectId="p1" enabledOverride />);
const errorEntry = await screen.findByTestId("ce-artifact-error");
expect(errorEntry).toBeInTheDocument();
expect(errorEntry.textContent).toMatch(/simulated read failure/i);
// Surfaced as an unreadable count in the summary.
expect(screen.getByTestId("ce-summary").textContent).toMatch(/unreadable/i);
});
it("does not fetch when the viewport-gated flag is disabled", async () => {
listArtifacts.mockResolvedValue(makeResult({}));
render(<CompoundEngineeringView projectId="p1" enabledOverride={false} />);
// Give effects a tick.
await waitFor(() => expect(screen.getByTestId("compound-engineering-view")).toBeInTheDocument());
expect(listArtifacts).not.toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,45 @@
import type { DiscoveryResult } from "../../artifacts/discovery.js";
const BASE = "/api/plugins/fusion-plugin-compound-engineering";
function qp(params: Record<string, string | undefined>): string {
const entries = Object.entries(params).filter(
([, v]) => typeof v === "string" && v.length > 0,
) as Array<[string, string]>;
if (entries.length === 0) return "";
return `?${entries.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&")}`;
}
async function request<T>(path: string, init?: RequestInit, responseType: "json" | "text" = "json"): Promise<T> {
const response = await fetch(`${BASE}${path}`, init);
if (!response.ok) {
let message = `${response.status} ${response.statusText}`;
try {
const data = (await response.json()) as { error?: string };
if (data.error) message = data.error;
} catch {
// ignore
}
throw new Error(message);
}
if (responseType === "text") return (await response.text()) as T;
return (await response.json()) as T;
}
export async function listArtifacts(projectId?: string): Promise<DiscoveryResult> {
return request<DiscoveryResult>(`/artifacts${qp({ projectId })}`);
}
export async function getArtifact(
id: string,
projectId?: string,
): Promise<{ content: string; name: string }> {
const data = await request<{ artifact: { name: string }; content: string }>(
`/artifacts/${encodeURIComponent(id)}${qp({ projectId })}`,
);
return { content: data.content, name: data.artifact.name };
}
export function getArtifactPreviewUrl(id: string, projectId?: string): string {
return `${BASE}/artifacts/${encodeURIComponent(id)}/preview.html${qp({ projectId })}`;
}

View File

@@ -0,0 +1,80 @@
import { useEffect, useMemo, useState } from "react";
import { listArtifacts } from "./api.js";
import type { DiscoveryResult } from "../../artifacts/discovery.js";
/**
* Short-TTL discovery cache keyed by `projectId` (the discovery scan has no
* per-row id, so the project is the cache unit). Mirrors the dashboard
* performance kit (docs/performance/dashboard-load.md): a 30s TTL balances
* freshness against repeated viewport-driven refetches.
*/
const CACHE_TTL_MS = 30_000;
const discoveryCache = new Map<string, { value: DiscoveryResult; expiresAt: number }>();
function cacheKey(projectId?: string): string {
return `discovery:${projectId ?? "__default__"}`;
}
/** Exposed for tests. */
export function __test_clearArtifactsCache(): void {
discoveryCache.clear();
}
export interface UseArtifactsResult {
result?: DiscoveryResult;
loading: boolean;
error?: string;
}
/**
* Discover CE artifacts for the active project. The fetch is viewport-gated via
* the `enabled` flag — when the CE view is offscreen/disabled it returns stable
* empty state and triggers no network request (performance kit). Results are
* served from a short-TTL cache to collapse repeated mounts.
*/
export function useArtifacts({
projectId,
enabled = true,
}: {
projectId?: string;
enabled?: boolean;
}): UseArtifactsResult {
const [result, setResult] = useState<DiscoveryResult | undefined>(() => {
const cached = discoveryCache.get(cacheKey(projectId));
return cached && cached.expiresAt > Date.now() ? cached.value : undefined;
});
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | undefined>();
useEffect(() => {
if (!enabled) return;
const key = cacheKey(projectId);
const cached = discoveryCache.get(key);
if (cached && cached.expiresAt > Date.now()) {
setResult(cached.value);
setError(undefined);
return;
}
const controller = new AbortController();
setLoading(true);
listArtifacts(projectId)
.then((value) => {
if (controller.signal.aborted) return;
discoveryCache.set(key, { value, expiresAt: Date.now() + CACHE_TTL_MS });
setResult(value);
setError(undefined);
})
.catch((err: unknown) => {
if (controller.signal.aborted) return;
setError(err instanceof Error ? err.message : "Failed to load artifacts");
})
.finally(() => {
if (!controller.signal.aborted) setLoading(false);
});
return () => controller.abort();
}, [projectId, enabled]);
return useMemo(() => ({ result, loading, error }), [result, loading, error]);
}

View File

@@ -0,0 +1,29 @@
import { useEffect, useState } from "react";
/**
* Tracks viewport state for the CE hub. `mobile` mirrors the dashboard's mobile
* breakpoint (includes landscape phones). `active` reports whether the document
* is currently visible — used to viewport-gate the discovery fetch so an
* offscreen/backgrounded hub triggers no network work (performance kit).
*/
export function useViewportMode() {
const [mobile, setMobile] = useState(false);
const [active, setActive] = useState(true);
useEffect(() => {
const mq = window.matchMedia("(max-width: 768px), (max-height: 480px)");
const onChange = () => setMobile(mq.matches);
onChange();
mq.addEventListener("change", onChange);
return () => mq.removeEventListener("change", onChange);
}, []);
useEffect(() => {
const onVisibility = () => setActive(document.visibilityState !== "hidden");
onVisibility();
document.addEventListener("visibilitychange", onVisibility);
return () => document.removeEventListener("visibilitychange", onVisibility);
}, []);
return { mobile, active };
}

View File

@@ -0,0 +1,26 @@
import "@testing-library/jest-dom/vitest";
import { cleanup } from "@testing-library/react";
import { afterEach, vi } from "vitest";
// @testing-library/react only auto-registers cleanup when vitest globals are
// enabled. We don't enable globals here, so we wire it manually — otherwise
// React leaves the test tree mounted, its scheduler fires a deferred update
// via setImmediate after the jsdom environment is torn down, and the suite
// fails with "ReferenceError: window is not defined".
afterEach(() => cleanup());
if (typeof window !== "undefined") {
Object.defineProperty(window, "matchMedia", {
writable: true,
value: vi.fn().mockImplementation((query: string) => ({
matches: false,
media: query,
onchange: null,
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
addListener: vi.fn(),
removeListener: vi.fn(),
dispatchEvent: vi.fn(),
})),
});
}

View File

@@ -3,6 +3,7 @@ import { COMPOUND_ENGINEERING_SKILLS } from "./skills.js";
import { installBundledCeSkills } from "./skill-installation.js";
import { ensureCeSchema } from "./schema.js";
import { createSessionRoutes } from "./routes/session-routes.js";
import { createArtifactRoutes } from "./routes/artifact-routes.js";
export { CompoundEngineeringDashboardView } from "./dashboard-view.js";
export { COMPOUND_ENGINEERING_SKILLS } from "./skills.js";
@@ -60,7 +61,7 @@ const plugin = definePlugin({
}
},
},
routes: [...createSessionRoutes()],
routes: [...createSessionRoutes(), ...createArtifactRoutes()],
dashboardViews: [
{
viewId: "compound-engineering",

View File

@@ -0,0 +1,77 @@
import { afterEach, describe, expect, it } from "vitest";
import { mkdirSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import type { PluginRouteResponse } from "@fusion/core";
import { createArtifactRoutes } from "../artifact-routes.js";
import { makeHarness, type TestHarness } from "../../__tests__/_harness.js";
function route(path: string, method = "GET") {
const def = createArtifactRoutes().find((r) => r.path === path && r.method === method);
if (!def) throw new Error(`route not found: ${method} ${path}`);
return def;
}
describe("artifact routes", () => {
let h: TestHarness;
afterEach(() => h?.close());
it("GET /artifacts lists discovered artifacts grouped by stage", async () => {
h = makeHarness();
writeFileSync(join(h.projectRoot, "STRATEGY.md"), "# Strategy");
mkdirSync(join(h.projectRoot, "docs/plans"), { recursive: true });
writeFileSync(join(h.projectRoot, "docs/plans/p.md"), "plan body");
const res = (await route("/artifacts").handler({ query: {} }, h.ctx)) as PluginRouteResponse;
expect(res.status).toBe(200);
const body = res.body as { totalArtifacts: number; groups: Array<{ stage: string; entries: unknown[] }> };
expect(body.totalArtifacts).toBe(2);
const plan = body.groups.find((g) => g.stage === "plan")!;
expect(plan.entries).toHaveLength(1);
});
it("GET /artifacts/:id returns raw content", async () => {
h = makeHarness();
writeFileSync(join(h.projectRoot, "STRATEGY.md"), "# Strategy body");
const res = (await route("/artifacts/:id").handler(
{ params: { id: encodeURIComponent("strategy:STRATEGY.md") }, query: {} },
h.ctx,
)) as PluginRouteResponse;
expect(res.status).toBe(200);
expect((res.body as { content: string }).content).toContain("Strategy body");
});
it("GET /artifacts/:id 404s an unknown id", async () => {
h = makeHarness();
const res = (await route("/artifacts/:id").handler(
{ params: { id: encodeURIComponent("strategy:STRATEGY.md") }, query: {} },
h.ctx,
)) as PluginRouteResponse;
expect(res.status).toBe(404);
});
it("GET /artifacts/:id/preview.html returns a self-contained, escaped HTML document", async () => {
h = makeHarness();
// Content with an injection attempt — must be escaped, not executed.
writeFileSync(join(h.projectRoot, "STRATEGY.md"), "<script>alert('x')</script>\n# Plan");
const res = (await route("/artifacts/:id/preview.html").handler(
{ params: { id: encodeURIComponent("strategy:STRATEGY.md") }, query: {} },
h.ctx,
)) as PluginRouteResponse;
expect(res.status).toBe(200);
expect(res.contentType).toContain("text/html");
const html = res.body as string;
expect(html.startsWith("<!DOCTYPE html>")).toBe(true);
// Self-contained: inlined <style>, no remote asset URLs.
expect(html).toContain("<style>");
expect(html).not.toMatch(/https?:\/\//);
// data-section markers present (reports rendering contract).
expect(html).toContain('data-section="content"');
// The raw script tag is escaped, not embedded as live markup.
expect(html).toContain("&lt;script&gt;");
expect(html).not.toContain("<script>alert");
});
});

View File

@@ -0,0 +1,130 @@
import type { PluginContext, PluginRouteDefinition, PluginRouteResponse } from "@fusion/core";
import { discoverArtifacts, readArtifactById } from "../artifacts/discovery.js";
/**
* Artifact routes (U3): list discovered CE artifacts grouped by stage, and read
* a single artifact's content. The render endpoint returns a SELF-CONTAINED HTML
* document (sandboxed `srcDoc`, inlined styles, no remote assets) mirroring the
* reports preview/export pattern (docs/plugins/reports.md) so the dashboard can
* embed it in a sandboxed iframe without leaking host styles or scripts.
*
* Project root: artifacts live on disk relative to the project root, which the
* route reaches via `ctx.taskStore.getRootDir()` (the same root the U5
* orchestrator writes artifacts to). When a `projectId` is supplied and the host
* exposes `resolveProjectTaskStore`, the per-project root is used.
*/
interface RouteRequest {
params: Record<string, string>;
query?: Record<string, string | string[] | undefined>;
}
function asString(v: unknown): string | undefined {
return typeof v === "string" && v.length > 0 ? v : undefined;
}
async function resolveProjectRoot(ctx: PluginContext, projectId?: string): Promise<string> {
if (projectId && ctx.resolveProjectTaskStore) {
try {
const store = await ctx.resolveProjectTaskStore(projectId);
return store.getRootDir();
} catch {
// Fall through to the default task store root.
}
}
return ctx.taskStore.getRootDir();
}
function escapeHtml(input: string): string {
return input
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#39;");
}
/**
* Wrap raw artifact markdown in a fully self-contained HTML document. Content is
* HTML-escaped and rendered inside `<pre>` so nothing in the artifact can inject
* markup or script; styles are inlined; there are no remote `href`/`src` URLs.
* `data-section` markers mirror the reports rendering contract so an embedding
* viewer can offer section quick-jumps without re-parsing.
*/
export function renderArtifactDocument(name: string, content: string): string {
const escaped = escapeHtml(content);
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>${escapeHtml(name)}</title>
<style>
:root { color-scheme: light dark; }
* { box-sizing: border-box; }
body { margin: 0; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; line-height: 1.5; background: #ffffff; color: #1a1a1a; }
@media (prefers-color-scheme: dark) { body { background: #16181d; color: #e6e6e6; } }
.artifact-doc { padding: 1.25rem 1.5rem; }
.artifact-doc h1 { font-size: 1.1rem; margin: 0 0 1rem; font-family: ui-sans-serif, system-ui, sans-serif; }
.artifact-body { white-space: pre-wrap; word-break: break-word; margin: 0; font-size: 0.85rem; }
</style>
</head>
<body>
<main class="artifact-doc" data-section="artifact">
<h1 data-section="title">${escapeHtml(name)}</h1>
<pre class="artifact-body" data-section="content">${escaped}</pre>
</main>
</body>
</html>`;
}
export function createArtifactRoutes(): PluginRouteDefinition[] {
return [
{
method: "GET",
path: "/artifacts",
description: "List discovered CE artifacts grouped by stage.",
handler: async (req: unknown, ctx: PluginContext): Promise<PluginRouteResponse> => {
const query = (req as RouteRequest).query ?? {};
const projectId = asString(query.projectId);
const root = await resolveProjectRoot(ctx, projectId);
const result = discoverArtifacts(root);
return { status: 200, body: result };
},
},
{
method: "GET",
path: "/artifacts/:id",
description: "Read a single CE artifact's raw content (JSON).",
handler: async (req: unknown, ctx: PluginContext): Promise<PluginRouteResponse> => {
const request = req as RouteRequest;
const id = decodeURIComponent(request.params.id);
const projectId = asString(request.query?.projectId);
const root = await resolveProjectRoot(ctx, projectId);
const result = readArtifactById(root, id);
if (!result) return { status: 404, body: { error: `Artifact ${id} not found` } };
if ("error" in result) return { status: 422, body: { error: result.error } };
return { status: 200, body: { artifact: result.artifact, content: result.content } };
},
},
{
method: "GET",
path: "/artifacts/:id/preview.html",
description: "Read a single CE artifact rendered as a self-contained HTML document.",
handler: async (req: unknown, ctx: PluginContext): Promise<PluginRouteResponse> => {
const request = req as RouteRequest;
const id = decodeURIComponent(request.params.id);
const projectId = asString(request.query?.projectId);
const root = await resolveProjectRoot(ctx, projectId);
const result = readArtifactById(root, id);
if (!result) return { status: 404, body: { error: `Artifact ${id} not found` } };
if ("error" in result) return { status: 422, body: { error: result.error } };
return {
status: 200,
contentType: "text/html; charset=utf-8",
body: renderArtifactDocument(result.artifact.name, result.content),
};
},
},
];
}

View File

@@ -4,6 +4,11 @@ import { computeMaxWorkers } from "../../packages/core/src/__test-utils__/vitest
const maxWorkers = computeMaxWorkers();
const coreSetup = fileURLToPath(
new URL("../../packages/core/src/__test-utils__/vitest-setup.ts", import.meta.url),
);
const dashboardSetup = fileURLToPath(new URL("./src/dashboard/test-setup.ts", import.meta.url));
export default defineConfig({
resolve: {
alias: [
@@ -28,12 +33,32 @@ export default defineConfig({
],
},
test: {
include: ["src/**/*.test.{ts,tsx}"],
environment: "jsdom",
setupFiles: [fileURLToPath(new URL("../../packages/core/src/__test-utils__/vitest-setup.ts", import.meta.url))],
// coreSetup runs for all projects via extends: true inheritance.
setupFiles: [coreSetup],
globalSetup: [fileURLToPath(new URL("../../packages/core/src/__test-utils__/vitest-teardown.ts", import.meta.url))],
pool: "threads",
maxWorkers,
poolOptions: { threads: { minThreads: 1, maxThreads: maxWorkers }, forks: { minForks: 1, maxForks: maxWorkers } },
projects: [
{
extends: true,
test: {
name: "compound-engineering-dashboard",
environment: "jsdom",
include: ["src/dashboard/**/__tests__/**/*.test.{ts,tsx}", "src/dashboard/**/*.test.{ts,tsx}"],
// jsdom-specific setup; coreSetup is inherited via extends: true.
setupFiles: [dashboardSetup],
},
},
{
extends: true,
test: {
name: "compound-engineering-node",
environment: "node",
include: ["src/**/__tests__/**/*.test.{ts,tsx}", "src/**/*.test.{ts,tsx}"],
exclude: ["src/dashboard/**/__tests__/**/*.test.{ts,tsx}", "src/dashboard/**/*.test.{ts,tsx}"],
},
},
],
},
});

3
pnpm-lock.yaml generated
View File

@@ -209,6 +209,9 @@ importers:
'@fusion-plugin-examples/cli-printing-press':
specifier: workspace:*
version: link:../../plugins/fusion-plugin-cli-printing-press
'@fusion-plugin-examples/compound-engineering':
specifier: workspace:*
version: link:../../plugins/fusion-plugin-compound-engineering
'@fusion-plugin-examples/cursor-runtime':
specifier: workspace:*
version: link:../../plugins/fusion-plugin-cursor-runtime