Ship the three C4 examples and model:scaffold --example

- 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 <name> copies one and renames id, name,
  description and the overview title; never overwrites
- tests cover every example and the copy; docs updated
This commit is contained in:
root-at-skic committed 2026-10-19 12:30:07 +03:00
1 parent b6b5e57922
commit 55894fbade
27 files changed
+538 -17

No files matched your search

+1 -1
View File
@@ -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.
+9 -2
View File
@@ -40,10 +40,11 @@ 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". |
| `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: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. |
| `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=<checkout>` 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`.
@@ -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
@@ -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 }
@@ -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 }
@@ -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 }
@@ -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 }
@@ -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.
@@ -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.
@@ -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.
@@ -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 }
@@ -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 }
@@ -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 }
+29
View File
@@ -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 }
@@ -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.
@@ -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.
@@ -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.
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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.
+77 -7
View File
@@ -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 <path> folder of the model; created when missing (required)
--id <id> model id, letters/digits/-/_ (required)
--name <text> model name (required)
--id <id> model id, letters/digits/-/_ (required unless --example)
--name <text> model name (required unless --example)
--description <text> model description (optional)
--c4 also create registry/, 0100-context, 0200-containers, 0300-components`;
--c4 also create registry/, 0100-context, 0200-containers, 0300-components
--example <name> 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);
+32 -2
View File
@@ -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 });
}