0.11.0: arc42 and ArchiMate examples; ref in every diagram kind

Six example models next to the three C4 ones: arc42 (Cinema Tickets, IoT
Telemetry, Team Wiki) by section, ArchiMate (Insurance Claims, HR
Onboarding, Logistics Tracking) by layer, each validated with 0 errors and
0 warnings. resolveRefs and the editor schemas now take ref: in workflow
and dataflow nodes, sequence participants and lifecycle states, as the
skill already did. Example tests check each framework's folders.
This commit is contained in:
root-at-skic committed 2026-11-08 22:54:23 +02:00
1 parent f4df6a0834
commit 0745de822a
85 files changed
+2945 -67

No files matched your search

+11 -1
View File
@@ -4,7 +4,8 @@ import { modelIdFromName, titleFromName } from './templates';
/**
* Example models shipped with the extension (templates/models/<folder>).
* "Archify | New Model…" offers them; each is a complete model folder:
* main.archify, registry/*.archify-registry and diagrams by level.
* main.archify, registry/*.archify-registry, decisions and diagrams in the
* framework's folders: C4 levels, arc42 sections or ArchiMate layers.
*/
export interface ModelExample {
/** Folder under templates/models and the default name of the new model folder. */
@@ -17,8 +18,17 @@ export const MODEL_EXAMPLES: readonly ModelExample[] = [
{ folder: 'c4-standard-web-app', label: 'C4 - Standard Web App', description: 'Single-page app, API, database, cache, file storage, identity provider' },
{ folder: 'c4-e-shop', label: 'C4 - E-Shop', description: 'Web shop, API, worker, queue, payment and email providers' },
{ folder: 'c4-event-based-integration', label: 'C4 - Event-Based Integration', description: 'Event bus, router, dead-letter queue, event store, consumers' },
{ folder: 'arc42-cinema-tickets', label: 'arc42 - Cinema Tickets', description: 'Context, building blocks, runtime and deployment views; seat booking with payment' },
{ folder: 'arc42-iot-telemetry', label: 'arc42 - IoT Telemetry', description: 'Devices over MQTT, ingest, time-series store, alerting, dashboards' },
{ folder: 'arc42-team-wiki', label: 'arc42 - Team Wiki', description: 'A small self-hosted wiki: the arc42 sections at their smallest' },
{ folder: 'archimate-insurance-claims', label: 'ArchiMate - Insurance Claims', description: 'Business, application and technology layers of claim handling' },
{ folder: 'archimate-hr-onboarding', label: 'ArchiMate - HR Onboarding', description: 'Onboarding a new employee across HR, identity and IT services' },
{ folder: 'archimate-logistics-tracking', label: 'ArchiMate - Logistics Tracking', description: 'Shipment tracking from pick-up to proof of delivery, layer by layer' },
];
/** The framework prefix of an example folder; the model id is the folder without it. */
export const EXAMPLE_PREFIX = /^(c4|arc42|archimate)-/;
/**
* The example's main.archify for a new model folder. When the folder keeps
* the example's name the file is used as written; otherwise id, name and the
+23 -11
View File
@@ -163,13 +163,21 @@ export interface DecisionRef {
/** Collections whose items may carry `decisions` (and `diagram` links), per diagram kind. */
const NODE_COLLECTIONS = ['components', 'nodes', 'participants', 'states'] as const;
/** The list that holds a diagram kind's nodes, where `ref` is resolved. */
const NODE_COLLECTION_OF: Record<string, (typeof NODE_COLLECTIONS)[number]> = {
architecture: 'components',
workflow: 'nodes',
dataflow: 'nodes',
sequence: 'participants',
lifecycle: 'states',
};
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;
problems: ModelProblem[];
/** Component index → the registry element it came from. */
/** Node index (in the kind's node list) → the registry element it came from. */
refs: Map<number, RegistryElement>;
/** Node id → diagram path (relative to the model root) it links to. */
links: Map<string, string>;
@@ -253,10 +261,14 @@ export function resolveRefs(doc: unknown, options: ResolveOptions): ResolvedDiag
delete diagram.model;
const decisionRefs = takeDecisionRefs(diagram, options, problems);
const { registry } = options;
const components = Array.isArray(diagram.components) ? diagram.components : undefined;
if (!components) return { diagram, problems, refs, links, used, descriptions, decisionRefs };
// Each kind keeps its nodes in its own list; sequence participants have no tag.
const kind = str(diagram.diagram_type);
const collection = (kind && NODE_COLLECTION_OF[kind]) ?? NODE_COLLECTIONS.find((key) => Array.isArray(diagram[key]));
const nodes = collection && Array.isArray(diagram[collection]) ? (diagram[collection] as unknown[]) : undefined;
if (!collection || !nodes) return { diagram, problems, refs, links, used, descriptions, decisionRefs };
const copied: readonly string[] = kind === 'sequence' ? COPIED_FIELDS.filter((key) => key !== 'tag') : COPIED_FIELDS;
diagram.components = components.map((raw, index) => {
diagram[collection] = nodes.map((raw, index) => {
if (!isObj(raw)) return raw;
const ref = str(raw.ref);
const linkFromComponent = str(raw.diagram);
@@ -275,7 +287,7 @@ export function resolveRefs(doc: unknown, options: ResolveOptions): ResolvedDiag
? `Unknown element "${ref}": no registry of this model defines it.`
: `"${ref}" cannot be resolved: ${options.fileName.split(/[\\/]/).pop()} is not inside a model (no ${MAIN_FILE} above it).`,
severity: 'error',
pointer: `/components/${index}/ref`,
pointer: `/${collection}/${index}/ref`,
});
return { type: 'external', label: ref, ...own, id };
}
@@ -283,19 +295,19 @@ export function resolveRefs(doc: unknown, options: ResolveOptions): ResolvedDiag
refs.set(index, element);
if (element.description) descriptions.set(id, element.description.trim());
for (const key of Object.keys(own)) {
if ((COPIED_FIELDS as readonly string[]).includes(key) && !(OVERRIDABLE_FIELDS as readonly string[]).includes(key)) {
if (copied.includes(key) && !(OVERRIDABLE_FIELDS as readonly string[]).includes(key)) {
problems.push({
message: `"${key}" comes from the registry element "${ref}" and cannot be changed in a diagram.`,
severity: 'error',
pointer: `/components/${index}/${key}`,
pointer: `/${collection}/${index}/${key}`,
});
}
}
const merged: Obj = { id };
for (const key of COPIED_FIELDS) if (element[key] !== undefined) merged[key] = element[key];
for (const key of copied) if (element[key as (typeof COPIED_FIELDS)[number]] !== undefined) merged[key] = element[key as (typeof COPIED_FIELDS)[number]];
for (const [key, value] of Object.entries(own)) {
if (key === 'id') continue;
if (!(COPIED_FIELDS as readonly string[]).includes(key) || (OVERRIDABLE_FIELDS as readonly string[]).includes(key)) merged[key] = value;
if (!copied.includes(key) || (OVERRIDABLE_FIELDS as readonly string[]).includes(key)) merged[key] = value;
}
const link = linkFromComponent ?? element.diagram;
if (link) links.set(id, link);
@@ -353,8 +365,8 @@ export function summarizeDiagram(text: string, relativePath: string): DiagramSum
}
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))];
const items = NODE_COLLECTIONS.flatMap((key) => (Array.isArray(obj[key]) ? (obj[key] as unknown[]) : []));
const used = [...new Set(items.map((c) => (isObj(c) ? str(c.ref) : undefined)).filter((r): r is string => !!r))];
const decisions = takeDecisionRefs({ ...obj }, { registry: { elements: new Map(), files: [], problems: [] }, fileName: relativePath }, []);
const nodes: DiagramNode[] = [];
const seen = new Set<string>();