archivision 0.0.0-stage → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,151 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ const fs = require('fs'), path = require('path'), { execFileSync, spawn } = require('child_process');
4
+
5
+ const HELP = `archivision: a 3D iso map you can open, linked to the code
6
+
7
+ archivision repo <path> [options] parse a TypeScript, JavaScript, Python or Go repo and map it (no AI)
8
+ --out <file.html> the page (default: ./<repo>.archivision.html)
9
+ --src <folder> a code root, repeatable or comma-separated (default: src/, the workspace
10
+ packages, or the whole repo when it has neither)
11
+ --map <map.json> group with this map instead of the saved one
12
+ --remap write the saved map again from the code
13
+ --bases dev,main branches to compare against (default: dev,main,master)
14
+ --since <days> compare branches active in the last <days> days (default 60)
15
+ --max <n> at most <n> branches (default 24)
16
+ --compare a..b,c..d extra pairs to compare
17
+ --fetch git fetch --prune first (off by default)
18
+ --ripwire <binary> ripwire, for Go (default: ARCHIVISION_RIPWIRE, else ripwire on the PATH; never one in the repo)
19
+
20
+ archivision arch <files or folders…> [options] draw an architecture from documents and diagrams (AI)
21
+ --out <file.html> the page (default: ./<name>.archivision.html)
22
+ --name <name> the system's name
23
+ --ai <backend> auto (default), claude or codex: the CLIs you are logged in to
24
+ --model <id> model for that backend
25
+ --effort <level> low, medium, high (default), xhigh or max
26
+ --focus <text> what to concentrate on
27
+
28
+ archivision render <model.arch.json> [--out file.html] draw an architecture model again, after editing it by hand
29
+
30
+ Any command: --open opens the page when it is written.
31
+
32
+ Sources for arch: .md .txt .mmd .puml .dot .json .yaml .html .svg .drawio, images (.png .jpg .webp .gif) and PDFs.
33
+ The repo map (<page>.map.json) and the architecture model (<page>.arch.json) sit next to the page; edit them and run again.
34
+ birdview, the old name, runs the same commands.`;
35
+
36
+ const BOOL = new Set(['remap', 'fetch', 'open', 'help']), MANY = new Set(['src']);
37
+ function parse(argv) {
38
+ const pos = [], o = {};
39
+ for (let i = 0; i < argv.length; i++) {
40
+ const a = argv[i];
41
+ if (a === '-h') o.help = true;
42
+ else if (a.startsWith('--')) {
43
+ const [k, v] = a.slice(2).split(/=(.*)/s);
44
+ const put = x => { o[k] = MANY.has(k) ? [...(o[k] || []), x] : x; };
45
+ if (v !== undefined) put(v);
46
+ else if (BOOL.has(k)) o[k] = true;
47
+ else if (argv[i + 1] !== undefined && !argv[i + 1].startsWith('--')) put(argv[++i]);
48
+ else throw new Error(`--${k} needs a value`);
49
+ } else pos.push(a);
50
+ }
51
+ return { pos, o };
52
+ }
53
+
54
+ const rel = p => path.relative(process.cwd(), p) || p;
55
+ const say = s => process.stdout.write(s + '\n');
56
+ const stem = out => out.replace(/\.html?$/i, '');
57
+ // a page's name without the tool's suffix: x.archivision.arch.json and x.birdview.arch.json (the old name) are both x
58
+ const base = f => f.replace(/(\.(archivision|birdview))?(\.arch)?\.json$/i, '');
59
+ const openIt = file => { try { spawn(process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'explorer' : 'xdg-open', [file], { detached: true, stdio: 'ignore' }).unref(); } catch {} };
60
+ const lib = () => require('../lib');
61
+
62
+ function cmdRepo(pos, o) {
63
+ const { scanRepo, autoMap, buildRepo, writePage } = lib();
64
+ const REPO = path.resolve(pos[0] || '.');
65
+ if (!fs.existsSync(REPO)) throw new Error(`no such folder: ${REPO}`);
66
+ const name = path.basename(REPO), OUT = path.resolve(o.out || `${name}.archivision.html`);
67
+ fs.mkdirSync(path.dirname(OUT), { recursive: true });
68
+ const t0 = Date.now();
69
+ if (o.fetch) { say('fetching…'); execFileSync('git', ['-C', REPO, 'fetch', '--prune'], { stdio: 'inherit' }); }
70
+ // 1. parse
71
+ const S = scanRepo(REPO, { bases: o.bases, since: o.since, max: o.max, compare: o.compare, src: o.src, ripwire: o.ripwire });
72
+ say(`1 parse ${S.summary}`);
73
+ // 2. group: a given map, the map from an earlier run, or a new one from the code
74
+ const mapF = o.map ? path.resolve(o.map) : `${stem(OUT)}.map.json`;
75
+ // the map an earlier run saved under the old name (<repo>.birdview.map.json) carries over, with its edits by hand
76
+ const oldF = path.resolve(`${name}.birdview.map.json`);
77
+ if (!o.out && !o.map && !o.remap && !fs.existsSync(mapF) && fs.existsSync(oldF)) { fs.copyFileSync(oldF, mapF); say(` carried over ${rel(oldF)} → ${rel(mapF)}`); }
78
+ let map;
79
+ if (!o.map && (o.remap || !fs.existsSync(mapF))) {
80
+ map = autoMap(S); const { summary, ...keep } = map;
81
+ fs.writeFileSync(mapF, JSON.stringify(keep, null, 2) + '\n'); say(`2 group ${summary} → ${rel(mapF)}`);
82
+ } else { map = JSON.parse(fs.readFileSync(mapF, 'utf8')); say(`2 group using ${rel(mapF)}`); }
83
+ // 3. join the map with the parse, check every anchor
84
+ const B = buildRepo(map, S);
85
+ say('3 build ' + B.summary.join('\n '));
86
+ if (B.warnings.length) say(' ' + B.warnings.slice(0, 20).join('\n ') + (B.warnings.length > 20 ? `\n … ${B.warnings.length - 20} more` : ''));
87
+ // 4. the page
88
+ const size = writePage(B, `${S.repo.name} · archivision`, OUT);
89
+ say(`4 page ${rel(OUT)} · ${(size / 1e6).toFixed(1)} MB · ${((Date.now() - t0) / 1000).toFixed(1)} s`);
90
+ return OUT;
91
+ }
92
+
93
+ function report(fixes, unclear) {
94
+ if (fixes.length) say(` repaired ${fixes.length}:\n ${fixes.slice(0, 15).join('\n ')}${fixes.length > 15 ? `\n … ${fixes.length - 15} more` : ''}`);
95
+ if (unclear && unclear.length) say(` the sources leave open:\n ${unclear.join('\n ')}`);
96
+ }
97
+
98
+ async function cmdArch(pos, o) {
99
+ const { readInputs, generate, validate, archToScene, writePage } = lib();
100
+ if (!pos.length) throw new Error('give the documents or folders to read: archivision arch docs/ diagram.png');
101
+ const t0 = Date.now();
102
+ const { docs, skipped } = readInputs(pos);
103
+ if (!docs.length) throw new Error(`nothing readable in ${pos.join(', ')}${skipped.length ? `: ${skipped.join('; ')}` : ''}`);
104
+ say(`1 read ${docs.length} sources: ${docs.map(d => `${d.name} (${d.type})`).join(', ')}`);
105
+ if (skipped.length) say(` skipped ${skipped.join('; ')}`);
106
+ const r = await generate(docs, { ai: o.ai, model: o.model, effort: o.effort, name: o.name, focus: o.focus, log: s => say(' ' + s.trim()) });
107
+ say(`2 model by ${r.by} · ${((Date.now() - t0) / 1000).toFixed(0)} s`);
108
+ const { model, fixes } = validate(r.model);
109
+ if (o.name) model.title = o.name;
110
+ const OUT = path.resolve(o.out || `${require('../lib/arch/model').slug(model.title)}.archivision.html`), modelF = `${stem(OUT)}.arch.json`;
111
+ fs.mkdirSync(path.dirname(OUT), { recursive: true });
112
+ const source = `drawn by ${r.by} from ${docs.map(d => d.name).join(', ')} · ${new Date().toISOString().slice(0, 10)}`;
113
+ fs.writeFileSync(modelF, JSON.stringify({ ...model, _by: r.by, _source: source }, null, 2) + '\n');
114
+ say(`3 check ${rel(modelF)}`); report(fixes, model.unclear);
115
+ const data = archToScene(model, { by: r.by, source });
116
+ say(` ${data.summary}`);
117
+ const size = writePage(data, `${model.title} · archivision`, OUT);
118
+ say(`4 page ${rel(OUT)} · ${(size / 1e6).toFixed(1)} MB`);
119
+ return OUT;
120
+ }
121
+
122
+ function cmdRender(pos, o) {
123
+ const { validate, archToScene, writePage } = lib();
124
+ if (!pos[0]) throw new Error('give the model file: archivision render system.arch.json');
125
+ const F = path.resolve(pos[0]), raw = JSON.parse(fs.readFileSync(F, 'utf8'));
126
+ const { model, fixes } = validate(raw);
127
+ if (o.name) model.title = o.name;
128
+ report(fixes, []);
129
+ const OUT = path.resolve(o.out || `${base(F)}.archivision.html`);
130
+ const data = archToScene(model, { by: raw._by || 'hand', source: raw._source || `from ${path.basename(F)}` });
131
+ const size = writePage(data, `${model.title} · archivision`, OUT);
132
+ say(`${data.summary}\npage ${rel(OUT)} · ${(size / 1e6).toFixed(1)} MB`);
133
+ return OUT;
134
+ }
135
+
136
+ (async () => {
137
+ let args;
138
+ try { args = parse(process.argv.slice(2)); } catch (e) { console.error(e.message); process.exit(2); }
139
+ const { pos, o } = args, cmd = pos.shift();
140
+ if (!cmd || o.help || cmd === 'help') { say(HELP); return; }
141
+ const run = { repo: cmdRepo, arch: cmdArch, render: cmdRender }[cmd];
142
+ if (!run) { console.error(`unknown command «${cmd}»\n\n${HELP}`); process.exit(2); }
143
+ try {
144
+ const out = await run(pos, o);
145
+ if (o.open && out) openIt(out);
146
+ } catch (e) {
147
+ console.error(`archivision ${cmd}: ${e.message}`);
148
+ if (process.env.ARCHIVISION_DEBUG || process.env.BIRDVIEW_DEBUG) console.error(e.stack);
149
+ process.exit(1);
150
+ }
151
+ })();
@@ -0,0 +1,85 @@
1
+ # Corner Shop: architecture
2
+
3
+ A small online shop: people browse a catalog, pay by card and get their parcel. Staff manage stock and refunds from a back office. This document is the input for the `archivision arch` example.
4
+
5
+ ## Areas
6
+
7
+ - **Storefront**: what customers use (web shop and emails).
8
+ - **Services**: the backend services behind the storefront and the back office.
9
+ - **Data and outside services**: the database, the event bus and the providers we pay.
10
+
11
+ ## Blocks
12
+
13
+ ### Web shop (Next.js)
14
+ - *Catalog pages* list products and search them through the Catalog service.
15
+ - *Cart* keeps the basket in a cookie and asks the Pricing step of the Order service for totals.
16
+ - *Checkout* collects the address and opens the Stripe payment sheet.
17
+ - *Account* shows past orders from the Order service.
18
+
19
+ ### Back office (Next.js, staff only)
20
+ - *Stock editor* changes quantities through the Catalog service.
21
+ - *Refunds* asks the Payment service to refund an order.
22
+ - Staff sign in with Google Workspace through the Auth gate.
23
+
24
+ ### Auth gate
25
+ Checks sessions for both apps. Customers use email links; staff use Google Workspace SSO. Exposes `GET /session`.
26
+
27
+ ### Catalog service
28
+ - `GET /products`, `GET /products/:id`, `PATCH /stock/:sku`.
29
+ - Keeps products and stock in Postgres (tables `products`, `stock`).
30
+ - Publishes `stock.changed` on the event bus when a quantity changes.
31
+
32
+ ### Order service
33
+ Steps, in order: **Receive** (`POST /orders` from Checkout), **Price** (applies the price list and the discount codes), **Reserve** (holds stock by calling the Catalog service), **Confirm** (waits for `payment.succeeded`), **Keep** (writes `orders` and `order_lines`).
34
+ Publishes `order.confirmed`.
35
+
36
+ ### Payment service
37
+ Wraps Stripe. Creates payment intents for Checkout, receives Stripe webhooks at `POST /stripe/webhook`, publishes `payment.succeeded` and `payment.failed`, and runs refunds for the back office.
38
+
39
+ ### Fulfilment worker
40
+ Subscribes to `order.confirmed`, buys a label from the Sendcloud API and publishes `parcel.shipped`.
41
+
42
+ ### Notifier
43
+ Subscribes to `order.confirmed` and `parcel.shipped` and sends emails through Postmark using the templates `order-confirmed` and `parcel-on-its-way`.
44
+
45
+ ## The event bus
46
+
47
+ NATS, with numbered subjects so the diagram stays readable:
48
+
49
+ | Port | Subject | Publishers | Subscribers |
50
+ |---|---|---|---|
51
+ | 1 | `stock.changed` | Catalog service | Web shop (cache refresh) |
52
+ | 2 | `payment.succeeded` / `payment.failed` | Payment service | Order service |
53
+ | 3 | `order.confirmed` | Order service | Fulfilment worker, Notifier |
54
+ | 4 | `parcel.shipped` | Fulfilment worker | Notifier |
55
+
56
+ ## Main flow
57
+
58
+ ```mermaid
59
+ sequenceDiagram
60
+ participant W as Web shop
61
+ participant O as Order service
62
+ participant C as Catalog service
63
+ participant P as Payment service
64
+ participant S as Stripe
65
+ participant B as Event bus
66
+ participant F as Fulfilment worker
67
+ participant N as Notifier
68
+ W->>O: POST /orders (cart, address)
69
+ O->>C: reserve stock
70
+ O-->>W: order id, total
71
+ W->>P: create payment intent
72
+ P->>S: PaymentIntent
73
+ S-->>P: webhook payment_intent.succeeded
74
+ P->>B: payment.succeeded (port 2)
75
+ B->>O: payment.succeeded
76
+ O->>B: order.confirmed (port 3)
77
+ B->>F: order.confirmed
78
+ B->>N: order.confirmed
79
+ F->>B: parcel.shipped (port 4)
80
+ B->>N: parcel.shipped
81
+ ```
82
+
83
+ ## Refunds
84
+
85
+ Back office → Payment service (`POST /refunds`) → Stripe refund → `payment.failed` is **not** used for refunds; the Payment service writes the refund to its own `refunds` table. Whether the Order service should hear about refunds is still open.