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:
root-at-skic committed 2026-10-19 23:13:05 +03:00
1 parent 33ebdd6328
commit ebf320a023
49 files changed
+1637 -55

No files matched your search

+27 -3
View File
@@ -2,6 +2,7 @@ import path from 'node:path';
import * as vscode from 'vscode';
import { log } from '../util/log';
import {
DecisionAttachment,
DiagramSummary,
MAIN_FILE,
ModelMeta,
@@ -17,6 +18,7 @@ import {
summarizeDiagram,
} from './format';
import { parse as parseYaml } from 'yaml';
import { DECISION_EXT, DecisionSet, isDecisionFile, mergeDecisions, parseDecision, validateDecisions } from './decisions';
/**
* Finds models in the workspace and keeps what they contain. A model is the
@@ -36,6 +38,10 @@ export interface ModelIndex {
groups: { group: string; diagrams: DiagramSummary[] }[];
/** Element id → diagrams (relative paths) that refer to it. */
usage: Map<string, string[]>;
/** The model's decisions, with link problems already checked. */
decisions: DecisionSet;
/** Decision id → diagrams that point at it, with where. */
decisionUsage: Map<string, { path: string; attachedTo: DecisionAttachment }[]>;
/** Nested main.archify files found below the root; not supported. */
nested: string[];
}
@@ -71,7 +77,7 @@ export class ModelService implements vscode.Disposable {
private readonly disposables: vscode.Disposable[];
constructor() {
const watcher = vscode.workspace.createFileSystemWatcher(`**/*{.archify,${REGISTRY_EXT}}`);
const watcher = vscode.workspace.createFileSystemWatcher(`**/*{.archify,${REGISTRY_EXT},${DECISION_EXT}}`);
const changed = (uri: vscode.Uri) => void this.onFileChanged(uri);
this.disposables = [
watcher,
@@ -80,7 +86,7 @@ export class ModelService implements vscode.Disposable {
watcher.onDidChange(changed),
vscode.workspace.onDidChangeTextDocument((event) => {
// Registry edits in the editor should re-render open diagrams before a save.
if (isRegistryFile(event.document.fileName) || isMainFile(event.document.fileName)) changed(event.document.uri);
if (isRegistryFile(event.document.fileName) || isMainFile(event.document.fileName) || isDecisionFile(event.document.fileName)) changed(event.document.uri);
}),
this.changeEmitter,
];
@@ -118,6 +124,12 @@ export class ModelService implements vscode.Disposable {
return found ?? undefined;
}
/** The model index of the model a file belongs to, or undefined outside a model. */
async indexFor(uri: vscode.Uri): Promise<ModelIndex | undefined> {
const root = await this.rootOf(uri);
return root ? this.index(root) : undefined;
}
/** The registry of the model a file belongs to, or undefined outside a model. */
async registryFor(uri: vscode.Uri): Promise<{ root: vscode.Uri; registry: Registry } | undefined> {
const root = await this.rootOf(uri);
@@ -175,7 +187,19 @@ export class ModelService implements vscode.Disposable {
}
const usage = new Map<string, string[]>();
for (const d of diagrams) for (const id of d.used) usage.set(id, [...(usage.get(id) ?? []), d.relativePath]);
return { root, main, meta, mainHasDiagram, registry, diagrams, groups: sortedGroups(diagrams), usage, nested };
const decisionFiles = await vscode.workspace.findFiles(new vscode.RelativePattern(root, `**/*${DECISION_EXT}`), SKIP);
const decisionParts = await Promise.all(
decisionFiles.sort((a, b) => a.fsPath.localeCompare(b.fsPath)).map(async (file) => {
const rel = path.relative(root.fsPath, file.fsPath).split(path.sep).join('/');
return { file: rel, ...parseDecision(await readText(file), rel) };
}),
);
const decisions = mergeDecisions(decisionParts);
decisions.problems.push(...validateDecisions(decisions, registry, new Set(diagrams.map((d) => d.relativePath))));
const decisionUsage = new Map<string, { path: string; attachedTo: DecisionAttachment }[]>();
for (const d of diagrams) for (const ref of d.decisions) decisionUsage.set(ref.id, [...(decisionUsage.get(ref.id) ?? []), { path: d.relativePath, attachedTo: ref.attachedTo }]);
return { root, main, meta, mainHasDiagram, registry, diagrams, groups: sortedGroups(diagrams), usage, decisions, decisionUsage, nested };
}
dispose(): void {