@laitszkin/apollo-toolkit 3.10.0 → 3.11.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.
Files changed (47) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/analyse-app-logs/scripts/__pycache__/filter_logs_by_time.cpython-312.pyc +0 -0
  3. package/analyse-app-logs/scripts/__pycache__/log_cli_utils.cpython-312.pyc +0 -0
  4. package/analyse-app-logs/scripts/__pycache__/search_logs.cpython-312.pyc +0 -0
  5. package/docs-to-voice/scripts/__pycache__/docs_to_voice.cpython-312.pyc +0 -0
  6. package/generate-spec/SKILL.md +17 -15
  7. package/generate-spec/agents/openai.yaml +1 -1
  8. package/generate-spec/references/TEMPLATE_SPEC.md +103 -84
  9. package/generate-spec/scripts/__pycache__/create-specscpython-312.pyc +0 -0
  10. package/init-project-html/SKILL.md +82 -126
  11. package/init-project-html/agents/openai.yaml +17 -8
  12. package/init-project-html/lib/atlas/assets/architecture.css +140 -0
  13. package/init-project-html/lib/atlas/assets/viewer.client.js +93 -0
  14. package/init-project-html/lib/atlas/cli.js +995 -0
  15. package/init-project-html/lib/atlas/layout.js +229 -0
  16. package/init-project-html/lib/atlas/render.js +485 -0
  17. package/init-project-html/lib/atlas/schema.js +310 -0
  18. package/init-project-html/lib/atlas/state.js +402 -0
  19. package/init-project-html/references/TEMPLATE_SPEC.md +123 -84
  20. package/init-project-html/sample-demo/resources/project-architecture/assets/architecture.css +139 -1058
  21. package/init-project-html/sample-demo/resources/project-architecture/assets/viewer.client.js +93 -0
  22. package/init-project-html/sample-demo/resources/project-architecture/atlas/atlas.index.yaml +34 -0
  23. package/init-project-html/sample-demo/resources/project-architecture/atlas/features/get-invite-codes.yaml +159 -0
  24. package/init-project-html/sample-demo/resources/project-architecture/atlas/features/invite-code-registration.yaml +160 -0
  25. package/init-project-html/sample-demo/resources/project-architecture/features/get-invite-codes/index.html +67 -52
  26. package/init-project-html/sample-demo/resources/project-architecture/features/get-invite-codes/invite-code-generator.html +48 -163
  27. package/init-project-html/sample-demo/resources/project-architecture/features/get-invite-codes/invite-issuance-service.html +70 -196
  28. package/init-project-html/sample-demo/resources/project-architecture/features/get-invite-codes/postgresql.html +64 -163
  29. package/init-project-html/sample-demo/resources/project-architecture/features/get-invite-codes/public-api.html +68 -150
  30. package/init-project-html/sample-demo/resources/project-architecture/features/get-invite-codes/web-get-invite-ui.html +65 -138
  31. package/init-project-html/sample-demo/resources/project-architecture/features/invite-code-registration/index.html +61 -51
  32. package/init-project-html/sample-demo/resources/project-architecture/features/invite-code-registration/postgresql.html +66 -159
  33. package/init-project-html/sample-demo/resources/project-architecture/features/invite-code-registration/public-api.html +63 -143
  34. package/init-project-html/sample-demo/resources/project-architecture/features/invite-code-registration/registration-service.html +77 -188
  35. package/init-project-html/sample-demo/resources/project-architecture/features/invite-code-registration/web-register-ui.html +65 -138
  36. package/init-project-html/sample-demo/resources/project-architecture/index.html +232 -335
  37. package/init-project-html/scripts/architecture.js +65 -247
  38. package/katex/scripts/__pycache__/render_katex.cpython-312.pyc +0 -0
  39. package/open-github-issue/scripts/__pycache__/open_github_issue.cpython-312.pyc +0 -0
  40. package/package.json +6 -2
  41. package/read-github-issue/scripts/__pycache__/find_issues.cpython-312.pyc +0 -0
  42. package/read-github-issue/scripts/__pycache__/read_issue.cpython-312.pyc +0 -0
  43. package/resolve-review-comments/scripts/__pycache__/review_threads.cpython-312.pyc +0 -0
  44. package/spec-to-project-html/SKILL.md +61 -63
  45. package/spec-to-project-html/agents/openai.yaml +14 -8
  46. package/spec-to-project-html/references/TEMPLATE_SPEC.md +96 -83
  47. package/text-to-short-video/scripts/__pycache__/enforce_video_aspect_ratio.cpython-312.pyc +0 -0
