Address PR review feedback (#2001)

- scope session collections to the active project\n- clean up runtime stage registration in discovery tests\n- model brainstorms and plans as repeatable artifact collections\n- compact the singleton Strategy presentation
This commit is contained in:
gsxdsm
2026-07-10 23:47:14 -07:00
parent 668b8e0fec
commit 3a4fd67718
11 changed files with 138 additions and 30 deletions

View File

@@ -2,6 +2,6 @@
"@runfusion/fusion": minor
---
summary: Clarify Compound Engineering progress and preserve one plan from brainstorm through delivery.
summary: Support repeatable Compound Engineering cycles with project-scoped progress and artifact collections.
category: feature
dev: Adds a stage rail, safer session controls, explicit choice confirmation, and terminal Work progression.
dev: Adds collection-aware stages, safer session controls, explicit choice confirmation, and terminal Work progression.

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5e4dc3baed78f895c3a954ade947e3ee211da7612b6f1693127f8f67f74689a3
size 234370
oid sha256:a6c218824cbd21ec36305437e2adc5c810163ab25cd28fdee705bb12ddf48b93
size 270173

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7e1320ecaac17cc84399f7cf58de13df5100175dd57e34fd1d6c0039e630f487
size 90474
oid sha256:9bbcbf6767d6982ebb4340498396dc1453eb2e7458c3f0877c20d7a4f5ece9d3
size 95990

View File

@@ -133,6 +133,20 @@ describe("session routes (polling transport)", () => {
expect(sessions.map((s) => s.stage).sort()).toEqual(["brainstorm", "plan"]);
});
it("GET /sessions scopes every session consumer to the requested project", async () => {
const { getCeSessionStore } = await import("../session/session-store.js");
const store = getCeSessionStore(h.ctx);
const projectA = store.create({ stage: "brainstorm", projectId: "project-a" });
store.create({ stage: "plan", projectId: "project-b" });
store.create({ stage: "debug" });
const res = await call("GET", "/sessions", { params: {}, query: { projectId: "project-a" } }, h.ctx);
expect(res.status).toBe(200);
const sessions = (res.body as { sessions: Array<{ id: string; projectId: string | null }> }).sessions;
expect(sessions).toEqual([expect.objectContaining({ id: projectA.id, projectId: "project-a" })]);
});
it("GET /sessions keeps error, interrupted, awaiting_input, active, and completed rows independently manageable", async () => {
const { getCeSessionStore } = await import("../session/session-store.js");
const store = getCeSessionStore(h.ctx);

View File

@@ -2,7 +2,7 @@ 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";
import { registerStage } from "../../session/stage-registry.js";
import { registerStage, unregisterStage } from "../../session/stage-registry.js";
// Mock node:fs so we can observe/inject behaviour around readFileSync and
// accessSync without relying on vi.spyOn (ESM namespace exports are not
@@ -40,6 +40,7 @@ describe("discoverArtifacts", () => {
afterEach(() => {
if (root) rmSync(root, { recursive: true, force: true });
unregisterStage("publish-check");
readFileHook = undefined;
accessHook = undefined;
vi.restoreAllMocks();
@@ -52,6 +53,8 @@ describe("discoverArtifacts", () => {
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/requirements.md"), "requirements");
mkdirSync(join(root, "docs/plans"), { recursive: true });
writeFileSync(join(root, "docs/plans/plan1.md"), "plan 1");
mkdirSync(join(root, "docs/work"), { recursive: true });
@@ -64,20 +67,21 @@ describe("discoverArtifacts", () => {
const result = discoverArtifacts(root);
const byStage = Object.fromEntries(result.groups.map((g) => [g.stage, g]));
expect(result.totalArtifacts).toBe(8);
expect(result.totalArtifacts).toBe(9);
expect(result.totalErrors).toBe(0);
expect(byStage.strategy.entries).toHaveLength(1);
expect(byStage.concepts.entries).toHaveLength(1);
expect(byStage.ideate.entries).toHaveLength(2);
expect(byStage.plan.entries).toHaveLength(1);
expect(byStage.plan.entries[0]).toMatchObject({ path: "docs/plans/plan1.md" });
expect(byStage.plan.label).toBe("Brainstorm / Plan");
expect(byStage.brainstorm.entries).toHaveLength(1);
expect(byStage.plan.label).toBe("Plan");
expect(byStage.work.entries[0]).toMatchObject({ path: "docs/work/work.md" });
expect(byStage.debug.entries[0]).toMatchObject({ path: "docs/debug/debug.md" });
expect(byStage.solution.entries).toHaveLength(1);
// Every group present is flagged present.
expect(byStage.ideate.present).toBe(true);
expect(byStage.brainstorm).toBeUndefined();
expect(byStage.brainstorm.entries[0]).toMatchObject({ path: "docs/brainstorms/requirements.md" });
// All entries are artifacts in the happy path.
expect(result.groups.flatMap((g) => g.entries).every((e) => e.kind === "artifact")).toBe(true);
});
@@ -112,7 +116,7 @@ describe("discoverArtifacts", () => {
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(7);
expect(result.groups).toHaveLength(8);
});
it("returns an all-empty result when nothing is present (first-run)", () => {

View File

@@ -55,14 +55,17 @@ interface ConventionalLocation {
*/
/*
FNXC:CompoundEngineeringArtifacts 2026-07-10-12:00:
The artifact hub must follow registered stage output locations so Work and Debug remain discoverable as the pipeline evolves. Brainstorm and Plan share one durable docs/plans collection; exposing the removed docs/brainstorms location or two groups for the same path misrepresents the unified-plan contract.
The artifact hub must follow registered stage output locations so Work and Debug remain discoverable as the pipeline evolves.
FNXC:CompoundEngineeringArtifacts 2026-07-10-23:40:
The upstream Compound Engineering workflow writes repeatable requirements documents to docs/brainstorms and implementation plans to docs/plans. Discovery must retain the dedicated requirements history while also supporting requirements-only unified plans produced by newer in-place handoffs.
*/
function stageLocations(): ConventionalLocation[] {
return listStages().filter((definition) => definition.stageId !== "brainstorm").map((definition) => {
const path = definition.artifactLocation.replace(/\/$/, "");
return {
stage: definition.stageId,
label: definition.stageId === "plan" ? "Brainstorm / Plan" : definition.label,
label: definition.label,
path,
kind: definition.artifactLocation.endsWith("/") ? "directory" : "file",
};
@@ -72,6 +75,7 @@ function stageLocations(): ConventionalLocation[] {
function conventionalLocations(): ConventionalLocation[] {
return [
...stageLocations(),
{ stage: "brainstorm", label: "Brainstorms", path: "docs/brainstorms", kind: "directory" },
{ stage: "solution", label: "Solutions", path: "docs/solutions", kind: "directory" },
{ stage: "concepts", label: "Concepts", path: "CONCEPTS.md", kind: "file" },
];

View File

@@ -83,6 +83,20 @@ The shared ViewHeader sits directly above plugin content, so the body needs top
gap: var(--space-sm);
}
/* Strategy is one durable upstream anchor; keep it as a compact row while repeatable workflow outputs retain collection cards. */
.ce-group[data-layout="singleton"] {
grid-column: 1 / -1;
display: grid;
grid-template-columns: minmax(7rem, 0.3fr) minmax(0, 1fr);
align-items: center;
column-gap: var(--space-lg);
padding-block: var(--space-sm);
}
.ce-group[data-layout="singleton"] .ce-artifact-list {
min-width: 0;
}
.ce-group[data-empty="true"] {
color: var(--text-muted);
}
@@ -179,6 +193,11 @@ The shared ViewHeader sits directly above plugin content, so the body needs top
padding: var(--space-sm);
}
.ce-view[data-mobile="true"] .ce-group[data-layout="singleton"] {
grid-template-columns: 1fr;
row-gap: var(--space-xs);
}
.ce-view[data-mobile="true"] .ce-groups {
grid-template-columns: 1fr;
}

View File

@@ -177,23 +177,23 @@ function nextPipelineStageId(stageId: string): string | undefined {
type RailStatus = "Not started" | "Active" | "Needs input" | "Complete";
function artifactForStage(stageId: string, groups: CeArtifactGroup[]): CeArtifactEntry | undefined {
const groupId = stageId === "brainstorm" ? "plan" : stageId;
const entries = groups.find((group) => group.stage === groupId)?.entries ?? [];
function artifactsForStage(stageId: string, groups: CeArtifactGroup[]): CeArtifactEntry[] {
const entries = groups.find((group) => group.stage === stageId)?.entries ?? [];
if (stageId === "brainstorm") {
return entries.find((entry) => (
const unifiedEntries = groups.find((group) => group.stage === "plan")?.entries ?? [];
return [...entries, ...unifiedEntries.filter((entry) => (
entry.kind === "artifact"
&& entry.artifactContract === "ce-unified-plan/v1"
&& (entry.artifactReadiness === "requirements-only" || entry.artifactReadiness === "implementation-ready")
));
&& (entry.artifactReadiness === "requirements-only" || !entry.productContractSource || entry.productContractSource === "ce-brainstorm")
))];
}
if (stageId === "plan") {
return entries.find((entry) => (
return entries.filter((entry) => (
entry.kind === "artifact"
&& (entry.artifactReadiness === "implementation-ready" || !entry.artifactReadiness)
));
}
return entries.find((entry) => entry.kind === "artifact");
return entries.filter((entry) => entry.kind === "artifact");
}
function railStatus(stageId: string, sessions: CeSession[], artifact?: CeArtifactEntry): RailStatus {
@@ -206,7 +206,10 @@ function railStatus(stageId: string, sessions: CeSession[], artifact?: CeArtifac
/**
* FNXC:CompoundEngineeringUI 2026-07-10-12:00:
* Operators need the five-stage compounding pipeline as the overview's primary navigation. Each stage shows actionable state and its latest durable artifact, while Debug remains a separate investigation action because it is not pipeline progression.
* Operators need the five-stage compounding loop as the overview's primary navigation. Stages report repeatable run and artifact collections instead of implying one permanent completion; Strategy remains the upstream singleton anchor, while Debug remains a separate investigation action because it is not pipeline progression.
*
* FNXC:CompoundEngineeringUI 2026-07-10-23:40:
* Compound Engineering follows the upstream repeatable loop: brainstorm requirements, plan implementation, work, review, compound, then repeat with better context. Brainstorm and Plan therefore expose collection counts and latest activity over multiple unified-plan files rather than a single artifact slot.
*/
function PipelineOverview({
groups,
@@ -240,8 +243,10 @@ function PipelineOverview({
</header>
<ol className="ce-stage-rail">
{stages.map((stage) => {
const artifact = artifactForStage(stage.stageId, groups);
const latestSession = sessions.find((session) => session.stage === stage.stageId);
const artifacts = artifactsForStage(stage.stageId, groups);
const artifact = artifacts[0];
const stageSessions = sessions.filter((session) => session.stage === stage.stageId);
const latestSession = stageSessions[0];
const status = railStatus(stage.stageId, sessions, artifact);
const Icon = resolveIcon(stage.icon);
return (
@@ -257,7 +262,9 @@ function PipelineOverview({
<span className="ce-stage-icon"><Icon size={17} aria-hidden="true" /></span>
<span className="ce-stage-copy">
<strong>{stage.label}</strong>
<span className="ce-stage-status">{status}</span>
<span className="ce-stage-status">
{status} · {stageSessions.length} {stageSessions.length === 1 ? "run" : "runs"} · {artifacts.length} {artifacts.length === 1 ? "artifact" : "artifacts"}
</span>
</span>
</button>
{artifact?.kind === "artifact" ? (
@@ -370,8 +377,9 @@ function StageGroup({
openFile?: PluginDashboardViewContext["openFile"];
}) {
const empty = group.entries.length === 0;
const singleton = group.stage === "strategy";
return (
<section className="ce-group card" data-testid="ce-group" data-stage={group.stage} data-empty={empty ? "true" : "false"}>
<section className="ce-group card" data-testid="ce-group" data-stage={group.stage} data-layout={singleton ? "singleton" : "collection"} data-empty={empty ? "true" : "false"}>
<header className="ce-group-header">
<h3>{group.label}</h3>
<span className="ce-group-count">{group.entries.length}</span>
@@ -397,6 +405,33 @@ function StageGroup({
);
}
function artifactDisplayGroups(groups: CeArtifactGroup[]): CeArtifactGroup[] {
const conventionalBrainstorms = groups.find((group) => group.stage === "brainstorm");
return groups.flatMap((group) => {
if (group.stage === "brainstorm") return [];
if (group.stage !== "plan") return [group];
const brainstorms = group.entries.filter((entry) => (
entry.kind === "artifact"
&& entry.artifactContract === "ce-unified-plan/v1"
&& entry.artifactReadiness === "requirements-only"
));
const plans = group.entries.filter((entry) => (
entry.kind === "error"
|| (entry.artifactReadiness !== "requirements-only")
));
return [
{
...(conventionalBrainstorms ?? group),
stage: "brainstorm",
label: "Brainstorms",
present: Boolean(conventionalBrainstorms?.present || brainstorms.length > 0),
entries: [...(conventionalBrainstorms?.entries ?? []), ...brainstorms],
},
{ ...group, label: "Plans", entries: plans },
];
});
}
export function CompoundEngineeringView(props: CompoundEngineeringViewProps) {
const projectId = readProjectId(props);
const { mobile, active } = useViewportMode();
@@ -647,7 +682,7 @@ export function CompoundEngineeringView(props: CompoundEngineeringViewProps) {
{result && hasAnything ? (
<div className="ce-groups" data-partial={isPartial ? "true" : "false"}>
{result.groups.map((group) => (
{artifactDisplayGroups(result.groups).map((group) => (
<StageGroup
key={group.stage}
group={group}

View File

@@ -130,6 +130,29 @@ describe("CompoundEngineeringView", () => {
// 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);
expect(document.querySelector('.ce-groups [data-stage="strategy"]')).toHaveAttribute("data-layout", "singleton");
});
it("presents brainstorm and plan artifacts as repeatable collections", async () => {
listArtifacts.mockResolvedValue(
makeResult({
plan: [
{ kind: "artifact", id: "plan:docs/plans/requirements.md", stage: "plan", path: "docs/plans/requirements.md", name: "requirements.md", size: 5, updatedAt: 3, artifactContract: "ce-unified-plan/v1", artifactReadiness: "requirements-only", productContractSource: "ce-brainstorm" },
{ kind: "artifact", id: "plan:docs/plans/ready.md", stage: "plan", path: "docs/plans/ready.md", name: "ready.md", size: 5, updatedAt: 2, artifactContract: "ce-unified-plan/v1", artifactReadiness: "implementation-ready", productContractSource: "ce-brainstorm" },
{ kind: "artifact", id: "plan:docs/plans/legacy.md", stage: "plan", path: "docs/plans/legacy.md", name: "legacy.md", size: 5, updatedAt: 1 },
],
}),
);
render(<CompoundEngineeringView projectId="p1" enabledOverride />);
await screen.findByTestId("ce-summary");
const brainstorms = document.querySelector('.ce-groups [data-stage="brainstorm"]')!;
const plans = document.querySelector('.ce-groups [data-stage="plan"]')!;
expect(brainstorms).toHaveTextContent("Brainstorms");
expect(brainstorms.querySelectorAll('[data-testid="ce-artifact"]')).toHaveLength(1);
expect(plans).toHaveTextContent("Plans");
expect(plans.querySelectorAll('[data-testid="ce-artifact"]')).toHaveLength(2);
});
it("opens an artifact in the built-in file viewer via context.openFile", async () => {

View File

@@ -138,13 +138,18 @@ export function createSessionRoutes(): PluginRouteDefinition[] {
{
method: "GET",
path: "/sessions",
description: "List CE sessions (optionally filtered by status/stage).",
description: "List CE sessions (optionally filtered by project/status/stage).",
handler: async (req: unknown, ctx: PluginContext): Promise<PluginRouteResponse> => {
recoverStaleSessionsForContext(ctx, { reason: "route" });
const query = (req as RouteRequest).query ?? {};
const status = asCeSessionStatus(typeof query.status === "string" ? query.status : undefined);
const stage = typeof query.stage === "string" ? query.stage : undefined;
const sessions = getCeSessionStore(ctx).list({ status, stage });
const projectId = typeof query.projectId === "string" ? query.projectId : undefined;
/*
FNXC:CompoundEngineering 2026-07-10-23:40:
Dashboard session collections must be scoped at the route/store boundary so resume, URL restoration, stage state, and history cannot expose another project's Compound Engineering runs.
*/
const sessions = getCeSessionStore(ctx).list({ status, stage, projectId });
return { status: 200, body: { sessions } };
},
},

View File

@@ -215,7 +215,7 @@ export class CeSessionStore {
return row ? rowToSession(row) : undefined;
}
list(filter: { status?: CeSessionStatus; stage?: string } = {}): CeSession[] {
list(filter: { status?: CeSessionStatus; stage?: string; projectId?: string } = {}): CeSession[] {
const clauses: string[] = [];
const params: unknown[] = [];
if (filter.status) {
@@ -226,6 +226,10 @@ export class CeSessionStore {
clauses.push("stage = ?");
params.push(filter.stage);
}
if (filter.projectId) {
clauses.push("projectId = ?");
params.push(filter.projectId);
}
const where = clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "";
const rows = this.db
.prepare(`SELECT * FROM ce_sessions ${where} ORDER BY updatedAt DESC, id`)