@laitszkin/apollo-toolkit 3.10.0 → 3.11.1

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 +37 -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 +117 -125
  11. package/init-project-html/agents/openai.yaml +18 -9
  12. package/init-project-html/lib/atlas/assets/architecture.css +161 -0
  13. package/init-project-html/lib/atlas/assets/viewer.client.js +136 -0
  14. package/init-project-html/lib/atlas/cli.js +1023 -0
  15. package/init-project-html/lib/atlas/layout.js +330 -0
  16. package/init-project-html/lib/atlas/render.js +583 -0
  17. package/init-project-html/lib/atlas/schema.js +347 -0
  18. package/init-project-html/lib/atlas/state.js +402 -0
  19. package/init-project-html/references/TEMPLATE_SPEC.md +140 -83
  20. package/init-project-html/sample-demo/resources/project-architecture/assets/architecture.css +160 -1058
  21. package/init-project-html/sample-demo/resources/project-architecture/assets/viewer.client.js +136 -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 +172 -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 +64 -163
  27. package/init-project-html/sample-demo/resources/project-architecture/features/get-invite-codes/invite-issuance-service.html +102 -196
  28. package/init-project-html/sample-demo/resources/project-architecture/features/get-invite-codes/postgresql.html +82 -163
  29. package/init-project-html/sample-demo/resources/project-architecture/features/get-invite-codes/public-api.html +88 -150
  30. package/init-project-html/sample-demo/resources/project-architecture/features/get-invite-codes/web-get-invite-ui.html +83 -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 +84 -159
  33. package/init-project-html/sample-demo/resources/project-architecture/features/invite-code-registration/public-api.html +81 -143
  34. package/init-project-html/sample-demo/resources/project-architecture/features/invite-code-registration/registration-service.html +98 -188
  35. package/init-project-html/sample-demo/resources/project-architecture/features/invite-code-registration/web-register-ui.html +83 -138
  36. package/init-project-html/sample-demo/resources/project-architecture/index.html +256 -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 +74 -67
  45. package/spec-to-project-html/agents/openai.yaml +14 -8
  46. package/spec-to-project-html/references/TEMPLATE_SPEC.md +98 -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,155 @@
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
21
+
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 dataflow step (string OR object — see below) | 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
+ Each step is either a plain string OR an object with one required and three optional fields. The renderer arranges them top-to-bottom inside a pan/zoom viewport; `--fn` becomes a function pill at the top of the step box, `--reads` becomes a green chip on the bottom-left, `--writes` becomes an orange chip on the bottom-right.
94
+
95
+ | Field | Type | Required | Notes |
96
+ | ------- | ---- | -------- | ----- |
97
+ | step | string | yes | The action / observation in one short sentence. |
98
+ | fn | string | no | Name of a `function` already declared in the SAME sub-module. `validate` fails otherwise. Use it to surface function-to-function transitions inside the sub-module. |
99
+ | reads | array of variable names | no | Each name must be a `variable` declared in the SAME sub-module. Shows up as `← reads: …` chip. |
100
+ | writes | array of variable names | no | Same constraint as `reads`. Shows up as `→ writes: …` chip. |
101
+
102
+ 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`. `--reads` / `--writes` accept comma-separated lists (`--reads "a, b"`).
103
+
104
+ CLI:
56
105
 
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
106
  ```
107
+ apltk architecture dataflow add --feature X --submodule Y --step "..." [--fn <declared-fn>] [--reads "v1,v2"] [--writes "v3,v4"]
108
+ ```
109
+
110
+ ### `error` row
111
+
112
+ | Field | Type | Required | Notes |
113
+ | ----- | ---- | -------- | ----- |
114
+ | name | string | yes | Symbolic name (e.g. `ErrInvalidCode`). |
115
+ | when | string | no | Condition that raises this error. |
116
+ | means | string | no | Observable outcome (HTTP status, user feedback). |
117
+
118
+ CLI: `apltk architecture error add --feature X --submodule Y --name ErrCode --when "..." --means "..."`
119
+
120
+ ### `edge`
121
+
122
+ | Field | Type | Required | Notes |
123
+ | ----- | ---- | -------- | ----- |
124
+ | id | kebab-case | no (auto if omitted) | Stable so cross-references survive renames. |
125
+ | from | `feature/submodule` (cross-feature) or `submodule` (intra-feature, when `--from` and `--to` share a feature) | yes | |
126
+ | to | same shape | yes | |
127
+ | kind | enum `call` `return` `data-row` `failure` | yes | Drives stroke/dash/colour and arrow head. |
128
+ | label | string | no | Rendered at the middle of the edge path. |
129
+
130
+ CLI: `apltk architecture edge add --from <feature>[/sub] --to <feature>[/sub] --kind call --label "..."`
131
+
132
+ ## Class hooks on rendered HTML
78
133
 
79
- Scope chip vocabulary: `sub-vars__scope--call` (single call), `--tx` (transaction-bound), `--persist` (persisted), `--instance` (fixed at construction; lifetime-shared), `--loop` (retry/loop).
134
+ 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.
80
135
 
81
- ### `sub-dataflow` small SVG sizing
136
+ | Hook | Page |
137
+ | --- | --- |
138
+ | `.atlas-header`, `.atlas-summary`, `.atlas-canvas`, `.atlas-submodule-index`, `.atlas-legend` | macro |
139
+ | `.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 |
140
+ | `.feature-header`, `.feature-story`, `.submodule-nav`, `.submodule-card`, `.feature-edges` | feature page |
141
+ | `.submodule-header`, `.submodule-role`, `.sub-io`, `.sub-vars`, `.sub-dataflow`, `.sub-dataflow__canvas`, `.sub-dataflow__toolbar`, `.sub-dataflow__viewport`, `.sub-dataflow__step`, `.sub-dataflow__badge`, `.sub-dataflow__fn-text`, `.sub-dataflow__chip--reads`, `.sub-dataflow__chip--writes`, `.sub-errors`, `.submodule-kind--<kind>` | sub-module page |
82
142
 
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.
143
+ ## Pan/zoom
86
144
 
87
- ## Edge-kind vocabulary (for macro `flow-edge-manifest`)
145
+ The CLI copies `assets/viewer.client.js` into the atlas. Two viewports exist:
88
146
 
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 |
147
+ - macro `index.html` — one `[data-pan-zoom-viewport]` wrapping the atlas SVG.
148
+ - each sub-module page — one `[data-pan-zoom-viewport]` wrapping the sub-dataflow SVG (when the sub-module has any `dataflow` step).
95
149
 
96
- ## Typography hint
150
+ For every viewport the script wires:
97
151
 
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.
152
+ - mouse wheel zoom around the cursor,
153
+ - click + drag to pan,
154
+ - `+` / `−` / `Fit` buttons on the toolbar (scoped to the surrounding `[data-pan-zoom-container]`),
155
+ - keyboard `←` `→` `↑` `↓` (pan), `+` `=` (zoom in), `−` `_` (zoom out), `0` (reset) — applied to the page's primary viewport.