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.
- package/CHANGELOG.md +49 -0
- package/LICENSE +21 -0
- package/NOTICE +9 -0
- package/README.md +164 -2
- package/assets/ISO-GLOW-LICENSE +22 -0
- package/assets/figure.js +1253 -0
- package/assets/shell-3d.html +2976 -0
- package/bin/archivision.js +151 -0
- package/examples/shop/architecture.md +85 -0
- package/examples/shop/shop.arch.json +1273 -0
- package/lib/arch/ai.js +110 -0
- package/lib/arch/inputs.js +107 -0
- package/lib/arch/model.js +152 -0
- package/lib/arch/prompt.js +29 -0
- package/lib/arch/schema.js +80 -0
- package/lib/index.js +13 -0
- package/lib/layout.js +117 -0
- package/lib/page.js +35 -0
- package/lib/repo/automap.js +352 -0
- package/lib/repo/build.js +347 -0
- package/lib/repo/facts.js +160 -0
- package/lib/repo/roots.js +18 -0
- package/lib/repo/scan/compare.js +93 -0
- package/lib/repo/scan/discover.js +221 -0
- package/lib/repo/scan/git.js +61 -0
- package/lib/repo/scan/go.js +157 -0
- package/lib/repo/scan/index.js +124 -0
- package/lib/repo/scan/lambda.js +93 -0
- package/lib/repo/scan/manifests.js +338 -0
- package/lib/repo/scan/next.js +46 -0
- package/lib/repo/scan/playwright.js +20 -0
- package/lib/repo/scan/prisma.js +81 -0
- package/lib/repo/scan/python.js +188 -0
- package/lib/repo/scan/ripwire.js +231 -0
- package/lib/repo/scan/tsc.js +381 -0
- package/lib/scan-schema.js +106 -0
- package/package.json +25 -4
- package/python/scan.py +2047 -0
|
@@ -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.
|