@laitszkin/apollo-toolkit 3.11.4 → 3.11.6
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/AGENTS.md +1 -0
- package/CHANGELOG.md +34 -0
- package/README.md +5 -1
- 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/commit-and-push/README.md +0 -2
- package/commit-and-push/SKILL.md +5 -5
- package/commit-and-push/agents/openai.yaml +1 -1
- package/docs-to-voice/scripts/__pycache__/docs_to_voice.cpython-312.pyc +0 -0
- package/generate-spec/SKILL.md +17 -13
- package/generate-spec/agents/openai.yaml +3 -3
- package/generate-spec/scripts/__pycache__/create-specscpython-312.pyc +0 -0
- package/init-project-html/lib/atlas/cli.js +208 -91
- package/init-project-html/lib/atlas/render.js +29 -0
- package/init-project-html/lib/atlas/state.js +89 -7
- package/init-project-html/scripts/architecture.js +10 -17
- 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 +1 -1
- 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/review-change-set/README.md +3 -3
- package/review-change-set/SKILL.md +36 -24
- package/review-change-set/agents/openai.yaml +2 -2
- package/review-spec-related-changes/README.md +3 -2
- package/review-spec-related-changes/SKILL.md +12 -7
- package/review-spec-related-changes/agents/openai.yaml +1 -1
- package/spec-to-project-html/SKILL.md +4 -4
- package/spec-to-project-html/agents/openai.yaml +1 -1
- package/text-to-short-video/scripts/__pycache__/enforce_video_aspect_ratio.cpython-312.pyc +0 -0
- package/update-project-html/README.md +38 -0
- package/update-project-html/SKILL.md +125 -0
- package/update-project-html/agents/openai.yaml +11 -0
- package/version-release/README.md +0 -2
- package/version-release/SKILL.md +3 -3
- package/version-release/agents/openai.yaml +1 -1
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
//
|
|
24
24
|
// Global flags:
|
|
25
25
|
// --project <root> project root; creates resources/project-architecture/ if missing
|
|
26
|
-
// --spec <spec_dir>
|
|
26
|
+
// --spec <spec_dir> single specs write to <spec_dir>/architecture_diff/atlas/; batch member paths resolve to the coordination.md root
|
|
27
27
|
// --no-render skip auto-render after a mutation
|
|
28
28
|
// --no-open for open/diff: skip launching the browser
|
|
29
29
|
// --out <dir> for diff: override viewer output directory
|
|
@@ -41,6 +41,7 @@ const ATLAS_INDEX_REL = path.join(ATLAS_REL, 'index.html');
|
|
|
41
41
|
const ATLAS_DIRNAME = stateLib.ATLAS_DIRNAME;
|
|
42
42
|
const DIFF_DIRNAME = 'architecture_diff';
|
|
43
43
|
const PLANS_REL = path.join('docs', 'plans');
|
|
44
|
+
const COORDINATION_FILE = 'coordination.md';
|
|
44
45
|
const REMOVED_TXT = '_removed.txt';
|
|
45
46
|
const DEFAULT_DIFF_OUT_REL = path.join('.apollo-toolkit', 'architecture-diff');
|
|
46
47
|
|
|
@@ -63,12 +64,12 @@ Verbs:
|
|
|
63
64
|
meta set edit meta.title / meta.summary
|
|
64
65
|
actor add|remove manage top-level actors
|
|
65
66
|
validate run schema + referential checks
|
|
66
|
-
undo revert the most recent mutation
|
|
67
|
+
undo revert the most recent mutation (use --steps <n> for multi-step rollback)
|
|
67
68
|
help show this help
|
|
68
69
|
|
|
69
70
|
Global flags:
|
|
70
71
|
--project <root> explicit project root (default: nearest ancestor with atlas markers, else cwd); missing directories under resources/project-architecture/ are created automatically
|
|
71
|
-
--spec <spec_dir>
|
|
72
|
+
--spec <spec_dir> single specs write to <spec_dir>/architecture_diff/atlas/; batch member paths write to the coordination.md root
|
|
72
73
|
--no-render skip auto-render after a mutation
|
|
73
74
|
--no-open for open/diff: skip launching the browser
|
|
74
75
|
--out <dir> for diff: override viewer output directory
|
|
@@ -81,6 +82,7 @@ Examples:
|
|
|
81
82
|
apltk architecture dataflow add --feature register --submodule api --step "Validate body" --fn handlePost --reads "body" --writes "token"
|
|
82
83
|
apltk architecture --spec docs/plans/2026-05-11/add-2fa submodule set --feature register --slug api --role "..."
|
|
83
84
|
apltk architecture validate
|
|
85
|
+
apltk architecture undo --steps 3 --spec docs/plans/2026-05-11/add-2fa
|
|
84
86
|
apltk architecture diff
|
|
85
87
|
`;
|
|
86
88
|
|
|
@@ -171,7 +173,15 @@ function resolveProjectRoot(flags) {
|
|
|
171
173
|
|
|
172
174
|
function specOverlayDir(projectRoot, specFlag) {
|
|
173
175
|
const specDir = path.isAbsolute(String(specFlag)) ? String(specFlag) : path.resolve(projectRoot, String(specFlag));
|
|
174
|
-
|
|
176
|
+
const plansRoot = path.join(projectRoot, PLANS_REL);
|
|
177
|
+
const batchRoot = fs.existsSync(path.join(specDir, COORDINATION_FILE)) ? specDir : findBatchRoot(specDir, plansRoot);
|
|
178
|
+
const rootDir = batchRoot || specDir;
|
|
179
|
+
return {
|
|
180
|
+
specDir,
|
|
181
|
+
rootDir,
|
|
182
|
+
overlayDir: path.join(rootDir, DIFF_DIRNAME, ATLAS_DIRNAME),
|
|
183
|
+
htmlOutDir: path.join(rootDir, DIFF_DIRNAME),
|
|
184
|
+
};
|
|
175
185
|
}
|
|
176
186
|
|
|
177
187
|
function baseAtlasDir(projectRoot) {
|
|
@@ -207,26 +217,21 @@ function ensureBaseAtlasDir(projectRoot) {
|
|
|
207
217
|
async function performMutation(projectRoot, flags, action, args, mutate) {
|
|
208
218
|
const isSpec = Boolean(flags.spec);
|
|
209
219
|
const base = stateLib.load(baseAtlasDir(projectRoot));
|
|
210
|
-
let overlay = null;
|
|
211
220
|
let merged = base;
|
|
212
|
-
let touchedFeatureSlugs = new Set();
|
|
213
221
|
|
|
214
222
|
if (isSpec) {
|
|
215
223
|
const { overlayDir } = specOverlayDir(projectRoot, flags.spec);
|
|
216
|
-
overlay = stateLib.loadOverlay(overlayDir);
|
|
224
|
+
const overlay = stateLib.loadOverlay(overlayDir);
|
|
217
225
|
merged = stateLib.mergeOverlay(base, overlay);
|
|
218
226
|
const before = JSON.parse(JSON.stringify({ base, overlay }));
|
|
219
|
-
|
|
220
|
-
if (result.touchedFeatures) for (const slug of result.touchedFeatures) touchedFeatureSlugs.add(slug);
|
|
227
|
+
mutate(merged, base, overlay);
|
|
221
228
|
stateLib.writeUndoSnapshot(overlayDir, before);
|
|
222
|
-
|
|
223
|
-
stateLib.saveOverlay(overlayDir, overlay);
|
|
229
|
+
stateLib.saveOverlay(overlayDir, stateLib.deriveOverlay(base, merged));
|
|
224
230
|
stateLib.appendHistory(overlayDir, { action, args, mode: 'spec' });
|
|
225
231
|
} else {
|
|
226
232
|
ensureBaseAtlasDir(projectRoot);
|
|
227
233
|
const before = JSON.parse(JSON.stringify({ base }));
|
|
228
|
-
|
|
229
|
-
if (result.touchedFeatures) for (const slug of result.touchedFeatures) touchedFeatureSlugs.add(slug);
|
|
234
|
+
mutate(base, base, null);
|
|
230
235
|
stateLib.writeUndoSnapshot(baseAtlasDir(projectRoot), before);
|
|
231
236
|
stateLib.save(baseAtlasDir(projectRoot), base);
|
|
232
237
|
stateLib.appendHistory(baseAtlasDir(projectRoot), { action, args, mode: 'base' });
|
|
@@ -237,47 +242,6 @@ async function performMutation(projectRoot, flags, action, args, mutate) {
|
|
|
237
242
|
}
|
|
238
243
|
}
|
|
239
244
|
|
|
240
|
-
function syncOverlayFromMerged({ base, overlay, merged, touchedFeatureSlugs, removalsHint }) {
|
|
241
|
-
// Compare merged top-level fields against base; if they differ, sync into overlay.
|
|
242
|
-
if (JSON.stringify(merged.meta || {}) !== JSON.stringify(base.meta || {})) overlay.meta = merged.meta;
|
|
243
|
-
if (JSON.stringify(merged.actors || []) !== JSON.stringify(base.actors || [])) overlay.actors = merged.actors || [];
|
|
244
|
-
if (JSON.stringify(merged.edges || []) !== JSON.stringify(base.edges || [])) overlay.edges = merged.edges || [];
|
|
245
|
-
|
|
246
|
-
const baseOrder = (base.features || []).map((f) => f.slug);
|
|
247
|
-
const mergedOrder = (merged.features || []).map((f) => f.slug);
|
|
248
|
-
if (JSON.stringify(baseOrder) !== JSON.stringify(mergedOrder)) {
|
|
249
|
-
overlay.featureOrder = mergedOrder;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
// Persist touched (or simply differing) features into overlay.features
|
|
253
|
-
const baseFeatureMap = new Map((base.features || []).map((f) => [f.slug, f]));
|
|
254
|
-
const mergedFeatureMap = new Map((merged.features || []).map((f) => [f.slug, f]));
|
|
255
|
-
for (const slug of touchedFeatureSlugs) {
|
|
256
|
-
if (!mergedFeatureMap.has(slug)) continue;
|
|
257
|
-
const baseFeat = baseFeatureMap.get(slug);
|
|
258
|
-
const mergedFeat = mergedFeatureMap.get(slug);
|
|
259
|
-
if (!baseFeat || JSON.stringify(baseFeat) !== JSON.stringify(mergedFeat)) {
|
|
260
|
-
overlay.features[slug] = mergedFeat;
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
// Apply hinted removals
|
|
264
|
-
if (removalsHint) {
|
|
265
|
-
if (Array.isArray(removalsHint.features)) {
|
|
266
|
-
const seen = new Set(overlay.removed.features || []);
|
|
267
|
-
for (const slug of removalsHint.features) seen.add(slug);
|
|
268
|
-
overlay.removed.features = [...seen];
|
|
269
|
-
// also drop from overlay.features if present
|
|
270
|
-
for (const slug of removalsHint.features) delete overlay.features[slug];
|
|
271
|
-
}
|
|
272
|
-
if (Array.isArray(removalsHint.submodules)) {
|
|
273
|
-
const seen = new Map();
|
|
274
|
-
for (const item of overlay.removed.submodules || []) seen.set(`${item.feature}::${item.submodule}`, item);
|
|
275
|
-
for (const item of removalsHint.submodules) seen.set(`${item.feature}::${item.submodule}`, item);
|
|
276
|
-
overlay.removed.submodules = [...seen.values()];
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
|
|
281
245
|
async function runRender({ projectRoot, flags }) {
|
|
282
246
|
if (flags.spec) {
|
|
283
247
|
const { overlayDir, htmlOutDir } = specOverlayDir(projectRoot, flags.spec);
|
|
@@ -642,9 +606,14 @@ async function verbValidate(flags, projectRoot, io) {
|
|
|
642
606
|
|
|
643
607
|
async function verbUndo(flags, projectRoot, io) {
|
|
644
608
|
const dir = flags.spec ? specOverlayDir(projectRoot, flags.spec).overlayDir : baseAtlasDir(projectRoot);
|
|
645
|
-
const
|
|
609
|
+
const stepsRaw = flags.steps === undefined ? 1 : Number(flags.steps);
|
|
610
|
+
if (!Number.isInteger(stepsRaw) || stepsRaw < 1) {
|
|
611
|
+
io.stderr.write('--steps must be a positive integer.\n');
|
|
612
|
+
return 1;
|
|
613
|
+
}
|
|
614
|
+
const snapshot = stateLib.consumeUndoSnapshot(dir, stepsRaw);
|
|
646
615
|
if (!snapshot) {
|
|
647
|
-
io.stderr.write('No undo snapshot found.\n');
|
|
616
|
+
io.stderr.write(stepsRaw === 1 ? 'No undo snapshot found.\n' : `Unable to undo ${stepsRaw} steps; history is shorter.\n`);
|
|
648
617
|
return 1;
|
|
649
618
|
}
|
|
650
619
|
if (flags.spec) {
|
|
@@ -655,9 +624,8 @@ async function verbUndo(flags, projectRoot, io) {
|
|
|
655
624
|
stateLib.save(baseAtlasDir(projectRoot), snapshot.base);
|
|
656
625
|
stateLib.appendHistory(baseAtlasDir(projectRoot), { action: 'undo', mode: 'base' });
|
|
657
626
|
}
|
|
658
|
-
stateLib.clearUndoSnapshot(dir);
|
|
659
627
|
if (!flags['no-render']) await runRender({ projectRoot, flags });
|
|
660
|
-
io.stdout.write(
|
|
628
|
+
io.stdout.write(`atlas: undo applied (${stepsRaw} step${stepsRaw === 1 ? '' : 's'})\n`);
|
|
661
629
|
return 0;
|
|
662
630
|
}
|
|
663
631
|
|
|
@@ -678,36 +646,27 @@ async function verbOpen(flags, projectRoot, io) {
|
|
|
678
646
|
async function verbDiff(flags, projectRoot, io) {
|
|
679
647
|
const outDir = flags.out ? path.resolve(String(flags.out)) : path.join(projectRoot, DEFAULT_DIFF_OUT_REL);
|
|
680
648
|
fs.mkdirSync(outDir, { recursive: true });
|
|
649
|
+
const changes = await collectDiffChanges({ projectRoot, outDir });
|
|
681
650
|
|
|
651
|
+
const html = renderDiffViewer({ changes, projectRoot, outDir });
|
|
652
|
+
const indexPath = path.join(outDir, 'index.html');
|
|
653
|
+
fs.writeFileSync(indexPath, html, 'utf8');
|
|
654
|
+
io.stdout.write(`${indexPath}\n`);
|
|
655
|
+
io.stdout.write(`Diff pages: ${changes.length} (modified=${changes.filter((c) => c.kind === 'modified').length}, added=${changes.filter((c) => c.kind === 'added').length}, removed=${changes.filter((c) => c.kind === 'removed').length})\n`);
|
|
656
|
+
if (!flags['no-open']) openInBrowser(indexPath);
|
|
657
|
+
return 0;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
async function collectDiffChanges({ projectRoot, outDir }) {
|
|
682
661
|
const plansRoot = path.join(projectRoot, PLANS_REL);
|
|
683
|
-
const
|
|
684
|
-
const resourcesRoot = path.join(projectRoot, ATLAS_REL);
|
|
662
|
+
const groups = groupDiffDirsByBatch({ projectRoot, plansRoot });
|
|
685
663
|
const changes = [];
|
|
686
664
|
|
|
687
|
-
for (const
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
const beforeExists = fs.existsSync(beforeAbs);
|
|
693
|
-
changes.push({
|
|
694
|
-
kind: beforeExists ? 'modified' : 'added',
|
|
695
|
-
rel: after.rel,
|
|
696
|
-
spec: specLabel,
|
|
697
|
-
beforePath: beforeExists ? path.relative(projectRoot, beforeAbs) : null,
|
|
698
|
-
afterPath: path.relative(projectRoot, after.abs),
|
|
699
|
-
});
|
|
700
|
-
}
|
|
701
|
-
for (const removedRel of readRemovedManifest(diffDir)) {
|
|
702
|
-
const beforeAbs = path.join(resourcesRoot, removedRel);
|
|
703
|
-
if (!fs.existsSync(beforeAbs)) continue;
|
|
704
|
-
changes.push({
|
|
705
|
-
kind: 'removed',
|
|
706
|
-
rel: removedRel,
|
|
707
|
-
spec: specLabel,
|
|
708
|
-
beforePath: path.relative(projectRoot, beforeAbs),
|
|
709
|
-
afterPath: null,
|
|
710
|
-
});
|
|
665
|
+
for (const group of groups) {
|
|
666
|
+
if (group.kind === 'batch') {
|
|
667
|
+
changes.push(...await collectBatchGroupChanges({ projectRoot, outDir, group }));
|
|
668
|
+
} else {
|
|
669
|
+
changes.push(...collectSingleSpecChanges({ projectRoot, specDir: group.specDir, specLabel: group.label }));
|
|
711
670
|
}
|
|
712
671
|
}
|
|
713
672
|
|
|
@@ -716,14 +675,171 @@ async function verbDiff(flags, projectRoot, io) {
|
|
|
716
675
|
if (a.kind !== b.kind) return a.kind.localeCompare(b.kind);
|
|
717
676
|
return a.rel.localeCompare(b.rel);
|
|
718
677
|
});
|
|
678
|
+
return changes;
|
|
679
|
+
}
|
|
719
680
|
|
|
720
|
-
|
|
721
|
-
const
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
681
|
+
function groupDiffDirsByBatch({ projectRoot, plansRoot }) {
|
|
682
|
+
const groups = new Map();
|
|
683
|
+
for (const diffDir of walkArchitectureDiffDirs(plansRoot)) {
|
|
684
|
+
const specDir = path.dirname(diffDir);
|
|
685
|
+
const batchRoot = findBatchRoot(specDir, plansRoot);
|
|
686
|
+
const isBatchMember = Boolean(batchRoot && batchRoot !== specDir);
|
|
687
|
+
const key = isBatchMember ? batchRoot : specDir;
|
|
688
|
+
if (!groups.has(key)) {
|
|
689
|
+
groups.set(key, {
|
|
690
|
+
kind: isBatchMember ? 'batch' : 'single',
|
|
691
|
+
key,
|
|
692
|
+
label: path.relative(projectRoot, key),
|
|
693
|
+
specDir: isBatchMember ? null : specDir,
|
|
694
|
+
members: [],
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
groups.get(key).members.push({ specDir, diffDir, label: path.relative(projectRoot, specDir) });
|
|
698
|
+
}
|
|
699
|
+
return [...groups.values()]
|
|
700
|
+
.map((group) => ({ ...group, members: group.members.sort((a, b) => a.specDir.localeCompare(b.specDir)) }))
|
|
701
|
+
.sort((a, b) => a.key.localeCompare(b.key));
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
function findBatchRoot(specDir, plansRoot) {
|
|
705
|
+
const absolutePlansRoot = path.resolve(plansRoot);
|
|
706
|
+
let current = path.resolve(path.dirname(specDir));
|
|
707
|
+
while (current.startsWith(`${absolutePlansRoot}${path.sep}`) || current === absolutePlansRoot) {
|
|
708
|
+
if (fs.existsSync(path.join(current, COORDINATION_FILE))) return current;
|
|
709
|
+
if (current === absolutePlansRoot) break;
|
|
710
|
+
current = path.dirname(current);
|
|
711
|
+
}
|
|
712
|
+
return null;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
function collectSingleSpecChanges({ projectRoot, specDir, specLabel }) {
|
|
716
|
+
const overlayDir = path.join(specDir, DIFF_DIRNAME, ATLAS_DIRNAME);
|
|
717
|
+
if (!hasOverlayState(overlayDir)) {
|
|
718
|
+
return collectHtmlManifestChanges({ projectRoot, diffDir: path.join(specDir, DIFF_DIRNAME), specLabel });
|
|
719
|
+
}
|
|
720
|
+
const base = stateLib.load(baseAtlasDir(projectRoot));
|
|
721
|
+
const overlay = stateLib.loadOverlay(overlayDir);
|
|
722
|
+
const merged = stateLib.mergeOverlay(base, overlay);
|
|
723
|
+
const diff = stateLib.diffPages(base, merged);
|
|
724
|
+
return diffToChanges({
|
|
725
|
+
projectRoot,
|
|
726
|
+
specLabel,
|
|
727
|
+
htmlRoot: path.join(specDir, DIFF_DIRNAME),
|
|
728
|
+
diff,
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
function hasOverlayState(overlayDir) {
|
|
733
|
+
return fs.existsSync(path.join(overlayDir, stateLib.INDEX_FILE))
|
|
734
|
+
|| fs.existsSync(path.join(overlayDir, stateLib.FEATURES_DIR))
|
|
735
|
+
|| fs.existsSync(path.join(overlayDir, stateLib.REMOVED_FILE));
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
async function collectBatchGroupChanges({ projectRoot, outDir, group }) {
|
|
739
|
+
const batchRootOverlayDir = path.join(group.key, DIFF_DIRNAME, ATLAS_DIRNAME);
|
|
740
|
+
if (hasOverlayState(batchRootOverlayDir)) {
|
|
741
|
+
return collectSingleSpecChanges({ projectRoot, specDir: group.key, specLabel: group.label });
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
const memberOverlayDirs = group.members.map((member) => ({
|
|
745
|
+
...member,
|
|
746
|
+
overlayDir: path.join(member.specDir, DIFF_DIRNAME, ATLAS_DIRNAME),
|
|
747
|
+
}));
|
|
748
|
+
if (memberOverlayDirs.some((member) => !hasOverlayState(member.overlayDir))) {
|
|
749
|
+
return group.members.flatMap((member) => (
|
|
750
|
+
collectSingleSpecChanges({ projectRoot, specDir: member.specDir, specLabel: member.label })
|
|
751
|
+
));
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
const base = stateLib.load(baseAtlasDir(projectRoot));
|
|
755
|
+
let merged = JSON.parse(JSON.stringify(base));
|
|
756
|
+
for (const member of memberOverlayDirs) {
|
|
757
|
+
const overlay = stateLib.loadOverlay(member.overlayDir);
|
|
758
|
+
merged = stateLib.mergeOverlay(merged, overlay);
|
|
759
|
+
}
|
|
760
|
+
const diff = stateLib.diffPages(base, merged);
|
|
761
|
+
const htmlRoot = path.join(outDir, '_batch', group.label);
|
|
762
|
+
await renderLib.renderAll({
|
|
763
|
+
outDir: htmlRoot,
|
|
764
|
+
state: merged,
|
|
765
|
+
scope: renderLib.scopeFromDiff(diff),
|
|
766
|
+
removedPaths: renderLib.removedPagePathsFromDiff(diff),
|
|
767
|
+
});
|
|
768
|
+
return diffToChanges({
|
|
769
|
+
projectRoot,
|
|
770
|
+
specLabel: group.label,
|
|
771
|
+
htmlRoot,
|
|
772
|
+
diff,
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
function diffToChanges({ projectRoot, specLabel, htmlRoot, diff }) {
|
|
777
|
+
const resourcesRoot = path.join(projectRoot, ATLAS_REL);
|
|
778
|
+
const changes = [];
|
|
779
|
+
const add = (kind, rel) => {
|
|
780
|
+
const beforeAbs = path.join(resourcesRoot, rel);
|
|
781
|
+
const afterAbs = kind === 'removed' ? null : path.join(htmlRoot, rel);
|
|
782
|
+
if (kind === 'removed' && !fs.existsSync(beforeAbs)) return;
|
|
783
|
+
changes.push({
|
|
784
|
+
kind,
|
|
785
|
+
rel,
|
|
786
|
+
spec: specLabel,
|
|
787
|
+
beforePath: kind === 'added' ? null : path.relative(projectRoot, beforeAbs),
|
|
788
|
+
afterPath: afterAbs ? path.relative(projectRoot, afterAbs) : null,
|
|
789
|
+
});
|
|
790
|
+
};
|
|
791
|
+
|
|
792
|
+
if (diff.macroChanged) {
|
|
793
|
+
add('modified', renderLib.pagePathFor('macro'));
|
|
794
|
+
}
|
|
795
|
+
for (const slug of diff.modifiedFeatures || []) {
|
|
796
|
+
add('modified', renderLib.pagePathFor('feature', { featureSlug: slug }));
|
|
797
|
+
}
|
|
798
|
+
for (const slug of diff.addedFeatures || []) {
|
|
799
|
+
add('added', renderLib.pagePathFor('feature', { featureSlug: slug }));
|
|
800
|
+
}
|
|
801
|
+
for (const item of diff.modifiedSubmodules || []) {
|
|
802
|
+
add('modified', renderLib.pagePathFor('submodule', { featureSlug: item.feature, submoduleSlug: item.submodule }));
|
|
803
|
+
}
|
|
804
|
+
for (const item of diff.addedSubmodules || []) {
|
|
805
|
+
add('added', renderLib.pagePathFor('submodule', { featureSlug: item.feature, submoduleSlug: item.submodule }));
|
|
806
|
+
}
|
|
807
|
+
for (const slug of diff.removedFeatures || []) {
|
|
808
|
+
add('removed', renderLib.pagePathFor('feature', { featureSlug: slug }));
|
|
809
|
+
}
|
|
810
|
+
for (const item of diff.removedSubmodules || []) {
|
|
811
|
+
add('removed', renderLib.pagePathFor('submodule', { featureSlug: item.feature, submoduleSlug: item.submodule }));
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
return changes;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
function collectHtmlManifestChanges({ projectRoot, diffDir, specLabel }) {
|
|
818
|
+
const resourcesRoot = path.join(projectRoot, ATLAS_REL);
|
|
819
|
+
const changes = [];
|
|
820
|
+
for (const after of walkAfterStateHtml(diffDir)) {
|
|
821
|
+
const beforeAbs = path.join(resourcesRoot, after.rel);
|
|
822
|
+
const beforeExists = fs.existsSync(beforeAbs);
|
|
823
|
+
changes.push({
|
|
824
|
+
kind: beforeExists ? 'modified' : 'added',
|
|
825
|
+
rel: after.rel,
|
|
826
|
+
spec: specLabel,
|
|
827
|
+
beforePath: beforeExists ? path.relative(projectRoot, beforeAbs) : null,
|
|
828
|
+
afterPath: path.relative(projectRoot, after.abs),
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
for (const removedRel of readRemovedManifest(diffDir)) {
|
|
832
|
+
const beforeAbs = path.join(resourcesRoot, removedRel);
|
|
833
|
+
if (!fs.existsSync(beforeAbs)) continue;
|
|
834
|
+
changes.push({
|
|
835
|
+
kind: 'removed',
|
|
836
|
+
rel: removedRel,
|
|
837
|
+
spec: specLabel,
|
|
838
|
+
beforePath: path.relative(projectRoot, beforeAbs),
|
|
839
|
+
afterPath: null,
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
return changes;
|
|
727
843
|
}
|
|
728
844
|
|
|
729
845
|
function walkArchitectureDiffDirs(plansRoot) {
|
|
@@ -1021,6 +1137,7 @@ module.exports = {
|
|
|
1021
1137
|
specOverlayDir,
|
|
1022
1138
|
runRender,
|
|
1023
1139
|
walkArchitectureDiffDirs,
|
|
1140
|
+
collectDiffChanges,
|
|
1024
1141
|
walkAfterStateHtml,
|
|
1025
1142
|
readRemovedManifest,
|
|
1026
1143
|
renderDiffViewer,
|
|
@@ -560,11 +560,40 @@ async function renderAll({ outDir, state, scope = null, removedPaths = [] }) {
|
|
|
560
560
|
// do not linger with the previous (broken) markup or styling.
|
|
561
561
|
if (!scope) {
|
|
562
562
|
sweepOrphanFeaturePages(outDir, state);
|
|
563
|
+
} else {
|
|
564
|
+
sweepScopedHtml(outDir, new Set(written.map((file) => file.split(path.sep).join('/'))));
|
|
563
565
|
}
|
|
564
566
|
|
|
565
567
|
return { written, layout };
|
|
566
568
|
}
|
|
567
569
|
|
|
570
|
+
function sweepScopedHtml(outDir, keepPaths) {
|
|
571
|
+
function recurse(dir) {
|
|
572
|
+
let entries;
|
|
573
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (_e) { return; }
|
|
574
|
+
for (const entry of entries) {
|
|
575
|
+
if (entry.name === 'assets' || entry.name === 'atlas' || entry.name.startsWith('.')) continue;
|
|
576
|
+
const full = path.join(dir, entry.name);
|
|
577
|
+
if (entry.isDirectory()) {
|
|
578
|
+
recurse(full);
|
|
579
|
+
let remaining;
|
|
580
|
+
try { remaining = fs.readdirSync(full); } catch (_e) { remaining = null; }
|
|
581
|
+
if (remaining && remaining.length === 0) {
|
|
582
|
+
fs.rmSync(full, { recursive: true, force: true });
|
|
583
|
+
}
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
586
|
+
if (!entry.isFile() || !entry.name.toLowerCase().endsWith('.html')) continue;
|
|
587
|
+
const rel = path.relative(outDir, full).split(path.sep).join('/');
|
|
588
|
+
if (!keepPaths.has(rel)) {
|
|
589
|
+
fs.rmSync(full, { force: true });
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
recurse(outDir);
|
|
595
|
+
}
|
|
596
|
+
|
|
568
597
|
function sweepOrphanFeaturePages(outDir, state) {
|
|
569
598
|
const featuresRoot = path.join(outDir, 'features');
|
|
570
599
|
if (!fs.existsSync(featuresRoot)) return;
|
|
@@ -26,6 +26,7 @@ const REMOVED_FILE = '_removed.yaml';
|
|
|
26
26
|
const FEATURES_DIR = 'features';
|
|
27
27
|
const HISTORY_FILE = 'atlas.history.log';
|
|
28
28
|
const UNDO_FILE = 'atlas.history.undo.json';
|
|
29
|
+
const UNDO_STACK_FILE = 'atlas.history.undo.stack.json';
|
|
29
30
|
const ATLAS_DIRNAME = 'atlas';
|
|
30
31
|
|
|
31
32
|
function readYaml(file) {
|
|
@@ -257,6 +258,51 @@ function mergeOverlay(base, overlay) {
|
|
|
257
258
|
return merged;
|
|
258
259
|
}
|
|
259
260
|
|
|
261
|
+
function deriveOverlay(base, merged) {
|
|
262
|
+
const overlay = {
|
|
263
|
+
meta: null,
|
|
264
|
+
actors: null,
|
|
265
|
+
edges: null,
|
|
266
|
+
featureOrder: null,
|
|
267
|
+
features: {},
|
|
268
|
+
removed: { features: [], submodules: [] },
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
if (JSON.stringify(merged.meta || {}) !== JSON.stringify(base.meta || {})) {
|
|
272
|
+
overlay.meta = merged.meta || {};
|
|
273
|
+
}
|
|
274
|
+
if (JSON.stringify(merged.actors || []) !== JSON.stringify(base.actors || [])) {
|
|
275
|
+
overlay.actors = merged.actors || [];
|
|
276
|
+
}
|
|
277
|
+
if (JSON.stringify(merged.edges || []) !== JSON.stringify(base.edges || [])) {
|
|
278
|
+
overlay.edges = merged.edges || [];
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const baseOrder = (base.features || []).map((feature) => feature.slug);
|
|
282
|
+
const mergedOrder = (merged.features || []).map((feature) => feature.slug);
|
|
283
|
+
if (JSON.stringify(mergedOrder) !== JSON.stringify(baseOrder)) {
|
|
284
|
+
overlay.featureOrder = mergedOrder;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const baseFeatures = new Map((base.features || []).map((feature) => [feature.slug, feature]));
|
|
288
|
+
const mergedFeatures = new Map((merged.features || []).map((feature) => [feature.slug, feature]));
|
|
289
|
+
|
|
290
|
+
for (const [slug, feature] of mergedFeatures) {
|
|
291
|
+
const baseFeature = baseFeatures.get(slug);
|
|
292
|
+
if (!baseFeature || JSON.stringify(feature) !== JSON.stringify(baseFeature)) {
|
|
293
|
+
overlay.features[slug] = feature;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
for (const slug of baseFeatures.keys()) {
|
|
298
|
+
if (!mergedFeatures.has(slug)) {
|
|
299
|
+
overlay.removed.features.push(slug);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return overlay;
|
|
304
|
+
}
|
|
305
|
+
|
|
260
306
|
// diffPages compares the merged after-state against the base and
|
|
261
307
|
// classifies which HTML pages must be regenerated (modified) versus
|
|
262
308
|
// emitted fresh (added) versus listed in _removed.txt (removed).
|
|
@@ -361,19 +407,52 @@ function appendHistory(atlasDir, entry) {
|
|
|
361
407
|
}
|
|
362
408
|
|
|
363
409
|
function writeUndoSnapshot(atlasDir, state) {
|
|
364
|
-
|
|
365
|
-
|
|
410
|
+
const stack = readUndoStack(atlasDir);
|
|
411
|
+
stack.push(state);
|
|
412
|
+
writeUndoStack(atlasDir, stack);
|
|
366
413
|
}
|
|
367
414
|
|
|
368
415
|
function readUndoSnapshot(atlasDir) {
|
|
369
|
-
const
|
|
370
|
-
|
|
371
|
-
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
416
|
+
const stack = readUndoStack(atlasDir);
|
|
417
|
+
return stack.length > 0 ? stack[stack.length - 1] : null;
|
|
372
418
|
}
|
|
373
419
|
|
|
374
420
|
function clearUndoSnapshot(atlasDir) {
|
|
375
|
-
|
|
376
|
-
|
|
421
|
+
writeUndoStack(atlasDir, []);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function consumeUndoSnapshot(atlasDir, steps = 1) {
|
|
425
|
+
if (!Number.isInteger(steps) || steps < 1) return null;
|
|
426
|
+
const stack = readUndoStack(atlasDir);
|
|
427
|
+
if (stack.length < steps) return null;
|
|
428
|
+
const snapshot = stack[stack.length - steps];
|
|
429
|
+
writeUndoStack(atlasDir, stack.slice(0, stack.length - steps));
|
|
430
|
+
return snapshot;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function readUndoStack(atlasDir) {
|
|
434
|
+
const stackFile = path.join(atlasDir, UNDO_STACK_FILE);
|
|
435
|
+
if (fs.existsSync(stackFile)) {
|
|
436
|
+
return JSON.parse(fs.readFileSync(stackFile, 'utf8'));
|
|
437
|
+
}
|
|
438
|
+
const latestFile = path.join(atlasDir, UNDO_FILE);
|
|
439
|
+
if (fs.existsSync(latestFile)) {
|
|
440
|
+
return [JSON.parse(fs.readFileSync(latestFile, 'utf8'))];
|
|
441
|
+
}
|
|
442
|
+
return [];
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function writeUndoStack(atlasDir, stack) {
|
|
446
|
+
const stackFile = path.join(atlasDir, UNDO_STACK_FILE);
|
|
447
|
+
const latestFile = path.join(atlasDir, UNDO_FILE);
|
|
448
|
+
if (!stack || stack.length === 0) {
|
|
449
|
+
if (fs.existsSync(stackFile)) fs.rmSync(stackFile);
|
|
450
|
+
if (fs.existsSync(latestFile)) fs.rmSync(latestFile);
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
fs.mkdirSync(atlasDir, { recursive: true });
|
|
454
|
+
fs.writeFileSync(stackFile, JSON.stringify(stack, null, 2), 'utf8');
|
|
455
|
+
fs.writeFileSync(latestFile, JSON.stringify(stack[stack.length - 1], null, 2), 'utf8');
|
|
377
456
|
}
|
|
378
457
|
|
|
379
458
|
module.exports = {
|
|
@@ -383,6 +462,7 @@ module.exports = {
|
|
|
383
462
|
FEATURES_DIR,
|
|
384
463
|
HISTORY_FILE,
|
|
385
464
|
UNDO_FILE,
|
|
465
|
+
UNDO_STACK_FILE,
|
|
386
466
|
readYaml,
|
|
387
467
|
writeYaml,
|
|
388
468
|
load,
|
|
@@ -390,6 +470,7 @@ module.exports = {
|
|
|
390
470
|
loadOverlay,
|
|
391
471
|
saveOverlay,
|
|
392
472
|
mergeOverlay,
|
|
473
|
+
deriveOverlay,
|
|
393
474
|
diffPages,
|
|
394
475
|
normalizeFeature,
|
|
395
476
|
normalizeSubmodule,
|
|
@@ -397,6 +478,7 @@ module.exports = {
|
|
|
397
478
|
writeUndoSnapshot,
|
|
398
479
|
readUndoSnapshot,
|
|
399
480
|
clearUndoSnapshot,
|
|
481
|
+
consumeUndoSnapshot,
|
|
400
482
|
macroVisualOf,
|
|
401
483
|
featureVisualOf,
|
|
402
484
|
};
|
|
@@ -43,12 +43,12 @@ Usage:
|
|
|
43
43
|
Manage component rows and edges
|
|
44
44
|
apltk architecture meta set Update meta.title / meta.summary
|
|
45
45
|
apltk architecture actor add|remove Manage top-level actors
|
|
46
|
-
apltk architecture undo
|
|
46
|
+
apltk architecture undo [--steps <n>] Revert the most recent mutation or roll back multiple steps
|
|
47
47
|
apltk architecture --help Show this help
|
|
48
48
|
|
|
49
49
|
Global flags:
|
|
50
50
|
--project <root> Project root (default: nearest ancestor with resources/project-architecture/, else cwd); missing layout dirs are created when needed
|
|
51
|
-
--spec <spec_dir>
|
|
51
|
+
--spec <spec_dir> Single specs write locally; batch member paths write to the coordination.md root architecture_diff/atlas/
|
|
52
52
|
--no-render Skip auto-render after a mutation
|
|
53
53
|
--no-open For open/diff: skip launching the browser
|
|
54
54
|
--out <dir> For diff: override viewer output directory
|
|
@@ -255,21 +255,14 @@ function runDiff(opts, io) {
|
|
|
255
255
|
if (!projectRoot) {
|
|
256
256
|
projectRoot = process.cwd();
|
|
257
257
|
}
|
|
258
|
-
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
const
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
io.stdout.write(`${indexPath}\n`);
|
|
268
|
-
io.stdout.write(
|
|
269
|
-
`Diff pages: ${changes.length} (modified=${changes.filter((c) => c.kind === 'modified').length}, added=${changes.filter((c) => c.kind === 'added').length}, removed=${changes.filter((c) => c.kind === 'removed').length})\n`,
|
|
270
|
-
);
|
|
271
|
-
if (opts.open) openInBrowser(indexPath);
|
|
272
|
-
return 0;
|
|
258
|
+
const bootstrap = path.join(__dirname, 'architecture-bootstrap-render.js');
|
|
259
|
+
const args = [bootstrap, 'diff', '--project', projectRoot];
|
|
260
|
+
if (opts.out) args.push('--out', opts.out);
|
|
261
|
+
if (!opts.open) args.push('--no-open');
|
|
262
|
+
const result = spawnSync(process.execPath, args, { encoding: 'utf8' });
|
|
263
|
+
if (result.stdout) io.stdout.write(result.stdout);
|
|
264
|
+
if (result.stderr) io.stderr.write(result.stderr);
|
|
265
|
+
return result.status || 0;
|
|
273
266
|
}
|
|
274
267
|
|
|
275
268
|
// main(argv, io) is sync and supports the legacy verbs `open` and
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|