// Requirements pane — list of extracted requirements with traceability +
// unsupported flags. Click a requirement's traced block → focus that block
// in Model. Re-running Analyze MERGES; new and deprecated requirements
// surface for the user to keep or discard.
"use client";
import { useMemo } from "react";
import { PaneFrame } from "../PaneFrame";
import { PaneEmpty } from "../PaneEmpty";
import { PaneDrawer } from "../PaneDrawer";
import { StatusChip } from "../StatusChip";
import { useAnalysis, type ClientRequirement } from "../../../lib/workspace/analysisStore";
import { useOpenPanes } from "../../../lib/workspace/openPanesStore";
import { useModelStore } from "../../../lib/sync/ModelStore";
interface RequirementsPaneProps {
index: number;
onClose: () => void;
}
export function RequirementsPane({ index, onClose }: RequirementsPaneProps) {
const { requirements, analyze, inFlight } = useAnalysis();
const { pushFrom } = useOpenPanes();
const { model } = useModelStore();
const running = inFlight.has("requirements") || inFlight.has("all");
const kept = useMemo(
() => requirements.filter(r => r.status === "accepted"),
[requirements]
);
const pending = useMemo(
() => requirements.filter(r => r.status !== "accepted"),
[requirements]
);
const unsupportedCount = kept.filter(r => r.unsupported).length;
const labelOf = (id: string) => model.blocks.find(b => b.id === id)?.label ?? id;
const subtitle =
requirements.length === 0
? "no requirements yet"
: pending.length > 0
? `${kept.length} kept · ${pending.length} pending`
: unsupportedCount > 0
? `${kept.length} total · ${unsupportedCount} unsupported`
: `${kept.length} total · all traced`;
const right = (
void analyze("requirements")}
disabled={running}
>
{running ? "Analyzing…" : "Analyze"}
);
const renderRow = (r: ClientRequirement) => (
pushFrom(index, { kind: "requirement", id: r.id })}
onJumpToBlock={id => pushFrom(index, { kind: "block", id })}
onJumpToTerm={id => pushFrom(index, { kind: "term", id })}
/>
);
return (
{kept.length === 0 && pending.length === 0 ? (
Requirements are extracted from sentences in your prose that say the system
must / should / needs to . Run Analyze to scan.
>
}
action={{
label: running ? "Analyzing…" : "Run Analyze →",
onClick: () => void analyze("requirements"),
disabled: running,
}}
/>
) : kept.length === 0 ? (
) : (
)}
{pending.length > 0 ? (
) : null}
);
}
interface RequirementItemProps {
req: ClientRequirement;
labelOf: (id: string) => string;
onOpenSelf: () => void;
onJumpToBlock: (id: string) => void;
onJumpToTerm: (id: string) => void;
}
function RequirementItem({ req, labelOf, onOpenSelf, onJumpToBlock, onJumpToTerm }: RequirementItemProps) {
const { decideRequirement } = useAnalysis();
return (
{
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
onOpenSelf();
}
}}
>
{req.tag}
{req.unsupported ? : null}
{req.text}
{req.linkedTermId ? (
{
e.stopPropagation();
onJumpToTerm(req.linkedTermId!);
}}
title="Open concept detail"
>
↪ concept
) : null}
{req.tracedToIds.length > 0 ? (
e.stopPropagation()}>
Traced to:{" "}
{req.tracedToIds.map((id, i) => (
{
e.stopPropagation();
onJumpToBlock(id);
}}
>
{labelOf(id)}
{i < req.tracedToIds.length - 1 ? ", " : ""}
))}
) : null}
{req.status !== "accepted" ? (
e.stopPropagation()}>
{
e.stopPropagation();
void decideRequirement(req.id, "keep");
}}
>
Keep
{
e.stopPropagation();
void decideRequirement(req.id, "discard");
}}
>
Discard
) : null}
);
}
function ReqStatusBadge({ status }: { status: ClientRequirement["status"] }) {
if (status === "suggested") return ;
if (status === "deprecated") return ;
return null;
}