Compare commits

..
Author SHA1 Message Date
root-at-skic efc4078e83 README: link to the articles on Medium 2026-11-09 00:03:42 +02:00
root-at-skic ef251352c6 ArchiMate examples: cross-layer labels in the gaps (extension 0.11.2) 2026-11-08 23:35:34 +02:00
root-at-skic 900e0986e6 arc42 and ArchiMate layouts in the folder guide; arc42 examples cover all sections; 1.3
references/folders.md gains arc42 (sections as numbered folders and cards,
black boxes, quality tree) and ArchiMate (layers, type in tag and
sublabel, element mapping). Gotchas: ref in every kind needs extension
0.11.0; tags are not drawn on the box.
2026-11-08 23:11:46 +02:00
root-at-skic d9777fb90e arc42 and ArchiMate examples: nine models, three per framework; 1.2
The six models from extension 0.11.0 (arc42 by section, ArchiMate by
layer) next to the three C4 ones; model:scaffold --example offers them
and the tests validate each with 0 problems.
2026-11-08 22:57:00 +02:00
root-at-skic 21770c241d README: coffee button at the top and in Support 2026-11-08 22:08:51 +02:00
root-at-skic f9bc461361 Extension 0.9.2: Source view checks keys model:validate does not 2026-10-28 22:42:51 +02:00
root-at-skic 887ca973a0 model:graph: the model as one graph with the Graph page's queries; 1.1
- services/graph.mjs builds the union graph (elements, connections, decision and registry nodes) from the loaded model
- queries: text, type:, diagram:, decision:, uses:, near:, from: to:, unused, shared, -shared; walks follow connections only
- task model:graph with --query and --links; three suite checks; SKILL.md task row and gotcha
2026-10-21 23:08:17 +03:00
root-at-skic 6daf70fef3 README: marketplace moved to vscode-extensions-skills 2026-10-20 22:20:47 +03:00
root-at-skic 35adfb480c Merge pull request 'Claude Code plugin manifest' (#1) from feat/claude-plugin into main 2026-10-20 12:11:13 -07:00
86 changed files with 3560 additions and 11 deletions

No files matched your search

+9 -2
View File
@@ -1,7 +1,11 @@
# archify-vscode-ext
[![Buy me a coffee](https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png)](https://buymeacoffee.com/root.at.skic)
Agent Skill for Archify diagrams and models as read by the [Archify Diagram Viewer](https://marketplace.visualstudio.com/items?itemName=root-at-skic.archify-vscode-ext) VS Code extension: how to write `.archify` diagrams (architecture, workflow, sequence, dataflow, lifecycle), how to put them together as a model (`main.archify`, shared elements in `*.archify-registry` files, `ref:` components, `diagram:` links, C4-style folders), and tasks that validate, list, scaffold, resolve and render them. The Archify validator and renderer (MIT, by tt-a1i) are bundled under `scripts/vendor`, so nothing else needs to be installed.
How the skill and an agent keep a model together, with examples: [articles on Medium](https://medium.com/@root.at.skic).
## Install and update
One command, any agent (Claude Code, Codex, Copilot, Cline, …):
@@ -14,7 +18,7 @@ npx skills update --global
In Claude Code, the same skill is also a plugin of the `root-at-skic` marketplace. Every commit on `main` is a new version, so an update always brings the latest; switch on auto-update for the marketplace and it arrives on its own:
```
/plugin marketplace add https://gitea.lego-cloud.eu/global-skills/marketplace.git
/plugin marketplace add https://gitea.lego-cloud.eu/vscode-extensions-skills/marketplace.git
/plugin install archify-vscode-ext@root-at-skic
```
@@ -26,6 +30,7 @@ Quick start:
cd scripts
task model:scaffold -- --dir="/path/to/online-shop" --id="online-shop" --name="Online Shop" --c4
task model:validate -- --dir="/path/to/online-shop"
task model:graph -- --dir="/path/to/online-shop" --query="from:web to:db"
task diagram:render -- --file="/path/to/online-shop/0200-containers/online-shop.archify" --out="/tmp/containers.html"
```
@@ -35,4 +40,6 @@ Requires Node.js 18+, bash and [go-task](https://taskfile.dev). The skill target
## Support
If this saves you time, you can buy me a coffee: https://buymeacoffee.com/root.at.skic
If this saves you time, a coffee keeps it going:
[![Buy me a coffee](https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png)](https://buymeacoffee.com/root.at.skic)
+9 -5
View File
@@ -15,7 +15,7 @@ license: MIT
compatibility: Requires Node.js 18+, bash and go-task (task). No network access; Archify is bundled under scripts/vendor.
metadata:
author: root-at-skic
version: "1.0"
version: "1.3"
origin-repository: https://gitea.lego-cloud.eu/vscode-extensions-skills/archify-vscode-ext.git
targets: Archify Diagram Viewer (root-at-skic.archify-vscode-ext)
---
@@ -39,8 +39,9 @@ task --list-all
|---|---|
| `task model:validate -- --dir="<model or file inside>"` | Registry, every diagram (refs resolved, Archify validator), every `diagram:` link. Exit 1 on any error. Works on a single diagram outside a model too. |
| `task model:list -- --dir="<model>"` | What the model page shows: metadata, diagrams by folder, registry elements with "used in N diagrams", decisions with status and the diagrams that reference them. |
| `task model:graph -- --dir="<model>" [--query="uses:api"] [--links]` | The model as one graph, as the extension's **Graph** page shows it: every element, every connection once, plus a node per decision and per registry file. `--query` narrows it with the page's language: `database`, `type:backend`, `diagram:0200-*`, `decision:<id>`, `uses:api`, `near:api,2`, `from:web to:db` (shortest path), `unused`, `shared`, `-shared`; terms AND together, a leading `-` excludes. Use it to answer "what talks to X", "how does A reach B", "what does this decision touch", "which registry elements are unused". |
| `task model:scaffold -- --dir="<folder>" --id="<id>" --name="<name>" [--description="…"] [--c4]` | Creates `main.archify`; with `--c4` also `registry/` and the `0100-context`, `0200-containers`, `0300-components` folders with starter diagrams. Never overwrites. |
| `task model:scaffold -- --dir="<folder>" --example="<name>" [--id="<id>"] [--name="<name>"] [--description="…"]` | Copies a complete example model: `c4-standard-web-app`, `c4-e-shop` or `c4-event-based-integration` (the same three the extension offers under *Archify \| New Model…*). Id, name and description default to the example's own. Never overwrites. |
| `task model:scaffold -- --dir="<folder>" --example="<name>" [--id="<id>"] [--name="<name>"] [--description="…"]` | Copies a complete example model: C4 `c4-standard-web-app`, `c4-e-shop`, `c4-event-based-integration`; arc42 `arc42-cinema-tickets`, `arc42-iot-telemetry`, `arc42-team-wiki`; ArchiMate `archimate-insurance-claims`, `archimate-hr-onboarding`, `archimate-logistics-tracking` (the same nine the extension offers under *Archify \| New Model…*). Id, name and description default to the example's own. Never overwrites. |
| `task model:resolve -- --file="<diagram>" [--out="<file.json>"]` | The Archify JSON the extension hands to Archify, for the plain Archify CLI or CI. |
| `task diagram:validate -- --file="<diagram>"` | One diagram through Archify (refs resolved when inside a model). |
| `task diagram:render -- --file="<diagram>" --out="<file.html>"` | Standalone HTML page for people without VS Code. |
@@ -174,8 +175,11 @@ Diagrams next to `main.archify` are listed first, ungrouped; deeper ones by fold
- **Boxes need clear gaps.** Components closer than 8px fail; labels wider than the box fail (`6.5px` per character plus 13). The grid in `references/authoring.md` keeps a standard layout clear.
- **Links are relative to the model root**, never to the file, and must end in `.archify` and exist. `..` is refused.
- **A second `main.archify` deeper down** is reported, not nested. One model per tree.
- **Diagram kinds other than architecture** take `ref` too (workflow and dataflow `nodes`, sequence `participants`, lifecycle `states`), but the registry `type` must fit that kind: lifecycle states use their own type enum, and sequence participants have no `tag`.
- **Diagram kinds other than architecture** take `ref` too (workflow and dataflow `nodes`, sequence `participants`, lifecycle `states`), but the registry `type` must fit that kind: lifecycle states use their own type enum, and sequence participants have no `tag`. The extension resolves these since 0.11.0; older versions showed such a diagram with schema errors.
- **Tags are not drawn on the box.** Archify shows `tag` only in the focus panel and search. When a type must be visible (an ArchiMate element type, a C4 level), put it in `sublabel` too.
- **Decisions are files, ids are words.** One `*.archify-decision` per decision (`decision:` block with id, title, status, date, elements, diagrams, supersedes; prose in context, decision, consequences). Diagrams point at them with `decisions: [<id>]` on the diagram, a node or an edge; the resolver strips the key. An unquoted numeric id (`0001`) is refused because YAML reads it as 1. When an agent changes the shape of a model (a new store, a new queue, a new boundary), it writes a decision and links the elements it touched.
- **The graph answers structure questions.** Before guessing from file contents, run `task model:graph` with a query: `uses:<id>` for neighbours, `from:<a> to:<b>` for the route, `decision:<id>` for what a decision touches, `unused` for registry elements no diagram draws. Walks follow the diagrams' connections only; decision and registry nodes follow the elements they link to. The extension's Graph page (the **Model | Graph | Source** switch on `main.archify`) shows the same graph with the same queries.
- **The extension checks keys the skill does not.** Since extension 0.9.2 every Archify page has a **Source** view (Diagram | Graph | Source, View | Source on registries and decisions) that checks the YAML against the bundled JSON schemas without a YAML extension: an unknown key in a registry element (`lable:`) or a diagram is an error there. `task model:validate` does not check registry keys yet and passes such a file, so read registry edits back for typos. When a user says "the Source view shows an error", the message names the key and path (`Unknown key "lable" in elements.api.`).
- **Descriptions stay in the registry.** The extension shows an element's description as a tooltip in its Elements panel; nothing is generated into the diagram. Write `cards:` in a diagram only when you want text blocks below it.
- **No em dashes or smart quotes in ids**; ids match `^[a-zA-Z][a-zA-Z0-9_-]*$`.
@@ -183,5 +187,5 @@ Diagrams next to `main.archify` are listed first, ungrouped; deeper ones by fold
- Read `references/authoring.md` when writing or repairing a diagram: kinds, fields, grid, labels, boundaries, brands, validator codes and fixes.
- Read `references/formats.md` when writing `main.archify`, a registry, a `ref` or a link, or when unsure what the tasks strip before Archify runs.
- Read `references/folders.md` when laying out a model folder or explaining how the extension finds the root and groups diagrams.
- `examples/c4-standard-web-app/`, `examples/c4-e-shop/` and `examples/c4-event-based-integration/` are complete C4-style models that validate with 0 problems; copy their shape, or copy one with `model:scaffold --example`.
- Read `references/folders.md` when laying out a model folder or explaining how the extension finds the root and groups diagrams; it has the C4, arc42 and ArchiMate layouts.
- `examples/` holds nine complete models that validate with 0 problems; copy their shape, or copy one with `model:scaffold --example`. Three C4 (`c4-*`: context, containers, components). Three arc42 (`arc42-*`: the overview is section 3 business context with goals, constraints, quality, risks and glossary as `cards`; `0300-context`, `0500-building-blocks`, `0600-runtime` sequence diagrams, `0700-deployment`; decisions are section 9). Three ArchiMate (`archimate-*`: the overview is a layered view in three region boundaries; `0100-business`, `0200-application`, `0300-technology`; one registry file per layer; each element's ArchiMate type as its `tag` and `sublabel`, since Archify shows tags only in the focus panel).
@@ -0,0 +1,39 @@
schema_version: 1
diagram_type: architecture
meta:
title: Technical context
output: technical-context.html
components:
- ref: customer
pos: [40, 60]
size: [130, 60]
- ref: usher
pos: [40, 260]
size: [130, 60]
- ref: tickets
pos: [290, 160]
size: [150, 60]
- ref: psp
pos: [560, 60]
size: [150, 60]
- ref: mail
pos: [560, 160]
size: [150, 60]
- ref: pos
pos: [560, 260]
size: [150, 60]
connections:
- { from: customer, to: tickets, label: HTTPS JSON, variant: emphasis }
- { from: usher, to: tickets, label: HTTPS app }
- { from: tickets, to: psp, label: REST webhook, variant: security }
- { from: tickets, to: mail, label: SMTP TLS }
- { from: tickets, to: pos, label: REST VPN }
cards:
- dot: cyan
title: 3 Domain interfaces
items:
- "Moviegoer: in shows, seats, payment start; out seat map, QR tickets"
- "Usher: in scanned QR codes; out valid or invalid, seat"
- "Payment provider: out checkout session; in paid or failed webhook"
- "E-mail service: out ticket e-mails with QR and receipt"
- "Box-office POS: in counter sales; out seat availability"
@@ -0,0 +1,49 @@
schema_version: 1
diagram_type: architecture
meta:
title: Booking API - level 2
output: booking.html
decisions: [seat-holds-with-ttl]
components:
- ref: web
pos: [40, 160]
size: [130, 60]
- id: programme
type: backend
label: Programme
sublabel: shows, halls
pos: [250, 60]
size: [130, 60]
- id: seats
type: backend
label: Seat Holds
sublabel: lock, release
pos: [250, 160]
size: [130, 60]
- id: checkout
type: backend
label: Checkout
sublabel: orders, webhook
pos: [250, 260]
size: [130, 60]
- ref: seatlock
pos: [460, 160]
size: [130, 60]
decisions: [seat-holds-with-ttl]
- ref: db
pos: [460, 60]
size: [130, 60]
- ref: psp
pos: [460, 360]
size: [150, 60]
boundaries:
- kind: region
label: Booking API
wraps: [programme, seats, checkout]
connections:
- { from: web, to: programme, label: browse }
- { from: web, to: seats, label: hold, variant: emphasis }
- { from: web, to: checkout, label: pay }
- { from: programme, to: db, label: SQL }
- { from: seats, to: seatlock, label: SET NX, decisions: [seat-holds-with-ttl] }
- { from: checkout, to: psp, label: session, variant: security }
@@ -0,0 +1,66 @@
schema_version: 1
diagram_type: architecture
meta:
title: Building blocks - level 1
output: level-1.html
decisions: [booking-owns-seat-inventory]
components:
- ref: web
pos: [40, 110]
size: [130, 60]
- ref: mobile
pos: [40, 210]
size: [130, 60]
- ref: scanner
pos: [40, 360]
size: [130, 60]
- ref: booking
pos: [250, 160]
size: [130, 60]
- ref: seatlock
pos: [460, 60]
size: [130, 60]
- ref: db
pos: [460, 260]
size: [130, 60]
- ref: ticketing
pos: [250, 360]
size: [130, 60]
- ref: possync
pos: [670, 260]
size: [130, 60]
- ref: psp
pos: [900, 160]
size: [150, 60]
- ref: pos
pos: [900, 260]
size: [150, 60]
- ref: mail
pos: [900, 460]
size: [150, 60]
boundaries:
- kind: region
label: Cinema Tickets
wraps: [web, mobile, scanner, booking, seatlock, db, ticketing, possync]
connections:
- { from: web, to: booking, label: JSON, variant: emphasis }
- { from: mobile, to: booking, label: JSON }
- { from: booking, to: seatlock, label: hold }
- { from: booking, to: db, label: SQL }
- { from: booking, to: psp, label: checkout, variant: security }
- { from: booking, to: ticketing, label: issue, labelAt: [315, 234] }
- { from: scanner, to: ticketing, label: check-in }
- { from: ticketing, to: db, label: SQL }
- { from: ticketing, to: mail, label: QR mail }
- { from: possync, to: db, label: SQL }
- { from: possync, to: pos, label: sales, decisions: [booking-owns-seat-inventory] }
cards:
- dot: amber
title: 8 Crosscutting concepts
items:
- "Persistence: PostgreSQL is the truth; Redis only for holds"
- "Security: OIDC for staff apps; card data never stored"
- "Errors: idempotency keys; failed webhooks are retried"
- "Times in UTC; shown in cinema local time"
- "QR payload signed with an Ed25519 key"
- "Structured logs with order id as trace key"
@@ -0,0 +1,27 @@
schema_version: 1
diagram_type: sequence
meta:
title: Runtime - book seats
output: book-seats.html
viewBox: [1060, 680]
column_fit: spread
decisions: [seat-holds-with-ttl]
participants:
- { ref: customer }
- { ref: web }
- { ref: booking }
- { ref: seatlock }
- { ref: psp }
- { ref: ticketing }
- { ref: mail }
messages:
- { from: customer, to: web, y: 170, label: pick seats, variant: emphasis }
- { from: web, to: booking, y: 214, label: POST /holds }
- { from: booking, to: seatlock, y: 258, label: SET NX TTL 8m, decisions: [seat-holds-with-ttl] }
- { from: seatlock, to: booking, y: 302, label: OK, variant: return }
- { from: booking, to: web, y: 346, label: hold id, variant: return }
- { from: web, to: psp, y: 390, label: hosted checkout, variant: security }
- { from: psp, to: booking, y: 434, label: webhook paid, variant: security }
- { from: booking, to: ticketing, y: 478, label: issue tickets }
- { from: ticketing, to: mail, y: 522, label: QR e-mail }
- { from: mail, to: customer, y: 566, label: tickets, variant: return }
@@ -0,0 +1,19 @@
schema_version: 1
diagram_type: sequence
meta:
title: Runtime - check-in at the door
output: check-in.html
viewBox: [900, 480]
column_fit: spread
decisions: [signed-qr-codes]
participants:
- { ref: usher }
- { ref: scanner }
- { ref: ticketing }
- { ref: db }
messages:
- { from: usher, to: scanner, y: 180, label: scan QR, variant: emphasis, note: signature checked offline, decisions: [signed-qr-codes] }
- { from: scanner, to: ticketing, y: 230, label: POST /checkins }
- { from: ticketing, to: db, y: 280, label: first scan wins }
- { from: ticketing, to: scanner, y: 330, label: valid seat 7F, variant: return }
- { from: scanner, to: usher, y: 380, label: green light, variant: return }
@@ -0,0 +1,74 @@
schema_version: 1
diagram_type: architecture
meta:
title: Deployment - production
output: production.html
components:
- ref: customer
pos: [40, 260]
size: [130, 60]
- ref: mobile
pos: [40, 60]
size: [130, 60]
- id: ingress
type: security
label: Ingress
sublabel: TLS, WAF
pos: [250, 160]
size: [130, 60]
- ref: web
pos: [460, 60]
size: [130, 60]
- ref: booking
pos: [460, 160]
size: [130, 60]
- ref: ticketing
pos: [460, 260]
size: [130, 60]
- ref: possync
pos: [460, 360]
size: [130, 60]
- ref: seatlock
pos: [710, 110]
size: [130, 60]
- ref: db
pos: [710, 260]
size: [130, 60]
- ref: pos
pos: [250, 460]
size: [150, 60]
- ref: scanner
pos: [40, 460]
size: [130, 60]
boundaries:
- kind: region
label: EU cloud - k8s cluster
wraps: [ingress, web, booking, ticketing, possync]
- kind: security-group
label: Managed data zone
wraps: [seatlock, db]
- kind: region
label: Customer phones
wraps: [mobile]
- kind: region
label: Cinema site
wraps: [pos, scanner]
connections:
- { from: customer, to: ingress, label: HTTPS, variant: emphasis }
- { from: ingress, to: web, label: route }
- { from: ingress, to: booking, label: route }
- { from: booking, to: seatlock, label: TLS }
- { from: booking, to: db, label: TLS }
- { from: ticketing, to: db, label: TLS }
- { from: possync, to: pos, label: VPN, variant: security }
- { from: scanner, to: ingress, label: HTTPS }
- { from: mobile, to: ingress, label: HTTPS }
cards:
- dot: slate
title: 7 Mapping
items:
- "Web Shop, Booking API, Ticket Service, POS Sync -> EU k8s cluster"
- "Seat Locks -> managed Redis; Tickets DB -> managed PostgreSQL"
- "Mobile App -> customer phones"
- "Scanner App -> door phones at the cinema site"
- "Box-office POS -> cinema site, VPN to POS Sync"
@@ -0,0 +1,75 @@
schema_version: 1
diagram_type: architecture
meta:
title: Quality tree
output: quality-tree.html
components:
- id: quality
type: cloud
label: Quality
pos: [40, 260]
size: [130, 60]
- id: reliability
type: backend
label: Reliability
pos: [250, 60]
size: [130, 60]
- id: performance
type: backend
label: Performance
pos: [250, 210]
size: [130, 60]
- id: availability
type: backend
label: Availability
pos: [250, 360]
size: [130, 60]
- id: security
type: security
label: Security
pos: [250, 460]
size: [130, 60]
- id: s-nodouble
type: external
label: No double sale
pos: [460, 10]
size: [150, 60]
- id: s-payfail
type: external
label: Failed pay frees seats
pos: [460, 110]
size: [170, 60]
- id: s-peak
type: external
label: Checkout < 2 s at peak
pos: [460, 210]
size: [170, 60]
- id: s-offline
type: external
label: Offline door scans
pos: [460, 360]
size: [170, 60]
- id: s-forged
type: external
label: Forged QR rejected
pos: [460, 460]
size: [170, 60]
connections:
- { from: quality, to: reliability }
- { from: quality, to: performance }
- { from: quality, to: availability }
- { from: quality, to: security }
- { from: reliability, to: s-nodouble }
- { from: reliability, to: s-payfail }
- { from: performance, to: s-peak }
- { from: availability, to: s-offline }
- { from: security, to: s-forged }
cards:
- dot: emerald
title: 10 Quality scenarios
items:
- "500 holds on one show in 1 min: no seat sold twice"
- "Payment fails or is abandoned: seats free again within 8 min"
- "Friday 19:00 peak, 300 checkouts/min: p95 under 2 s"
- "Wi-Fi down at the door: scans go on, check-ins sync later"
- "Edited or forged QR code scanned: rejected at once"
@@ -0,0 +1,20 @@
schema_version: 1
decision:
id: seat-holds-with-ttl
title: Seat holds as Redis keys with a TTL
status: accepted
date: 2026-09-08
elements: [booking, seatlock]
diagrams: [0500-building-blocks/booking.archify, 0600-runtime/book-seats.archify]
context: >
A customer may spend minutes at the payment page. Holding seats
as rows in the database left orphans when a browser closed, and
a nightly job freed them far too late for Friday shows.
decision: >
A hold is one Redis key per seat, set with SET NX and an
8-minute TTL. The payment webhook turns holds into an order in
PostgreSQL; an expired key frees the seat without any job.
consequences: >
Holds survive no Redis restart; at worst a customer re-picks
seats. The webhook must check the hold still exists before it
confirms the order, otherwise it refunds.
@@ -0,0 +1,20 @@
schema_version: 1
decision:
id: booking-owns-seat-inventory
title: The Booking API owns the seat inventory
status: accepted
date: 2026-09-22
elements: [booking, db, possync, pos]
diagrams: [0500-building-blocks/level-1.archify]
context: >
The box-office POS and the web shop both sold seats from their
own copy of the seat map. Twice a month a seat was sold at the
counter and online.
decision: >
The Tickets DB is the only seat inventory. The POS asks POS Sync
for a seat before it prints a ticket; POS Sync writes the sale
through the same rules as online orders.
consequences: >
The counter needs the network to sell; a fallback block of
reserved seats per show covers outages. The POS vendor adapter
is ours to maintain.
@@ -0,0 +1,19 @@
schema_version: 1
decision:
id: signed-qr-codes
title: Signed QR codes checked offline at the door
status: accepted
date: 2026-10-06
elements: [ticketing, scanner]
diagrams: [0600-runtime/check-in.archify]
context: >
Cinema Wi-Fi drops when a full hall arrives. A scanner that
needs the server for every ticket stops the queue.
decision: >
The Ticket Service signs each QR payload (ticket id, show,
seat) with a private key. The scanner app verifies it with the
public key and queues check-ins until it is online again.
consequences: >
A forwarded ticket scans twice while offline; the second scan
is flagged when the queue syncs. Key rotation needs an app
update window.
@@ -0,0 +1,91 @@
schema_version: 1
model:
id: cinema-tickets
name: Cinema Tickets
description: >
An arc42 example: seat booking for the fictional Lantern Cinemas.
1 Requirements, quality goals, stakeholders; 2 Constraints;
4 Solution strategy; 11 Risks; 12 Glossary: cards on this overview.
3 Context: this overview (business) and 0300-context (technical).
5 Building blocks: 0500-building-blocks, black boxes in registry/.
6 Runtime: 0600-runtime. 7 Deployment: 0700-deployment.
8 Crosscutting concepts: card on 0500-building-blocks/level-1.
9 Decisions: decisions/. 10 Quality: 1000-quality.
diagram_type: architecture
meta:
title: Cinema Tickets - business context
output: main.html
components:
- ref: customer
pos: [40, 60]
size: [130, 60]
- ref: usher
pos: [40, 260]
size: [130, 60]
- ref: tickets
pos: [280, 160]
size: [150, 60]
- ref: psp
pos: [540, 60]
size: [150, 60]
- ref: mail
pos: [540, 160]
size: [150, 60]
- ref: pos
pos: [540, 260]
size: [150, 60]
connections:
- { from: customer, to: tickets, label: books seats, variant: emphasis }
- { from: usher, to: tickets, label: checks in }
- { from: tickets, to: psp, label: payments, variant: security }
- { from: tickets, to: mail, label: tickets }
- { from: tickets, to: pos, label: seat sales }
cards:
- dot: orange
title: 1.1 Requirements
items:
- "Browse shows and pick seats on a seat map"
- "Hold seats, pay online, get QR tickets by e-mail"
- "Ushers check tickets in at the door"
- "Box-office POS sells from the same seats"
- dot: cyan
title: 1.2 Quality goals
items:
- "No seat is ever sold twice"
- "Checkout under 2 s at Friday peak"
- "Door check-in works offline"
- dot: amber
title: 1.3 Stakeholders
items:
- "Moviegoer: quick booking, tickets on the phone"
- "Usher: fast scans even without Wi-Fi"
- "Box-office staff: one seat map with online sales"
- "Lantern Cinemas management: fewer refunds, low cost"
- dot: slate
title: 2 Constraints
items:
- "Existing box-office POS stays"
- "Card data only at the provider"
- "Team of four; hosted in the EU"
- dot: emerald
title: 4 Solution strategy
items:
- "Node.js, PostgreSQL, Redis on managed EU k8s: fits a team of four"
- "Three services by job: booking, tickets, POS sync"
- "No double sale: Booking API sole seat owner (booking-owns-seat-inventory)"
- "Checkout speed: Redis holds with TTL (seat-holds-with-ttl)"
- "Offline check-in: signed QR codes (signed-qr-codes)"
- dot: rose
title: 11 Risks and technical debt
items:
- "Risk: POS vendor API is slow and undocumented"
- "Risk: forwarded QR e-mails, first scan wins"
- "Debt: POS Sync adapter has no automated tests"
- "Debt: QR key rotation needs an app release"
- dot: violet
title: 12 Glossary
items:
- "Show: one screening of a film in a hall"
- "Hold: a seat locked for one customer"
- "Check-in: first valid scan of a ticket"
- "POS: point-of-sale till at the box office"
@@ -0,0 +1,72 @@
schema_version: 1
elements:
web:
type: frontend
label: Web Shop
sublabel: React
brand: react
description: >
Responsibility: programme, seat map and checkout in the browser.
Interfaces: provides the web UI; requires Booking API (JSON)
and the provider's hosted checkout page.
mobile:
type: frontend
label: Mobile App
sublabel: iOS and Android
description: >
Responsibility: the web shop's booking flow plus a ticket wallet.
Interfaces: provides the iOS/Android UI; requires Booking API
(JSON).
scanner:
type: frontend
label: Scanner App
sublabel: door phones
description: >
Responsibility: reads QR codes at the door, checks signatures
offline. Interfaces: provides the usher UI; requires Ticket
Service (POST /checkins), queued while offline.
booking:
type: backend
label: Booking API
sublabel: Node.js
brand: node-js
description: >
Responsibility: shows, seat maps, holds and orders; the only
writer of the seat inventory. Interfaces: provides REST JSON to
the apps and the payment webhook; requires Seat Locks, Tickets
DB, payment provider, Ticket Service.
diagram: 0500-building-blocks/booking.archify
ticketing:
type: backend
label: Ticket Service
sublabel: QR, check-in
description: >
Responsibility: issues signed QR tickets, mails them and records
check-ins. Interfaces: provides issue and POST /checkins;
requires Tickets DB and the e-mail service (SMTP).
possync:
type: backend
label: POS Sync
sublabel: adapter
description: >
Responsibility: exchanges seat sales with the box-office POS.
Interfaces: provides a REST adapter to the POS over VPN;
requires Tickets DB (SQL).
seatlock:
type: database
label: Seat Locks
sublabel: Redis
brand: redis
description: >
Responsibility: short-lived seat holds with a TTL while the
customer pays. Interfaces: provides Redis SET NX/GET/DEL to the
Booking API; requires nothing.
db:
type: database
label: Tickets DB
sublabel: PostgreSQL
brand: postgresql
description: >
Responsibility: shows, seats, orders and tickets; the source of
truth. Interfaces: provides SQL to Booking API, Ticket Service
and POS Sync; requires nothing.
@@ -0,0 +1,14 @@
schema_version: 1
elements:
customer:
type: external
label: Moviegoer
sublabel: web and mobile
description: >
Picks a show, holds seats and pays; gets the tickets by e-mail.
usher:
type: external
label: Usher
sublabel: door staff
description: >
Scans tickets at the auditorium door with the scanner app.
@@ -0,0 +1,30 @@
schema_version: 1
elements:
tickets:
type: cloud
label: Cinema Tickets
sublabel: the system
description: >
Online booking, seat holds, e-mail tickets and door check-in
for the five cinemas of the fictional Lantern Cinemas chain.
diagram: 0500-building-blocks/level-1.archify
psp:
type: external
label: Payment provider
sublabel: hosted checkout
description: >
Takes card and wallet payments on its own page; we never see
card numbers.
pos:
type: external
label: Box-office POS
sublabel: in each cinema
description: >
Till system at the counter; sells walk-in seats from the same
seat map.
mail:
type: external
label: E-mail service
sublabel: transactional
description: >
Delivers ticket e-mails with QR codes and receipts.
@@ -0,0 +1,45 @@
schema_version: 1
diagram_type: architecture
meta:
title: Technical context
output: technical-context.html
decisions: [mqtt-with-mutual-tls]
components:
- ref: homeowner
pos: [40, 60]
size: [130, 60]
- ref: ops-engineer
pos: [40, 160]
size: [130, 60]
- ref: installer
pos: [40, 260]
size: [130, 60]
- ref: telemetry-platform
pos: [280, 160]
size: [150, 60]
- ref: heat-pump
pos: [540, 60]
size: [130, 60]
- ref: push-service
pos: [540, 160]
size: [130, 60]
- ref: service-desk
pos: [540, 260]
size: [130, 60]
connections:
- { from: homeowner, to: telemetry-platform, label: HTTPS }
- { from: ops-engineer, to: telemetry-platform, label: HTTPS, variant: security }
- { from: installer, to: telemetry-platform, label: HTTPS }
- { from: heat-pump, to: telemetry-platform, label: MQTT 8883, variant: emphasis, decisions: [mqtt-with-mutual-tls] }
- { from: telemetry-platform, to: push-service, label: HTTP/2 }
- { from: telemetry-platform, to: service-desk, label: webhook }
cards:
- dot: cyan
title: 3 Domain interfaces
items:
- "Heat pump: in telemetry and fault codes; out none"
- "Homeowner: in app requests; out status, history, alerts"
- "Ops engineer: in queries; out fleet dashboards"
- "Installer: in device serial; out device certificate"
- "Push service: out alert notifications"
- "Service desk: out tickets for faults needing a visit"
@@ -0,0 +1,50 @@
schema_version: 1
diagram_type: architecture
meta:
title: Ingest, level 2
output: ingest.html
components:
- ref: mqtt-broker
pos: [40, 160]
size: [130, 60]
- id: decoder
type: backend
label: Decoder
sublabel: payload v1, v2
pos: [250, 160]
size: [130, 60]
- id: validator
type: backend
label: Validator
sublabel: ranges, clock
pos: [460, 160]
size: [130, 60]
- id: writer
type: backend
label: Batch writer
sublabel: 500 rows
pos: [670, 160]
size: [130, 60]
- id: dead-letter
type: database
label: Dead letters
sublabel: rejected readings
pos: [460, 310]
size: [130, 60]
- ref: tsdb
pos: [880, 160]
size: [130, 60]
- ref: rules
pos: [670, 310]
size: [130, 60]
boundaries:
- kind: region
label: Ingest
wraps: [decoder, validator, writer, dead-letter]
connections:
- { from: mqtt-broker, to: decoder, label: messages, variant: emphasis }
- { from: decoder, to: validator, label: readings, variant: emphasis }
- { from: validator, to: writer, label: valid, variant: emphasis }
- { from: validator, to: dead-letter, label: invalid, variant: dashed, labelAt: [525, 234] }
- { from: writer, to: tsdb, label: COPY, variant: emphasis }
- { from: writer, to: rules, label: stream, labelAt: [735, 234] }
@@ -0,0 +1,80 @@
schema_version: 1
diagram_type: architecture
meta:
title: Building blocks, level 1
output: level-1.html
decisions: [timescaledb-for-telemetry]
components:
- ref: heat-pump
pos: [40, 160]
size: [130, 60]
- ref: installer
pos: [40, 360]
size: [130, 60]
- ref: mqtt-broker
pos: [250, 160]
size: [130, 60]
decisions: [mqtt-with-mutual-tls]
- ref: provisioning
pos: [250, 360]
size: [130, 60]
- ref: rules
pos: [460, 60]
size: [130, 60]
- ref: ingest
pos: [460, 160]
size: [130, 60]
- ref: device-registry
pos: [460, 360]
size: [130, 60]
- ref: tsdb
pos: [670, 160]
size: [130, 60]
- ref: device-api
pos: [670, 360]
size: [130, 60]
- ref: ops-dashboard
pos: [880, 160]
size: [130, 60]
- ref: customer-app
pos: [880, 360]
size: [130, 60]
- ref: push-service
pos: [1090, 60]
size: [130, 60]
- ref: ops-engineer
pos: [1090, 160]
size: [130, 60]
- ref: homeowner
pos: [1090, 360]
size: [130, 60]
boundaries:
- kind: region
label: IoT Telemetry
wraps: [mqtt-broker, provisioning, rules, ingest, device-registry, tsdb, device-api, ops-dashboard, customer-app]
connections:
- { from: heat-pump, to: mqtt-broker, label: publish, variant: emphasis, decisions: [mqtt-with-mutual-tls] }
- { from: mqtt-broker, to: ingest, label: subscribe, variant: emphasis }
- { from: ingest, to: tsdb, label: write, variant: emphasis }
- { from: ingest, to: rules, label: stream }
- { from: ingest, to: device-registry, label: lookup, labelAt: [525, 234] }
- { from: rules, to: push-service, label: alert }
- { from: installer, to: provisioning, label: register }
- { from: provisioning, to: mqtt-broker, label: certs, variant: security }
- { from: provisioning, to: device-registry, label: store }
- { from: device-api, to: tsdb, label: read }
- { from: device-api, to: device-registry, label: read }
- { from: tsdb, to: ops-dashboard, label: query }
- { from: customer-app, to: device-api, label: REST }
- { from: ops-engineer, to: ops-dashboard, label: views }
- { from: homeowner, to: customer-app, label: uses }
cards:
- dot: amber
title: 8 Crosscutting concepts
items:
- Device identity, one X.509 certificate per device
- Security, OIDC login for people, mTLS for devices
- Persistence, PostgreSQL dialect for all stores
- Idempotent writes, keyed by device id and timestamp
- Error handling, invalid readings to dead letters
- Observability, metrics and traces on every service
@@ -0,0 +1,20 @@
schema_version: 1
diagram_type: sequence
meta:
title: Device provisioning
output: device-provisioning.html
decisions: [mqtt-with-mutual-tls]
participants:
- { ref: installer }
- { ref: provisioning }
- { ref: device-registry, label: Devices }
- { ref: heat-pump }
- { ref: mqtt-broker }
messages:
- { from: installer, to: provisioning, y: 160, label: register serial, variant: emphasis }
- { from: provisioning, to: device-registry, y: 210, label: store device }
- { from: provisioning, to: installer, y: 260, label: certificate, variant: return }
- { from: installer, to: heat-pump, y: 310, label: load certificate }
- { from: heat-pump, to: mqtt-broker, y: 360, label: connect mTLS, variant: emphasis }
- { from: mqtt-broker, to: provisioning, y: 410, label: check revocation }
- { from: mqtt-broker, to: heat-pump, y: 460, label: CONNACK, variant: return }
@@ -0,0 +1,20 @@
schema_version: 1
diagram_type: sequence
meta:
title: Fault reading to alert
output: telemetry-to-alert.html
participants:
- { ref: heat-pump }
- { ref: mqtt-broker }
- { ref: ingest }
- { ref: tsdb, label: TSDB }
- { ref: rules }
- { ref: push-service }
messages:
- { from: heat-pump, to: mqtt-broker, y: 160, label: publish fault, variant: emphasis }
- { from: mqtt-broker, to: ingest, y: 210, label: deliver QoS 1 }
- { from: ingest, to: tsdb, y: 260, label: write batch }
- { from: tsdb, to: ingest, y: 310, label: committed, variant: return }
- { from: ingest, to: mqtt-broker, y: 360, label: ack, variant: return }
- { from: ingest, to: rules, y: 410, label: reading }
- { from: rules, to: push-service, y: 460, label: send alert, variant: emphasis }
@@ -0,0 +1,67 @@
schema_version: 1
diagram_type: architecture
meta:
title: Deployment, production
output: production.html
decisions: [timescaledb-for-telemetry]
components:
- ref: heat-pump
pos: [40, 160]
size: [130, 60]
- ref: mqtt-broker
pos: [280, 160]
size: [130, 60]
- ref: ingest
pos: [490, 160]
size: [130, 60]
- ref: rules
pos: [490, 60]
size: [130, 60]
- ref: provisioning
pos: [280, 260]
size: [130, 60]
- ref: device-api
pos: [490, 260]
size: [130, 60]
- ref: tsdb
pos: [760, 160]
size: [130, 60]
- ref: device-registry
pos: [760, 260]
size: [130, 60]
- ref: ops-dashboard
pos: [1030, 160]
size: [130, 60]
- ref: customer-app
pos: [490, 440]
size: [130, 60]
boundaries:
- kind: region
label: Kubernetes cluster, eu-central
wraps: [mqtt-broker, ingest, rules, provisioning, device-api]
- kind: security-group
label: Managed DB zone
wraps: [tsdb, device-registry]
- kind: region
label: Ops network, managed Grafana
wraps: [ops-dashboard]
- kind: region
label: Homeowner phones
wraps: [customer-app]
connections:
- { from: heat-pump, to: mqtt-broker, label: "8883", variant: emphasis }
- { from: mqtt-broker, to: ingest, label: in-cluster }
- { from: ingest, to: tsdb, label: "5432", variant: emphasis }
- { from: ingest, to: rules, label: gRPC }
- { from: device-api, to: device-registry, label: "5432" }
- { from: tsdb, to: ops-dashboard, label: read-only }
- { from: customer-app, to: device-api, label: HTTPS }
cards:
- dot: slate
title: 7 Mapping
items:
- "mqtt-broker, ingest, rules -> Kubernetes cluster, eu-central"
- "provisioning, device-api -> Kubernetes cluster, eu-central"
- "tsdb, device-registry -> managed DB zone, eu-central"
- "ops-dashboard -> ops network, managed Grafana"
- "customer-app -> homeowner phones, app stores"
@@ -0,0 +1,75 @@
schema_version: 1
diagram_type: architecture
meta:
title: Quality tree
output: quality-tree.html
components:
- id: quality
type: cloud
label: Quality
pos: [40, 240]
size: [130, 100]
- id: reliability
type: backend
label: Reliability
pos: [250, 60]
size: [130, 60]
- id: performance
type: backend
label: Performance
pos: [250, 210]
size: [130, 60]
- id: security
type: security
label: Security
pos: [250, 360]
size: [130, 60]
- id: operability
type: backend
label: Operability
pos: [250, 460]
size: [130, 60]
- id: q-no-loss
type: frontend
label: Broker restart, no loss
pos: [460, 60]
size: [180, 60]
- id: q-alert-60s
type: frontend
label: Fault alert in 60 s
pos: [460, 160]
size: [180, 60]
- id: q-peak-load
type: frontend
label: 4,000 readings/s
pos: [460, 260]
size: [180, 60]
- id: q-revoke
type: frontend
label: Revoke cert in 15 min
pos: [460, 360]
size: [180, 60]
- id: q-new-device
type: frontend
label: New device in 10 min
pos: [460, 460]
size: [180, 60]
connections:
- { from: quality, to: reliability }
- { from: quality, to: performance }
- { from: quality, to: security }
- { from: quality, to: operability }
- { from: reliability, to: q-no-loss }
- { from: performance, to: q-alert-60s }
- { from: performance, to: q-peak-load }
- { from: security, to: q-revoke }
- { from: operability, to: q-new-device }
cards:
- dot: emerald
title: 10 Quality scenarios
items:
- "Broker restarts under load: devices reconnect, no accepted reading lost"
- "Heat pump sends a fault code: homeowner gets a push within 60 s"
- "Fleet peaks at 4,000 readings/s: p95 ingest lag stays under 5 s"
- "A device certificate is stolen: ops revoke it, broker refuses it within 15 min"
- "Installer registers a new heat pump: data visible in the app within 10 min"
@@ -0,0 +1,21 @@
schema_version: 1
decision:
id: mqtt-with-mutual-tls
title: MQTT with mutual TLS for device traffic
status: accepted
date: 2026-08-12
elements: [heat-pump, mqtt-broker, provisioning]
diagrams: [0300-context/technical-context.archify, 0500-building-blocks/level-1.archify]
context: >
Heat pumps sit behind home routers on slow and flaky links.
The first prototype polled an HTTPS endpoint and drained
mobile data plans; shared API keys leaked from one device.
decision: >
Devices publish over MQTT 3.1.1 on port 8883. Each device
has its own client certificate issued by Provisioning;
the broker rejects any connection without one.
consequences: >
Small messages and one long-lived connection per device.
Certificates must be rotated and revoked, so Provisioning
is a critical building block. Installers need a step to
register the device before it can connect.
@@ -0,0 +1,20 @@
schema_version: 1
decision:
id: timescaledb-for-telemetry
title: TimescaleDB for telemetry
status: accepted
date: 2026-09-03
elements: [tsdb, ingest, ops-dashboard]
diagrams: [0500-building-blocks/level-1.archify, 0700-deployment/production.archify]
context: >
The fleet sends about 4,000 readings a second. A plain
PostgreSQL table grew too fast and dashboards slowed down
after a few weeks of data.
decision: >
Store telemetry in TimescaleDB hypertables with hourly
continuous aggregates. Raw data is kept 90 days, rollups
10 years. The team already runs PostgreSQL.
consequences: >
SQL stays the query language for the API and the
dashboard. Retention jobs must be monitored. Very high
device counts may need sharding later (see risk R1).
+97
View File
@@ -0,0 +1,97 @@
schema_version: 1
model:
id: iot-telemetry
name: IoT Telemetry
description: >
An arc42 example: a fictional heat pump maker collects device telemetry
over MQTT, stores it, raises alerts and shows it to operations and customers.
Where the 12 arc42 sections live:
1 introduction and goals, 2 constraints, 4 solution strategy,
11 risks and 12 glossary are cards on this overview;
3 business context is this overview, technical context in 0300-context;
5 building blocks in 0500-building-blocks (black boxes in the registry);
6 runtime in 0600-runtime; 7 deployment in 0700-deployment;
8 crosscutting concepts a card on 0500-building-blocks/level-1;
9 decisions in decisions/; 10 quality tree in 1000-quality.
diagram_type: architecture
meta:
title: Business context
output: main.html
components:
- ref: homeowner
pos: [40, 60]
size: [130, 60]
- ref: ops-engineer
pos: [40, 160]
size: [130, 60]
- ref: installer
pos: [40, 260]
size: [130, 60]
- ref: telemetry-platform
pos: [280, 160]
size: [150, 60]
- ref: heat-pump
pos: [540, 60]
size: [130, 60]
- ref: push-service
pos: [540, 160]
size: [130, 60]
- ref: service-desk
pos: [540, 260]
size: [130, 60]
connections:
- { from: homeowner, to: telemetry-platform, label: checks }
- { from: ops-engineer, to: telemetry-platform, label: monitors }
- { from: installer, to: telemetry-platform, label: registers }
- { from: heat-pump, to: telemetry-platform, label: telemetry, variant: emphasis }
- { from: telemetry-platform, to: push-service, label: alerts }
- { from: telemetry-platform, to: service-desk, label: tickets }
cards:
- dot: orange
title: 1.1 Requirements
items:
- Collect telemetry from every heat pump every 30 s
- Alert homeowner and ops on fault codes
- Show live and historic data in app and dashboard
- Register and provision new devices on site
- dot: cyan
title: 1.2 Quality goals
items:
- Reliability, no telemetry lost once accepted
- Timeliness, alert within 60 s of a fault
- Security, every device authenticated
- dot: emerald
title: 1.3 Stakeholders
items:
- "Homeowner: sees status, gets alerts quickly"
- "Ops engineer: fleet view, few false alarms"
- "Installer: register a device in minutes"
- "Firmware partner: stable MQTT contract"
- dot: slate
title: 2 Constraints
items:
- Devices speak MQTT only; firmware by a partner
- Data stays in one EU region
- Team of six; PostgreSQL skills in house
- dot: amber
title: 4 Solution strategy
items:
- MQTT with mutual TLS for devices (security, ADR 0001)
- TimescaleDB for telemetry, SQL everywhere (ADR 0002)
- Ingest acks only after commit (reliability)
- Rules engine on the ingest stream (timeliness)
- Split by flow, ingest vs read vs provisioning
- dot: rose
title: 11 Risks and technical debt
items:
- "Risk: fleet growth beyond one TSDB node"
- "Risk: partner firmware delays cert rotation"
- "Debt: payload v1 decoder kept for old firmware"
- "Debt: rules configured in code, no UI"
- dot: violet
title: 12 Glossary
items:
- Telemetry, readings a device sends
- Rollup, hourly aggregate of telemetry
- Provisioning, giving a device its identity
- Fault code, error number from the device
@@ -0,0 +1,75 @@
schema_version: 1
elements:
mqtt-broker:
type: messagebus
label: MQTT broker
sublabel: mutual TLS
description: >
Responsibility: terminates device connections, one topic per device.
Interfaces: provides MQTT 8883 with mTLS; requires certs and
revocation checks from Provisioning.
ingest:
type: backend
label: Ingest
sublabel: Go
brand: go
description: >
Responsibility: decodes, validates and stores telemetry.
Interfaces: requires MQTT subscription, TSDB, device registry;
provides the reading stream to the rules engine.
diagram: 0500-building-blocks/ingest.archify
tsdb:
type: database
label: Time series DB
sublabel: TimescaleDB
brand: postgresql
description: >
Responsibility: raw telemetry 90 days, hourly rollups 10 years.
Interfaces: provides SQL on 5432 to Device API and dashboard;
requires batched writes from Ingest.
rules:
type: backend
label: Rules engine
sublabel: alerting
description: >
Responsibility: evaluates thresholds and fault codes, raises alerts.
Interfaces: requires the reading stream from Ingest;
provides alerts to the push service and service desk.
provisioning:
type: security
label: Provisioning
sublabel: device certs
description: >
Responsibility: registers devices and issues client certificates.
Interfaces: provides a REST register call for installers and
revocation checks for the broker; requires the device registry.
device-registry:
type: database
label: Device registry
sublabel: PostgreSQL
brand: postgresql
description: >
Responsibility: devices, owners, firmware and certificate state.
Interfaces: provides SQL on 5432 to Ingest, Provisioning, Device API.
device-api:
type: backend
label: Device API
sublabel: REST
description: >
Responsibility: serves telemetry and device data to the customer app.
Interfaces: provides REST over HTTPS; requires TSDB and device registry.
ops-dashboard:
type: frontend
label: Ops dashboard
sublabel: Grafana
brand: grafana
description: >
Responsibility: fleet view for the operations team.
Interfaces: provides web UI over HTTPS; requires read-only SQL on TSDB.
customer-app:
type: frontend
label: Customer app
sublabel: mobile
description: >
Responsibility: shows the homeowner their heat pump and alerts.
Interfaces: provides the mobile UI; requires Device API and push.
@@ -0,0 +1,20 @@
schema_version: 1
elements:
homeowner:
type: external
label: Homeowner
sublabel: customer app
description: >
Owns a heat pump; checks temperatures and gets fault alerts.
ops-engineer:
type: external
label: Ops engineer
sublabel: operations team
description: >
Watches the fleet, handles alerts and plans service visits.
installer:
type: external
label: Installer
sublabel: field service
description: >
Mounts heat pumps on site and registers them with the platform.
@@ -0,0 +1,27 @@
schema_version: 1
elements:
telemetry-platform:
type: cloud
label: IoT Telemetry
sublabel: the system
description: >
Everything we build and run; broker, ingest, stores, rules and apps.
diagram: 0500-building-blocks/level-1.archify
heat-pump:
type: external
label: Heat pump
sublabel: field device
description: >
Sends telemetry every 30 seconds over MQTT; built by a partner.
push-service:
type: external
label: Push service
sublabel: mobile push
description: >
Delivers push notifications to the customer app.
service-desk:
type: external
label: Service desk
sublabel: ticketing
description: >
Opens service tickets for faults that need a visit.
@@ -0,0 +1,34 @@
schema_version: 1
diagram_type: architecture
meta:
title: Technical context
output: technical-context.html
components:
- ref: engineer
pos: [40, 60]
size: [130, 60]
- ref: wiki-admin
pos: [40, 260]
size: [130, 60]
- ref: wiki
pos: [280, 160]
size: [150, 60]
- ref: idp
pos: [520, 60]
size: [150, 60]
- ref: object-store
pos: [520, 260]
size: [150, 60]
connections:
- { from: engineer, to: wiki, label: HTTPS, variant: emphasis }
- { from: wiki-admin, to: wiki, label: kubectl }
- { from: wiki, to: idp, label: OIDC, variant: security }
- { from: wiki, to: object-store, label: S3 API }
cards:
- dot: cyan
title: 3 Domain interfaces
items:
- "Engineer: in edits, queries, files; out pages, results"
- "Wiki admin: in deploys, restore commands; out status, logs"
- "Identity provider: out sign-in code, groups; in token requests"
- "Object storage: out attachments, backups; in signed URL reads"
@@ -0,0 +1,50 @@
schema_version: 1
diagram_type: architecture
meta:
title: Building blocks - level 1
output: level-1.html
decisions: [postgres-full-text-search]
components:
- ref: engineer
pos: [40, 160]
size: [130, 60]
- ref: web
pos: [250, 160]
size: [130, 60]
- ref: api
pos: [460, 160]
size: [130, 60]
- ref: db
pos: [460, 360]
size: [130, 60]
- ref: backup-job
pos: [670, 360]
size: [130, 60]
- ref: idp
pos: [670, 60]
size: [130, 60]
- ref: object-store
pos: [880, 160]
size: [130, 60]
boundaries:
- kind: region
label: Team Wiki
wraps: [web, api, db, backup-job]
connections:
- { from: engineer, to: web, label: HTTPS, variant: emphasis }
- { from: web, to: api, label: JSON, variant: emphasis }
- { from: api, to: db, label: SQL, labelAt: [525, 234], decisions: [postgres-full-text-search] }
- { from: api, to: idp, label: OIDC, variant: security }
- { from: api, to: object-store, label: files }
- { from: backup-job, to: db, label: dump }
- { from: backup-job, to: object-store, label: upload }
cards:
- dot: amber
title: 8 Crosscutting concepts
items:
- "Security: OIDC session, every request checked against space groups"
- "Persistence: PostgreSQL is the only source of truth"
- "Errors: API returns problem JSON; the web app shows a retry"
- "Logging: JSON to stdout, collected by the cluster"
- "Backups: nightly dump to object storage, 30 days"
- "Config: environment variables, secrets from the vault"
@@ -0,0 +1,55 @@
schema_version: 1
diagram_type: architecture
meta:
title: Building blocks - level 2, Wiki API
output: wiki-api.html
components:
- ref: web
pos: [40, 160]
size: [130, 60]
- id: auth
type: security
label: Auth
sublabel: OIDC session
pos: [250, 60]
size: [130, 60]
- id: pages
type: backend
label: Pages
sublabel: revisions
pos: [250, 160]
size: [130, 60]
- id: search
type: backend
label: Search
sublabel: full-text
pos: [250, 260]
size: [130, 60]
decisions: [postgres-full-text-search]
- id: attachments
type: backend
label: Attachments
pos: [250, 360]
size: [130, 60]
- ref: idp
pos: [460, 60]
size: [130, 60]
- ref: db
pos: [460, 210]
size: [130, 60]
- ref: object-store
pos: [460, 360]
size: [130, 60]
boundaries:
- kind: region
label: Wiki API
wraps: [auth, pages, search, attachments]
connections:
- { from: web, to: auth, label: sign in, variant: security }
- { from: web, to: pages, label: edit, labelAt: [222, 182] }
- { from: web, to: search, label: query }
- { from: web, to: attachments, label: upload }
- { from: auth, to: idp, label: OIDC, variant: security }
- { from: pages, to: db, label: SQL }
- { from: search, to: db, label: SQL }
- { from: attachments, to: object-store, label: S3 }
@@ -0,0 +1,20 @@
schema_version: 1
diagram_type: sequence
meta:
title: Runtime - search and open a page
output: search-page.html
participants:
- ref: engineer
- ref: web
- ref: api
- { ref: db, label: DB }
- { ref: object-store, label: Object store }
messages:
- { from: engineer, to: web, y: 160, label: type query, variant: emphasis }
- { from: web, to: api, y: 210, label: GET /search }
- { from: api, to: db, y: 260, label: full-text query }
- { from: db, to: api, y: 310, label: ranked pages, variant: return }
- { from: api, to: web, y: 360, label: results, variant: return }
- { from: web, to: api, y: 410, label: GET /pages/id }
- { from: api, to: object-store, y: 460, label: signed URL }
- { from: api, to: web, y: 510, label: page, variant: return }
@@ -0,0 +1,19 @@
schema_version: 1
diagram_type: sequence
meta:
title: Runtime - sign in
output: sign-in.html
decisions: [sso-only-via-company-idp]
participants:
- ref: engineer
- ref: web
- ref: api
- { ref: idp, label: Company IdP }
messages:
- { from: engineer, to: web, y: 160, label: open wiki, variant: emphasis }
- { from: web, to: api, y: 210, label: GET /session }
- { from: api, to: web, y: 260, label: redirect, variant: return }
- { from: web, to: idp, y: 310, label: OIDC login, variant: security }
- { from: idp, to: api, y: 360, label: code, variant: return }
- { from: api, to: idp, y: 410, label: token exchange, variant: security }
- { from: api, to: web, y: 460, label: session cookie, variant: return }
@@ -0,0 +1,60 @@
schema_version: 1
diagram_type: architecture
meta:
title: Deployment - production
output: production.html
components:
- ref: engineer
pos: [40, 160]
size: [130, 60]
- id: ingress
type: security
label: Ingress
sublabel: TLS
pos: [250, 160]
size: [130, 60]
- ref: web
pos: [460, 60]
size: [130, 60]
- ref: api
pos: [460, 160]
size: [130, 60]
- ref: db
pos: [670, 160]
size: [130, 60]
- ref: backup-job
pos: [670, 360]
size: [130, 60]
- ref: idp
pos: [460, 460]
size: [130, 60]
- ref: object-store
pos: [880, 460]
size: [130, 60]
boundaries:
- kind: region
label: Company cluster
wraps: [ingress, web, api, db, backup-job]
- kind: security-group
label: App node pool
wraps: [web, api]
- kind: security-group
label: Data node
wraps: [db, backup-job]
connections:
- { from: engineer, to: ingress, label: HTTPS, variant: emphasis }
- { from: ingress, to: web, label: static }
- { from: ingress, to: api, label: /api, variant: emphasis }
- { from: api, to: db, label: SQL }
- { from: api, to: idp, label: OIDC, variant: security, labelAt: [525, 234] }
- { from: backup-job, to: db, label: dump }
- { from: backup-job, to: object-store, label: S3 }
cards:
- dot: slate
title: 7 Mapping
items:
- "Web App -> app node pool, nginx pod, namespace team-wiki"
- "Wiki API -> app node pool, 2 pods behind the ingress"
- "Database -> data node, PostgreSQL stateful set with volume"
- "Backup job -> data node, Kubernetes CronJob"
- "Attachments, backups -> company object storage buckets"
@@ -0,0 +1,77 @@
schema_version: 1
diagram_type: architecture
meta:
title: Quality tree
output: quality-tree.html
legend:
mode: hidden
components:
- id: quality
type: cloud
label: Quality
pos: [40, 200]
size: [130, 120]
- id: usability
type: frontend
label: Usability
pos: [250, 60]
size: [130, 60]
- id: security
type: security
label: Security
pos: [250, 160]
size: [130, 60]
- id: reliability
type: backend
label: Reliability
pos: [250, 310]
size: [130, 60]
- id: maintainability
type: backend
label: Maintainability
pos: [250, 460]
size: [130, 60]
- id: q-search
type: external
label: Search under 1 s
pos: [460, 60]
size: [170, 60]
- id: q-leaver
type: external
label: Leaver out in 5 min
pos: [460, 160]
size: [170, 60]
- id: q-restore
type: external
label: Restore in 30 min
pos: [460, 260]
size: [170, 60]
- id: q-pod
type: external
label: Pod back in 1 min
pos: [460, 360]
size: [170, 60]
- id: q-upgrade
type: external
label: Upgrade in 1 hour
pos: [460, 460]
size: [170, 60]
connections:
- { from: quality, to: usability }
- { from: quality, to: security }
- { from: quality, to: reliability }
- { from: quality, to: maintainability }
- { from: usability, to: q-search }
- { from: security, to: q-leaver }
- { from: reliability, to: q-restore }
- { from: reliability, to: q-pod }
- { from: maintainability, to: q-upgrade }
cards:
- dot: emerald
title: 10 Quality scenarios
items:
- "Search: an engineer searches 5000 pages; results in under 1 s"
- "Leaver: HR disables an account; wiki access ends within 5 minutes"
- "Restore: the database is lost; the admin restores last night in 30 minutes"
- "Crash: an API pod dies; the cluster restarts it, no data lost, back in 1 minute"
- "Upgrade: a new wiki version ships; one admin rolls it out in 1 hour"
@@ -0,0 +1,20 @@
schema_version: 1
decision:
id: postgres-full-text-search
title: PostgreSQL full-text search, no search engine
status: accepted
date: 2026-10-05
elements: [api, db]
diagrams: [0500-building-blocks/level-1.archify, 0500-building-blocks/wiki-api.archify]
context: >
The wiki holds a few thousand pages for one team. A
separate search engine would be one more service to run,
back up and keep in sync.
decision: >
Search uses PostgreSQL full-text search on a generated
column of each page. The API queries it in the same
transaction as the page data.
consequences: >
Results are never stale and backups cover search too.
Ranking is simpler than a search engine. Revisit when
the wiki grows past 100000 pages.
@@ -0,0 +1,19 @@
schema_version: 1
decision:
id: sso-only-via-company-idp
title: Sign-in only through the company identity provider
status: accepted
date: 2026-10-12
elements: [idp, api, web]
diagrams: [0300-context/technical-context.archify, 0600-runtime/sign-in.archify]
context: >
Leavers kept access to the old wiki because it had its
own passwords. Nobody owned the user list.
decision: >
The wiki has no local accounts. Users sign in with OIDC
at the company identity provider; team groups map to
wiki spaces.
consequences: >
Offboarding removes wiki access at once. The wiki cannot
be used when the identity provider is down. The admin
needs a break-glass account kept in the vault.
+85
View File
@@ -0,0 +1,85 @@
schema_version: 1
model:
id: team-wiki
name: Team Wiki
description: >
An arc42 example: a small self-hosted wiki for an engineering team.
1 Requirements, quality goals, stakeholders; 2 Constraints;
4 Solution strategy; 11 Risks; 12 Glossary: cards on this overview.
3 Context: this overview (business) and 0300-context (technical).
5 Building blocks: 0500-building-blocks, black boxes in registry/.
6 Runtime: 0600-runtime. 7 Deployment: 0700-deployment.
8 Crosscutting concepts: card on 0500-building-blocks/level-1.
9 Decisions: decisions/. 10 Quality: 1000-quality.
diagram_type: architecture
meta:
title: Team Wiki - business context
output: main.html
decisions: [sso-only-via-company-idp]
components:
- ref: engineer
pos: [40, 60]
size: [130, 60]
- ref: wiki-admin
pos: [40, 260]
size: [130, 60]
- ref: wiki
pos: [280, 160]
size: [150, 60]
- ref: idp
pos: [520, 60]
size: [150, 60]
- ref: object-store
pos: [520, 260]
size: [150, 60]
connections:
- { from: engineer, to: wiki, label: reads, variant: emphasis }
- { from: wiki-admin, to: wiki, label: runs }
- { from: wiki, to: idp, label: sign-in, variant: security }
- { from: wiki, to: object-store, label: files }
cards:
- dot: orange
title: 1.1 Requirements
items:
- "Create, edit and version pages in team spaces"
- "Full-text search over every page a user may read"
- "Attach files to pages"
- "Admin restores the wiki from a nightly backup"
- dot: cyan
title: 1.2 Quality goals
items:
- "Findability: any page found in two searches"
- "Security: only current staff can read"
- "Operability: one person runs it in an hour a week"
- dot: amber
title: 1.3 Stakeholders
items:
- "Engineer: fast search, simple editor"
- "Wiki admin: little routine work, easy restore"
- "Security officer: no access after leaving"
- "Team lead: one place for team knowledge"
- dot: slate
title: 2 Constraints
items:
- "Runs on the company Kubernetes cluster"
- "Sign-in only via the company identity provider"
- dot: emerald
title: 4 Solution strategy
items:
- "React, Node.js API, PostgreSQL: one stack the team knows"
- "Three blocks plus a backup job: operability"
- "Findability: PostgreSQL full-text search (postgres-full-text-search)"
- "Security: OIDC at the company IdP only (sso-only-via-company-idp)"
- "Operability: files and backups in company object storage"
- dot: rose
title: 11 Risks and technical debt
items:
- "Risk: identity provider outage blocks all sign-ins"
- "Risk: backups untested until a restore drill is run"
- "Debt: attachments are not virus-scanned yet"
- dot: violet
title: 12 Glossary
items:
- "Space: pages of one team, mapped to a group"
- "Revision: one saved version of a page"
- "Attachment: a file stored beside a page"
@@ -0,0 +1,37 @@
schema_version: 1
elements:
web:
type: frontend
label: Web App
sublabel: React
brand: react
description: >
Responsibility: page editor, viewer and search box in the
browser. Interfaces: provides the UI over HTTPS; requires
the Wiki API (JSON).
api:
type: backend
label: Wiki API
sublabel: REST
description: >
Responsibility: pages, search, attachments and access checks;
the only writer of the database. Interfaces: provides REST
JSON; requires the IdP (OIDC), the database (SQL), object storage (S3).
diagram: 0500-building-blocks/wiki-api.archify
db:
type: database
label: Database
sublabel: PostgreSQL
brand: postgresql
description: >
Responsibility: pages, revisions, users and the full-text
index; the source of truth. Interfaces: provides SQL to the
API and dumps to the backup job.
backup-job:
type: backend
label: Backup job
sublabel: nightly
description: >
Responsibility: dumps the database at 02:00, keeps 30 days.
Interfaces: requires the database (pg_dump) and the backup
bucket (S3); provides none.
@@ -0,0 +1,15 @@
schema_version: 1
elements:
engineer:
type: external
label: Engineer
sublabel: team member
description: >
Reads and edits pages, searches, attaches files; signs in
with the company account.
wiki-admin:
type: external
label: Wiki admin
sublabel: platform team
description: >
Runs the wiki, restores backups and manages spaces.
@@ -0,0 +1,24 @@
schema_version: 1
elements:
wiki:
type: cloud
label: Team Wiki
sublabel: the system
description: >
Everything we build and run; web app, API, database and
the backup job.
diagram: 0500-building-blocks/level-1.archify
idp:
type: security
label: Identity provider
sublabel: company SSO
description: >
The company identity provider; signs users in over OIDC
and owns team membership.
object-store:
type: external
label: Object storage
sublabel: S3-compatible
description: >
Company object storage; holds attachments and nightly
backups in separate buckets.
@@ -0,0 +1,36 @@
schema_version: 1
diagram_type: architecture
meta:
title: Business actors and services
output: business-services.html
components:
- ref: hr
pos: [40, 60]
size: [130, 60]
- ref: manager
pos: [40, 160]
size: [130, 60]
- ref: service-desk
pos: [40, 260]
size: [130, 60]
- ref: onboard
pos: [250, 160]
size: [150, 60]
diagram: 0100-business/onboard-employee.archify
- ref: onboarding-svc
pos: [480, 60]
size: [130, 60]
- ref: workplace-svc
pos: [480, 260]
size: [130, 60]
- ref: new-hire
pos: [690, 160]
size: [130, 60]
connections:
- { from: hr, to: onboard, label: assigned }
- { from: manager, to: onboard, label: assigned }
- { from: service-desk, to: onboard, label: assigned }
- { from: onboard, to: onboarding-svc, label: realizes }
- { from: onboard, to: workplace-svc, label: realizes }
- { from: onboarding-svc, to: new-hire, label: serves, variant: emphasis }
- { from: workplace-svc, to: new-hire, label: serves }
@@ -0,0 +1,25 @@
schema_version: 2
diagram_type: workflow
meta:
title: Onboard Employee
output: onboard-employee.html
viewBox: [1040, 660]
lanes:
- { id: hr-lane, label: HR }
- { id: manager-lane, label: Hiring manager }
- { id: desk-lane, label: IT service desk }
- { id: hire-lane, label: New hire }
mainPath: [contract-signed, record, request, create-accounts, issue-laptop, first-day]
nodes:
- { ref: contract-signed, lane: hr-lane, col: 0, width: 120 }
- { id: record, lane: hr-lane, col: 1, type: backend, label: Create record, tag: Business Process }
- { id: request, lane: manager-lane, col: 2, type: frontend, label: Request access, tag: Business Process }
- { ref: create-accounts, lane: desk-lane, col: 3, width: 120, decisions: [hr-system-drives-identity] }
- { ref: issue-laptop, lane: desk-lane, col: 4, decisions: [zero-touch-laptops] }
- { ref: first-day, lane: hire-lane, col: 5 }
edges:
- { from: contract-signed, to: record, label: triggers }
- { from: record, to: request, label: notifies }
- { from: request, to: create-accounts, label: ticket }
- { from: create-accounts, to: issue-laptop, label: then }
- { from: issue-laptop, to: first-day, label: hand over }
@@ -0,0 +1,44 @@
schema_version: 1
diagram_type: architecture
meta:
title: Application components, services and data
output: application-components.html
components:
- ref: provisioning-svc
pos: [250, 60]
size: [130, 60]
- ref: portal
pos: [460, 60]
size: [130, 60]
- ref: device-svc
pos: [670, 60]
size: [130, 60]
- ref: hr-system
pos: [40, 160]
size: [130, 60]
- ref: iam
pos: [250, 160]
size: [130, 60]
- ref: itsm
pos: [460, 160]
size: [130, 60]
- ref: mdm-app
pos: [670, 160]
size: [130, 60]
- ref: employee-record
pos: [40, 260]
size: [130, 60]
- ref: account
pos: [250, 260]
size: [130, 60]
- ref: asset
pos: [460, 260]
size: [130, 60]
connections:
- { from: iam, to: provisioning-svc, label: realizes }
- { from: itsm, to: portal, label: realizes }
- { from: mdm-app, to: device-svc, label: realizes }
- { from: hr-system, to: employee-record, label: writes, labelAt: [105, 234] }
- { from: iam, to: account, label: writes, labelAt: [315, 234] }
- { from: itsm, to: asset, label: writes, labelAt: [525, 234] }
- { from: mdm-app, to: asset, label: reads }
@@ -0,0 +1,29 @@
schema_version: 1
diagram_type: architecture
meta:
title: Application cooperation
output: application-cooperation.html
decisions: [hr-system-drives-identity]
components:
- ref: hr-system
pos: [40, 160]
size: [130, 60]
- ref: iam
pos: [250, 60]
size: [130, 60]
diagram: 0200-application/application-components.archify
- ref: itsm
pos: [250, 260]
size: [130, 60]
- ref: collab
pos: [460, 60]
size: [130, 60]
- ref: mdm-app
pos: [460, 260]
size: [130, 60]
connections:
- { from: hr-system, to: iam, label: joiner event, variant: emphasis, decisions: [hr-system-drives-identity] }
- { from: hr-system, to: itsm, label: laptop request }
- { from: iam, to: collab, label: provisions }
- { from: iam, to: mdm-app, label: users }
- { from: itsm, to: mdm-app, label: assigns }
@@ -0,0 +1,42 @@
schema_version: 1
diagram_type: architecture
meta:
title: Technology layer
output: infrastructure.html
decisions: [zero-touch-laptops]
components:
- ref: hr-saas
pos: [40, 60]
size: [130, 60]
- ref: sso
pos: [250, 60]
size: [130, 60]
- ref: directory
pos: [460, 60]
size: [130, 60]
- ref: itsm-saas
pos: [40, 160]
size: [130, 60]
- ref: mdm-saas
pos: [460, 160]
size: [130, 60]
- ref: laptop
pos: [460, 360]
size: [130, 60]
- ref: office-net
pos: [250, 360]
size: [130, 60]
boundaries:
- kind: region
label: SaaS tenants
wraps: [hr-saas, sso, directory, itsm-saas, mdm-saas]
- kind: security-group
label: Office
wraps: [laptop, office-net]
connections:
- { from: directory, to: sso, label: realizes }
- { from: sso, to: hr-saas, label: serves }
- { from: sso, to: itsm-saas, label: serves }
- { from: directory, to: mdm-saas, label: devices, labelAt: [525, 134] }
- { from: mdm-saas, to: laptop, label: manages, labelAt: [525, 234], variant: security, decisions: [zero-touch-laptops] }
- { from: laptop, to: office-net, label: connects }
@@ -0,0 +1,20 @@
schema_version: 1
decision:
id: hr-system-drives-identity
title: The HR system drives identity
status: accepted
date: 2026-09-08
elements: [hr-system, iam, account, employee-record]
diagrams: [0200-application/application-cooperation.archify, 0100-business/onboard-employee.archify]
context: >
Accounts were created by hand from emails sent by HR.
New hires waited days for access, and leavers kept
accounts for weeks after their last day.
decision: >
The HR system is the source of truth for joiners and
leavers. Identity management reads its events and
creates, changes and disables accounts automatically.
consequences: >
No account without an employee record. HR data quality
now decides access, so start dates must be right. The
service desk only handles exceptions.
@@ -0,0 +1,20 @@
schema_version: 1
decision:
id: zero-touch-laptops
title: Zero-touch enrollment for laptops
status: accepted
date: 2026-10-05
elements: [mdm-app, mdm-saas, laptop, device-svc, asset]
diagrams: [0300-technology/infrastructure.archify, 0100-business/onboard-employee.archify]
context: >
Every laptop was imaged by hand at the service desk,
which took half a day per device and left settings that
differed from one laptop to the next.
decision: >
Laptops are registered in device management at purchase.
They configure themselves at first sign-in over any
internet connection; the office network needs no VPN.
consequences: >
Laptops can be shipped straight to the new hire. A lost
laptop is wiped remotely. Setup depends on the MDM
tenant being reachable on the first day.
@@ -0,0 +1,59 @@
schema_version: 1
model:
id: hr-onboarding
name: HR Onboarding
description: >
An ArchiMate example: how a new employee is onboarded, from signed
contract to first day. Business, application and technology layers
map to folders; each element's ArchiMate type is shown as its tag.
diagram_type: architecture
meta:
title: HR Onboarding - layered view
output: main.html
components:
- ref: new-hire
pos: [40, 70]
size: [170, 60]
- ref: onboarding-svc
pos: [270, 70]
size: [170, 60]
- ref: onboard
pos: [500, 70]
size: [170, 60]
- ref: portal
pos: [40, 270]
size: [170, 60]
- ref: iam
pos: [270, 270]
size: [170, 60]
- ref: provisioning-svc
pos: [500, 270]
size: [170, 60]
- ref: office-net
pos: [40, 470]
size: [170, 60]
- ref: sso
pos: [270, 470]
size: [170, 60]
- ref: directory
pos: [500, 470]
size: [170, 60]
boundaries:
- kind: region
label: Business layer
wraps: [new-hire, onboarding-svc, onboard]
- kind: region
label: Application layer
wraps: [portal, iam, provisioning-svc]
- kind: region
label: Technology layer
wraps: [office-net, sso, directory]
connections:
- { from: onboarding-svc, to: new-hire, label: serves, variant: emphasis }
- { from: onboard, to: onboarding-svc, label: realizes }
- { from: portal, to: new-hire, label: serves, labelAt: [125, 197] }
- { from: provisioning-svc, to: onboard, label: serves, labelAt: [585, 197] }
- { from: iam, to: provisioning-svc, label: realizes }
- { from: office-net, to: portal, label: serves, labelAt: [125, 397] }
- { from: sso, to: iam, label: serves, labelAt: [355, 397] }
- { from: directory, to: sso, label: realizes }
@@ -0,0 +1,81 @@
schema_version: 1
elements:
hr-system:
type: backend
label: HR system
sublabel: App Component
tag: App Component
description: >
Holds employee records and contracts; the source of truth for joiners.
diagram: 0200-application/application-cooperation.archify
iam:
type: security
label: Identity mgmt
sublabel: App Component
tag: App Component
description: >
Creates and removes accounts from HR events; single sign-on for all apps.
diagram: 0200-application/application-components.archify
itsm:
type: backend
label: IT service mgmt
sublabel: App Component
tag: App Component
description: >
Tickets, requests and the asset inventory of the IT service desk.
mdm-app:
type: backend
label: Device mgmt
sublabel: App Component
tag: App Component
description: >
Enrolls laptops, pushes policies and apps, wipes lost devices.
collab:
type: backend
label: Mail and chat
sublabel: App Component
tag: App Component
description: >
Mailbox, calendar and chat; accounts provisioned by identity management.
provisioning-svc:
type: cloud
label: Provisioning
sublabel: App Service
tag: App Service
description: >
Account create, change and disable, triggered by the HR system.
device-svc:
type: cloud
label: Enrollment
sublabel: App Service
tag: App Service
description: >
Zero-touch enrollment; the laptop configures itself at first sign-in.
portal:
type: frontend
label: Service portal
sublabel: App Interface
tag: App Interface
description: >
Where managers request equipment and access for a new hire.
employee-record:
type: database
label: Employee record
sublabel: Data Object
tag: Data Object
description: >
Name, role, department, manager and start date of an employee.
account:
type: database
label: Account
sublabel: Data Object
tag: Data Object
description: >
User id, groups and licences of one person.
asset:
type: database
label: Asset
sublabel: Data Object
tag: Data Object
description: >
A laptop with serial number, owner and enrollment state.
@@ -0,0 +1,81 @@
schema_version: 1
elements:
new-hire:
type: external
label: New hire
sublabel: Business Actor
tag: Business Actor
description: >
The person joining the company; signs the contract and starts on day one.
hr:
type: external
label: HR officer
sublabel: Business Role
tag: Business Role
description: >
Prepares the contract and the employee record; owns the onboarding process.
manager:
type: external
label: Hiring manager
sublabel: Business Role
tag: Business Role
description: >
Requests access and equipment for the new hire and plans the first day.
service-desk:
type: external
label: IT service desk
sublabel: Business Role
tag: Business Role
description: >
Creates accounts, prepares the laptop and hands it over.
onboard:
type: backend
label: Onboard Employee
sublabel: Business Process
tag: Business Process
description: >
From signed contract to a productive first day.
diagram: 0100-business/onboard-employee.archify
onboarding-svc:
type: cloud
label: Onboarding
sublabel: Business Service
tag: Business Service
description: >
What the new hire gets from HR; contract, record, welcome and first day plan.
diagram: 0100-business/business-services.archify
workplace-svc:
type: cloud
label: Workplace setup
sublabel: Business Service
tag: Business Service
description: >
Accounts and a ready laptop, delivered by the IT service desk.
contract-signed:
type: messagebus
label: Contract signed
sublabel: Business Event
tag: Business Event
description: >
Starts the process; HR records the start date.
create-accounts:
type: backend
label: Create accounts
sublabel: Business Process
tag: Business Process
description: >
Service desk confirms the accounts the identity system provisioned.
issue-laptop:
type: backend
label: Issue laptop
sublabel: Business Process
tag: Business Process
description: >
An enrolled laptop is assigned and handed over.
first-day:
type: messagebus
label: First day
sublabel: Business Event
tag: Business Event
description: >
The new hire signs in for the first time; the process ends.
@@ -0,0 +1,53 @@
schema_version: 1
elements:
hr-saas:
type: cloud
label: HR tenant
sublabel: Node
tag: Node
description: >
The SaaS tenant that runs the HR system.
diagram: 0300-technology/infrastructure.archify
itsm-saas:
type: cloud
label: ITSM tenant
sublabel: Node
tag: Node
description: >
The SaaS tenant that runs IT service management.
mdm-saas:
type: cloud
label: MDM tenant
sublabel: Node
tag: Node
description: >
The SaaS tenant that runs device management.
directory:
type: backend
label: Directory
sublabel: System Software
tag: System Software
description: >
Cloud directory service; users, groups and devices.
diagram: 0300-technology/infrastructure.archify
sso:
type: security
label: Single sign-on
sublabel: Tech Service
tag: Tech Service
description: >
SAML and OIDC sign-in for every SaaS tenant, backed by the directory.
laptop:
type: frontend
label: Laptop
sublabel: Device
tag: Device
description: >
Company laptop, enrolled before it is handed over.
office-net:
type: messagebus
label: Office network
sublabel: Network
tag: Network
description: >
Wired and wireless network of the office; internet only, no VPN.
@@ -0,0 +1,31 @@
schema_version: 1
diagram_type: architecture
meta:
title: Business layer
output: business-layer.html
components:
- ref: customer
pos: [40, 160]
size: [130, 60]
- ref: insurant
pos: [250, 160]
size: [130, 60]
- ref: claim-registration
pos: [460, 60]
size: [150, 60]
- ref: claim-payment
pos: [460, 260]
size: [150, 60]
- ref: handle-claim
pos: [690, 160]
size: [150, 60]
- ref: claims-handler
pos: [920, 160]
size: [130, 60]
connections:
- { from: customer, to: insurant, label: assigned to }
- { from: claim-registration, to: insurant, label: serves, variant: emphasis }
- { from: claim-payment, to: insurant, label: serves }
- { from: handle-claim, to: claim-registration, label: realizes }
- { from: handle-claim, to: claim-payment, label: realizes }
- { from: claims-handler, to: handle-claim, label: assigned to }
@@ -0,0 +1,29 @@
schema_version: 2
diagram_type: workflow
meta:
title: Handle Claim
output: handle-claim.html
viewBox: [840, 534]
lanes:
- { id: insurant-lane, label: Insurant }
- { id: handler-lane, label: Claims handler }
- { id: systems-lane, label: Applications }
mainPath: [damage, register, accept, valuate, pay]
nodes:
- { id: damage, lane: insurant-lane, col: 0, type: external, label: Damage occurs, tag: Business Event }
- { ref: insurant, lane: insurant-lane, col: 5 }
- { id: register, lane: handler-lane, col: 1, type: backend, label: Register, tag: Business Process }
- { id: accept, lane: handler-lane, col: 2, type: security, label: Accept, sublabel: check coverage, tag: Business Process }
- { id: valuate, lane: handler-lane, col: 3, type: backend, label: Valuate, tag: Business Process }
- { id: pay, lane: handler-lane, col: 4, type: backend, label: Pay, tag: Business Process }
- { ref: claims-app, lane: systems-lane, col: 2 }
- { ref: payment, lane: systems-lane, col: 4 }
edges:
- { from: damage, to: register, label: report }
- { from: register, to: accept }
- { from: accept, to: valuate, label: covered }
- { from: valuate, to: pay, label: approved }
- { from: pay, to: insurant, label: paid }
- { from: accept, to: insurant, label: rejected, role: branch, variant: dashed }
- { from: claims-app, to: accept, label: serves, role: async }
- { from: payment, to: pay, label: serves, role: async }
@@ -0,0 +1,32 @@
schema_version: 1
diagram_type: architecture
meta:
title: Application cooperation
output: application-cooperation.html
components:
- ref: portal
pos: [40, 260]
size: [130, 60]
- ref: claims-app
pos: [250, 260]
size: [130, 60]
- ref: crm
pos: [250, 60]
size: [130, 60]
- ref: policy-admin
pos: [460, 160]
size: [130, 60]
decisions: [policies-stay-on-mainframe]
- ref: doc-mgmt
pos: [460, 360]
size: [130, 60]
decisions: [documents-in-object-storage]
- ref: payment
pos: [250, 460]
size: [130, 60]
connections:
- { from: portal, to: claims-app, label: claims, variant: emphasis }
- { from: crm, to: claims-app, label: customer, labelAt: [315, 134] }
- { from: policy-admin, to: claims-app, label: coverage, decisions: [policies-stay-on-mainframe] }
- { from: claims-app, to: doc-mgmt, label: documents }
- { from: claims-app, to: payment, label: payments, labelAt: [315, 334] }
@@ -0,0 +1,36 @@
schema_version: 1
diagram_type: architecture
meta:
title: Application structure
output: application-structure.html
components:
- ref: portal
pos: [40, 160]
size: [130, 60]
- ref: claims-admin
pos: [250, 160]
size: [130, 60]
- ref: claims-app
pos: [460, 160]
size: [130, 60]
- ref: claim
pos: [460, 360]
size: [130, 60]
- ref: crm
pos: [670, 60]
size: [130, 60]
- ref: customer-file
pos: [880, 60]
size: [130, 60]
- ref: policy-admin
pos: [670, 260]
size: [130, 60]
- ref: policy
pos: [880, 260]
size: [130, 60]
connections:
- { from: claims-admin, to: portal, label: serves, variant: emphasis }
- { from: claims-app, to: claims-admin, label: realizes }
- { from: claims-app, to: claim, label: accesses, labelAt: [525, 234] }
- { from: crm, to: customer-file, label: accesses }
- { from: policy-admin, to: policy, label: accesses }
@@ -0,0 +1,45 @@
schema_version: 1
diagram_type: architecture
meta:
title: Technology layer
output: infrastructure.html
components:
- ref: dmz
pos: [40, 160]
size: [130, 60]
- ref: doc-store
pos: [250, 60]
size: [130, 60]
decisions: [documents-in-object-storage]
- ref: app-servers
pos: [250, 160]
size: [130, 60]
- ref: db-service
pos: [250, 260]
size: [130, 60]
- ref: db-cluster
pos: [250, 360]
size: [130, 60]
- ref: mq
pos: [460, 160]
size: [130, 60]
- ref: mainframe
pos: [670, 160]
size: [130, 60]
decisions: [policies-stay-on-mainframe]
- ref: dc-lan
pos: [670, 360]
size: [130, 60]
boundaries:
- kind: region
label: Data centre
wraps: [doc-store, app-servers, db-service, db-cluster, mq, mainframe, dc-lan]
connections:
- { from: dmz, to: app-servers, label: HTTPS, variant: security }
- { from: app-servers, to: doc-store, label: S3 API }
- { from: db-service, to: app-servers, label: serves }
- { from: db-cluster, to: db-service, label: realizes }
- { from: app-servers, to: mq, label: MQ }
- { from: mq, to: mainframe, label: bridge }
- { from: db-cluster, to: dc-lan, label: LAN }
- { from: mainframe, to: dc-lan, label: LAN, labelAt: [735, 234] }
@@ -0,0 +1,20 @@
schema_version: 1
decision:
id: policies-stay-on-mainframe
title: Policies stay on the mainframe
status: accepted
date: 2026-09-08
elements: [policy-admin, mainframe, mq, claims-app]
diagrams: [0200-application/application-cooperation.archify, 0300-technology/infrastructure.archify]
context: >
The new claims application needs coverage data for every
claim. Policy administration has run on the mainframe for
twenty years and a migration was estimated at three years.
decision: >
Policy administration stays on the mainframe. The claims
application reads coverage through the message queue bridge
and never copies policies into its own database.
consequences: >
Accepting a claim depends on the mainframe being up during
office hours. The queue bridge must be kept and tested.
A later migration needs its own decision.
@@ -0,0 +1,20 @@
schema_version: 1
decision:
id: documents-in-object-storage
title: Claim documents in object storage
status: accepted
date: 2026-10-14
elements: [doc-mgmt, doc-store, claims-app]
diagrams: [0200-application/application-cooperation.archify, 0300-technology/infrastructure.archify]
context: >
Photos and expert reports were stored as blobs in the claims
database. The database grew by forty gigabytes a month and
backups no longer fitted the nightly window.
decision: >
Document management stores every file in the document store
and keeps only a reference in the claim. The claims
application never writes files itself.
consequences: >
Database backups shrink and stay fast. Documents get their own
retention rules. A claim and its files are restored from two
places after an incident.
@@ -0,0 +1,59 @@
schema_version: 1
model:
id: insurance-claims
name: Insurance Claims
description: >
ArchiMate example: how Halbrook Mutual handles claims. The business,
application and technology layers are mapped to folders, and each
element's ArchiMate type is shown as its tag.
diagram_type: architecture
meta:
title: Insurance Claims - layered view
output: main.html
components:
- ref: insurant
pos: [40, 70]
size: [170, 60]
- ref: claim-registration
pos: [270, 70]
size: [170, 60]
- ref: handle-claim
pos: [500, 70]
size: [170, 60]
- ref: portal
pos: [40, 270]
size: [170, 60]
- ref: claims-app
pos: [270, 270]
size: [170, 60]
- ref: claims-admin
pos: [500, 270]
size: [170, 60]
- ref: dmz
pos: [40, 470]
size: [170, 60]
- ref: app-servers
pos: [270, 470]
size: [170, 60]
- ref: db-cluster
pos: [500, 470]
size: [170, 60]
boundaries:
- kind: region
label: Business layer
wraps: [insurant, claim-registration, handle-claim]
- kind: region
label: Application layer
wraps: [portal, claims-app, claims-admin]
- kind: region
label: Technology layer
wraps: [dmz, app-servers, db-cluster]
connections:
- { from: claim-registration, to: insurant, label: serves, variant: emphasis }
- { from: handle-claim, to: claim-registration, label: realizes }
- { from: portal, to: insurant, label: serves, labelAt: [125, 197] }
- { from: claims-admin, to: handle-claim, label: serves, labelAt: [585, 197] }
- { from: claims-app, to: claims-admin, label: realizes }
- { from: dmz, to: portal, label: serves, labelAt: [125, 397] }
- { from: app-servers, to: claims-app, label: serves, labelAt: [355, 397] }
- { from: db-cluster, to: app-servers, label: serves }
@@ -0,0 +1,76 @@
schema_version: 1
elements:
portal:
type: frontend
label: Customer Portal
sublabel: App Interface
tag: App Interface
description: >
Web and mobile front end where customers report claims and
follow their status.
claims-admin:
type: cloud
label: Claims Admin
sublabel: App Service
tag: App Service
description: >
Register, look up and update claims; used by the portal and
by claims handlers.
claims-app:
type: backend
label: Claims App
sublabel: App Component
tag: App Component
description: >
Owns claims from registration to payment; the system of
record for claims.
diagram: 0200-application/application-structure.archify
crm:
type: backend
label: CRM
sublabel: App Component
tag: App Component
description: >
Customer contacts, addresses and the customer file.
policy-admin:
type: backend
label: Policy Admin
sublabel: App Component
tag: App Component
description: >
Policies, coverage and premiums; runs on the mainframe.
doc-mgmt:
type: backend
label: Document Mgmt
sublabel: App Component
tag: App Component
description: >
Scanned letters, photos and expert reports attached to claims.
payment:
type: backend
label: Payment
sublabel: App Component
tag: App Component
description: >
Sends approved claim payments to the bank in daily batches.
claim:
type: database
label: Claim
sublabel: Data Object
tag: Data Object
description: >
One reported damage, its valuation and its payment status.
policy:
type: database
label: Policy
sublabel: Data Object
tag: Data Object
description: >
Coverage, deductible and term of an insurance contract.
customer-file:
type: database
label: Customer File
sublabel: Data Object
tag: Data Object
description: >
Everything known about a customer; contacts, policies, claims.
@@ -0,0 +1,50 @@
schema_version: 1
elements:
customer:
type: external
label: Customer
sublabel: Business Actor
tag: Business Actor
description: >
A person or small business holding one or more policies
with Halbrook Mutual.
insurant:
type: external
label: Insurant
sublabel: Business Role
tag: Business Role
description: >
The role a customer plays while a claim is open; reports
damage and receives the payment.
claims-handler:
type: external
label: Claims handler
sublabel: Business Role
tag: Business Role
description: >
Employee of the claims department who accepts, valuates
and approves claims.
handle-claim:
type: backend
label: Handle Claim
sublabel: Business Process
tag: Business Process
description: >
Register, accept, valuate and pay a claim; the core process
of the claims department.
diagram: 0100-business/handle-claim.archify
claim-registration:
type: frontend
label: Claim Registration
sublabel: Business Service
tag: Business Service
description: >
Lets an insurant report damage by phone, mail or the
customer portal.
claim-payment:
type: frontend
label: Claim Payment
sublabel: Business Service
tag: Business Service
description: >
Pays the valuated amount to the insurant or a repairer.
@@ -0,0 +1,62 @@
schema_version: 1
elements:
app-servers:
type: backend
label: App Servers
sublabel: Node
tag: Node
description: >
Two virtual machine hosts running the claims app, CRM and the
portal back end.
diagram: 0300-technology/infrastructure.archify
db-cluster:
type: database
label: DB Cluster
sublabel: Node
brand: postgresql
tag: Node
description: >
Primary and standby database servers for claims and CRM data.
doc-store:
type: database
label: Document Store
sublabel: System Software
tag: System Software
description: >
Object storage for scans and photos; written by document
management only.
mainframe:
type: backend
label: Mainframe
sublabel: Node
tag: Node
description: >
Hosts policy administration and the nightly payment batch.
mq:
type: messagebus
label: Message Queue
sublabel: System Software
tag: System Software
description: >
Queue manager between the app servers and the mainframe.
db-service:
type: cloud
label: DB Service
sublabel: Tech Service
tag: Tech Service
description: >
Managed SQL access with backups, offered to all applications.
dc-lan:
type: messagebus
label: Data Centre LAN
sublabel: Network
tag: Network
description: >
Internal network linking app servers, databases and mainframe.
dmz:
type: security
label: DMZ Gateway
sublabel: Network
tag: Network
description: >
Reverse proxy and firewall between the internet and the LAN.
@@ -0,0 +1,34 @@
schema_version: 1
diagram_type: architecture
meta:
title: Business actors and services
output: business-services.html
components:
- ref: shipper
pos: [40, 60]
size: [130, 60]
- ref: recipient
pos: [40, 260]
size: [130, 60]
- ref: delivery-service
pos: [250, 60]
size: [130, 60]
- ref: tracking-service
pos: [250, 260]
size: [130, 60]
- ref: deliver-shipment
pos: [460, 160]
size: [130, 60]
- ref: driver
pos: [670, 60]
size: [130, 60]
- ref: customer-service
pos: [670, 260]
size: [130, 60]
connections:
- { from: delivery-service, to: shipper, label: serves, variant: emphasis }
- { from: tracking-service, to: recipient, label: serves }
- { from: deliver-shipment, to: delivery-service, label: realizes }
- { from: deliver-shipment, to: tracking-service, label: realizes }
- { from: driver, to: deliver-shipment, label: assigned to }
- { from: customer-service, to: tracking-service, label: assigned to }
@@ -0,0 +1,25 @@
schema_version: 2
diagram_type: workflow
meta:
title: Deliver Shipment
output: deliver-shipment.html
viewBox: [840, 534]
lanes:
- { id: customer-lane, label: Shipper and recipient }
- { id: driver-lane, label: Driver }
- { id: hub-lane, label: Hub }
mainPath: [book, pick-up, sort, line-haul, last-mile, pod-step]
nodes:
- { id: book, lane: customer-lane, col: 0, type: external, label: Book shipment, tag: Business Event }
- { id: pick-up, lane: driver-lane, col: 1, type: backend, label: Pick up, tag: Business Process }
- { id: sort, lane: hub-lane, col: 2, type: backend, label: Sort at hub, tag: Business Process }
- { id: line-haul, lane: hub-lane, col: 3, type: backend, label: Line haul, tag: Business Process }
- { id: last-mile, lane: driver-lane, col: 4, type: backend, label: Last mile, tag: Business Process }
- { id: pod-step, lane: customer-lane, col: 5, type: security, label: Delivered, sublabel: proof of delivery, tag: Business Process }
edges:
- { from: book, to: pick-up, label: triggers }
- { from: pick-up, to: sort, label: parcel }
- { from: sort, to: line-haul, label: trailer }
- { from: line-haul, to: last-mile, label: route }
- { from: last-mile, to: pod-step, label: hand over }
- { from: last-mile, to: sort, label: not home, role: return, variant: dashed }
@@ -0,0 +1,32 @@
schema_version: 1
diagram_type: architecture
meta:
title: Application cooperation
output: application-cooperation.html
decisions: [edi-gateway-in-front-of-tms]
components:
- ref: shipper
pos: [40, 260]
size: [130, 60]
- ref: edi-gateway
pos: [250, 260]
size: [130, 60]
decisions: [edi-gateway-in-front-of-tms]
- ref: tms
pos: [460, 260]
size: [130, 60]
- ref: driver-app
pos: [460, 60]
size: [130, 60]
- ref: telematics
pos: [460, 460]
size: [130, 60]
- ref: tracking-portal
pos: [670, 260]
size: [130, 60]
connections:
- { from: shipper, to: edi-gateway, label: bookings, variant: emphasis }
- { from: edi-gateway, to: tms, label: shipments, variant: emphasis }
- { from: driver-app, to: tms, label: scans, labelAt: [525, 134], decisions: [broker-for-tracking-events] }
- { from: telematics, to: tms, label: positions, decisions: [broker-for-tracking-events] }
- { from: tms, to: tracking-portal, label: status }
@@ -0,0 +1,34 @@
schema_version: 1
diagram_type: architecture
meta:
title: Application services and data
output: application-services.html
components:
- ref: tracking-portal
pos: [40, 160]
size: [130, 60]
- ref: tracking-svc
pos: [250, 160]
size: [130, 60]
- ref: tms
pos: [460, 160]
size: [130, 60]
- ref: driver-app
pos: [670, 160]
size: [130, 60]
- ref: shipment
pos: [250, 360]
size: [130, 60]
- ref: tracking-event
pos: [460, 360]
size: [130, 60]
- ref: pod
pos: [670, 360]
size: [130, 60]
connections:
- { from: tracking-svc, to: tracking-portal, label: serves, variant: emphasis }
- { from: tms, to: tracking-svc, label: realizes }
- { from: driver-app, to: tms, label: uses }
- { from: tms, to: shipment, label: accesses }
- { from: tms, to: tracking-event, label: accesses, labelAt: [525, 234] }
- { from: driver-app, to: pod, label: accesses, labelAt: [735, 234] }
@@ -0,0 +1,44 @@
schema_version: 1
diagram_type: architecture
meta:
title: Technology infrastructure
output: infrastructure.html
decisions: [broker-for-tracking-events]
components:
- ref: handhelds
pos: [40, 160]
size: [130, 60]
- ref: telematics-units
pos: [40, 360]
size: [130, 60]
- ref: mobile-network
pos: [250, 260]
size: [130, 60]
- ref: cloud-cluster
pos: [460, 160]
size: [130, 60]
- ref: message-broker
pos: [460, 360]
size: [130, 60]
decisions: [broker-for-tracking-events]
- ref: db-server
pos: [670, 160]
size: [130, 60]
- ref: event-streaming
pos: [670, 360]
size: [130, 60]
boundaries:
- kind: region
label: Field devices
wraps: [handhelds, telematics-units]
- kind: security-group
label: Cloud platform
wraps: [cloud-cluster, message-broker, db-server, event-streaming]
connections:
- { from: handhelds, to: mobile-network, label: LTE }
- { from: telematics-units, to: mobile-network, label: LTE }
- { from: mobile-network, to: cloud-cluster, label: HTTPS, variant: security }
- { from: mobile-network, to: message-broker, label: MQTT, variant: security }
- { from: cloud-cluster, to: message-broker, label: hosts, labelAt: [525, 234] }
- { from: cloud-cluster, to: db-server, label: SQL }
- { from: message-broker, to: event-streaming, label: realizes }
@@ -0,0 +1,20 @@
schema_version: 1
decision:
id: broker-for-tracking-events
title: A message broker for tracking events
status: accepted
date: 2026-09-08
elements: [message-broker, telematics, driver-app, tms, tracking-event]
diagrams: [0300-technology/infrastructure.archify, 0200-application/application-cooperation.archify]
context: >
Handhelds and telematics units posted every scan and position
straight to the TMS. At evening peaks the TMS fell behind and
dropped events when a device retried over a weak signal.
decision: >
Devices publish tracking events to a Kafka message broker on
the cloud cluster. The TMS and telematics read the topics at
their own pace; events are kept for seven days for replay.
consequences: >
Spikes no longer reach the TMS. Status on the tracking portal
can lag by seconds. Consumers must handle duplicate events,
and the broker is one more platform service to run.
@@ -0,0 +1,20 @@
schema_version: 1
decision:
id: edi-gateway-in-front-of-tms
title: An EDI gateway in front of the TMS
status: accepted
date: 2026-09-22
elements: [edi-gateway, tms, shipper, shipment]
diagrams: [0200-application/application-cooperation.archify]
context: >
Large shippers send bookings as EDI files in three dialects.
Each dialect was parsed inside the TMS, so a new shipper meant
a TMS release.
decision: >
A separate EDI gateway receives, validates and translates
every booking into one shipment message for the TMS. It also
sends status messages back in the shipper's dialect.
consequences: >
New shippers are onboarded in the gateway alone. The TMS has
one booking interface. A rejected booking now fails at the
gateway, which customer service must be able to see.
@@ -0,0 +1,17 @@
schema_version: 1
decision:
id: photo-proof-of-delivery
title: Photo as proof of delivery
status: proposed
date: 2026-10-27
elements: [pod, driver-app, handhelds]
diagrams: [0200-application/application-services.archify, 0100-business/deliver-shipment.archify]
context: >
Most parcels are left at the door when nobody signs. Disputes
about such parcels take customer service days to settle.
decision: >
The driver app takes a photo of every parcel left without a
signature and stores it with the proof of delivery.
consequences: >
Disputes are settled from the photo. Photos are personal data
and are deleted after 90 days. Uploads add mobile data cost.
@@ -0,0 +1,59 @@
schema_version: 1
model:
id: logistics-tracking
name: Logistics Tracking
description: >
An ArchiMate example: a parcel carrier lets shippers and recipients
track shipments. Business, application and technology layers are
mapped to folders; each element's ArchiMate type is shown as its tag.
diagram_type: architecture
meta:
title: Logistics Tracking - layered view
output: main.html
components:
- ref: recipient
pos: [40, 70]
size: [170, 60]
- ref: tracking-service
pos: [270, 70]
size: [170, 60]
- ref: deliver-shipment
pos: [500, 70]
size: [170, 60]
- ref: tracking-portal
pos: [40, 270]
size: [170, 60]
- ref: tms
pos: [270, 270]
size: [170, 60]
- ref: tracking-svc
pos: [500, 270]
size: [170, 60]
- ref: mobile-network
pos: [40, 470]
size: [170, 60]
- ref: cloud-cluster
pos: [270, 470]
size: [170, 60]
- ref: event-streaming
pos: [500, 470]
size: [170, 60]
boundaries:
- kind: region
label: Business layer
wraps: [recipient, tracking-service, deliver-shipment]
- kind: region
label: Application layer
wraps: [tracking-portal, tms, tracking-svc]
- kind: region
label: Technology layer
wraps: [mobile-network, cloud-cluster, event-streaming]
connections:
- { from: tracking-service, to: recipient, label: serves, variant: emphasis }
- { from: deliver-shipment, to: tracking-service, label: realizes }
- { from: tracking-portal, to: recipient, label: serves, labelAt: [125, 197] }
- { from: tracking-svc, to: deliver-shipment, label: serves, labelAt: [585, 197] }
- { from: tms, to: tracking-svc, label: realizes }
- { from: mobile-network, to: tracking-portal, label: serves, labelAt: [125, 397] }
- { from: cloud-cluster, to: tms, label: serves, labelAt: [355, 397] }
- { from: event-streaming, to: tracking-svc, label: serves, labelAt: [585, 397] }
@@ -0,0 +1,67 @@
schema_version: 1
elements:
tms:
type: backend
label: TMS
sublabel: App Component
tag: App Component
description: >
Owns shipments, routes and hub plans; the source of truth for status.
diagram: 0200-application/application-cooperation.archify
tracking-portal:
type: frontend
label: Tracking portal
sublabel: App Component
tag: App Component
description: >
Public site where shippers and recipients follow a shipment.
edi-gateway:
type: security
label: EDI gateway
sublabel: App Component
tag: App Component
description: >
Receives booking messages from large shippers and returns status.
driver-app:
type: frontend
label: Driver app
sublabel: App Component
tag: App Component
description: >
Scans parcels at every hand-over and captures proof of delivery.
telematics:
type: backend
label: Telematics
sublabel: App Component
tag: App Component
description: >
Collects vehicle positions and turns them into arrival estimates.
tracking-svc:
type: backend
label: Tracking service
sublabel: App Service
tag: App Service
description: >
Status and arrival estimate of one shipment, offered by the TMS.
diagram: 0200-application/application-services.archify
shipment:
type: database
label: Shipment
sublabel: Data Object
tag: Data Object
description: >
One booked parcel with sender, recipient, route and status.
tracking-event:
type: database
label: Tracking event
sublabel: Data Object
tag: Data Object
description: >
A scan or position report; the history of a shipment.
pod:
type: database
label: Proof of delivery
sublabel: Data Object
tag: Data Object
description: >
Signature or photo taken at the door; closes the shipment.
@@ -0,0 +1,54 @@
schema_version: 1
elements:
shipper:
type: external
label: Shipper
sublabel: Business Actor
tag: Business Actor
description: >
Books shipments, hands parcels over at pick-up and follows
them until delivery.
recipient:
type: external
label: Recipient
sublabel: Business Actor
tag: Business Actor
description: >
Receives the parcel; tracks it and may reschedule delivery.
driver:
type: external
label: Driver
sublabel: Business Role
tag: Business Role
description: >
Collects parcels, drives the routes and captures proof of delivery.
customer-service:
type: external
label: Customer service
sublabel: Business Role
tag: Business Role
description: >
Answers shipper and recipient questions about late or lost parcels.
deliver-shipment:
type: backend
label: Deliver Shipment
sublabel: Business Process
tag: Business Process
description: >
Pick up, sort at hub, line haul, last mile and proof of delivery.
diagram: 0100-business/deliver-shipment.archify
delivery-service:
type: backend
label: Parcel delivery
sublabel: Business Service
tag: Business Service
description: >
What the carrier sells; a parcel collected and delivered on time.
diagram: 0100-business/business-services.archify
tracking-service:
type: backend
label: Shipment tracking
sublabel: Business Service
tag: Business Service
description: >
Where a parcel is and when it arrives, for shippers and recipients.
@@ -0,0 +1,55 @@
schema_version: 1
elements:
cloud-cluster:
type: cloud
label: Cloud cluster
sublabel: Node
brand: kubernetes
tag: Node
description: >
Container cluster that runs the TMS, portal, EDI gateway and telematics.
diagram: 0300-technology/infrastructure.archify
message-broker:
type: messagebus
label: Message broker
sublabel: System Software
brand: apache-kafka
tag: System Software
description: >
Carries tracking events from devices and vehicles to the applications.
db-server:
type: database
label: Database server
sublabel: System Software
brand: postgresql
tag: System Software
description: >
Stores shipments, tracking events and proofs of delivery.
event-streaming:
type: backend
label: Event streaming
sublabel: Tech Service
tag: Tech Service
description: >
Durable, ordered topics that applications publish to and read from.
handhelds:
type: external
label: Handhelds
sublabel: Device
tag: Device
description: >
Scanners carried by drivers and hub staff; run the driver app.
telematics-units:
type: external
label: Telematics units
sublabel: Device
tag: Device
description: >
GPS boxes in vans and trucks; report position every minute.
mobile-network:
type: security
label: Mobile network
sublabel: Network
tag: Network
description: >
Carrier-grade mobile network that links devices to the cluster.
+59
View File
@@ -77,6 +77,65 @@ The numbered folders are only for sorting; nothing depends on the names. A team
Component types that fit C4: people are `external`; the system itself and other systems are `cloud` (own) or `external` (others); containers are `frontend`, `backend`, `database`, `messagebus`, `security`; components inside a container use the same set, wrapped in a `region` boundary named after the container.
## arc42
[arc42](https://arc42.org) is a documentation template with twelve sections; four of them are views (diagrams), the rest are text. In a model, the views are folders numbered like their section, the decisions are section 9, and the text sections are `cards` (title plus short items) on the diagram they belong to. Card titles carry the section number so a reader finds each section.
```
cinema-tickets/
main.archify section 3 business context (the system and its neighbours);
cards 1.1 Requirements, 1.2 Quality goals, 1.3 Stakeholders,
2 Constraints, 4 Solution strategy, 11 Risks and technical debt,
12 Glossary
registry/
people.archify-registry users and operators
systems.archify-registry neighbouring systems
building-blocks.archify-registry each block's description in black-box form:
"Responsibility: ... Interfaces: ..." (section 5)
0300-context/
technical-context.archify channels and protocols; card 3 Domain interfaces
0500-building-blocks/
level-1.archify the system's white box; card 8 Crosscutting concepts
booking.archify level 2 of one block, linked from its registry entry
0600-runtime/
book-seats.archify key scenarios as sequence diagrams
0700-deployment/
production.archify building blocks inside boundaries for nodes and zones;
card 7 Mapping (block -> node)
1000-quality/
quality-tree.archify Quality -> characteristics -> scenarios;
card 10 Quality scenarios (stimulus and response)
decisions/ section 9, one *.archify-decision per decision
```
Say where each section lives in `model.description`. Cards hold short lines, not prose: an arc42 document with long text belongs next to the model, not in it. `examples/arc42-cinema-tickets/`, `arc42-iot-telemetry/` and `arc42-team-wiki/` (the smallest) are complete.
## ArchiMate
[ArchiMate](https://www.opengroup.org/archimate-forum) models an enterprise in layers. Archify has no ArchiMate notation, so each element is an Archify component type with its ArchiMate type written out: in `tag` (searchable, shown in the focus panel) and in `sublabel` (shown on the box; Archify draws tags only in the focus panel).
```
insurance-claims/
main.archify layered view: Business, Application, Technology as three
region boundaries, top to bottom, three elements each in the
same columns, serves/realizes arrows between layers
registry/
business.archify-registry actors, roles, processes, services, events
application.archify-registry components, services, interfaces, data objects
technology.archify-registry nodes, system software, technology services, networks
0100-business/
business-layer.archify actors, roles and services
handle-claim.archify the business process as a workflow (lanes per role)
0200-application/
application-structure.archify components, services, interfaces, data objects
application-cooperation.archify how the components talk
0300-technology/
infrastructure.archify nodes and networks inside boundaries
decisions/
```
A mapping that reads well: actors and roles `external`; processes `backend`; business and application services `cloud` or `frontend`; application components `backend` or `frontend`; data objects `database`; nodes `backend`, `cloud` or `database`; networks `messagebus` or `security`. Name relationships in connection labels: serves, realizes, uses, accesses, assigned to, triggers. Keep type names short enough for the box (`App Component`, `Tech Service`). `examples/archimate-insurance-claims/`, `archimate-hr-onboarding/` and `archimate-logistics-tracking/` are complete.
## How the extension knows it is in a model
The nearest folder above a file that contains `main.archify` is the root. Under that root, at any depth: `*.archify` are member diagrams, `*.archify-registry` make up the registry, subfolders are groups. `node_modules` and `.git` are skipped.
+7
View File
@@ -30,3 +30,10 @@ tasks:
- |
bash ./.scripts/model/api/model--resolve.sh {{ .CLI_ARGS }}
silent: true
graph:
desc: 'The model as one graph, with queries -- --dir="<model folder or any file inside>" [--query="uses:api"] [--links]'
cmds:
- |
bash ./.scripts/model/api/model--graph.sh {{ .CLI_ARGS }}
silent: true
@@ -0,0 +1,8 @@
#!/bin/bash
# -------------------------------------------------------------------------------------
# archify-vscode-ext Agent Skill (MIT)
# -------------------------------------------------------------------------------------
. ./.scripts/model/lib/--index.sh
_model__model__graph "$@"
@@ -8,3 +8,4 @@
. ./.scripts/model/lib/-model--list.sh
. ./.scripts/model/lib/-model--scaffold.sh
. ./.scripts/model/lib/-model--resolve.sh
. ./.scripts/model/lib/-model--graph.sh
@@ -0,0 +1,9 @@
#!/bin/bash
# -------------------------------------------------------------------------------------
# archify-vscode-ext Agent Skill (MIT)
# -------------------------------------------------------------------------------------
# Runs the "model-graph" action of the Node CLI. Flags are forwarded unchanged.
_model__model__graph() {
_model__node__run --action model-graph "$@"
}
@@ -0,0 +1,38 @@
// The model as one graph, as the extension's Graph page shows it, with the
// same queries: answer "what connects to X", "the path from A to B", "what
// a decision touches" or "which registry elements no diagram draws".
import fs from 'node:fs';
import path from 'node:path';
import { MAIN, findRoot, loadModel } from '../../services/model.mjs';
import { buildGraph, runQuery } from '../../services/graph.mjs';
export const help = `model-graph — the whole model as one graph, narrowed by a query
--dir <path> model folder, or any file/folder inside it (required)
--query <text> database · type:backend · diagram:0200-* · decision:<id> · uses:api · near:api,2 · from:web to:db · unused · shared · -shared
terms AND together, a leading - excludes; walks follow the diagrams' connections only
--links also list the links from decisions and registries to their elements`;
export async function run(input) {
if (!input.dir) return { success: false, error: '--dir is required: the model folder or any file inside it.', help };
const target = path.resolve(String(input.dir));
if (!fs.existsSync(target)) return { success: false, error: `${target} does not exist.` };
const root = findRoot(target);
if (!root) return { success: false, error: `No ${MAIN} in ${target} or any folder above it.` };
const graph = buildGraph(loadModel(root));
const query = typeof input.query === 'string' ? input.query : '';
const result = runQuery(graph, query);
const elements = graph.nodes.filter((n) => n.kind === 'element');
const shown = graph.nodes.filter((n) => result.nodes.has(n.id));
const edges = graph.edges.filter((e) => result.edges.has(e.id) && (input.links || e.kind === 'connection'));
return {
success: !result.message,
root,
query: query || undefined,
message: result.message,
summary: { elements: elements.length, connections: graph.edges.filter((e) => e.kind === 'connection').length, registries: graph.nodes.filter((n) => n.kind === 'registry').length, decisions: graph.decisions.length, diagrams: graph.diagrams.length, matching: shown.filter((n) => n.kind === 'element').length },
path: result.path,
nodes: shown.map((n) => ({ id: n.id, kind: n.kind, label: n.label, type: n.type, ...(n.sublabel ? { sublabel: n.sublabel } : {}), ...(n.kind === 'element' ? { shared: n.shared, diagrams: n.diagrams, decisions: n.decisions } : {}), ...(n.kind === 'decision' ? { status: n.status, file: n.file } : {}), ...(n.kind === 'registry' ? { file: n.file, elements: n.elements } : {}) })),
connections: edges.map((e) => ({ from: e.from, to: e.to, kind: e.kind, ...(e.labels.length ? { label: e.labels.join(' / ') } : {}), ...(e.diagrams.length ? { diagrams: e.diagrams } : {}), ...(e.decisions.length ? { decisions: e.decisions } : {}) })),
};
}
@@ -16,6 +16,12 @@ export const EXAMPLES = {
'c4-standard-web-app': 'C4 - Standard Web App: single-page app, API, database, cache, file storage, identity provider',
'c4-e-shop': 'C4 - E-Shop: web shop, API, worker, queue, payment and email providers',
'c4-event-based-integration': 'C4 - Event-Based Integration: event bus, router, dead-letter queue, event store, consumers',
'arc42-cinema-tickets': 'arc42 - Cinema Tickets: context, building blocks, runtime and deployment; seat booking with payment',
'arc42-iot-telemetry': 'arc42 - IoT Telemetry: devices over MQTT, ingest, time-series store, alerting, dashboards',
'arc42-team-wiki': 'arc42 - Team Wiki: a small self-hosted wiki, the arc42 sections at their smallest',
'archimate-insurance-claims': 'ArchiMate - Insurance Claims: business, application and technology layers of claim handling',
'archimate-hr-onboarding': 'ArchiMate - HR Onboarding: onboarding a new employee across HR, identity and IT services',
'archimate-logistics-tracking': 'ArchiMate - Logistics Tracking: shipment tracking from pick-up to proof of delivery',
};
export const help = `model-scaffold — create main.archify (and with --c4 the C4 folders), or copy an example
+2
View File
@@ -6,6 +6,7 @@ import * as modelValidate from '../actions/model-validate/action.mjs';
import * as modelList from '../actions/model-list/action.mjs';
import * as modelScaffold from '../actions/model-scaffold/action.mjs';
import * as modelResolve from '../actions/model-resolve/action.mjs';
import * as modelGraph from '../actions/model-graph/action.mjs';
import * as diagramValidate from '../actions/diagram-validate/action.mjs';
import * as diagramRender from '../actions/diagram-render/action.mjs';
import { toYaml } from '../services/yaml.mjs';
@@ -15,6 +16,7 @@ const ACTIONS = {
'model-list': modelList,
'model-scaffold': modelScaffold,
'model-resolve': modelResolve,
'model-graph': modelGraph,
'diagram-validate': diagramValidate,
'diagram-render': diagramRender,
};
+226
View File
@@ -0,0 +1,226 @@
// The model as one graph, as the extension's Graph page shows it: every element any
// diagram draws plus every registry element, every connection once, plus one node
// per decision and per registry file. The query language is the page's: terms AND
// together, a leading "-" excludes, key:value terms filter or walk the graph.
import { NODE_COLLECTION } from './model.mjs';
import { effectiveStatus } from './decisions.mjs';
const EDGE_COLLECTIONS = ['connections', 'edges', 'flows', 'transitions', 'messages'];
const NODE_COLLECTIONS = [...new Set(Object.values(NODE_COLLECTION))];
const str = (v) => (typeof v === 'string' && v.length ? v : undefined);
const push = (list, value) => { if (!list.includes(value)) list.push(value); };
export const edgeId = (from, to) => `${from}>${to}`;
export const decisionNodeId = (id) => `decision:${id}`;
export const registryNodeId = (file) => `registry:${file}`;
export function buildGraph(model) {
const nodes = new Map();
const edges = new Map();
for (const e of Object.values(model.registry.elements)) {
nodes.set(e.id, { id: e.id, kind: 'element', label: e.label ?? e.id, sublabel: e.sublabel, type: e.type, brand: e.brand, shared: true, description: e.description?.trim() || undefined, file: e.file, line: e.line, link: e.diagram, diagrams: [], decisions: [] });
}
const diagrams = model.diagrams.filter((d) => d.resolved && !d.isMain).sort((a, b) => a.file.localeCompare(b.file));
for (const d of diagrams) {
const ids = new Set();
for (const key of NODE_COLLECTIONS) {
for (const item of Array.isArray(d.resolved[key]) ? d.resolved[key] : []) {
if (!item || typeof item !== 'object') continue;
const id = str(item.id);
if (!id || ids.has(id)) continue;
ids.add(id);
let node = nodes.get(id);
if (!node) {
node = { id, kind: 'element', label: str(item.label) ?? id, sublabel: str(item.sublabel), type: str(item.type), brand: str(item.brand), shared: false, diagrams: [], decisions: [] };
nodes.set(id, node);
}
push(node.diagrams, d.file);
}
}
for (const key of EDGE_COLLECTIONS) {
for (const item of Array.isArray(d.resolved[key]) ? d.resolved[key] : []) {
const from = str(item?.from);
const to = str(item?.to);
if (!from || !to || !ids.has(from) || !ids.has(to)) continue;
const id = edgeId(from, to);
let edge = edges.get(id);
if (!edge) { edge = { id, kind: 'connection', from, to, labels: [], diagrams: [], decisions: [] }; edges.set(id, edge); }
if (str(item.label)) push(edge.labels, item.label);
push(edge.diagrams, d.file);
}
}
}
const all = model.decisions.decisions;
for (const d of Object.values(all)) for (const id of d.elements ?? []) if (nodes.has(id)) push(nodes.get(id).decisions, d.id);
for (const [decisionId, uses] of Object.entries(model.decisionUsage ?? {})) {
if (!all[decisionId]) continue;
for (const use of uses) {
if (use.attachedTo?.kind === 'node' && nodes.has(use.attachedTo.id)) push(nodes.get(use.attachedTo.id).decisions, decisionId);
if (use.attachedTo?.kind === 'edge') { const edge = edges.get(edgeId(use.attachedTo.from, use.attachedTo.to)); if (edge) push(edge.decisions, decisionId); }
}
}
const byFile = new Map();
for (const e of Object.values(model.registry.elements)) byFile.set(e.file, [...(byFile.get(e.file) ?? []), e.id]);
for (const { file } of model.registry.files ?? []) if (!byFile.has(file)) byFile.set(file, []);
for (const [file, ids] of byFile) {
const id = registryNodeId(file);
nodes.set(id, { id, kind: 'registry', label: file.replace(/\.archify-registry$/, ''), type: 'registry', shared: true, file, diagrams: [], decisions: [], elements: ids });
for (const element of ids) { const e = edgeId(id, element); edges.set(e, { id: e, kind: 'defines', from: id, to: element, labels: [], diagrams: [], decisions: [] }); }
}
for (const d of Object.values(all)) {
const id = decisionNodeId(d.id);
nodes.set(id, { id, kind: 'decision', label: d.title ?? d.id, sublabel: d.id, type: 'decision', shared: true, description: [d.context, d.decision].filter(Boolean).join('\n\n') || undefined, file: d.file, diagrams: [...(d.diagrams ?? [])], decisions: [], status: effectiveStatus(d, all).status });
}
for (const node of [...nodes.values()]) {
if (node.kind !== 'element') continue;
for (const decision of node.decisions) { if (!all[decision]) continue; const e = edgeId(decisionNodeId(decision), node.id); edges.set(e, { id: e, kind: 'decides', from: decisionNodeId(decision), to: node.id, labels: [], diagrams: [], decisions: [] }); }
}
for (const d of Object.values(all)) for (const other of d.supersedes ?? []) { if (!all[other]) continue; const e = edgeId(decisionNodeId(d.id), decisionNodeId(other)); edges.set(e, { id: e, kind: 'supersedes', from: decisionNodeId(d.id), to: decisionNodeId(other), labels: ['supersedes'], diagrams: [], decisions: [] }); }
return {
nodes: [...nodes.values()].sort((a, b) => a.id.localeCompare(b.id)),
edges: [...edges.values()].sort((a, b) => a.id.localeCompare(b.id)),
decisions: Object.values(all).map((d) => ({ id: d.id, title: d.title, status: effectiveStatus(d, all).status, file: d.file })).sort((a, b) => a.id.localeCompare(b.id)),
diagrams: diagrams.map((d) => ({ path: d.file, title: d.resolved?.meta?.title, kind: d.diagramType })),
};
}
// ----------------------------------------------------------------- queries
const WALK_KEYS = new Set(['uses', 'near', 'from', 'to']);
const FIELD_KEYS = new Set(['type', 'diagram', 'decision', 'shared', 'unused', 'text']);
export function parseQuery(text) {
const tokens = String(text ?? '').trim().split(/\s+/).filter(Boolean);
const terms = [];
for (let i = 0; i < tokens.length; i += 1) {
let token = tokens[i];
const negate = token.startsWith('-') && token.length > 1;
if (negate) token = token.slice(1);
const colon = token.indexOf(':');
let key = colon > 0 ? token.slice(0, colon).toLowerCase() : token.toLowerCase();
let value = colon > 0 ? token.slice(colon + 1) : '';
if (colon > 0 && !WALK_KEYS.has(key) && !FIELD_KEYS.has(key)) { key = 'text'; value = token; }
else if (colon < 0 && key !== 'shared' && key !== 'unused') { key = 'text'; value = token; }
if (key === 'near' && !value.includes(',') && /^\d+$/.test(tokens[i + 1] ?? '')) { value = `${value},${tokens[i + 1]}`; i += 1; }
if (!value && key === 'text') continue;
terms.push({ key, value, negate });
}
return terms;
}
const lower = (s) => (s ?? '').toLowerCase();
const isElement = (n) => n.kind === 'element';
const isConnection = (e) => e.kind === 'connection';
export function findNode(graph, ref) {
const elements = graph.nodes.filter(isElement);
return elements.find((n) => n.id === ref) ?? elements.find((n) => n.id.toLowerCase() === ref.toLowerCase()) ?? elements.find((n) => n.label.toLowerCase() === ref.toLowerCase());
}
function diagramMatches(file, pattern) {
const bare = file.replace(/\.archify$/, '');
const wanted = pattern.replace(/\.archify$/, '');
if (/[*?]/.test(wanted)) { const re = new RegExp(`^${wanted.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*').replace(/\?/g, '.')}$`, 'i'); return re.test(bare) || re.test(file); }
const l = wanted.toLowerCase();
const b = bare.toLowerCase();
return b === l || b.startsWith(l) || b.includes(`/${l}`);
}
function neighbours(graph) {
const map = new Map(graph.nodes.map((n) => [n.id, new Set()]));
for (const e of graph.edges.filter(isConnection)) { map.get(e.from)?.add(e.to); map.get(e.to)?.add(e.from); }
return map;
}
function within(graph, start, hops) {
const adjacency = neighbours(graph);
let frontier = new Set([start]);
const seen = new Set([start]);
for (let i = 0; i < hops && frontier.size; i += 1) {
const next = new Set();
for (const id of frontier) for (const other of adjacency.get(id) ?? []) if (!seen.has(other)) { seen.add(other); next.add(other); }
frontier = next;
}
return seen;
}
export function shortestPath(graph, from, to) {
const attempt = (directed) => {
const out = new Map(graph.nodes.map((n) => [n.id, []]));
for (const e of graph.edges.filter(isConnection)) { out.get(e.from)?.push({ to: e.to, edge: e }); if (!directed) out.get(e.to)?.push({ to: e.from, edge: e }); }
const previous = new Map();
const queue = [from];
const seen = new Set([from]);
while (queue.length) {
const id = queue.shift();
if (id === to) break;
for (const step of out.get(id) ?? []) { if (seen.has(step.to)) continue; seen.add(step.to); previous.set(step.to, { from: id, edge: step.edge }); queue.push(step.to); }
}
if (!seen.has(to)) return undefined;
const nodes = [to]; const edges = [];
for (let at = to; at !== from;) { const step = previous.get(at); edges.unshift(step.edge.id); nodes.unshift(step.from); at = step.from; }
return { nodes, edges };
};
if (from === to) return { nodes: [from], edges: [] };
return attempt(true) ?? attempt(false);
}
export function runQuery(graph, text) {
const terms = parseQuery(text);
let result = new Set(graph.nodes.map((n) => n.id));
const notes = [];
let from; let to; let path; let pathEdges;
const matching = (term) => {
const v = term.value;
switch (term.key) {
case 'text': { const l = v.toLowerCase(); return new Set(graph.nodes.filter((n) => [n.id, n.label, n.sublabel, n.brand, n.type].some((f) => lower(f).includes(l))).map((n) => n.id)); }
case 'type': return new Set(graph.nodes.filter((n) => lower(n.type) === v.toLowerCase()).map((n) => n.id));
case 'diagram': return new Set(graph.nodes.filter((n) => n.diagrams.some((d) => diagramMatches(d, v))).map((n) => n.id));
case 'decision': {
const ids = new Set(graph.nodes.filter((n) => n.decisions.includes(v)).map((n) => n.id));
for (const e of graph.edges) if (e.decisions.includes(v)) { ids.add(e.from); ids.add(e.to); }
if (!graph.decisions.some((d) => d.id === v)) notes.push(`No decision "${v}" in this model.`); else ids.add(decisionNodeId(v));
return ids;
}
case 'shared': return new Set(graph.nodes.filter((n) => isElement(n) && n.shared).map((n) => n.id));
case 'unused': return new Set(graph.nodes.filter((n) => isElement(n) && n.shared && !n.diagrams.length).map((n) => n.id));
case 'uses': case 'near': {
const [ref, hopsText] = term.key === 'near' ? v.split(',') : [v, '1'];
const node = findNode(graph, ref);
if (!node) { notes.push(`No element "${ref}".`); return new Set(); }
return within(graph, node.id, Math.max(1, Math.min(Number(hopsText) || 1, 50)));
}
case 'from': from = v; return undefined;
case 'to': to = v; return undefined;
default: return undefined;
}
};
const negated = [];
for (const term of terms) {
const ids = matching(term);
if (!ids) continue;
if (term.negate) { negated.push(ids); for (const id of ids) result.delete(id); }
else result = new Set([...result].filter((id) => ids.has(id)));
}
if (terms.length) {
for (const e of graph.edges) if (!isConnection(e) && result.has(e.to) && graph.nodes.some((n) => n.id === e.from && !isElement(n))) result.add(e.from);
for (const ids of negated) for (const id of ids) result.delete(id);
}
if (from || to) {
if (!from || !to) notes.push('A path needs both from: and to:.');
else {
const a = findNode(graph, from); const b = findNode(graph, to);
if (!a || !b) notes.push(`No element "${!a ? from : to}".`);
else {
const found = shortestPath(graph, a.id, b.id);
if (!found) notes.push(`No path from ${a.id} to ${b.id}.`);
else { path = found.nodes; pathEdges = new Set(found.edges); result = new Set(found.nodes.filter((id) => result.has(id))); }
}
if (!path) result = new Set();
}
}
const edges = new Set();
for (const e of graph.edges) { if (!result.has(e.from) || !result.has(e.to)) continue; if (pathEdges && !pathEdges.has(e.id)) continue; edges.add(e.id); }
return { nodes: result, edges, path, message: notes[0], terms };
}
+28 -4
View File
@@ -69,6 +69,28 @@ try {
assert(/referencedBy:\n\s+- 0200-containers\/online-shop\.archify/.test(r.stdout), 'referencedBy missing');
});
check('model-graph example: 14 elements, 17 connections, decisions and registries as nodes', ['--action', 'model-graph', '--dir', example], (r, notes) => {
assert(r.status === 0, `exit ${r.status}`);
assert(yamlValue(r.stdout, ' elements') === '14' && yamlValue(r.stdout, ' connections') === '17', 'expected 14 elements and 17 connections');
assert(yamlValue(r.stdout, ' registries') === '3' && yamlValue(r.stdout, ' decisions') === '3', 'expected 3 registries and 3 decisions');
assert(/- id: decision:queue-between-api-and-worker\n\s+kind: decision/.test(r.stdout), 'decision node missing');
assert(/- id: registry:registry\/containers\.archify-registry\n\s+kind: registry/.test(r.stdout), 'registry node missing');
notes.push('14 elements, 17 connections, 3 registries, 3 decisions');
});
check('model-graph example: from:customer to:db is the path customer → web → api → db', ['--action', 'model-graph', '--dir', example, '--query', 'from:customer to:db'], (r, notes) => {
assert(r.status === 0, `exit ${r.status}`);
assert(/path:\n\s+- customer\n\s+- web\n\s+- api\n\s+- db/.test(r.stdout), 'path missing');
assert(yamlValue(r.stdout, ' matching') === '4', 'expected 4 matching elements');
notes.push('path of 4');
});
check('model-graph example: uses:api is api and its neighbours; an unknown element is reported', ['--action', 'model-graph', '--dir', example, '--query', 'uses:api -type:decision -type:registry'], (r, notes) => {
assert(r.status === 0, `exit ${r.status}`);
assert(yamlValue(r.stdout, ' matching') === '5', 'expected api plus 4 neighbours');
const unknown = cliRun(['--action', 'model-graph', '--dir', example, '--query', 'uses:nothing']);
assert(unknown.status !== 0 && /No element \\?"nothing\\?"/.test(unknown.stdout), 'unknown element not reported');
notes.push('5 elements; unknown element reported');
});
check('model-validate from a file inside the model', ['--action', 'model-validate', '--dir', path.join(example, '0300-components', 'api.archify')], (r) => {
assert(r.status === 0, `exit ${r.status}`);
assert(yamlValue(r.stdout, 'mode') === 'model', 'root was not found from a file inside');
@@ -184,17 +206,19 @@ try {
assert(r.status === 0, `exit ${r.status}`);
});
// The shipped examples: the same three the extension offers under "Archify | New Model…".
// The shipped examples: the same nine the extension offers under "Archify | New Model…".
const examplesDir = path.join(skill, 'examples');
const exampleNames = fs.readdirSync(examplesDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).sort();
check('examples/ holds exactly the three C4 examples', [], () => {
assert(exampleNames.join(',') === 'c4-e-shop,c4-event-based-integration,c4-standard-web-app', `unexpected examples: ${exampleNames.join(',')}`);
check('examples/ holds the nine examples: three C4, three arc42, three ArchiMate', [], () => {
const expected = 'arc42-cinema-tickets,arc42-iot-telemetry,arc42-team-wiki,archimate-hr-onboarding,archimate-insurance-claims,archimate-logistics-tracking,c4-e-shop,c4-event-based-integration,c4-standard-web-app';
assert(exampleNames.join(',') === expected, `unexpected examples: ${exampleNames.join(',')}`);
});
for (const name of exampleNames) {
check(`example ${name} validates with 0 problems`, ['--action', 'model-validate', '--dir', path.join(examplesDir, name)], (r) => {
assert(r.status === 0, `exit ${r.status}`);
assert(yamlValue(r.stdout, ' errors') === '0' && yamlValue(r.stdout, ' warnings') === '0', 'example has problems');
assert(yamlValue(r.stdout, ' diagrams') === '4', 'expected 4 diagrams');
const diagrams = Number(yamlValue(r.stdout, ' diagrams'));
assert(diagrams >= 4 && yamlValue(r.stdout, ' diagramsOk') === String(diagrams), `expected at least 4 diagrams, all ok (${diagrams})`);
});
}
const fromExample = path.join(tmp, 'from-example');