// The speckit SDLC's FIRST-PARTY code extension (VIZ-4/VIZ-14). AC = user story (priority + MVP // + scenarios + tasks, with verification-layer commits). Issue-level panels render the rest of // the captured Spec Kit process: metadata, FR/SC, key entities/edge cases/assumptions/ // clarifications, the plan (technical context + Constitution Check), constitution principles, // non-story task phases, and design-artifact presence. // // Vocabulary (statusOrder/acUnitLabel) is NOT here โ€” it moved to DATA, the `visualizer` block in // `boilerplates/presets/speckit.ts` (VIZ-2), because this file conforms to the render-only // `VisualizerExtension` contract (`client/extensions.tsx`), which deliberately excludes // vocabulary members (VIZ-14's drift guard). This module is discovered by filename at // bundle-build time (server.ts's generated entry scans `client/presets/*.tsx`, imports this // file's default export, and calls `registerExtension('speckit', ...)`) โ€” no hardcoded // name->extension map anywhere. import React from 'react'; import type { VisualizerExtension } from '../extensions'; type Task = { id: string; title: string; status: string; parallel?: boolean; commit?: string; dependsOn?: string[] }; function TaskList({ tasks }: { tasks: Task[] }) { if (!tasks.length) return null; return (
{tasks.map((t) => (
{t.status === 'done' ? '[x]' : '[ ]'} {t.id} {t.parallel && P} {t.title} {t.commit && {t.commit.slice(0, 7)}}
))}
); } function Panel({ title, count, children }: { title: string; count?: number; children: React.ReactNode }) { return

{title}

{count !== undefined && {count}}
{children}
; } const speckitExtension: VisualizerExtension = { acText: (ac) => { const a = ac as { id: string; text?: string; priority?: string; mvp?: boolean; needsClarification?: boolean }; return ( <> {a.priority && {a.priority}}{a.mvp && ๐ŸŽฏ MVP}{' '} {a.id} {a.text} {a.needsClarification && NEEDS CLARIFICATION} ); }, acEvidence: (ac) => { const a = ac as { tasks?: Task[]; scenarios?: Array<{ id: string; text: string }> }; return ( <> {a.scenarios && a.scenarios.length > 0 &&
{a.scenarios.map((s) =>
{s.text}
)}
} ); }, // The kit's issuePanels receives (issue, projectUrl) โ€” the same `/project/` URL mapper // acEvidence already gets โ€” so a panel can link evidence/design-artifact files under the // project root. This preset's captured process is all inline text/structure today, so // projectUrl is unused here, but the parameter is wired through at the call site (main.tsx) // for any first-party or repo-local extension (VIZ-13) that needs it. issuePanels: (issue, _projectUrl) => { const i = issue as { metadata?: { featureBranch?: string; status?: string; created?: string; input?: string }; requirements?: Array<{ id: string; text: string; needsClarification: boolean }>; successCriteria?: Array<{ id: string; text: string; needsClarification: boolean }>; keyEntities?: Array<{ name: string; description: string }>; edgeCases?: string[]; assumptions?: string[]; clarifications?: Array<{ text: string }>; phases?: Array<{ name: string; kind: string; tasks: Task[] }>; plan?: { present: boolean; technicalContext: Array<{ field: string; value: string }>; constitutionGates: Array<{ text: string; passed?: boolean }>; complexity: string[] }; constitution?: { present: boolean; principles: string[] }; artifacts?: { research: boolean; dataModel: boolean; quickstart: boolean; contracts: string[] }; }; const m = i.metadata ?? {}, reqs = i.requirements ?? [], scs = i.successCriteria ?? [], plan = i.plan, con = i.constitution, art = i.artifacts; return ( <> {(m.featureBranch || m.status || m.created || m.input) && (
{m.featureBranch && <>branch{m.featureBranch}} {m.status && <>status{m.status}} {m.created && <>created{m.created}} {m.input && <>input{m.input}}
)} {reqs.length > 0 &&
{reqs.map((r) =>
{r.id} {r.text}{r.needsClarification && NEEDS CLARIFICATION}
)}
} {scs.length > 0 &&
{scs.map((c) =>
{c.id} {c.text}
)}
} {i.keyEntities && i.keyEntities.length > 0 &&
{i.keyEntities.map((e) =>
{e.name} โ€” {e.description}
)}
} {((i.edgeCases?.length ?? 0) > 0 || (i.assumptions?.length ?? 0) > 0 || (i.clarifications?.length ?? 0) > 0) && (
{(i.edgeCases ?? []).map((e, n) =>
edge {e}
)} {(i.assumptions ?? []).map((a, n) =>
assume {a}
)} {(i.clarifications ?? []).map((c, n) =>
clarified {c.text}
)}
)} {plan?.present && ( {plan.technicalContext.length > 0 &&
{plan.technicalContext.map((f) => {f.field}{f.value})}
} {plan.constitutionGates.length > 0 && (
Constitution Check
{plan.constitutionGates.map((g, n) =>
{g.passed === false ? 'โœ—' : 'โœ“'} {g.text}
)}
)}
)} {con?.present && con.principles.length > 0 &&
{con.principles.map((p) =>
{p}
)}
} {(i.phases ?? []).filter((p) => p.tasks.length > 0).map((p) => ( {p.kind} ))} {art && (art.research || art.dataModel || art.quickstart || art.contracts.length > 0) && (
{art.research && research.md} {art.dataModel && data-model.md} {art.quickstart && quickstart.md} {art.contracts.map((c) => {c.split('/').pop()})}
)} ); }, }; // Default export ONLY โ€” registration itself happens in the generated bundle entry (server.ts), // which scans this directory, imports each file's default export, and calls // `registerExtension(, )`. Filename = canonical preset name (mirroring // the boilerplates/presets two-file convention) is the ONE place that name is decided, so this // file itself never hardcodes it. export default speckitExtension;