feat: add authoring lifecycle transformation diagrams

This commit is contained in:
lda
2026-07-14 01:19:17 +07:00 Verified
parent cd9b28fb9f
commit 8e7af123b8
2 changed files with 202 additions and 0 deletions
@@ -0,0 +1,55 @@
import { cleanup, render, screen, within } from "@testing-library/react";
import { afterEach, describe, expect, it } from "vitest";
import { projectPreparedLifecycleStep } from "./authoring-projection.js";
import { AuthoringLifecycleDiagram } from "./AuthoringLifecycleDiagram.js";
afterEach(cleanup);
const renderLifecycle = (step: "discover" | "artifact" | "deployment") => {
const projection = projectPreparedLifecycleStep(step);
if (
projection.evidence.kind !== "inventory"
&& projection.evidence.kind !== "artifact"
&& projection.evidence.kind !== "deployment"
) {
throw new Error(`unexpected evidence for ${step}`);
}
return render(<AuthoringLifecycleDiagram evidence={projection.evidence} />);
};
describe("AuthoringLifecycleDiagram", () => {
it("shows configured sources feeding the discovered capability", () => {
renderLifecycle("discover");
const diagram = screen.getByRole("img", { name: /source capability map/i });
expect(within(diagram).getByText("local.lda_docs")).toBeInTheDocument();
expect(within(diagram).getByText("local.lda_report")).toBeInTheDocument();
expect(within(diagram).getByText("local.issue_board")).toBeInTheDocument();
expect(within(diagram).getByText("local.lda_report.analyze_documents")).toBeInTheDocument();
expect(diagram).toHaveAttribute("data-lifecycle-diagram", "inventory");
});
it("keeps the workflow silhouette and requirements attached to the immutable version", () => {
renderLifecycle("artifact");
const diagram = screen.getByRole("img", { name: /versioned artifact map/i });
expect(within(diagram).getByText("read_documents")).toBeInTheDocument();
expect(within(diagram).getByText("analyze")).toBeInTheDocument();
expect(within(diagram).getByText("lda_report_case_study")).toBeInTheDocument();
expect(within(diagram).getByText("Version 1")).toBeInTheDocument();
expect(within(diagram).getByText("local.lda_docs")).toBeInTheDocument();
expect(diagram).toHaveAttribute("data-lifecycle-diagram", "artifact");
});
it("maps every requirement to a concrete source and one deployment", () => {
renderLifecycle("deployment");
const diagram = screen.getByRole("img", { name: /deployment binding map/i });
expect(within(diagram).getAllByText("local.lda_docs")).toHaveLength(2);
expect(within(diagram).getAllByText("local.lda_report")).toHaveLength(2);
expect(within(diagram).getAllByText("local.issue_board")).toHaveLength(2);
expect(within(diagram).getByText("lda_report_case_study.default")).toBeInTheDocument();
expect(within(diagram).getByText("Runnable")).toBeInTheDocument();
expect(diagram).toHaveAttribute("data-lifecycle-diagram", "deployment");
});
});
@@ -0,0 +1,147 @@
import {
ArrowRight,
Braces,
Cable,
CircleCheck,
Database,
FileLock2,
Workflow,
} from "lucide-react";
import type { ReviewedAuthoringEvidence } from "./reviewed-authoring-evidence.js";
import { reviewedAuthoringEvidenceFor } from "./reviewed-authoring-evidence.js";
type LifecycleEvidence = Extract<
ReviewedAuthoringEvidence,
{ readonly kind: "inventory" | "artifact" | "deployment" }
>;
type AuthoringLifecycleDiagramProps = {
readonly evidence: LifecycleEvidence;
};
const SourceCapabilityMap = ({
evidence,
}: {
readonly evidence: Extract<LifecycleEvidence, { readonly kind: "inventory" }>;
}) => (
<div
className="authoring-lifecycle-diagram authoring-lifecycle-diagram--inventory"
role="img"
aria-label="Source capability map"
data-lifecycle-diagram="inventory"
>
<div className="authoring-lifecycle-diagram__source-stack">
{evidence.sources.map((source) => (
<span key={source} className="authoring-lifecycle-diagram__source">
<Database aria-hidden="true" />
<code>{source}</code>
</span>
))}
</div>
<div className="authoring-lifecycle-diagram__flow" aria-hidden="true">
<span>Discover</span>
<ArrowRight />
</div>
<section className="authoring-lifecycle-diagram__capability">
<Braces aria-hidden="true" />
<span>Typed capability</span>
<strong>{evidence.capability.name}</strong>
<dl>
<div><dt>Input</dt><dd>{evidence.capability.inputs.join(", ")}</dd></div>
<div><dt>Output</dt><dd>{evidence.capability.outputs.join(", ")}</dd></div>
<div><dt>Outcome</dt><dd>{evidence.capability.outcomes.join(", ")}</dd></div>
</dl>
</section>
</div>
);
const VersionedArtifactMap = ({
evidence,
}: {
readonly evidence: Extract<LifecycleEvidence, { readonly kind: "artifact" }>;
}) => {
const draftEvidence = reviewedAuthoringEvidenceFor("draft");
if (draftEvidence.kind !== "draft") {
throw new Error("reviewed artifact diagram requires reviewed draft evidence");
}
return (
<div
className="authoring-lifecycle-diagram authoring-lifecycle-diagram--artifact"
role="img"
aria-label="Versioned artifact map"
data-lifecycle-diagram="artifact"
>
<section className="authoring-lifecycle-diagram__draft-shape">
<Workflow aria-hidden="true" />
<span>Validated draft</span>
<div>
{draftEvidence.steps.map((step) => <code key={step}>{step}</code>)}
<code>END</code>
</div>
</section>
<div className="authoring-lifecycle-diagram__flow" aria-hidden="true">
<span>Freeze</span>
<ArrowRight />
</div>
<section className="authoring-lifecycle-diagram__artifact-object">
<FileLock2 aria-hidden="true" />
<span>Immutable artifact</span>
<strong>{evidence.artifactId}</strong>
<b>Version {evidence.version}</b>
<div className="authoring-lifecycle-diagram__requirements">
{evidence.requiredSources.map((source) => <code key={source}>{source}</code>)}
</div>
</section>
</div>
);
};
const DeploymentBindingMap = ({
evidence,
}: {
readonly evidence: Extract<LifecycleEvidence, { readonly kind: "deployment" }>;
}) => (
<div
className="authoring-lifecycle-diagram authoring-lifecycle-diagram--deployment"
role="img"
aria-label="Deployment binding map"
data-lifecycle-diagram="deployment"
>
<div className="authoring-lifecycle-diagram__bindings">
<div className="authoring-lifecycle-diagram__binding-headings" aria-hidden="true">
<span>Requirement</span>
<span>Configured source</span>
</div>
{evidence.bindings.map((binding) => (
<div className="authoring-lifecycle-diagram__binding" key={binding.requirement}>
<code>{binding.requirement}</code>
<Cable aria-hidden="true" />
<code>{binding.source}</code>
</div>
))}
</div>
<div className="authoring-lifecycle-diagram__flow" aria-hidden="true">
<span>Bind</span>
<ArrowRight />
</div>
<section className="authoring-lifecycle-diagram__deployment-object">
<CircleCheck aria-hidden="true" />
<span>Deployment</span>
<strong>{evidence.deploymentId}</strong>
<b>{evidence.status === "runnable" ? "Runnable" : evidence.status}</b>
</section>
</div>
);
/** Renders fixed lifecycle mappings where graph interaction would add no value. */
export const AuthoringLifecycleDiagram = ({ evidence }: AuthoringLifecycleDiagramProps) => {
switch (evidence.kind) {
case "inventory":
return <SourceCapabilityMap evidence={evidence} />;
case "artifact":
return <VersionedArtifactMap evidence={evidence} />;
case "deployment":
return <DeploymentBindingMap evidence={evidence} />;
}
};