@@ -0,0 +1,402 @@
1
+ 'use strict';
2
+
3
+ // state.js — YAML persistence for the declarative atlas.
4
+ //
5
+ // On-disk layout (base mode):
6
+ // <project>/resources/project-architecture/atlas/
7
+ // ├── atlas.index.yaml # meta, actors, ordered feature slug list, cross-feature edges
8
+ // ├── features/<slug>.yaml # one file per feature (submodules + intra-feature edges)
9
+ // ├── atlas.history.log # append-only audit JSONL
10
+ // └── atlas.history.undo.json # single-step undo snapshot
11
+ //
12
+ // Overlay layout (spec mode mirrors base, plus _removed.yaml):
13
+ // <spec_dir>/architecture_diff/atlas/
14
+ // ├── atlas.index.yaml # optional partial override of meta/actors/edges/feature ordering
15
+ // ├── features/<slug>.yaml # full proposed state of any changed feature
16
+ // └── _removed.yaml # {features: [...], submodules: [{feature, submodule}]}
17
+
18
+ const fs = require('node:fs');
19
+ const path = require('node:path');
20
+ const yaml = require('js-yaml');
21
+
22
+ const { emptyState } = require('./schema');
23
+
24
+ const INDEX_FILE = 'atlas.index.yaml';
25
+ const REMOVED_FILE = '_removed.yaml';
26
+ const FEATURES_DIR = 'features';
27
+ const HISTORY_FILE = 'atlas.history.log';
28
+ const UNDO_FILE = 'atlas.history.undo.json';
29
+ const ATLAS_DIRNAME = 'atlas';
30
+
31
+ function readYaml(file) {
32
+ if (!fs.existsSync(file)) return null;
33
+ const text = fs.readFileSync(file, 'utf8');
34
+ if (text.trim().length === 0) return null;
35
+ return yaml.load(text);
36
+ }
37
+
38
+ function writeYaml(file, data) {
39
+ fs.mkdirSync(path.dirname(file), { recursive: true });
40
+ const text = yaml.dump(data, { sortKeys: false, lineWidth: 100, noRefs: true });
41
+ fs.writeFileSync(file, text, 'utf8');
42
+ }
43
+
44
+ // load(atlasDir) reads the full base state. Missing directories are
45
+ // treated as empty (returns emptyState()). Each referenced feature
46
+ // file is loaded eagerly; missing feature files are surfaced as empty
47
+ // stubs so validation can flag them.
48
+ function load(atlasDir) {
49
+ const state = emptyState();
50
+ const indexFile = path.join(atlasDir, INDEX_FILE);
51
+ const index = readYaml(indexFile);
52
+ if (!index) return state;
53
+
54
+ if (index.meta) state.meta = { ...state.meta, ...index.meta };
55
+ if (Array.isArray(index.actors)) state.actors = index.actors;
56
+ if (Array.isArray(index.edges)) state.edges = index.edges;
57
+
58
+ const featureList = Array.isArray(index.features) ? index.features : [];
59
+ state.features = featureList
60
+ .map((entry) => {
61
+ const slug = typeof entry === 'string' ? entry : entry && entry.slug;
62
+ if (!slug) return null;
63
+ const featureFile = path.join(atlasDir, FEATURES_DIR, `${slug}.yaml`);
64
+ const feature = readYaml(featureFile);
65
+ if (!feature) {
66
+ return { slug, title: slug, story: '', dependsOn: [], submodules: [], edges: [] };
67
+ }
68
+ return normalizeFeature({ ...feature, slug: feature.slug || slug });
69
+ })
70
+ .filter(Boolean);
71
+
72
+ return state;
73
+ }
74
+
75
+ function normalizeFeature(feature) {
76
+ return {
77
+ slug: feature.slug,
78
+ title: feature.title || feature.slug,
79
+ story: feature.story || '',
80
+ dependsOn: Array.isArray(feature.dependsOn) ? feature.dependsOn : [],
81
+ submodules: Array.isArray(feature.submodules) ? feature.submodules.map(normalizeSubmodule) : [],
82
+ edges: Array.isArray(feature.edges) ? feature.edges : [],
83
+ };
84
+ }
85
+
86
+ function normalizeSubmodule(sub) {
87
+ return {
88
+ slug: sub.slug,
89
+ kind: sub.kind || 'service',
90
+ role: sub.role || '',
91
+ functions: Array.isArray(sub.functions) ? sub.functions : [],
92
+ variables: Array.isArray(sub.variables) ? sub.variables : [],
93
+ dataflow: Array.isArray(sub.dataflow) ? sub.dataflow : [],
94
+ errors: Array.isArray(sub.errors) ? sub.errors : [],
95
+ };
96
+ }
97
+
98
+ // save(atlasDir, state, {touch=true}) writes the index + every feature
99
+ // YAML, dropping orphan feature files. When touch is true, meta.updatedAt
100
+ // is refreshed.
101
+ function save(atlasDir, state, options = {}) {
102
+ const { touch = true } = options;
103
+ const next = JSON.parse(JSON.stringify(state));
104
+ next.meta = next.meta || {};
105
+ if (touch) next.meta.updatedAt = new Date().toISOString();
106
+
107
+ const indexFile = path.join(atlasDir, INDEX_FILE);
108
+ const index = {
109
+ meta: next.meta,
110
+ actors: next.actors || [],
111
+ features: (next.features || []).map((f) => f.slug),
112
+ edges: next.edges || [],
113
+ };
114
+ writeYaml(indexFile, index);
115
+
116
+ const featuresDir = path.join(atlasDir, FEATURES_DIR);
117
+ fs.mkdirSync(featuresDir, { recursive: true });
118
+ const wanted = new Set((next.features || []).map((f) => `${f.slug}.yaml`));
119
+ for (const entry of fs.readdirSync(featuresDir)) {
120
+ if (entry.endsWith('.yaml') && !wanted.has(entry)) {
121
+ fs.rmSync(path.join(featuresDir, entry));
122
+ }
123
+ }
124
+ for (const feature of next.features || []) {
125
+ writeYaml(path.join(featuresDir, `${feature.slug}.yaml`), feature);
126
+ }
127
+ }
128
+
129
+ // loadOverlay reads the spec-mode overlay. Every field is optional.
130
+ // Returns a structured overlay object even when the overlay directory
131
+ // is missing (all-empty overlay, which merges to base unchanged).
132
+ function loadOverlay(overlayDir) {
133
+ const overlay = {
134
+ meta: null,
135
+ actors: null,
136
+ edges: null,
137
+ featureOrder: null,
138
+ features: {},
139
+ removed: { features: [], submodules: [] },
140
+ };
141
+ if (!fs.existsSync(overlayDir)) return overlay;
142
+
143
+ const index = readYaml(path.join(overlayDir, INDEX_FILE));
144
+ if (index) {
145
+ if (index.meta !== undefined) overlay.meta = index.meta;
146
+ if (index.actors !== undefined) overlay.actors = index.actors;
147
+ if (index.edges !== undefined) overlay.edges = index.edges;
148
+ if (Array.isArray(index.features) && index.features.length > 0) {
149
+ overlay.featureOrder = index.features.map((entry) => (typeof entry === 'string' ? entry : entry && entry.slug)).filter(Boolean);
150
+ }
151
+ }
152
+
153
+ const featuresDir = path.join(overlayDir, FEATURES_DIR);
154
+ if (fs.existsSync(featuresDir)) {
155
+ for (const entry of fs.readdirSync(featuresDir)) {
156
+ if (!entry.endsWith('.yaml')) continue;
157
+ const data = readYaml(path.join(featuresDir, entry));
158
+ if (data && data.slug) overlay.features[data.slug] = normalizeFeature(data);
159
+ }
160
+ }
161
+
162
+ const removed = readYaml(path.join(overlayDir, REMOVED_FILE));
163
+ if (removed) {
164
+ if (Array.isArray(removed.features)) overlay.removed.features = removed.features;
165
+ if (Array.isArray(removed.submodules)) overlay.removed.submodules = removed.submodules;
166
+ }
167
+
168
+ return overlay;
169
+ }
170
+
171
+ // saveOverlay writes only the components the caller provided. Unlike
172
+ // save(), this does not touch base files. Untouched features keep their
173
+ // base definition; explicitly written features land in
174
+ // overlayDir/features/<slug>.yaml; removed features/submodules land in
175
+ // _removed.yaml.
176
+ function saveOverlay(overlayDir, overlay) {
177
+ fs.mkdirSync(overlayDir, { recursive: true });
178
+
179
+ const indexPayload = {};
180
+ if (overlay.meta !== null && overlay.meta !== undefined) indexPayload.meta = overlay.meta;
181
+ if (overlay.actors !== null && overlay.actors !== undefined) indexPayload.actors = overlay.actors;
182
+ if (overlay.edges !== null && overlay.edges !== undefined) indexPayload.edges = overlay.edges;
183
+ if (overlay.featureOrder) indexPayload.features = overlay.featureOrder;
184
+ if (Object.keys(indexPayload).length > 0) {
185
+ writeYaml(path.join(overlayDir, INDEX_FILE), indexPayload);
186
+ } else if (fs.existsSync(path.join(overlayDir, INDEX_FILE))) {
187
+ fs.rmSync(path.join(overlayDir, INDEX_FILE));
188
+ }
189
+
190
+ const featuresDir = path.join(overlayDir, FEATURES_DIR);
191
+ fs.mkdirSync(featuresDir, { recursive: true });
192
+ const wanted = new Set(Object.keys(overlay.features || {}).map((slug) => `${slug}.yaml`));
193
+ for (const entry of fs.readdirSync(featuresDir)) {
194
+ if (entry.endsWith('.yaml') && !wanted.has(entry)) {
195
+ fs.rmSync(path.join(featuresDir, entry));
196
+ }
197
+ }
198
+ for (const [slug, feature] of Object.entries(overlay.features || {})) {
199
+ writeYaml(path.join(featuresDir, `${slug}.yaml`), feature);
200
+ }
201
+
202
+ const removedFile = path.join(overlayDir, REMOVED_FILE);
203
+ const hasRemoved = (overlay.removed.features && overlay.removed.features.length > 0)
204
+ || (overlay.removed.submodules && overlay.removed.submodules.length > 0);
205
+ if (hasRemoved) {
206
+ writeYaml(removedFile, overlay.removed);
207
+ } else if (fs.existsSync(removedFile)) {
208
+ fs.rmSync(removedFile);
209
+ }
210
+ }
211
+
212
+ // mergeOverlay produces the after-state given a base state and an
213
+ // overlay. Overlay features fully replace base features of the same
214
+ // slug; removed features/submodules drop from the merged result.
215
+ // When overlay.featureOrder is provided it controls the order of
216
+ // features in the merged output (unlisted features keep base ordering
217
+ // at the tail).
218
+ function mergeOverlay(base, overlay) {
219
+ const merged = JSON.parse(JSON.stringify(base));
220
+ if (overlay.meta) merged.meta = { ...merged.meta, ...overlay.meta };
221
+ if (overlay.actors !== null && overlay.actors !== undefined) merged.actors = overlay.actors || [];
222
+ if (overlay.edges !== null && overlay.edges !== undefined) merged.edges = overlay.edges || [];
223
+
224
+ const featureMap = new Map((merged.features || []).map((f) => [f.slug, f]));
225
+ for (const [slug, feature] of Object.entries(overlay.features || {})) {
226
+ featureMap.set(slug, feature);
227
+ }
228
+
229
+ if (overlay.removed && Array.isArray(overlay.removed.features)) {
230
+ for (const slug of overlay.removed.features) featureMap.delete(slug);
231
+ }
232
+ if (overlay.removed && Array.isArray(overlay.removed.submodules)) {
233
+ for (const { feature: fslug, submodule: sslug } of overlay.removed.submodules) {
234
+ const f = featureMap.get(fslug);
235
+ if (f) f.submodules = (f.submodules || []).filter((s) => s.slug !== sslug);
236
+ }
237
+ }
238
+
239
+ let orderedSlugs;
240
+ if (overlay.featureOrder) {
241
+ const seen = new Set();
242
+ orderedSlugs = [];
243
+ for (const slug of overlay.featureOrder) {
244
+ if (featureMap.has(slug) && !seen.has(slug)) {
245
+ orderedSlugs.push(slug);
246
+ seen.add(slug);
247
+ }
248
+ }
249
+ for (const slug of featureMap.keys()) {
250
+ if (!seen.has(slug)) orderedSlugs.push(slug);
251
+ }
252
+ } else {
253
+ orderedSlugs = [...featureMap.keys()];
254
+ }
255
+ merged.features = orderedSlugs.map((slug) => featureMap.get(slug));
256
+
257
+ return merged;
258
+ }
259
+
260
+ // diffPages compares the merged after-state against the base and
261
+ // classifies which HTML pages must be regenerated (modified) versus
262
+ // emitted fresh (added) versus listed in _removed.txt (removed).
263
+ function diffPages(base, merged) {
264
+ const baseFeatures = new Map((base.features || []).map((f) => [f.slug, f]));
265
+ const mergedFeatures = new Map((merged.features || []).map((f) => [f.slug, f]));
266
+
267
+ const addedFeatures = new Set();
268
+ const modifiedFeatures = new Set();
269
+ const removedFeatures = new Set();
270
+ const addedSubmodules = []; // {feature, submodule}
271
+ const modifiedSubmodules = [];
272
+ const removedSubmodules = [];
273
+
274
+ for (const [slug, mergedFeat] of mergedFeatures) {
275
+ const baseFeat = baseFeatures.get(slug);
276
+ if (!baseFeat) {
277
+ addedFeatures.add(slug);
278
+ for (const sub of mergedFeat.submodules || []) {
279
+ addedSubmodules.push({ feature: slug, submodule: sub.slug });
280
+ }
281
+ continue;
282
+ }
283
+ if (JSON.stringify(featureVisualOf(baseFeat)) !== JSON.stringify(featureVisualOf(mergedFeat))) {
284
+ modifiedFeatures.add(slug);
285
+ }
286
+ const baseSubMap = new Map((baseFeat.submodules || []).map((s) => [s.slug, s]));
287
+ const mergedSubMap = new Map((mergedFeat.submodules || []).map((s) => [s.slug, s]));
288
+ for (const [subSlug, mergedSub] of mergedSubMap) {
289
+ const baseSub = baseSubMap.get(subSlug);
290
+ if (!baseSub) addedSubmodules.push({ feature: slug, submodule: subSlug });
291
+ else if (JSON.stringify(baseSub) !== JSON.stringify(mergedSub)) {
292
+ modifiedSubmodules.push({ feature: slug, submodule: subSlug });
293
+ }
294
+ }
295
+ for (const subSlug of baseSubMap.keys()) {
296
+ if (!mergedSubMap.has(subSlug)) removedSubmodules.push({ feature: slug, submodule: subSlug });
297
+ }
298
+ }
299
+ for (const slug of baseFeatures.keys()) {
300
+ if (!mergedFeatures.has(slug)) {
301
+ removedFeatures.add(slug);
302
+ for (const sub of baseFeatures.get(slug).submodules || []) {
303
+ removedSubmodules.push({ feature: slug, submodule: sub.slug });
304
+ }
305
+ }
306
+ }
307
+
308
+ const macroChanged = (
309
+ JSON.stringify(macroVisualOf(base)) !== JSON.stringify(macroVisualOf(merged))
310
+ );
311
+
312
+ return {
313
+ addedFeatures,
314
+ modifiedFeatures,
315
+ removedFeatures,
316
+ addedSubmodules,
317
+ modifiedSubmodules,
318
+ removedSubmodules,
319
+ macroChanged,
320
+ };
321
+ }
322
+
323
+ // featureVisualOf returns the projection of a feature that drives its
324
+ // own page (title, story, dependsOn, submodule navigation cards, and
325
+ // intra-feature edge list). Sub-module internals are compared
326
+ // separately in diffPages.
327
+ function featureVisualOf(feature) {
328
+ return {
329
+ title: feature.title,
330
+ story: feature.story,
331
+ dependsOn: feature.dependsOn || [],
332
+ submodules: (feature.submodules || []).map((s) => ({ slug: s.slug, kind: s.kind, role: s.role })),
333
+ edges: feature.edges || [],
334
+ };
335
+ }
336
+
337
+ // macroVisualOf returns the projection of state that drives the macro
338
+ // SVG (cluster titles, submodule nodes + their kind/role badges, every
339
+ // edge label/kind). Sub-module-internal fields (functions, variables,
340
+ // dataflow, errors) are excluded so editing those alone does not force
341
+ // a macro re-render.
342
+ function macroVisualOf(state) {
343
+ return {
344
+ meta: state.meta || {},
345
+ actors: state.actors || [],
346
+ edges: state.edges || [],
347
+ features: (state.features || []).map((f) => ({
348
+ slug: f.slug,
349
+ title: f.title,
350
+ dependsOn: f.dependsOn || [],
351
+ submodules: (f.submodules || []).map((s) => ({ slug: s.slug, kind: s.kind, role: s.role })),
352
+ edges: f.edges || [],
353
+ })),
354
+ };
355
+ }
356
+
357
+ function appendHistory(atlasDir, entry) {
358
+ fs.mkdirSync(atlasDir, { recursive: true });
359
+ const file = path.join(atlasDir, HISTORY_FILE);
360
+ fs.appendFileSync(file, `${JSON.stringify({ ts: new Date().toISOString(), ...entry })}\n`, 'utf8');
361
+ }
362
+
363
+ function writeUndoSnapshot(atlasDir, state) {
364
+ fs.mkdirSync(atlasDir, { recursive: true });
365
+ fs.writeFileSync(path.join(atlasDir, UNDO_FILE), JSON.stringify(state, null, 2), 'utf8');
366
+ }
367
+
368
+ function readUndoSnapshot(atlasDir) {
369
+ const file = path.join(atlasDir, UNDO_FILE);
370
+ if (!fs.existsSync(file)) return null;
371
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
372
+ }
373
+
374
+ function clearUndoSnapshot(atlasDir) {
375
+ const file = path.join(atlasDir, UNDO_FILE);
376
+ if (fs.existsSync(file)) fs.rmSync(file);
377
+ }
378
+
379
+ module.exports = {
380
+ ATLAS_DIRNAME,
381
+ INDEX_FILE,
382
+ REMOVED_FILE,
383
+ FEATURES_DIR,
384
+ HISTORY_FILE,
385
+ UNDO_FILE,
386
+ readYaml,
387
+ writeYaml,
388
+ load,
389
+ save,
390
+ loadOverlay,
391
+ saveOverlay,
392
+ mergeOverlay,
393
+ diffPages,
394
+ normalizeFeature,
395
+ normalizeSubmodule,
396
+ appendHistory,
397
+ writeUndoSnapshot,
398
+ readUndoSnapshot,
399
+ clearUndoSnapshot,
400
+ macroVisualOf,
401
+ featureVisualOf,
402
+ };
@@ -1,98 +1,137 @@
1
- # HTML architecture atlas — reference cheat sheet
1
+ # Atlas component schema — reference cheat sheet
2
2
 
