import { Decision, effectiveStatus, sortDecisions } from './decisions'; import { DecisionAttachment, DiagramSummary, RegistryElement } from './format'; import { ModelIndex } from './service'; /** A decision as the lists show it. */ export interface DecisionRow { id: string; title?: string; /** Status shown: superseded when another decision supersedes it. */ status: string; supersededBy?: string; date?: string; file: string; } /** A diagram an element is used in, as the pages show it. */ export interface UsedIn { path: string; title?: string; kind?: string; } /** A registry element as the model page and the registry page show it. */ export interface ElementCard { id: string; label?: string; sublabel?: string; type?: string; brand?: string; description?: string; /** Diagram that explains the element, relative to the model root. */ diagram?: string; /** Registry file (relative to the model root) and 0-based line of the element. */ file: string; line: number; usedIn: UsedIn[]; /** Decisions about this element, from the decision files and from diagram attachments. */ decisions: DecisionRow[]; } /** What the model page shows; serialised into the webview. */ export interface ModelPanel { meta: { id?: string; name?: string; description?: string }; /** True when main.archify also carries a diagram, shown between metadata and the list. */ hasDiagram: boolean; groups: { group: string; diagrams: { path: string; title?: string; kind?: string; parseError?: string }[] }[]; registry: ElementCard[]; decisions: DecisionRow[]; problems: string[]; nested: string[]; } /** What the decision page (one .archify-decision file) shows. */ export interface DecisionPanel { file: string; inModel: boolean; model?: { id?: string; name?: string }; decision?: DecisionRow & { context?: string; decision?: string; consequences?: string; supersedes: DecisionRow[] }; /** Decisions that supersede this one. */ supersededBy: DecisionRow[]; elements: ElementCard[]; /** Diagrams the decision names. */ diagrams: UsedIn[]; /** Diagrams that point at the decision, with where. */ referencedBy: { path: string; title?: string; kind?: string; attachedTo: DecisionAttachment }[]; problems: string[]; } /** What the registry page (one .archify-registry file) shows. */ export interface RegistryPanel { /** The registry file, relative to the model root (or its base name outside a model). */ file: string; inModel: boolean; model?: { id?: string; name?: string }; elements: ElementCard[]; problems: string[]; } function usedIn(id: string, usage: Map, diagrams: DiagramSummary[]): UsedIn[] { return (usage.get(id) ?? []).map((path) => { const summary = diagrams.find((d) => d.relativePath === path); return { path, title: summary?.title, kind: summary?.kind }; }); } export function decisionRow(d: Decision, all: Map): DecisionRow { const { status, supersededBy } = effectiveStatus(d, all); return { id: d.id, title: d.title, status, supersededBy, date: d.date, file: d.file }; } /** Decisions about an element: named by the decision, or attached to its node in some diagram. */ function decisionsOfElement(id: string, index: Pick | undefined): DecisionRow[] { if (!index) return []; const all = index.decisions.decisions; const ids = new Set(); for (const d of all.values()) if (d.elements.includes(id)) ids.add(d.id); for (const [decisionId, uses] of index.decisionUsage) if (uses.some((u) => u.attachedTo.kind === 'node' && u.attachedTo.id === id) && all.has(decisionId)) ids.add(decisionId); return sortDecisions([...ids].map((x) => all.get(x)!), all).map((d) => decisionRow(d, all)); } export function elementCard(e: RegistryElement, usage: Map, diagrams: DiagramSummary[], index?: Pick): ElementCard { return { id: e.id, label: e.label, sublabel: e.sublabel, type: e.type, brand: e.brand, description: e.description, diagram: e.diagram, file: e.file, line: e.line, usedIn: usedIn(e.id, usage, diagrams), decisions: decisionsOfElement(e.id, index), }; } const byId = (a: { id: string }, b: { id: string }) => a.id.localeCompare(b.id); export function panelFromIndex(index: ModelIndex): ModelPanel { const registry = [...index.registry.elements.values()].sort(byId).map((e) => elementCard(e, index.usage, index.diagrams, index)); const all = index.decisions.decisions; return { meta: index.meta, decisions: sortDecisions([...all.values()], all).map((d) => decisionRow(d, all)), hasDiagram: index.mainHasDiagram, groups: index.groups.map((g) => ({ group: g.group, diagrams: g.diagrams.map((d) => ({ path: d.relativePath, title: d.title, kind: d.kind, parseError: d.parseError })), })), registry, problems: [...index.registry.problems, ...index.decisions.problems].map((p) => (p.file ? `${p.file}: ${p.message}` : p.message)), nested: index.nested, }; } /** The registry page of one file inside a model: its elements, in file order, with usage. */ export function registryPanelFromIndex(index: ModelIndex, file: string): RegistryPanel { const elements = [...index.registry.elements.values()] .filter((e) => e.file === file) .sort((a, b) => a.line - b.line) .map((e) => elementCard(e, index.usage, index.diagrams, index)); return { file, inModel: true, model: { id: index.meta.id, name: index.meta.name }, elements, problems: index.registry.problems.filter((p) => p.file === file).map((p) => p.message), }; } /** The registry page of a file outside a model: elements only, nothing is used anywhere. */ export function registryPanelStandalone(file: string, parsed: { elements: RegistryElement[]; problems: { message: string }[] }): RegistryPanel { return { file, inModel: false, elements: parsed.elements.map((e) => elementCard(e, new Map(), [])), problems: parsed.problems.map((p) => p.message), }; } /** The decision page of one file inside a model. */ export function decisionPanelFromIndex(index: ModelIndex, file: string): DecisionPanel { const all = index.decisions.decisions; const d = [...all.values()].find((x) => x.file === file); const problems = index.decisions.problems.filter((p) => p.file === file).map((p) => p.message); const base = { file, inModel: true, model: { id: index.meta.id, name: index.meta.name }, problems }; if (!d) return { ...base, supersededBy: [], elements: [], diagrams: [], referencedBy: [] }; const rows = (ids: string[]) => ids.filter((id) => all.has(id)).map((id) => decisionRow(all.get(id)!, all)); const elements = d.elements.filter((id) => index.registry.elements.has(id)).map((id) => elementCard(index.registry.elements.get(id)!, index.usage, index.diagrams, index)); const summary = (path: string) => index.diagrams.find((x) => x.relativePath === path); return { ...base, decision: { ...decisionRow(d, all), context: d.context, decision: d.decision, consequences: d.consequences, supersedes: rows(d.supersedes) }, supersededBy: [...all.values()].filter((x) => x.supersedes.includes(d.id)).map((x) => decisionRow(x, all)), elements, diagrams: d.diagrams.map((path) => ({ path, title: summary(path)?.title, kind: summary(path)?.kind })), referencedBy: (index.decisionUsage.get(d.id) ?? []).map((u) => ({ path: u.path, title: summary(u.path)?.title, kind: summary(u.path)?.kind, attachedTo: u.attachedTo })), }; } /** The decision page of a file outside a model: the text only, and a note that it has lost its model. */ export function decisionPanelStandalone(file: string, parsed: { decision?: Decision; problems: { message: string }[] }): DecisionPanel { const d = parsed.decision; return { file, inModel: false, decision: d ? { id: d.id, title: d.title, status: d.status ?? 'proposed', date: d.date, file, context: d.context, decision: d.decision, consequences: d.consequences, supersedes: d.supersedes.map((id) => ({ id, status: 'unknown', file: '' })) } : undefined, supersededBy: [], elements: [], diagrams: d ? d.diagrams.map((path) => ({ path })) : [], referencedBy: [], problems: parsed.problems.map((p) => p.message), }; }