feat: visualize prepared authoring phases

This commit is contained in:
lda
2026-07-11 06:47:57 +07:00 Verified
parent 2353ab114e
commit b5f8ae0c5e
5 changed files with 450 additions and 4 deletions
@@ -0,0 +1,46 @@
import { cleanup, render, screen } from "@testing-library/react";
import { afterEach, describe, expect, it } from "vitest";
import { projectPreparedAuthoringPhase } from "./authoring-projection.js";
import { AuthoringPhaseVisual } from "./AuthoringPhaseVisual.js";
afterEach(cleanup);
describe("AuthoringPhaseVisual", () => {
it("shows source inventory and the inspected contract", () => {
render(<AuthoringPhaseVisual projection={projectPreparedAuthoringPhase("discover")} />);
expect(screen.getByRole("region", { name: /discovery evidence/i })).toBeInTheDocument();
expect(screen.getByText("local.lda_docs")).toBeInTheDocument();
expect(screen.getByText("local.lda_report")).toBeInTheDocument();
expect(screen.getByText(/documents.*analysis/i)).toBeInTheDocument();
});
it("shows the declared draft graph and outcome route", () => {
render(<AuthoringPhaseVisual projection={projectPreparedAuthoringPhase("draft")} />);
expect(screen.getByRole("region", { name: /draft graph evidence/i })).toBeInTheDocument();
expect(screen.getByText("read_documents")).toBeInTheDocument();
expect(screen.getByText("analyze")).toBeInTheDocument();
expect(screen.getByText("ok → end")).toBeInTheDocument();
});
it("shows the validation diagnostic and repaired projection", () => {
render(<AuthoringPhaseVisual projection={projectPreparedAuthoringPhase("validate")} />);
expect(screen.getByText(/no state projection/i)).toBeInTheDocument();
expect(screen.getByText("analysis → state.analysis")).toBeInTheDocument();
expect(screen.getByText(/valid draft/i)).toBeInTheDocument();
});
it("shows immutable artifact identity and version", () => {
render(<AuthoringPhaseVisual projection={projectPreparedAuthoringPhase("artifact")} />);
expect(screen.getByText("lda_report_case_study")).toBeInTheDocument();
expect(screen.getByText("Version 1")).toBeInTheDocument();
expect(screen.getByText(/immutable/i)).toBeInTheDocument();
});
it("shows all concrete deployment bindings and validation", () => {
render(<AuthoringPhaseVisual projection={projectPreparedAuthoringPhase("deployment")} />);
expect(screen.getAllByText("local.lda_docs")).toHaveLength(2);
expect(screen.getAllByText("local.lda_report")).toHaveLength(2);
expect(screen.getAllByText("local.issue_board")).toHaveLength(2);
expect(screen.getByText(/deployment valid/i)).toBeInTheDocument();
});
});
@@ -0,0 +1,80 @@
import {
AlertTriangle,
ArrowRight,
CheckCircle2,
Database,
FileJson,
Link2,
LockKeyhole,
Workflow,
} from "lucide-react";
import type { AuthoringPhaseProjection } from "./authoring-projection.js";
type AuthoringPhaseVisualProps = {
readonly projection: AuthoringPhaseProjection;
};
const InventoryVisual = ({ visual }: { visual: Extract<AuthoringPhaseProjection["visual"], { kind: "inventory" }> }) => (
<section className="authoring-visual authoring-visual--inventory" role="region" aria-label="discovery evidence">
<div className="authoring-inventory__sources">
{visual.sources.map((source) => (
<div key={source}><Database aria-hidden="true" /><code>{source}</code></div>
))}
</div>
<ArrowRight className="authoring-visual__arrow" aria-hidden="true" />
<div className="authoring-inventory__contract">
<strong><Workflow aria-hidden="true" /> Inspected capability</strong>
<code>{visual.capability}</code>
<span><FileJson aria-hidden="true" />{visual.contract}</span>
</div>
</section>
);
const GraphVisual = ({ visual }: { visual: Extract<AuthoringPhaseProjection["visual"], { kind: "graph" }> }) => (
<section className="authoring-visual authoring-visual--graph" role="region" aria-label="draft graph evidence">
<div className="authoring-graph__node"><Database aria-hidden="true" /><strong>{visual.nodes[0]}</strong></div>
<div className="authoring-graph__edge"><span>{visual.inputBinding}</span><ArrowRight aria-hidden="true" /></div>
<div className="authoring-graph__node"><Workflow aria-hidden="true" /><strong>{visual.nodes[1]}</strong></div>
<div className="authoring-graph__route"><span>{visual.route}</span><ArrowRight aria-hidden="true" /></div>
<div className="authoring-graph__end">END</div>
</section>
);
const RepairVisual = ({ visual }: { visual: Extract<AuthoringPhaseProjection["visual"], { kind: "repair" }> }) => (
<section className="authoring-visual authoring-visual--repair" role="region" aria-label="validation repair evidence">
<div className="authoring-repair__diagnostic"><AlertTriangle aria-hidden="true" /><span>Diagnostic</span><strong>{visual.diagnostic}</strong></div>
<ArrowRight aria-hidden="true" />
<div className="authoring-repair__correction"><Link2 aria-hidden="true" /><span>Repair</span><code>{visual.correction}</code></div>
<div className="authoring-repair__status"><CheckCircle2 aria-hidden="true" /><strong>{visual.status}</strong></div>
</section>
);
const ArtifactVisual = ({ visual }: { visual: Extract<AuthoringPhaseProjection["visual"], { kind: "artifact" }> }) => (
<section className="authoring-visual authoring-visual--artifact" role="region" aria-label="artifact evidence">
<LockKeyhole aria-hidden="true" />
<div><span>Immutable workflow artifact</span><strong>{visual.artifactId}</strong></div>
<dl><div><dt>Version</dt><dd>Version {visual.version}</dd></div><div><dt>Requirements</dt><dd>{visual.requiredSources} local sources</dd></div></dl>
</section>
);
const BindingsVisual = ({ visual }: { visual: Extract<AuthoringPhaseProjection["visual"], { kind: "bindings" }> }) => (
<section className="authoring-visual authoring-visual--bindings" role="region" aria-label="deployment binding evidence">
<header><Link2 aria-hidden="true" /><div><span>Deployment</span><strong>{visual.deploymentId}</strong></div><b><CheckCircle2 aria-hidden="true" />{visual.status}</b></header>
<div className="authoring-bindings__rows">
{visual.bindings.map((binding) => (
<div key={binding.requirement}><code>{binding.requirement}</code><ArrowRight aria-hidden="true" /><code>{binding.source}</code></div>
))}
</div>
</section>
);
/** Renders the factual product artifact appropriate to one authoring phase. */
export const AuthoringPhaseVisual = ({ projection }: AuthoringPhaseVisualProps) => {
switch (projection.visual.kind) {
case "inventory": return <InventoryVisual visual={projection.visual} />;
case "graph": return <GraphVisual visual={projection.visual} />;
case "repair": return <RepairVisual visual={projection.visual} />;
case "artifact": return <ArtifactVisual visual={projection.visual} />;
case "bindings": return <BindingsVisual visual={projection.visual} />;
}
};
@@ -8,6 +8,7 @@ describe("projectPreparedAuthoringPhase", () => {
expect(phase.label).toBe("Discover");
expect(phase.commands.length).toBeGreaterThanOrEqual(2);
expect(phase.summary).toMatch(/sources|capabilities|schema/i);
expect(phase.visual).toMatchObject({ kind: "inventory" });
});
it("projects the draft phase with graph and routes", () => {
@@ -15,6 +16,7 @@ describe("projectPreparedAuthoringPhase", () => {
expect(phase.label).toBe("Draft");
expect(phase.commands.length).toBeGreaterThanOrEqual(2);
expect(phase.summary).toMatch(/graph|routes/i);
expect(phase.visual).toMatchObject({ kind: "graph" });
});
it("projects the validate phase with diagnosis and repair", () => {
@@ -22,13 +24,16 @@ describe("projectPreparedAuthoringPhase", () => {
expect(phase.label).toBe("Validate");
expect(phase.commands.length).toBeGreaterThanOrEqual(2);
expect(phase.summary).toMatch(/diagnos|repair/i);
expect(phase.visual).toMatchObject({ kind: "repair" });
});
it("projects the artifact phase with immutable ID and version", () => {
const phase = projectPreparedAuthoringPhase("artifact");
expect(phase.label).toBe("Artifact");
expect(phase.commands.length).toBeGreaterThanOrEqual(2);
expect(phase.summary).toMatch(/id|version/i);
expect(phase.summary).toMatch(/id/i);
expect(phase.summary).toMatch(/version/i);
expect(phase.visual).toMatchObject({ kind: "artifact" });
});
it("projects the deployment phase with bindings and validation", () => {
@@ -36,15 +41,16 @@ describe("projectPreparedAuthoringPhase", () => {
expect(phase.label).toBe("Deployment");
expect(phase.commands.length).toBeGreaterThanOrEqual(2);
expect(phase.summary).toMatch(/bind|validat/i);
expect(phase.visual).toMatchObject({ kind: "bindings" });
});
it("validates phase diagnostic status is 'repaired'", () => {
it("keeps the validation diagnostic and repair command distinct", () => {
const phase = projectPreparedAuthoringPhase("validate");
const diagnosticCmd = phase.commands.find(
(cmd) => cmd.result === "diagnostic" && cmd.detail?.includes("repaired"),
(cmd) => cmd.result === "diagnostic" && cmd.detail?.includes("no state projection"),
);
expect(diagnosticCmd).toBeDefined();
expect(diagnosticCmd!.detail).toContain("repaired");
expect(phase.commands.some((cmd) => cmd.command.includes("draft set-output"))).toBe(true);
});
it("uses real public command syntax from the recording", () => {
@@ -5,7 +5,85 @@ export type AuthoringPhaseProjection = {
readonly beatId: string;
readonly label: string;
readonly summary: string;
readonly proof: readonly string[];
readonly commands: readonly PreparedAuthoringCommand[];
readonly visual: AuthoringPhaseVisualModel;
};
export type AuthoringPhaseVisualModel =
| {
readonly kind: "inventory";
readonly sources: readonly string[];
readonly capability: string;
readonly contract: string;
}
| {
readonly kind: "graph";
readonly nodes: readonly string[];
readonly route: string;
readonly inputBinding: string;
}
| {
readonly kind: "repair";
readonly diagnostic: string;
readonly correction: string;
readonly status: string;
}
| {
readonly kind: "artifact";
readonly artifactId: string;
readonly version: number;
readonly requiredSources: number;
}
| {
readonly kind: "bindings";
readonly deploymentId: string;
readonly bindings: readonly { readonly requirement: string; readonly source: string }[];
readonly status: string;
};
const visualForPhase = (phase: AuthoringPhaseId): AuthoringPhaseVisualModel => {
switch (phase) {
case "discover":
return {
kind: "inventory",
sources: ["local.lda_docs", "local.lda_report", "local.issue_board"],
capability: "local.lda_report.analyze_documents",
contract: "documents → analysis",
};
case "draft":
return {
kind: "graph",
nodes: ["read_documents", "analyze"],
route: "ok → end",
inputBinding: "state.documents → documents",
};
case "validate":
return {
kind: "repair",
diagnostic: "analysis has no state projection",
correction: "analysis → state.analysis",
status: "Valid draft",
};
case "artifact":
return {
kind: "artifact",
artifactId: "lda_report_case_study",
version: 1,
requiredSources: 3,
};
case "deployment":
return {
kind: "bindings",
deploymentId: "lda_report_case_study.default",
bindings: [
{ requirement: "local.lda_docs", source: "local.lda_docs" },
{ requirement: "local.lda_report", source: "local.lda_report" },
{ requirement: "local.issue_board", source: "local.issue_board" },
],
status: "Deployment valid",
};
}
};
/**
@@ -28,6 +106,8 @@ export const projectPreparedAuthoringPhase = (
.filter((t) => t.role === "assistant")
.map((t) => t.text)
.join(" ") || found.label,
proof: found.proof,
commands: found.commands,
visual: visualForPhase(found.phase),
};
};
@@ -2863,3 +2863,237 @@
color: var(--text-secondary);
font: 0.66rem/1 var(--font-evidence);
}
.authoring-visual {
min-height: 0;
height: 100%;
color: var(--text-primary);
}
.authoring-visual svg {
width: 1.15rem;
height: 1.15rem;
flex: 0 0 auto;
}
.authoring-visual code {
font: 650 0.8rem/1.35 var(--font-evidence);
}
.authoring-visual--inventory {
display: grid;
grid-template-columns: minmax(15rem, 0.85fr) auto minmax(21rem, 1.15fr);
align-items: center;
gap: 1.5rem;
}
.authoring-inventory__sources {
display: grid;
gap: 0.65rem;
}
.authoring-inventory__sources > div,
.authoring-inventory__contract {
display: flex;
align-items: center;
gap: 0.7rem;
}
.authoring-inventory__sources > div {
min-height: 3.4rem;
padding: 0.8rem 1rem;
border-bottom: 1px solid var(--stage-line);
}
.authoring-inventory__contract {
align-items: flex-start;
flex-direction: column;
justify-content: center;
min-height: 11rem;
padding: 1.4rem;
background: var(--stage-inset);
border-radius: 0.65rem;
}
.authoring-inventory__contract strong,
.authoring-inventory__contract span {
display: flex;
align-items: center;
gap: 0.5rem;
}
.authoring-visual__arrow {
color: var(--accent-cyan);
}
.authoring-visual--graph {
display: flex;
align-items: center;
justify-content: center;
gap: 0.8rem;
}
.authoring-graph__node {
display: grid;
place-items: center;
gap: 0.55rem;
min-width: 10.5rem;
min-height: 7rem;
border: 2px solid var(--stage-line);
border-radius: 0.7rem;
background: var(--stage-inset);
}
.authoring-graph__edge,
.authoring-graph__route {
display: grid;
place-items: center;
gap: 0.3rem;
color: var(--accent-cyan);
font: 700 0.68rem/1.2 var(--font-evidence);
}
.authoring-graph__end {
display: grid;
place-items: center;
width: 4.4rem;
height: 4.4rem;
border: 2px solid var(--accent-cyan);
border-radius: 50%;
font: 800 0.78rem/1 var(--font-evidence);
}
.authoring-visual--repair {
display: grid;
grid-template-columns: minmax(15rem, 1fr) auto minmax(15rem, 1fr);
grid-template-rows: 1fr auto;
align-items: center;
gap: 1rem 1.4rem;
}
.authoring-repair__diagnostic,
.authoring-repair__correction {
display: grid;
grid-template-columns: auto 1fr;
gap: 0.55rem 0.7rem;
align-content: center;
min-height: 9rem;
padding: 1.2rem;
border: 1px solid var(--stage-line);
border-radius: 0.65rem;
}
.authoring-repair__diagnostic strong,
.authoring-repair__correction code {
grid-column: 1 / -1;
}
.authoring-repair__diagnostic svg {
color: var(--accent-amber);
}
.authoring-repair__correction svg,
.authoring-repair__status svg {
color: var(--success);
}
.authoring-repair__status {
grid-column: 1 / -1;
display: flex;
justify-content: center;
align-items: center;
gap: 0.55rem;
}
.authoring-visual--artifact {
display: grid;
grid-template-columns: auto 1fr auto;
align-items: center;
gap: 1.3rem;
width: min(100%, 52rem);
margin-inline: auto;
padding: 1.6rem 1.8rem;
border-block: 2px solid var(--stage-line);
}
.authoring-visual--artifact > svg {
width: 2.8rem;
height: 2.8rem;
color: var(--accent-cyan);
}
.authoring-visual--artifact > div {
display: grid;
gap: 0.3rem;
}
.authoring-visual--artifact > div strong {
font: 700 1.5rem/1.1 var(--font-evidence);
}
.authoring-visual--artifact dl {
display: flex;
gap: 1.5rem;
margin: 0;
}
.authoring-visual--artifact dl div {
display: grid;
gap: 0.2rem;
}
.authoring-visual--artifact dt {
color: var(--text-secondary);
font-size: 0.72rem;
}
.authoring-visual--artifact dd {
margin: 0;
font-weight: 700;
}
.authoring-visual--bindings {
display: grid;
grid-template-rows: auto 1fr;
gap: 0.8rem;
}
.authoring-visual--bindings header {
display: grid;
grid-template-columns: auto 1fr auto;
align-items: center;
gap: 0.8rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid var(--stage-line);
}
.authoring-visual--bindings header div {
display: grid;
}
.authoring-visual--bindings header b {
display: flex;
align-items: center;
gap: 0.4rem;
color: var(--success);
}
.authoring-bindings__rows {
display: grid;
align-content: center;
gap: 0.55rem;
}
.authoring-bindings__rows > div {
display: grid;
grid-template-columns: minmax(12rem, 1fr) auto minmax(12rem, 1fr);
align-items: center;
gap: 0.8rem;
min-height: 2.9rem;
padding-inline: 0.8rem;
background: var(--stage-inset);
}
.authoring-bindings__rows code:last-child {
color: var(--accent-cyan);
}