feat: visualize prepared authoring phases
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user