From 55894fbade0bcf287e99e520b4ec2527f0b7ae37 Mon Sep 17 00:00:00 2001 From: Root At Skic Date: Mon, 19 Oct 2026 12:30:07 +0300 Subject: [PATCH] Ship the three C4 examples and model:scaffold --example MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - examples/: c4-standard-web-app, c4-e-shop (was online-shop), c4-event-based-integration; the same models the extension offers under Archify | New Model… - model-scaffold --example copies one and renames id, name, description and the overview title; never overwrites - tests cover every example and the copy; docs updated --- README.md | 2 +- SKILL.md | 11 ++- .../0100-context/online-shop.archify | 0 .../0200-containers/online-shop.archify | 0 .../0300-components/api.archify | 0 .../{online-shop => c4-e-shop}/main.archify | 6 +- .../registry/containers.archify-registry | 0 .../registry/people.archify-registry | 0 .../registry/systems.archify-registry | 0 .../0100-context/platform.archify | 26 ++++++ .../0200-containers/platform.archify | 46 ++++++++++ .../0300-components/router.archify | 46 ++++++++++ .../c4-event-based-integration/main.archify | 29 +++++++ .../registry/containers.archify-registry | 40 +++++++++ .../registry/people.archify-registry | 7 ++ .../registry/systems.archify-registry | 23 +++++ .../0100-context/web-app.archify | 26 ++++++ .../0200-containers/web-app.archify | 38 +++++++++ .../0300-components/api.archify | 42 ++++++++++ examples/c4-standard-web-app/main.archify | 29 +++++++ .../registry/containers.archify-registry | 32 +++++++ .../registry/people.archify-registry | 12 +++ .../registry/systems.archify-registry | 18 ++++ references/authoring.md | 2 +- references/folders.md | 2 +- scripts/src/actions/model-scaffold/action.mjs | 84 +++++++++++++++++-- scripts/src/test/run.mjs | 34 +++++++- 27 files changed, 538 insertions(+), 17 deletions(-) rename examples/{online-shop => c4-e-shop}/0100-context/online-shop.archify (100%) rename examples/{online-shop => c4-e-shop}/0200-containers/online-shop.archify (100%) rename examples/{online-shop => c4-e-shop}/0300-components/api.archify (100%) rename examples/{online-shop => c4-e-shop}/main.archify (87%) rename examples/{online-shop => c4-e-shop}/registry/containers.archify-registry (100%) rename examples/{online-shop => c4-e-shop}/registry/people.archify-registry (100%) rename examples/{online-shop => c4-e-shop}/registry/systems.archify-registry (100%) create mode 100644 examples/c4-event-based-integration/0100-context/platform.archify create mode 100644 examples/c4-event-based-integration/0200-containers/platform.archify create mode 100644 examples/c4-event-based-integration/0300-components/router.archify create mode 100644 examples/c4-event-based-integration/main.archify create mode 100644 examples/c4-event-based-integration/registry/containers.archify-registry create mode 100644 examples/c4-event-based-integration/registry/people.archify-registry create mode 100644 examples/c4-event-based-integration/registry/systems.archify-registry create mode 100644 examples/c4-standard-web-app/0100-context/web-app.archify create mode 100644 examples/c4-standard-web-app/0200-containers/web-app.archify create mode 100644 examples/c4-standard-web-app/0300-components/api.archify create mode 100644 examples/c4-standard-web-app/main.archify create mode 100644 examples/c4-standard-web-app/registry/containers.archify-registry create mode 100644 examples/c4-standard-web-app/registry/people.archify-registry create mode 100644 examples/c4-standard-web-app/registry/systems.archify-registry diff --git a/README.md b/README.md index 38a9fae..8894523 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ task model:validate -- --dir="/path/to/online-shop" task diagram:render -- --file="/path/to/online-shop/0200-containers/online-shop.archify" --out="/tmp/containers.html" ``` -See [SKILL.md](SKILL.md) for the workflow, the task table and the gotchas; `examples/online-shop/` is a complete model that validates. +See [SKILL.md](SKILL.md) for the workflow, the task table and the gotchas. `examples/` holds three complete models that validate (C4 - Standard Web App, C4 - E-Shop, C4 - Event-Based Integration); `task model:scaffold -- --dir="…" --example="c4-e-shop"` copies one. Requires Node.js 18+, bash and [go-task](https://taskfile.dev). The skill targets the Archify Diagram Viewer extension; a diagram that validates here opens the same way in VS Code. diff --git a/SKILL.md b/SKILL.md index 10f8425..acb8db5 100644 --- a/SKILL.md +++ b/SKILL.md @@ -40,10 +40,11 @@ task --list-all | `task model:validate -- --dir=""` | 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=""` | What the model page shows: metadata, diagrams by folder, registry elements with "used in N diagrams". | | `task model:scaffold -- --dir="" --id="" --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="" --example="" [--id=""] [--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:resolve -- --file="" [--out=""]` | The Archify JSON the extension hands to Archify, for the plain Archify CLI or CI. | | `task diagram:validate -- --file=""` | One diagram through Archify (refs resolved when inside a model). | | `task diagram:render -- --file="" --out=""` | Standalone HTML page for people without VS Code. | -| `task test` | The skill's own checks against `examples/online-shop` and a scaffolded model. | +| `task test` | The skill's own checks against the examples and scaffolded models. | | `task sync` | Refresh `scripts/vendor/archify` from an Archify checkout (maintainers only). | Options for validate and render: `--quality=showcase` (stricter composition checks), `--repo-root=` for diagrams with `meta.repository` and `sources`. @@ -65,6 +66,12 @@ New folder: task model:scaffold -- --dir="/path/to/online-shop" --id="online-shop" --name="Online Shop" --description="Web shop, API, workers and stores." --c4 ``` +Or start from a complete example and rename it (`examples/` lists the three; C4 - Standard Web App, C4 - E-Shop, C4 - Event-Based Integration): + +```bash +task model:scaffold -- --dir="/path/to/hub" --example="c4-event-based-integration" --id="hub" --name="Integration Hub" +``` + Existing folder of diagrams: add `main.archify` with the `model` block (see `references/formats.md`); every `.archify` below it is now part of the model. `--c4` is a convention, not a requirement: any folders, any depth. The model's root is the nearest folder above a file that holds `main.archify`. Nothing else marks it. `main.archify` may also hold a diagram of its own (the overview), below the `model` block. @@ -175,4 +182,4 @@ 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/online-shop/` is a complete C4-style model that validates with 0 problems; copy its shape. +- `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`. diff --git a/examples/online-shop/0100-context/online-shop.archify b/examples/c4-e-shop/0100-context/online-shop.archify similarity index 100% rename from examples/online-shop/0100-context/online-shop.archify rename to examples/c4-e-shop/0100-context/online-shop.archify diff --git a/examples/online-shop/0200-containers/online-shop.archify b/examples/c4-e-shop/0200-containers/online-shop.archify similarity index 100% rename from examples/online-shop/0200-containers/online-shop.archify rename to examples/c4-e-shop/0200-containers/online-shop.archify diff --git a/examples/online-shop/0300-components/api.archify b/examples/c4-e-shop/0300-components/api.archify similarity index 100% rename from examples/online-shop/0300-components/api.archify rename to examples/c4-e-shop/0300-components/api.archify diff --git a/examples/online-shop/main.archify b/examples/c4-e-shop/main.archify similarity index 87% rename from examples/online-shop/main.archify rename to examples/c4-e-shop/main.archify index fc4be8a..8c6e712 100644 --- a/examples/online-shop/main.archify +++ b/examples/c4-e-shop/main.archify @@ -1,13 +1,13 @@ schema_version: 1 model: - id: online-shop - name: Online Shop + id: e-shop + name: E-Shop description: > The web shop, its API and workers, and the stores behind them. Context, containers and components, C4 style. diagram_type: architecture meta: - title: Online Shop — overview + title: E-Shop — overview output: main.html components: - ref: customer diff --git a/examples/online-shop/registry/containers.archify-registry b/examples/c4-e-shop/registry/containers.archify-registry similarity index 100% rename from examples/online-shop/registry/containers.archify-registry rename to examples/c4-e-shop/registry/containers.archify-registry diff --git a/examples/online-shop/registry/people.archify-registry b/examples/c4-e-shop/registry/people.archify-registry similarity index 100% rename from examples/online-shop/registry/people.archify-registry rename to examples/c4-e-shop/registry/people.archify-registry diff --git a/examples/online-shop/registry/systems.archify-registry b/examples/c4-e-shop/registry/systems.archify-registry similarity index 100% rename from examples/online-shop/registry/systems.archify-registry rename to examples/c4-e-shop/registry/systems.archify-registry diff --git a/examples/c4-event-based-integration/0100-context/platform.archify b/examples/c4-event-based-integration/0100-context/platform.archify new file mode 100644 index 0000000..bd48c26 --- /dev/null +++ b/examples/c4-event-based-integration/0100-context/platform.archify @@ -0,0 +1,26 @@ +schema_version: 1 +diagram_type: architecture +meta: + title: System context + output: context.html +components: + - ref: crm + pos: [40, 60] + size: [130, 60] + - ref: erp + pos: [40, 260] + size: [130, 60] + - ref: platform + pos: [280, 160] + size: [170, 60] + - ref: warehouse + pos: [540, 60] + size: [150, 60] + - ref: ops + pos: [540, 260] + size: [150, 60] +connections: + - { from: crm, to: platform, label: customer events, variant: emphasis } + - { from: erp, to: platform, label: order events, variant: emphasis } + - { from: platform, to: warehouse, label: pick lists } + - { from: ops, to: platform, label: replays, variant: dashed } diff --git a/examples/c4-event-based-integration/0200-containers/platform.archify b/examples/c4-event-based-integration/0200-containers/platform.archify new file mode 100644 index 0000000..61246f2 --- /dev/null +++ b/examples/c4-event-based-integration/0200-containers/platform.archify @@ -0,0 +1,46 @@ +schema_version: 1 +diagram_type: architecture +meta: + title: Containers + output: containers.html +components: + - ref: crm + pos: [40, 60] + size: [130, 60] + - ref: erp + pos: [40, 260] + size: [130, 60] + - ref: ingest + pos: [250, 160] + size: [140, 60] + - ref: bus + pos: [470, 160] + size: [140, 60] + - ref: router + pos: [690, 160] + size: [150, 60] + - ref: dlq + pos: [690, 320] + size: [150, 60] + - ref: store + pos: [920, 60] + size: [140, 60] + - ref: notifier + pos: [920, 160] + size: [140, 60] + - ref: analytics + pos: [920, 260] + size: [140, 60] +boundaries: + - kind: region + label: Integration platform + wraps: [ingest, bus, router, dlq, store, notifier, analytics] +connections: + - { from: crm, to: ingest, label: HTTPS, variant: emphasis } + - { from: erp, to: ingest, label: HTTPS, variant: emphasis } + - { from: ingest, to: bus, label: publish } + - { from: bus, to: router, label: consume } + - { from: router, to: store, label: append } + - { from: router, to: notifier, label: notify } + - { from: router, to: analytics, label: flatten } + - { from: router, to: dlq, label: failed, variant: dashed, labelDy: 24 } diff --git a/examples/c4-event-based-integration/0300-components/router.archify b/examples/c4-event-based-integration/0300-components/router.archify new file mode 100644 index 0000000..3c147ed --- /dev/null +++ b/examples/c4-event-based-integration/0300-components/router.archify @@ -0,0 +1,46 @@ +schema_version: 1 +diagram_type: architecture +meta: + title: Event router components + output: router.html +components: + - ref: bus + pos: [40, 160] + size: [140, 60] + - id: validate + type: security + label: Validate + sublabel: schema check + pos: [270, 160] + size: [130, 60] + - id: enrich + type: backend + label: Enrich + sublabel: reference data + pos: [480, 160] + size: [130, 60] + - id: dispatch + type: backend + label: Dispatch + pos: [690, 160] + size: [130, 60] + - ref: store + pos: [920, 60] + size: [140, 60] + - ref: notifier + pos: [920, 160] + size: [140, 60] + - ref: dlq + pos: [270, 320] + size: [150, 60] +boundaries: + - kind: region + label: Event router + wraps: [validate, enrich, dispatch] +connections: + - { from: bus, to: validate, label: event } + - { from: validate, to: enrich, label: valid } + - { from: enrich, to: dispatch, label: enriched } + - { from: dispatch, to: store, label: append } + - { from: dispatch, to: notifier, label: notify } + - { from: validate, to: dlq, label: invalid, variant: dashed, labelDy: 24 } diff --git a/examples/c4-event-based-integration/main.archify b/examples/c4-event-based-integration/main.archify new file mode 100644 index 0000000..925bbeb --- /dev/null +++ b/examples/c4-event-based-integration/main.archify @@ -0,0 +1,29 @@ +schema_version: 1 +model: + id: event-based-integration + name: Event-Based Integration + description: > + Systems that never call each other directly; events go through a bus, + a router validates and enriches them, consumers react. Context, + containers, the router's components and one flow, C4 style. +diagram_type: architecture +meta: + title: Event-Based Integration — overview + output: main.html +components: + - ref: crm + pos: [40, 60] + size: [130, 60] + - ref: erp + pos: [40, 260] + size: [130, 60] + - ref: platform + pos: [280, 160] + size: [170, 60] + - ref: warehouse + pos: [540, 160] + size: [150, 60] +connections: + - { from: crm, to: platform, label: customer events, variant: emphasis } + - { from: erp, to: platform, label: order events, variant: emphasis } + - { from: platform, to: warehouse, label: pick lists } diff --git a/examples/c4-event-based-integration/registry/containers.archify-registry b/examples/c4-event-based-integration/registry/containers.archify-registry new file mode 100644 index 0000000..a86285d --- /dev/null +++ b/examples/c4-event-based-integration/registry/containers.archify-registry @@ -0,0 +1,40 @@ +schema_version: 1 +elements: + ingest: + type: backend + label: Ingest API + sublabel: HTTPS in, events out + description: Accepts events from systems that cannot publish to the bus themselves. + bus: + type: messagebus + label: Event bus + sublabel: Kafka + brand: apache-kafka + description: One topic per event type; retained for seven days. + router: + type: backend + label: Event router + sublabel: validate, enrich, dispatch + description: Checks each event against its schema, adds reference data, hands it to the consumers. + diagram: 0300-components/router.archify + dlq: + type: messagebus + label: Dead-letter queue + sublabel: failed events + description: Events the router could not process; replayed by operations. + store: + type: database + label: Event store + sublabel: PostgreSQL + brand: postgresql + description: Every accepted event, for audit and replay. + notifier: + type: backend + label: Notifier + sublabel: email, chat + description: Turns selected events into messages to people. + analytics: + type: database + label: Analytics + sublabel: warehouse tables + description: Flattened events for reporting. diff --git a/examples/c4-event-based-integration/registry/people.archify-registry b/examples/c4-event-based-integration/registry/people.archify-registry new file mode 100644 index 0000000..c4702e0 --- /dev/null +++ b/examples/c4-event-based-integration/registry/people.archify-registry @@ -0,0 +1,7 @@ +schema_version: 1 +elements: + ops: + type: external + label: Operations + sublabel: on-call + description: Watches the dead-letter queue and replays failed events. diff --git a/examples/c4-event-based-integration/registry/systems.archify-registry b/examples/c4-event-based-integration/registry/systems.archify-registry new file mode 100644 index 0000000..a2f262d --- /dev/null +++ b/examples/c4-event-based-integration/registry/systems.archify-registry @@ -0,0 +1,23 @@ +schema_version: 1 +elements: + platform: + type: cloud + label: Integration platform + sublabel: the system + description: Receives events from the business systems, routes them, keeps a record. + diagram: 0200-containers/platform.archify + crm: + type: external + label: CRM + sublabel: customers + description: Source of customer events; created, updated, merged. + erp: + type: external + label: ERP + sublabel: orders and stock + description: Source of order events and the target of stock updates. + warehouse: + type: external + label: Warehouse system + sublabel: fulfilment + description: Receives pick lists; reports shipments back as events. diff --git a/examples/c4-standard-web-app/0100-context/web-app.archify b/examples/c4-standard-web-app/0100-context/web-app.archify new file mode 100644 index 0000000..c180390 --- /dev/null +++ b/examples/c4-standard-web-app/0100-context/web-app.archify @@ -0,0 +1,26 @@ +schema_version: 1 +diagram_type: architecture +meta: + title: System context + output: context.html +components: + - ref: user + pos: [40, 60] + size: [130, 60] + - ref: admin + pos: [40, 260] + size: [130, 60] + - ref: webapp + pos: [280, 160] + size: [160, 60] + - ref: idp + pos: [530, 60] + size: [150, 60] + - ref: email + pos: [530, 260] + size: [150, 60] +connections: + - { from: user, to: webapp, label: uses, variant: emphasis } + - { from: admin, to: webapp, label: administers } + - { from: webapp, to: idp, label: signs in via, variant: security } + - { from: webapp, to: email, label: sends mail } diff --git a/examples/c4-standard-web-app/0200-containers/web-app.archify b/examples/c4-standard-web-app/0200-containers/web-app.archify new file mode 100644 index 0000000..cef2e3f --- /dev/null +++ b/examples/c4-standard-web-app/0200-containers/web-app.archify @@ -0,0 +1,38 @@ +schema_version: 1 +diagram_type: architecture +meta: + title: Containers + output: containers.html +components: + - ref: user + pos: [40, 160] + size: [130, 60] + - ref: spa + pos: [250, 160] + size: [150, 60] + - ref: api + pos: [480, 160] + size: [130, 60] + - ref: idp + pos: [480, 20] + size: [130, 60] + - ref: cache + pos: [690, 60] + size: [130, 60] + - ref: db + pos: [690, 160] + size: [130, 60] + - ref: storage + pos: [690, 260] + size: [130, 60] +boundaries: + - kind: region + label: Web Application + wraps: [spa, api, cache, db, storage] +connections: + - { from: user, to: spa, label: HTTPS, variant: emphasis } + - { from: spa, to: api, label: JSON } + - { from: api, to: idp, label: verify token, variant: security, labelAt: [531, 110] } + - { from: api, to: cache, label: read } + - { from: api, to: db, label: SQL } + - { from: api, to: storage, label: files } diff --git a/examples/c4-standard-web-app/0300-components/api.archify b/examples/c4-standard-web-app/0300-components/api.archify new file mode 100644 index 0000000..930e8fc --- /dev/null +++ b/examples/c4-standard-web-app/0300-components/api.archify @@ -0,0 +1,42 @@ +schema_version: 1 +diagram_type: architecture +meta: + title: API components + output: api.html +components: + - ref: spa + pos: [40, 160] + size: [150, 60] + - id: auth + type: security + label: Auth + sublabel: token check + pos: [270, 40] + size: [130, 60] + - id: accounts + type: backend + label: Accounts + pos: [270, 160] + size: [130, 60] + - id: content + type: backend + label: Content + pos: [270, 280] + size: [130, 60] + - ref: db + pos: [500, 160] + size: [130, 60] + - ref: storage + pos: [500, 280] + size: [130, 60] +boundaries: + - kind: region + label: API + wraps: [auth, accounts, content] +connections: + - { from: spa, to: auth, label: token, variant: security } + - { from: spa, to: accounts, label: profile } + - { from: spa, to: content, label: pages } + - { from: accounts, to: db, label: SQL } + - { from: content, to: db, label: SQL } + - { from: content, to: storage, label: files } diff --git a/examples/c4-standard-web-app/main.archify b/examples/c4-standard-web-app/main.archify new file mode 100644 index 0000000..fcf3d61 --- /dev/null +++ b/examples/c4-standard-web-app/main.archify @@ -0,0 +1,29 @@ +schema_version: 1 +model: + id: standard-web-app + name: Standard Web App + description: > + A classic web application; a single-page app, an API, a database, + a cache and file storage, with sign-in through an identity provider. + Context, containers and the API's components, C4 style. +diagram_type: architecture +meta: + title: Standard Web App — overview + output: main.html +components: + - ref: user + pos: [40, 160] + size: [130, 60] + - ref: webapp + pos: [250, 160] + size: [160, 60] + - ref: idp + pos: [490, 60] + size: [150, 60] + - ref: email + pos: [490, 260] + size: [150, 60] +connections: + - { from: user, to: webapp, label: uses, variant: emphasis } + - { from: webapp, to: idp, label: signs in via, variant: security } + - { from: webapp, to: email, label: sends mail } diff --git a/examples/c4-standard-web-app/registry/containers.archify-registry b/examples/c4-standard-web-app/registry/containers.archify-registry new file mode 100644 index 0000000..8e62726 --- /dev/null +++ b/examples/c4-standard-web-app/registry/containers.archify-registry @@ -0,0 +1,32 @@ +schema_version: 1 +elements: + spa: + type: frontend + label: Single-page app + sublabel: React + brand: react + description: The user interface; talks only to the API. + api: + type: backend + label: API + sublabel: Node.js + brand: node-js + description: Owns accounts, content and files; checks tokens from the identity provider. + diagram: 0300-components/api.archify + db: + type: database + label: Database + sublabel: PostgreSQL + brand: postgresql + description: Accounts and content; the source of truth. + cache: + type: database + label: Cache + sublabel: Redis + brand: redis + description: Sessions and hot content; safe to lose. + storage: + type: cloud + label: Object storage + sublabel: uploaded files + description: User uploads and generated exports. diff --git a/examples/c4-standard-web-app/registry/people.archify-registry b/examples/c4-standard-web-app/registry/people.archify-registry new file mode 100644 index 0000000..9d9d3b4 --- /dev/null +++ b/examples/c4-standard-web-app/registry/people.archify-registry @@ -0,0 +1,12 @@ +schema_version: 1 +elements: + user: + type: external + label: User + sublabel: browser + description: Anyone with an account; uses the application from a browser. + admin: + type: external + label: Administrator + sublabel: back office + description: Manages users and content from the same application with extra rights. diff --git a/examples/c4-standard-web-app/registry/systems.archify-registry b/examples/c4-standard-web-app/registry/systems.archify-registry new file mode 100644 index 0000000..28046da --- /dev/null +++ b/examples/c4-standard-web-app/registry/systems.archify-registry @@ -0,0 +1,18 @@ +schema_version: 1 +elements: + webapp: + type: cloud + label: Web Application + sublabel: the system + description: Everything we build and run; the single-page app, the API and its stores. + diagram: 0200-containers/web-app.archify + idp: + type: external + label: Identity provider + sublabel: OpenID Connect + description: Signs users in and issues tokens; we never store passwords. + email: + type: external + label: Email service + sublabel: transactional mail + description: Sends sign-up confirmations and notifications. diff --git a/references/authoring.md b/references/authoring.md index 6ed2fb3..6125887 100644 --- a/references/authoring.md +++ b/references/authoring.md @@ -34,7 +34,7 @@ Ids match `^[a-zA-Z][a-zA-Z0-9_-]*$` and are unique in their collection. Relatio Collections: `components` (required), `connections`, `boundaries`, `cards`, `layout`. -Placement is yours; Archify routes the lines but never moves boxes. Use free positions on this grid, which is what the templates and `examples/online-shop` use: +Placement is yours; Archify routes the lines but never moves boxes. Use free positions on this grid, which is what the templates and `examples/c4-e-shop` use: - box `size: [130, 60]` (`[150, 60]` for a system-level box with a longer label) - columns at x = 40, 250, 460, 670, 880 (step 210: an 80px clear gap between boxes) diff --git a/references/folders.md b/references/folders.md index 28d4b90..106b41a 100644 --- a/references/folders.md +++ b/references/folders.md @@ -73,7 +73,7 @@ How the levels hang together: - The container "api" in `containers.archify-registry` points at `0300-components/api.archify`. Click the api, see its components. - The same api, drawn in the containers diagram and in several component diagrams, is one entry with one description. -The numbered folders are only for sorting; nothing depends on the names. A team that wants only context and containers has `main.archify`, two folders and two registry files. `task model:scaffold --c4` creates this shape with three levels and empty registries; `examples/online-shop/` in this skill is the filled-in version. +The numbered folders are only for sorting; nothing depends on the names. A team that wants only context and containers has `main.archify`, two folders and two registry files. `task model:scaffold --c4` creates this shape with three levels and empty registries; `examples/c4-e-shop/` in this skill is the filled-in version (two more examples sit next to it). 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. diff --git a/scripts/src/actions/model-scaffold/action.mjs b/scripts/src/actions/model-scaffold/action.mjs index fd9f3e5..a611d7e 100644 --- a/scripts/src/actions/model-scaffold/action.mjs +++ b/scripts/src/actions/model-scaffold/action.mjs @@ -1,17 +1,31 @@ // Creates a model: main.archify with the model block, and with --c4 an // overview diagram, empty registries and the C4 folders with starter -// diagrams that validate. Never overwrites existing files. +// diagrams that validate; or, with --example, a copy of one of the shipped +// example models (the same three the VS Code extension offers under +// "Archify | New Model…"). Never overwrites existing files. import fs from 'node:fs'; import path from 'node:path'; +import { fileURLToPath } from 'node:url'; import { ID, MAIN } from '../../services/model.mjs'; -export const help = `model-scaffold — create main.archify (and with --c4 the C4 folders) +const EXAMPLES_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..', '..', 'examples'); + +/** The shipped examples: folder under examples/ → what it shows. */ +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', +}; + +export const help = `model-scaffold — create main.archify (and with --c4 the C4 folders), or copy an example --dir folder of the model; created when missing (required) - --id model id, letters/digits/-/_ (required) - --name model name (required) + --id model id, letters/digits/-/_ (required unless --example) + --name model name (required unless --example) --description model description (optional) - --c4 also create registry/, 0100-context, 0200-containers, 0300-components`; + --c4 also create registry/, 0100-context, 0200-containers, 0300-components + --example copy a complete example instead: ${Object.keys(EXAMPLES).join(' | ')} + (id, name and description default to the example's own)`; function block(text) { // YAML folded scalar: safe for colons and long sentences. @@ -168,9 +182,65 @@ connections: - { from: orders, to: db, label: SQL } `; +function listFiles(dir, rel = '') { + return fs.readdirSync(path.join(dir, rel), { withFileTypes: true }).flatMap((entry) => { + const next = rel ? `${rel}/${entry.name}` : entry.name; + return entry.isDirectory() ? listFiles(dir, next) : [next]; + }); +} + +/** The example's main.archify with id, name, description and overview title replaced when given. */ +function mainFromExample(text, { id, name, description }) { + let out = text; + if (id) out = out.replace(/^( id: ).*$/m, `$1${id}`); + if (name) { + out = out.replace(/^( name: ).*$/m, `$1${JSON.stringify(name)}`); + out = out.replace(/^( title: ).*$/m, `$1${JSON.stringify(`${name} — overview`)}`); + } + if (description) out = out.replace(/^ description: >\n(?: .*\n)+/m, ` description: ${block(description)}\n`); + return out; +} + +async function copyExample(input) { + const example = String(input.example); + if (!EXAMPLES[example]) return { success: false, error: `Unknown example "${example}". Choose one of: ${Object.keys(EXAMPLES).join(', ')}.`, examples: EXAMPLES }; + const source = path.join(EXAMPLES_DIR, example); + const dir = path.resolve(String(input.dir)); + const id = input.id && input.id !== true ? String(input.id) : undefined; + if (id && !ID.test(id)) return { success: false, error: `--id "${id}" must match ${ID} (letters, digits, - and _; starts with a letter).` }; + const name = input.name && input.name !== true ? String(input.name) : undefined; + const description = input.description && input.description !== true ? String(input.description) : undefined; + + const files = listFiles(source); + const existing = files.filter((file) => fs.existsSync(path.join(dir, file))); + if (existing.length) return { success: false, error: `Refusing to overwrite existing files in ${dir}.`, existing }; + if (fs.existsSync(dir) && !fs.statSync(dir).isDirectory()) return { success: false, error: `${dir} is not a folder.` }; + for (const file of files) { + const full = path.join(dir, file); + fs.mkdirSync(path.dirname(full), { recursive: true }); + const text = fs.readFileSync(path.join(source, file), 'utf8'); + fs.writeFileSync(full, file === MAIN ? mainFromExample(text, { id, name, description }) : text); + } + const main = fs.readFileSync(path.join(dir, MAIN), 'utf8'); + return { + success: true, + root: dir, + example, + model: { id: /^ id: (.*)$/m.exec(main)?.[1], name: JSON.parse(/^ name: (".*"|.*)$/m.exec(main)?.[1].replace(/^([^"].*)$/, '"$1"') ?? '""') }, + created: files, + nextSteps: [ + `task model:validate -- --dir="${dir}"`, + 'Rename, add or remove elements in registry/*.archify-registry and the diagrams; keep "ref:" pointing at registry ids.', + `Open ${path.join(dir, MAIN)} in VS Code with the Archify Diagram Viewer extension to see the model page.`, + ], + }; +} + export async function run(input) { - const missing = ['dir', 'id', 'name'].filter((key) => !input[key] || input[key] === true); - if (missing.length) return { success: false, error: `Missing ${missing.map((key) => `--${key}`).join(', ')}.`, help }; + if (!input.dir || input.dir === true) return { success: false, error: 'Missing --dir.', help }; + if (input.example) return copyExample(input); + const missing = ['id', 'name'].filter((key) => !input[key] || input[key] === true); + if (missing.length) return { success: false, error: `Missing ${missing.map((key) => `--${key}`).join(', ')} (or pass --example).`, help }; const id = String(input.id); if (!ID.test(id)) return { success: false, error: `--id "${id}" must match ${ID} (letters, digits, - and _; starts with a letter).` }; const name = String(input.name); diff --git a/scripts/src/test/run.mjs b/scripts/src/test/run.mjs index 5ae5c6e..eb529cf 100644 --- a/scripts/src/test/run.mjs +++ b/scripts/src/test/run.mjs @@ -1,5 +1,5 @@ #!/usr/bin/env node -// The skill's own checks. Runs every action against examples/online-shop and +// The skill's own checks. Runs every action against examples/c4-e-shop and // a scaffolded temp model. Prints YAML (success first); exit 1 on any failure. import fs from 'node:fs'; @@ -11,7 +11,7 @@ import { toYaml } from '../services/yaml.mjs'; const scripts = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..'); const skill = path.resolve(scripts, '..'); -const example = path.join(skill, 'examples', 'online-shop'); +const example = path.join(skill, 'examples', 'c4-e-shop'); const cli = path.join(scripts, 'src', 'cli', 'commands.mjs'); const checks = []; @@ -168,6 +168,36 @@ try { check('plain model validates (model block only)', ['--action', 'model-validate', '--dir', plain], (r) => { assert(r.status === 0, `exit ${r.status}`); }); + + // The shipped examples: the same three 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(',')}`); + }); + 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 fromExample = path.join(tmp, 'from-example'); + check('model-scaffold --example copies and renames an example', ['--action', 'model-scaffold', '--dir', fromExample, '--example', 'c4-event-based-integration', '--id', 'hub', '--name', 'Integration Hub', '--description', 'Our hub: events between systems'], (r) => { + assert(r.status === 0, `exit ${r.status}`); + const main = fs.readFileSync(path.join(fromExample, 'main.archify'), 'utf8'); + assert(/^ id: hub$/m.test(main) && /^ name: "Integration Hub"$/m.test(main) && /Our hub: events between systems/.test(main) && /Integration Hub — overview/.test(main), `main.archify not renamed:\n${main}`); + assert(fs.existsSync(path.join(fromExample, 'registry', 'containers.archify-registry')) && fs.existsSync(path.join(fromExample, '0300-components', 'router.archify')), 'example files missing'); + }); + check('copied example validates', ['--action', 'model-validate', '--dir', fromExample], (r) => { + assert(r.status === 0, `exit ${r.status}`); + assert(yamlValue(r.stdout, ' errors') === '0' && yamlValue(r.stdout, ' warnings') === '0', 'copied example has problems'); + }); + check('model-scaffold --example refuses to overwrite and rejects unknown names', ['--action', 'model-scaffold', '--dir', fromExample, '--example', 'c4-e-shop'], (r) => { + assert(r.status === 1 && /Refusing to overwrite/.test(r.stdout), 'no refusal'); + const unknown = cliRun(['--action', 'model-scaffold', '--dir', path.join(tmp, 'x'), '--example', 'nope']); + assert(unknown.status === 1 && /Unknown example/.test(unknown.stdout), 'unknown example accepted'); + }); } finally { fs.rmSync(tmp, { recursive: true, force: true }); }