@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.
- package/CHANGELOG.md +37 -0
- package/analyse-app-logs/scripts/__pycache__/filter_logs_by_time.cpython-312.pyc +0 -0
- package/analyse-app-logs/scripts/__pycache__/log_cli_utils.cpython-312.pyc +0 -0
- package/analyse-app-logs/scripts/__pycache__/search_logs.cpython-312.pyc +0 -0
- package/docs-to-voice/scripts/__pycache__/docs_to_voice.cpython-312.pyc +0 -0
- package/generate-spec/SKILL.md +17 -15
- package/generate-spec/agents/openai.yaml +1 -1
- package/generate-spec/references/TEMPLATE_SPEC.md +103 -84
- package/generate-spec/scripts/__pycache__/create-specscpython-312.pyc +0 -0
- package/init-project-html/SKILL.md +117 -125
- package/init-project-html/agents/openai.yaml +18 -9
- package/init-project-html/lib/atlas/assets/architecture.css +161 -0
- package/init-project-html/lib/atlas/assets/viewer.client.js +136 -0
- package/init-project-html/lib/atlas/cli.js +1023 -0
- package/init-project-html/lib/atlas/layout.js +330 -0
- package/init-project-html/lib/atlas/render.js +583 -0
- package/init-project-html/lib/atlas/schema.js +347 -0
- package/init-project-html/lib/atlas/state.js +402 -0
- package/init-project-html/references/TEMPLATE_SPEC.md +140 -83
- package/init-project-html/sample-demo/resources/project-architecture/assets/architecture.css +160 -1058
- package/init-project-html/sample-demo/resources/project-architecture/assets/viewer.client.js +136 -0
- package/init-project-html/sample-demo/resources/project-architecture/atlas/atlas.index.yaml +34 -0
- package/init-project-html/sample-demo/resources/project-architecture/atlas/features/get-invite-codes.yaml +172 -0
- package/init-project-html/sample-demo/resources/project-architecture/atlas/features/invite-code-registration.yaml +160 -0
- package/init-project-html/sample-demo/resources/project-architecture/features/get-invite-codes/index.html +67 -52
- package/init-project-html/sample-demo/resources/project-architecture/features/get-invite-codes/invite-code-generator.html +64 -163
- package/init-project-html/sample-demo/resources/project-architecture/features/get-invite-codes/invite-issuance-service.html +102 -196
- package/init-project-html/sample-demo/resources/project-architecture/features/get-invite-codes/postgresql.html +82 -163
- package/init-project-html/sample-demo/resources/project-architecture/features/get-invite-codes/public-api.html +88 -150
- package/init-project-html/sample-demo/resources/project-architecture/features/get-invite-codes/web-get-invite-ui.html +83 -138
- package/init-project-html/sample-demo/resources/project-architecture/features/invite-code-registration/index.html +61 -51
- package/init-project-html/sample-demo/resources/project-architecture/features/invite-code-registration/postgresql.html +84 -159
- package/init-project-html/sample-demo/resources/project-architecture/features/invite-code-registration/public-api.html +81 -143
- package/init-project-html/sample-demo/resources/project-architecture/features/invite-code-registration/registration-service.html +98 -188
- package/init-project-html/sample-demo/resources/project-architecture/features/invite-code-registration/web-register-ui.html +83 -138
- package/init-project-html/sample-demo/resources/project-architecture/index.html +256 -335
- package/init-project-html/scripts/architecture.js +65 -247
- package/katex/scripts/__pycache__/render_katex.cpython-312.pyc +0 -0
- package/open-github-issue/scripts/__pycache__/open_github_issue.cpython-312.pyc +0 -0
- package/package.json +6 -2
- package/read-github-issue/scripts/__pycache__/find_issues.cpython-312.pyc +0 -0
- package/read-github-issue/scripts/__pycache__/read_issue.cpython-312.pyc +0 -0
- package/resolve-review-comments/scripts/__pycache__/review_threads.cpython-312.pyc +0 -0
- package/spec-to-project-html/SKILL.md +74 -67
- package/spec-to-project-html/agents/openai.yaml +14 -8
- package/spec-to-project-html/references/TEMPLATE_SPEC.md +98 -83
- 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
|
-
#
|
|
1
|
+
# Atlas component schema — reference cheat sheet
|
|
2
2
|
|
|
3
|
-
> Reference material only. The
|
|
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
|
-
##
|
|
5
|
+
## State files on disk
|
|
6
6
|
|
|
7
|
-
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
##
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
|
28
|
-
|
|
|
29
|
-
|
|
|
30
|
-
|
|
|
31
|
-
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
### `
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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 & 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
84
|
-
- Recommended viewBox: height ≤ 240, width ≤ 720.
|
|
85
|
-
- Nodes are this sub-module's internal variables/functions only.
|
|
143
|
+
## Pan/zoom
|
|
86
144
|
|
|
87
|
-
|
|
145
|
+
The CLI copies `assets/viewer.client.js` into the atlas. Two viewports exist:
|
|
88
146
|
|
|
89
|
-
|
|
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
|
-
|
|
150
|
+
For every viewport the script wires:
|
|
97
151
|
|
|
98
|
-
|
|
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.
|