3
- > Reference material only. The **binding rules** (page contracts, naming, assets, accessibility, forbidden shortcuts) live in `init-project-html/SKILL.md`. This file is a glossary + class-hook table + ready-to-copy DOM snippets so an agent does not have to re-derive the markup. `spec-to-project-html` reuses the same vocabulary when refreshing diagrams.
3
+ > Reference material only. The binding rules (read strategy, evidence requirements, what each verb means) live in `SKILL.md`. This file lists the exact fields and enum values that `apltk architecture` accepts; the renderer applies them to consistent DOM/CSS/ARIA hooks so agents never need to touch HTML.
4
4
 
5
- ## Vocabulary
5
+ ## State files on disk
6
6
 
7
- - **Feature module** — one **user-visible end-to-end capability** (e.g. "invite-code registration", "get-invite-codes"). One directory `features/<feature-slug>/`. It is **not** a single web layer or a single database.
8
- - **Sub-module** — an implementation boundary inside that capability (front-end page, public API, domain service, PostgreSQL, pure helpers, message queues…). One HTML page per sub-module, sibling to the feature's `index.html`.
9
-
10
- ## Directory layout (target output)
11
-
12
- ```text
7
+ ```
13
8
  resources/project-architecture/
14
- index.html # macro: feature × sub-module in one SVG with multi-edge + data-row flow
15
- assets/
16
- architecture.css
17
- features/
18
- <feature-slug>/ # one feature module = one directory
19
- index.html # lightweight overview (story + submodule nav)
20
- <sub-module-slug>.html # one HTML per sub-module (own I/O + internal flow)
9
+ ├── atlas/
10
+ │ ├── atlas.index.yaml # meta + actors + feature slug order + cross-feature edges
11
+ │ ├── features/<slug>.yaml # one file per feature (submodules + intra-feature edges)
12
+ │ ├── atlas.history.log # append-only audit log (JSONL)
13
+ │ └── atlas.history.undo.json # single-step undo snapshot
14
+ ├── index.html # rendered (do not hand-edit)
15
+ ├── features/<slug>/index.html # rendered
16
+ ├── features/<slug>/<sub>.html # rendered
17
+ └── assets/ # architecture.css + viewer.client.js (copied by the renderer)
21
18
  ```
22
19
 
23
- ## Macro SVG — CSS class hooks
24
-
25
- | Element | class |
26
- |---|---|
27
- | Actor block | `m-actor` |
28
- | Feature cluster frame | `m-cluster` / `m-cluster__rect` / `m-cluster__title` |
29
- | Sub-module node | `m-sub` (add `m-sub--db` for databases) |
30
- | Edge | `m-edge` + modifier `m-edge--call` / `m-edge--return` / `m-edge--cross` |
31
- | Edge label | `m-edge__label` (cross-feature labels add `m-edge__label--cross`) |
32
-
33
- ## DOM snippets
34
-
35
- ### `sub-io` function I/O table
36
-
37
- ```html
38
- <section class="sub-io">
39
- <h2>Function I/O</h2>
40
- <table>
41
- <thead><tr><th>Function</th><th>Signature</th><th>Side effects</th><th>Purpose</th></tr></thead>
42
- <tbody>
43
- <tr>
44
- <td><code>FunctionName</code></td>
45
- <td class="sub-io__signature">
46
- <strong>in:</strong> <code>T1</code>, <code>T2</code><br>
47
- <strong>out:</strong> <code>R</code> | <code>ErrX</code>
48
- </td>
49
- <td><span class="sub-io__side sub-io__side--pure">pure</span></td>
50
- <td>One-line purpose.</td>
51
- </tr>
52
- </tbody>
53
- </table>
54
- </section>
55
- ```
20
+ ## Components
56
21
 
57
- ### `sub-vars` variables-with-business-purpose table
58
-
59
- ```html
60
- <section class="sub-vars">
61
- <h2>Variables &amp; business purpose</h2>
62
- <p class="sub-vars__intro">Identifiers this sub-module holds or threads through. Types align readers; business purpose comes first.</p>
63
- <table>
64
- <thead>
65
- <tr><th>Variable</th><th>Type</th><th>Scope</th><th>Business purpose</th></tr>
66
- </thead>
67
- <tbody>
68
- <tr>
69
- <td class="sub-vars__name">someVar</td>
70
- <td class="sub-vars__type">SomeType</td>
71
- <td><span class="sub-vars__scope sub-vars__scope--call">call</span></td>
72
- <td>One line: this value decides branch X; without it Y breaks.</td>
73
- </tr>
74
- </tbody>
75
- </table>
76
- </section>
77
- ```
22
+ ### `meta`
23
+
24
+ | Field | Type | Required | Notes |
25
+ | ------- | ------ | -------- | ----- |
26
+ | title | string | yes | Macro page H1 + diff viewer title. |
27
+ | summary | string | no | Renders below the title; record scanned roots and deliberate omissions here. |
28
+ | updatedAt | string (ISO) | auto | Touched on every save; do not set manually. |
29
+
30
+ CLI: `apltk architecture meta set --title "..." --summary "..."`
31
+
32
+ ### `actors`
33
+
34
+ | Field | Type | Required | Notes |
35
+ | ----- | ---- | -------- | ----- |
36
+ | id | kebab-case slug | yes | Stable identity. |
37
+ | label | string | yes | Display name. |
38
+
39
+ CLI: `apltk architecture actor add --id end-user --label "End user"`
40
+
41
+ ### `feature`
42
+
43
+ | Field | Type | Required | Notes |
44
+ | ---------- | ---- | -------- | ----- |
45
+ | slug | kebab-case | yes | Matches the directory name `features/<slug>/`. |
46
+ | title | string | yes | User-language capability name. |
47
+ | story | string | no | 1–3 sentence user story shown on the feature page. |
48
+ | dependsOn | array of feature slugs | no | Shown as "Depends on:" links on the feature page. |
49
+ | submodules | array of submodule | yes | Render-order matches list order. |
50
+ | edges | array of edge | no | Intra-feature edges (see below). |
51
+
52
+ CLI: `apltk architecture feature add --slug <kebab> --title "..." --story "..." [--depends-on a,b]`
53
+
54
+ ### `submodule`
55
+
56
+ | Field | Type | Required | Notes |
57
+ | ---------- | ---- | -------- | ----- |
58
+ | slug | kebab-case | yes | Matches the HTML filename `features/<feature>/<slug>.html`. |
59
+ | kind | enum `ui` `api` `service` `db` `pure-fn` `queue` `external` | yes | Drives node colour + label. |
60
+ | role | string | no | Own responsibility in one sentence. Renders as macro node footnote + feature card subtitle. |
61
+ | functions | array of function row | no | Renders the `sub-io` table. |
62
+ | variables | array of variable row | no | Renders the `sub-vars` table. |
63
+ | dataflow | array of string (ordered) | no | Renders the `sub-dataflow` internal flow SVG. |
64
+ | errors | array of error row | no | Renders the `sub-errors` table. |
65
+
66
+ CLI: `apltk architecture submodule add --feature X --slug Y --kind api --role "..."`
67
+
68
+ ### `function` row
69
+
70
+ | Field | Type | Required | Notes |
71
+ | ------- | ---- | -------- | ----- |
72
+ | name | string | yes | Function or method name. |
73
+ | in | string | no | Comma-separated signature parts; rendered verbatim. |
74
+ | out | string | no | May include `\|` to denote error returns. |
75
+ | side | enum `pure` `io` `write` `tx` `lock` `network` | no | Side-effect chip. |
76
+ | purpose | string | no | One-line business purpose. |
77
+
78
+ CLI: `apltk architecture function add --feature X --submodule Y --name fn --in "..." --out "..." --side tx --purpose "..."`
79
+
80
+ ### `variable` row
81
+
82
+ | Field | Type | Required | Notes |
83
+ | ------- | ---- | -------- | ----- |
84
+ | name | string | yes | Parameter / field / column / counter name. |
85
+ | type | string | no | Free-form. |
86
+ | scope | enum `call` `tx` `persist` `instance` `loop` | no | Lifetime/scope chip. |
87
+ | purpose | string | no | **Business** purpose — why this identifier exists, which branch it gates, what breaks without it. |
88
+
89
+ CLI: `apltk architecture variable add --feature X --submodule Y --name v --type T --scope call --purpose "..."`
90
+
91
+ ### `dataflow` step
92
+
93
+ A simple ordered string. Appended at the tail by default; pass `--at N` to insert at index N. Reorder with `apltk architecture dataflow reorder --feature X --submodule Y --from i --to j`.
94
+
95
+ CLI: `apltk architecture dataflow add --feature X --submodule Y --step "..."`
96
+
97
+ ### `error` row
98
+
99
+ | Field | Type | Required | Notes |
100
+ | ----- | ---- | -------- | ----- |
101
+ | name | string | yes | Symbolic name (e.g. `ErrInvalidCode`). |
102
+ | when | string | no | Condition that raises this error. |
103
+ | means | string | no | Observable outcome (HTTP status, user feedback). |
104
+
105
+ CLI: `apltk architecture error add --feature X --submodule Y --name ErrCode --when "..." --means "..."`
106
+
107
+ ### `edge`
108
+
109
+ | Field | Type | Required | Notes |
110
+ | ----- | ---- | -------- | ----- |
111
+ | id | kebab-case | no (auto if omitted) | Stable so cross-references survive renames. |
112
+ | from | `feature/submodule` (cross-feature) or `submodule` (intra-feature, when `--from` and `--to` share a feature) | yes | |
113
+ | to | same shape | yes | |
114
+ | kind | enum `call` `return` `data-row` `failure` | yes | Drives stroke/dash/colour and arrow head. |
115
+ | label | string | no | Rendered at the middle of the edge path. |
78
116
 
79
- Scope chip vocabulary: `sub-vars__scope--call` (single call), `--tx` (transaction-bound), `--persist` (persisted), `--instance` (fixed at construction; lifetime-shared), `--loop` (retry/loop).
117
+ CLI: `apltk architecture edge add --from <feature>[/sub] --to <feature>[/sub] --kind call --label "..."`
80
118
 
81
- ### `sub-dataflow` small SVG sizing
119
+ ## Class hooks on rendered HTML
82
120
 
83
- - Node class: `d-node`; edge class: `d-edge` (side-effect edges use `d-edge--side`).
84
- - Recommended viewBox: height ≤ 240, width ≤ 720.
85
- - Nodes are this sub-module's internal variables/functions only.
121
+ These are emitted automatically by `lib/atlas/render.js`. Agents do **not** write them by hand — they are listed here only so reviewers know which selectors are stable.
86
122
 
87
- ## Edge-kind vocabulary (for macro `flow-edge-manifest`)
123
+ | Hook | Page |
124
+ | --- | --- |
125
+ | `.atlas-header`, `.atlas-summary`, `.atlas-canvas`, `.atlas-submodule-index`, `.atlas-legend` | macro |
126
+ | `.m-cluster`, `.m-cluster__title`, `.m-node`, `.m-node--<kind>`, `.m-node__title/__kind/__role`, `.m-edge`, `.m-edge--<kind>`, `.m-edge__label` | macro SVG |
127
+ | `.feature-header`, `.feature-story`, `.submodule-nav`, `.submodule-card`, `.feature-edges` | feature page |
128
+ | `.submodule-header`, `.submodule-role`, `.sub-io`, `.sub-vars`, `.sub-dataflow`, `.sub-errors`, `.submodule-kind--<kind>` | sub-module page |
88
129
 
89
- | `data-edge-kind` | meaning | typical visual |
90
- |---|---|---|
91
- | `call` | function call / HTTP request | solid arrow |
92
- | `return` | return value / response | thin dashed arrow |
93
- | `data-row` | cross-feature hand-off via data rows (not a function call) | warm-tone heavy dashed |
94
- | `failure` | failure branch | red solid arrow with `failure` chip in the manifest row |
130
+ ## Pan/zoom on the macro
95
131
 
96
- ## Typography hint
132
+ The CLI copies `assets/viewer.client.js` into the atlas. The macro page mounts a `[data-pan-zoom-viewport]` element wrapping the SVG; the script wires:
97
133
 
98
- Pair a recognisable display face (e.g. `Fraunces`) with `Plus Jakarta Sans`. Avoid the "AI-default purple gradient" and Inter look-alike. Detailed rules live in `init-project-html/SKILL.md` § Rule 6.
134
+ - mouse wheel zoom around the cursor,
135
+ - click + drag to pan,
136
+ - `+` / `−` / `Fit` buttons on the toolbar,
137
+ - keyboard `←` `→` `↑` `↓` (pan), `+` `=` (zoom in), `−` `_` (zoom out), `0` (reset).