Architecture decisions: files, diagram references, views and panels
- *.archify-decision files: id (unique, words), title, status, date, elements, diagrams, supersedes, and prose; parsed, merged and link-checked in the model index; diagnostics on the file - diagrams point at decisions with decisions: [...] on the diagram, nodes and edges; stripped before Archify, unknown ids reported, completion, hover and go-to-definition for the ids and for elements/diagrams/ supersedes in decision files - Decisions panel on the main.archify page; Decisions rail panel under a diagram (hover lights up the node or connection); decisions on element cards and in the element dialog; decision view for the file, with a lost-model note outside a model - Archify | New Decision… command and template; editor schemas; the three New Model examples carry decisions; docs, changelog, 0.8.0
This commit is contained in:
1 parent
33ebdd6328
commit
ebf320a023
49 files changed
+1637
-55
No files matched your search
+75
-6
@@ -150,6 +150,19 @@ export function mergeRegistries(parts: { elements: RegistryElement[]; problems:
|
||||
return { elements, files: parts.map((p) => p.file), problems };
|
||||
}
|
||||
|
||||
/** Where a diagram attaches a decision: the diagram itself, a node or an edge. */
|
||||
export type DecisionAttachment = { kind: 'diagram' } | { kind: 'node'; id: string } | { kind: 'edge'; from: string; to: string; label?: string };
|
||||
|
||||
export interface DecisionRef {
|
||||
id: string;
|
||||
pointer: string;
|
||||
attachedTo: DecisionAttachment;
|
||||
}
|
||||
|
||||
/** Collections whose items may carry `decisions` (and `diagram` links), per diagram kind. */
|
||||
const NODE_COLLECTIONS = ['components', 'nodes', 'participants', 'states'] as const;
|
||||
const EDGE_COLLECTIONS = ['connections', 'edges', 'flows', 'transitions', 'messages'] as const;
|
||||
|
||||
export interface ResolvedDiagram {
|
||||
/** The diagram as Archify expects it: refs expanded, model block and links removed. */
|
||||
diagram: Obj;
|
||||
@@ -162,14 +175,66 @@ export interface ResolvedDiagram {
|
||||
used: Set<string>;
|
||||
/** Node id → registry description, for the Elements panel. */
|
||||
descriptions: Map<string, string>;
|
||||
/** Decisions the diagram points at, with where they are attached. */
|
||||
decisionRefs: DecisionRef[];
|
||||
}
|
||||
|
||||
export interface ResolveOptions {
|
||||
/** Undefined when the diagram is not inside a model. */
|
||||
registry?: Registry;
|
||||
/** Known decision ids of the model; undefined outside a model or when unknown. */
|
||||
decisions?: Set<string>;
|
||||
fileName: string;
|
||||
}
|
||||
|
||||
/** Strips `decisions` keys from a diagram and collects what they point at. Mutates `diagram`. */
|
||||
function takeDecisionRefs(diagram: Obj, options: ResolveOptions, problems: ModelProblem[]): DecisionRef[] {
|
||||
const refs: DecisionRef[] = [];
|
||||
const take = (owner: Obj, pointer: string, attachedTo: DecisionAttachment) => {
|
||||
if (!('decisions' in owner)) return;
|
||||
const raw = owner.decisions;
|
||||
delete owner.decisions;
|
||||
const ids = Array.isArray(raw) ? raw : [raw];
|
||||
ids.forEach((value, i) => {
|
||||
const id = value === undefined || value === null ? undefined : String(value);
|
||||
if (!id) return;
|
||||
const at = `${pointer}/decisions/${i}`;
|
||||
if (typeof value === 'number') {
|
||||
problems.push({ message: `Decision id ${value} is a number to YAML (0001 becomes 1). Quote it ("${value}") or use words.`, severity: 'error', pointer: at });
|
||||
return;
|
||||
}
|
||||
if (!options.registry) {
|
||||
problems.push({ message: `Decision "${id}" cannot be resolved: ${options.fileName.split(/[\\/]/).pop()} is not inside a model (no ${MAIN_FILE} above it).`, severity: 'error', pointer: at });
|
||||
} else if (options.decisions && !options.decisions.has(id)) {
|
||||
problems.push({ message: `Unknown decision "${id}": no .archify-decision file of this model defines it.`, severity: 'error', pointer: at });
|
||||
}
|
||||
refs.push({ id, pointer: at, attachedTo });
|
||||
});
|
||||
};
|
||||
take(diagram, '', { kind: 'diagram' });
|
||||
for (const key of NODE_COLLECTIONS) {
|
||||
const items = diagram[key];
|
||||
if (!Array.isArray(items)) continue;
|
||||
diagram[key] = items.map((item, index) => {
|
||||
if (!isObj(item)) return item;
|
||||
const copy = { ...item };
|
||||
take(copy, `/${key}/${index}`, { kind: 'node', id: str(copy.id) ?? str(copy.ref) ?? String(index) });
|
||||
return copy;
|
||||
});
|
||||
}
|
||||
for (const key of EDGE_COLLECTIONS) {
|
||||
const items = diagram[key];
|
||||
if (!Array.isArray(items)) continue;
|
||||
diagram[key] = items.map((item, index) => {
|
||||
if (!isObj(item)) return item;
|
||||
const copy = { ...item };
|
||||
take(copy, `/${key}/${index}`, { kind: 'edge', from: str(copy.from) ?? '', to: str(copy.to) ?? '', label: str(copy.label) });
|
||||
return copy;
|
||||
});
|
||||
}
|
||||
return refs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expands `ref` components from the registry and strips what Archify does
|
||||
* not know. Component order and indexes are kept, so Archify's diagnostics
|
||||
@@ -181,12 +246,13 @@ export function resolveRefs(doc: unknown, options: ResolveOptions): ResolvedDiag
|
||||
const links = new Map<string, string>();
|
||||
const used = new Set<string>();
|
||||
const descriptions = new Map<string, string>();
|
||||
if (!isObj(doc)) return { diagram: {}, problems, refs, links, used, descriptions };
|
||||
if (!isObj(doc)) return { diagram: {}, problems, refs, links, used, descriptions, decisionRefs: [] };
|
||||
const diagram: Obj = { ...doc };
|
||||
delete diagram.model;
|
||||
const decisionRefs = takeDecisionRefs(diagram, options, problems);
|
||||
const { registry } = options;
|
||||
const components = Array.isArray(doc.components) ? doc.components : undefined;
|
||||
if (!components) return { diagram, problems, refs, links, used, descriptions };
|
||||
const components = Array.isArray(diagram.components) ? diagram.components : undefined;
|
||||
if (!components) return { diagram, problems, refs, links, used, descriptions, decisionRefs };
|
||||
|
||||
diagram.components = components.map((raw, index) => {
|
||||
if (!isObj(raw)) return raw;
|
||||
@@ -233,7 +299,7 @@ export function resolveRefs(doc: unknown, options: ResolveOptions): ResolvedDiag
|
||||
if (link) links.set(id, link);
|
||||
return merged;
|
||||
});
|
||||
return { diagram, problems, refs, links, used, descriptions };
|
||||
return { diagram, problems, refs, links, used, descriptions, decisionRefs };
|
||||
}
|
||||
|
||||
/** Group of a diagram inside a model: '' for the root folder, else the folder path with '/' separators. */
|
||||
@@ -249,6 +315,8 @@ export interface DiagramSummary {
|
||||
kind?: string;
|
||||
title?: string;
|
||||
used: string[];
|
||||
/** Decisions the diagram points at. */
|
||||
decisions: DecisionRef[];
|
||||
parseError?: string;
|
||||
}
|
||||
|
||||
@@ -258,13 +326,14 @@ export function summarizeDiagram(text: string, relativePath: string): DiagramSum
|
||||
try {
|
||||
doc = parseYaml(text, { prettyErrors: false, uniqueKeys: false });
|
||||
} catch (error) {
|
||||
return { relativePath, group: groupOf(relativePath), used: [], parseError: error instanceof Error ? error.message.split('\n')[0] : String(error) };
|
||||
return { relativePath, group: groupOf(relativePath), used: [], decisions: [], parseError: error instanceof Error ? error.message.split('\n')[0] : String(error) };
|
||||
}
|
||||
const obj = isObj(doc) ? doc : {};
|
||||
const meta = isObj(obj.meta) ? obj.meta : {};
|
||||
const components = Array.isArray(obj.components) ? obj.components : [];
|
||||
const used = [...new Set(components.map((c) => (isObj(c) ? str(c.ref) : undefined)).filter((r): r is string => !!r))];
|
||||
return { relativePath, group: groupOf(relativePath), kind: str(obj.diagram_type), title: str(meta.title), used };
|
||||
const decisions = takeDecisionRefs({ ...obj }, { registry: { elements: new Map(), files: [], problems: [] }, fileName: relativePath }, []);
|
||||
return { relativePath, group: groupOf(relativePath), kind: str(obj.diagram_type), title: str(meta.title), used, decisions };
|
||||
}
|
||||
|
||||
/** Groups in display order: the root group first, then folders in path order. */
|
||||
|
||||
Reference in new issue
Block a user