@laitszkin/apollo-toolkit 3.9.7 → 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 (55) hide show
  1. package/AGENTS.md +2 -0
  2. package/CHANGELOG.md +37 -0
  3. package/README.md +6 -0
  4. package/analyse-app-logs/scripts/__pycache__/filter_logs_by_time.cpython-312.pyc +0 -0
  5. package/analyse-app-logs/scripts/__pycache__/log_cli_utils.cpython-312.pyc +0 -0
  6. package/analyse-app-logs/scripts/__pycache__/search_logs.cpython-312.pyc +0 -0
  7. package/cjk-pdf/agents/openai.yaml +5 -0
  8. package/docs-to-voice/scripts/__pycache__/docs_to_voice.cpython-312.pyc +0 -0
  9. package/generate-spec/SKILL.md +26 -4
  10. package/generate-spec/agents/openai.yaml +1 -0
  11. package/generate-spec/references/TEMPLATE_SPEC.md +117 -0
  12. package/generate-spec/scripts/__pycache__/create-specscpython-312.pyc +0 -0
  13. package/init-project-html/SKILL.md +137 -0
  14. package/init-project-html/agents/openai.yaml +22 -0
  15. package/init-project-html/lib/atlas/assets/architecture.css +140 -0
  16. package/init-project-html/lib/atlas/assets/viewer.client.js +93 -0
  17. package/init-project-html/lib/atlas/cli.js +995 -0
  18. package/init-project-html/lib/atlas/layout.js +229 -0
  19. package/init-project-html/lib/atlas/render.js +485 -0
  20. package/init-project-html/lib/atlas/schema.js +310 -0
  21. package/init-project-html/lib/atlas/state.js +402 -0
  22. package/init-project-html/references/TEMPLATE_SPEC.md +137 -0
  23. package/init-project-html/references/architecture-page.template.html +35 -0
  24. package/init-project-html/references/architecture.css +1059 -0
  25. package/init-project-html/sample-demo/resources/project-architecture/assets/architecture.css +140 -0
  26. package/init-project-html/sample-demo/resources/project-architecture/assets/viewer.client.js +93 -0
  27. package/init-project-html/sample-demo/resources/project-architecture/atlas/atlas.index.yaml +34 -0
  28. package/init-project-html/sample-demo/resources/project-architecture/atlas/features/get-invite-codes.yaml +159 -0
  29. package/init-project-html/sample-demo/resources/project-architecture/atlas/features/invite-code-registration.yaml +160 -0
  30. package/init-project-html/sample-demo/resources/project-architecture/features/get-invite-codes/index.html +69 -0
  31. package/init-project-html/sample-demo/resources/project-architecture/features/get-invite-codes/invite-code-generator.html +50 -0
  32. package/init-project-html/sample-demo/resources/project-architecture/features/get-invite-codes/invite-issuance-service.html +72 -0
  33. package/init-project-html/sample-demo/resources/project-architecture/features/get-invite-codes/postgresql.html +66 -0
  34. package/init-project-html/sample-demo/resources/project-architecture/features/get-invite-codes/public-api.html +70 -0
  35. package/init-project-html/sample-demo/resources/project-architecture/features/get-invite-codes/web-get-invite-ui.html +67 -0
  36. package/init-project-html/sample-demo/resources/project-architecture/features/invite-code-registration/index.html +63 -0
  37. package/init-project-html/sample-demo/resources/project-architecture/features/invite-code-registration/postgresql.html +68 -0
  38. package/init-project-html/sample-demo/resources/project-architecture/features/invite-code-registration/public-api.html +65 -0
  39. package/init-project-html/sample-demo/resources/project-architecture/features/invite-code-registration/registration-service.html +79 -0
  40. package/init-project-html/sample-demo/resources/project-architecture/features/invite-code-registration/web-register-ui.html +67 -0
  41. package/init-project-html/sample-demo/resources/project-architecture/index.html +234 -0
  42. package/init-project-html/scripts/architecture.js +314 -0
  43. package/katex/scripts/__pycache__/render_katex.cpython-312.pyc +0 -0
  44. package/lib/cli.js +2 -0
  45. package/lib/tool-runner.js +7 -0
  46. package/merge-conflict-resolver/agents/openai.yaml +5 -0
  47. package/open-github-issue/scripts/__pycache__/open_github_issue.cpython-312.pyc +0 -0
  48. package/package.json +6 -2
  49. package/read-github-issue/scripts/__pycache__/find_issues.cpython-312.pyc +0 -0
  50. package/read-github-issue/scripts/__pycache__/read_issue.cpython-312.pyc +0 -0
  51. package/resolve-review-comments/scripts/__pycache__/review_threads.cpython-312.pyc +0 -0
  52. package/spec-to-project-html/SKILL.md +114 -0
  53. package/spec-to-project-html/agents/openai.yaml +18 -0
  54. package/spec-to-project-html/references/TEMPLATE_SPEC.md +111 -0
  55. 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
+ };
@@ -0,0 +1,137 @@
1
+ # Atlas component schema — reference cheat sheet
2
+
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
+
5
+ ## State files on disk
6
+
7
+ ```
8
+ resources/project-architecture/
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)
18
+ ```
19
+
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 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. |
116
+
117
+ CLI: `apltk architecture edge add --from <feature>[/sub] --to <feature>[/sub] --kind call --label "..."`
118
+
119
+ ## Class hooks on rendered HTML
120
+
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.
122
+
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 |
129
+
130
+ ## Pan/zoom on the macro
131
+
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:
133
+
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).
@@ -0,0 +1,35 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <meta name="generator" content="apollo-toolkit:init-project-html|spec-to-project-html" />
7
+ <!-- ARCHITECTURE_ROOT: default resources/project-architecture -->
8
+ <title>{{PAGE_TITLE}}</title>
9
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
10
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
11
+ <link
12
+ href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,500;600&family=Plus+Jakarta+Sans:wght@400;500;600&display=swap"
13
+ rel="stylesheet"
14
+ />
15
+ <link rel="stylesheet" href="{{CSS_PATH}}" />
16
+ </head>
17
+ <body>
18
+ <main class="atlas-page atlas-page--{{PAGE_KIND}}">
19
+ <header class="atlas-header">
20
+ <p class="atlas-kicker">Architecture atlas</p>
21
+ <h1 class="atlas-title">{{HEADLINE}}</h1>
22
+ <p class="atlas-meta">
23
+ Updated <time datetime="{{UPDATED_ISO}}">{{UPDATED_DISPLAY}}</time>
24
+ · {{VERSION_OR_REF}}
25
+ </p>
26
+ </header>
27
+
28
+ {{BODY_SECTIONS}}
29
+
30
+ <footer class="atlas-meta" style="margin-top: 2rem">
31
+ <p>{{FOOTER_NOTE}}</p>
32
+ </footer>
33
+ </main>
34
+ </body>
35
+ </html>