@laitszkin/apollo-toolkit 3.11.5 → 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/CHANGELOG.md +18 -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 -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/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/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,24 @@ All notable changes to this repository are documented in this file.
|
|
|
10
10
|
|
|
11
11
|
### Fixed
|
|
12
12
|
|
|
13
|
+
## [v3.11.6] - 2026-05-12
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- Tests: spec-mode batch-root overlay writes, combined batch diff rendering, legacy HTML-only batch diff fallback, repeated spec render cleanup, and multi-step `undo` coverage for the declarative atlas CLI.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- `apltk architecture --spec <spec_dir>` now keeps the existing single-spec layout for standalone plans, but batch member paths resolve to one shared `architecture_diff/` beside `coordination.md` so the whole batch maintains a single overlay and rendered architecture diff.
|
|
22
|
+
- `generate-spec` and `spec-to-project-html` now document the shared batch-root overlay behavior and tighten architecture-diff completion criteria: all intended cross-feature edges, feature-to-feature relationships, and sub-module relationships must be declared explicitly through the CLI instead of being left implicit in prose.
|
|
23
|
+
- Spec-mode atlas persistence now derives overlay state from the merged proposed-after graph, which lets repeated edits collapse back to a minimal diff automatically and adds `apltk architecture undo --steps <n>` for multi-step rollback.
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- `apltk architecture diff` now renders batch specs as one combined macro/viewer result instead of showing separate incomplete macro pages per member spec.
|
|
28
|
+
- `apltk architecture diff` preserves compatibility with legacy batch artifacts that only contain rendered HTML plus `_removed.txt`, instead of silently dropping those member diffs when atlas overlay state is absent.
|
|
29
|
+
- Spec-mode scoped renders now clean stale HTML pages and removal state correctly, preventing repeated render/diff runs from crashing on already-removed pages or leaving ghost overlay output behind.
|
|
30
|
+
|
|
13
31
|
## [v3.11.5] - 2026-05-12
|
|
14
32
|
|
|
15
33
|
### Added
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/generate-spec/SKILL.md
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: generate-spec
|
|
3
3
|
description: >-
|
|
4
|
-
Author docs/plans trees: run `apltk create-specs`,
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
Author docs/plans trees: run `apltk create-specs`, fill `spec/tasks/checklist/contract/design`
|
|
5
|
+
(+ `coordination.md`/`preparation.md` for batches), cite official docs, plan tests via
|
|
6
|
+
**`test-case-strategy`**, and block code edits until approval. Architecture deltas use
|
|
7
|
+
`apltk architecture --spec <spec_dir> …`: single specs write under `<spec_dir>/architecture_diff/`,
|
|
8
|
+
while batch member paths resolve to the shared batch-root `architecture_diff/` beside
|
|
9
|
+
`coordination.md`; `apltk architecture diff` renders the before/after viewer. Use for
|
|
10
|
+
drafting/refreshing specs or restructuring batches, not execution. Reject vague `tasks.md`
|
|
11
|
+
rows and split scope beyond 3 modules.
|
|
9
12
|
---
|
|
10
13
|
|
|
11
14
|
# Generate Spec
|
|
@@ -27,7 +30,7 @@ description: >-
|
|
|
27
30
|
- **`tasks.md` checklist items**: **every** `- [ ]` **MUST** specify (a) concrete file/function target, (b) specific modification and expected outcome, (c) a verification hook—**no** vague rows (`Implement integration`, `Add tests`). Forbidden vague items **MUST** be rewritten before approval.
|
|
28
31
|
- **MUST** use `test-case-strategy` when planning non-trivial logic tests and checklist mapping (test IDs, drift checks). Every **non-trivial** `tasks.md` implementation item **MUST** name a focused unit drift check, another concrete verification hook, or **`N/A`** with a concrete reason.
|
|
29
32
|
- **MUST NOT** modify implementation code before **explicit user approval** of the spec set. Clarifications **MUST** sync across affected files and **MUST** re-trigger approval. If scope becomes a **different issue**, **MUST** stop editing the old set and create a **new** `change_name`.
|
|
30
|
-
- **MUST** when the proposed change touches the architecture surface (feature / sub-module add / rename / remove, edge add / remove, variable rows, function I/O rows, internal dataflow / error deltas), declare the proposed-after state **only** through `apltk architecture --spec <spec_dir> …` using the exact verbs, subverbs, and flags from **`apltk architecture --help`** (this file must not be treated as the command list). The CLI writes overlay YAML to `<spec_dir>/architecture_diff/atlas/` and renders only the affected proposed-after HTML pages under `<spec_dir>/architecture_diff
|
|
33
|
+
- **MUST** when the proposed change touches the architecture surface (feature / sub-module add / rename / remove, edge add / remove, variable rows, function I/O rows, internal dataflow / error deltas), declare the proposed-after state **only** through `apltk architecture --spec <spec_dir> …` using the exact verbs, subverbs, and flags from **`apltk architecture --help`** (this file must not be treated as the command list). The CLI writes overlay YAML to `<spec_dir>/architecture_diff/atlas/` and renders only the affected proposed-after HTML pages under `<spec_dir>/architecture_diff/` for single-spec plans. For batch plans, passing any member spec path to `--spec` resolves to the shared batch-root overlay beside `coordination.md`, so the whole batch maintains **one** `architecture_diff/atlas/` and **one** rendered architecture diff. **`apltk architecture diff`** then builds a paginated **before/after viewer**: it walks every `docs/plans/**/architecture_diff/` tree, pairs each overlay HTML path with the matching file under `resources/project-architecture/` when it exists, and labels pages **modified**, **added**, or **removed** (via `_removed.txt` / removal manifests) so reviewers can scroll the whole architecture delta without opening files by hand. **MUST NOT** hand-author anything under `architecture_diff/**` — the renderer owns layout, DOM, CSS, ARIA, and pan/zoom. Use this skill’s `references/TEMPLATE_SPEC.md` for field/enum schema; use `init-project-html/SKILL.md` for semantic rules (**subagent gate** + edge kinds + dataflow integrity). When using subagents to draft atlas overlay, the authoring agent **MUST** wait until **all** feature subagents finish before declaring cross-feature **`edge`** (or overlay **`meta`** / **`actor`** that only exists to stitch features), matching `init-project-html` / `spec-to-project-html`.
|
|
31
34
|
- Write prose in the **user’s language** by default; keep requirement/task/test IDs traceable across `spec.md`, `tasks.md`, and `checklist.md`.
|
|
32
35
|
- **MUST** use **kebab-case** `change_name`; **MUST NOT** use spaces or arbitrary special characters in names.
|
|
33
36
|
|
|
@@ -85,15 +88,15 @@ Always materialize: `spec.md`, `tasks.md`, `checklist.md`, `contract.md`, `desig
|
|
|
85
88
|
|
|
86
89
|
When the spec changes a feature module, sub-module, edge, variable row, function I/O row, internal dataflow, or error row:
|
|
87
90
|
|
|
88
|
-
|
|
89
|
-
2. **Declare proposed-after state** with `apltk architecture --spec <spec_dir> …` for each mutation (pass `--no-render` while batching if you prefer a single render at the end).
|
|
90
|
-
3. **`apltk architecture render --spec <spec_dir>`** — emits/updates only the HTML files touched by this overlay plus assets.
|
|
91
|
-
4. **`apltk architecture validate --spec <spec_dir>`** — **MUST** return OK before the spec is approval-ready (resolve dangling edges, unknown enums, bad dataflow references).
|
|
92
|
-
5. **`apltk architecture diff`** — **MUST** run before hand-off when atlas changed; confirm the paginated viewer shows sensible **modified** / **added** / **removed** counts and that each interesting path pairs correctly (base `resources/project-architecture/…` vs `<spec_dir>/architecture_diff/…`). A page appearing as **remove + add** instead of **modified** usually means a **slug rename** was split wrong — fix with intentional remove+add or a single coherent mutation sequence.
|
|
91
|
+
**Completion standard:** the overlay is not complete until every intended cross-feature **edge**, every feature-to-feature dependency or call/return relationship, and every sub-module-to-sub-module relationship inside the affected scope has been declared precisely through the CLI. It is **not** acceptable to leave relationship structure implied only by prose in `spec.md` / `design.md`; the architecture diff must explicitly express the real proposed-after topology.
|
|
93
92
|
|
|
94
|
-
**
|
|
93
|
+
1. **Discover commands:** run **`apltk architecture --help`** in the target workspace; use that output for every `add` / `set` / `remove` / `reorder` spelling and required flag. Do not copy long command tables from skills — they go stale.
|
|
94
|
+
2. **Declare proposed-after state** with `apltk architecture --spec <spec_dir> …` for each mutation (pass `--no-render` while batching if you prefer a single render at the end). In a batch, keep using the member spec path; the CLI resolves writes to the batch-root `architecture_diff/` beside `coordination.md`.
|
|
95
|
+
3. **`apltk architecture render --spec <spec_dir>`** — emits/updates only the HTML files touched by this overlay plus assets. In a batch, this updates the shared batch-root render.
|
|
96
|
+
4. **`apltk architecture validate --spec <spec_dir>`** — **MUST** return OK before the spec is approval-ready (resolve dangling edges, unknown enums, bad dataflow references). In a batch, this validates the shared batch-root overlay.
|
|
97
|
+
5. **`apltk architecture diff`** — **MUST** run before hand-off when atlas changed; confirm the paginated viewer shows sensible **modified** / **added** / **removed** counts and that each interesting path pairs correctly (base `resources/project-architecture/…` vs the spec or batch-root `architecture_diff/…`). Use this pass to verify that the rendered graph actually contains the full intended relationship set: all relevant feature-level seams, all required sub-module seams, and no missing edge that the design prose depends on. A page appearing as **remove + add** instead of **modified** usually means a **slug rename** was split wrong — fix with intentional remove+add or a single coherent mutation sequence.
|
|
95
98
|
|
|
96
|
-
**
|
|
99
|
+
**Where files land:** single-spec plans write overlay YAML under `<spec_dir>/architecture_diff/atlas/` and rendered proposed-after HTML under `<spec_dir>/architecture_diff/`. Batch plans write both under the batch root beside `coordination.md`, even when the CLI call uses a member spec path. Cross-feature edges whose far endpoint exists only in the **base** atlas still resolve when merged — but you **must** `validate` to catch mistakes.
|
|
97
100
|
|
|
98
101
|
**Subagent coordination:** if multiple features are drafted in parallel, **do not** declare cross-feature **`edge`** (or overlay **`meta` / `actor`** used only to stitch features) until **all** feature workers report done — see `init-project-html/SKILL.md` Rule 3 and `spec-to-project-html`.
|
|
99
102
|
|
|
@@ -102,6 +105,7 @@ When the spec changes a feature module, sub-module, edge, variable row, function
|
|
|
102
105
|
- **Pause →** Did I touch any file under `architecture_diff/` by hand? Revert and re-run the CLI verb instead.
|
|
103
106
|
- **Pause →** Does `apltk architecture validate --spec <spec_dir>` return OK?
|
|
104
107
|
- **Pause →** Does `apltk architecture diff` pair the spec’s pages correctly?
|
|
108
|
+
- **Pause →** Have I explicitly declared every intended edge, feature-to-feature relationship, and sub-module relationship in the CLI output, or am I still relying on prose to imply missing structure?
|
|
105
109
|
|
|
106
110
|
### 4) Clarifications and approval
|
|
107
111
|
|
|
@@ -4,7 +4,7 @@ interface:
|
|
|
4
4
|
default_prompt: >-
|
|
5
5
|
Use $generate-spec to create or update single-spec plans under docs/plans/<date>/<change_name>/ or parallel batches under docs/plans/<date>/<batch_name>/<change_name>/ with shared coordination.md and, only when specs cannot be parallel-safe without prior shared work, minimal non-business preparation.md; treat references/templates/*.md as binding format; member specs assume preparation finished—do not duplicate preparation tasks; surface collisions early and resolve ownership via coordination.md; fill BDD in spec.md; integrate $test-case-strategy into tasks/checklists.
|
|
6
6
|
**Critical layering:** design.md + contract.md are higher-level guiding context only (architecture + cite-backed external truth; coarse INT-### / EXT-### anchors). tasks.md MUST be the ONLY enumerated runnable queue with path-level edits and verification hooks—derive tasks FROM spec + design + contract WITHOUT mirroring checklist rows into design/contract; optionally cite INT/EXT on task lines for traceability—never duplicate task choreography inside design.md or contract.md.
|
|
7
|
-
**Architecture overlay + diff:** When the spec touches atlas surface (feature/sub-module, edges, function/variable rows, dataflow, errors), declare proposed-after state ONLY via `apltk architecture --spec <spec_dir> …`. **Exact verbs/flags: ALWAYS `apltk architecture --help`.**
|
|
8
|
-
After mutations: `render --spec`, `validate --spec` (must be OK pre-approval). **`apltk architecture diff`** opens the paginated before/after viewer over all `docs/plans/**/architecture_diff/` vs base `resources/project-architecture/` — run it when atlas changed; verify modified/added/removed pairing.
|
|
9
|
-
|
|
7
|
+
**Architecture overlay + diff:** When the spec touches atlas surface (feature/sub-module, edges, function/variable rows, dataflow, errors), declare proposed-after state ONLY via `apltk architecture --spec <spec_dir> …`. **Exact verbs/flags: ALWAYS `apltk architecture --help`.** Single-spec plans write `<spec_dir>/architecture_diff/atlas/` + rendered HTML under `<spec_dir>/architecture_diff/`. Batch plans resolve any member `--spec` path to the shared batch-root `architecture_diff/` beside `coordination.md`, so the whole batch keeps one architecture diff. NEVER hand-author `architecture_diff/**`.
|
|
8
|
+
Completion standard for atlas work: every intended cross-feature edge, every feature-to-feature relationship, and every sub-module relationship in scope must be expressed precisely through the CLI output — not left implicit in prose. After mutations: `render --spec`, `validate --spec` (must be OK pre-approval). **`apltk architecture diff`** opens the paginated before/after viewer over all `docs/plans/**/architecture_diff/` vs base `resources/project-architecture/` — run it when atlas changed; verify modified/added/removed pairing and confirm the rendered graph contains the full intended relationship set.
|
|
9
|
+
If subagents draft multiple features: **wait until ALL finish** before cross-feature `edge` / stitching `meta` / `actor` (same gate as $init-project-html / $spec-to-project-html).
|
|
10
10
|
Field/enums: this skill's references/TEMPLATE_SPEC.md. Semantics: $init-project-html SKILL.md.
|
|
Binary file
|
|
@@ -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
|
|
Binary file
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: spec-to-project-html
|
|
3
3
|
description: >-
|
|
4
|
-
Sync the project HTML architecture atlas to active planning specs by driving `apltk architecture --spec <spec_dir>`.
|
|
4
|
+
Sync the project HTML architecture atlas to active planning specs by driving `apltk architecture --spec <spec_dir>`. Single specs write overlay YAML under `<spec_dir>/architecture_diff/atlas/`; batch member paths resolve to the shared batch-root `architecture_diff/` beside `coordination.md`. The CLI re-renders only the affected proposed-after HTML pages — macro SVG and per-sub-module internal-dataflow diagrams stay zoomable like the base atlas — so `apltk architecture diff` pairs before/after by path under `docs/plans/**/architecture_diff/`. Preserve the two-layer rule and the responsibility split: **subagents only** — each subagent reads ONE affected feature and declares EVERY intra-feature overlay change; the main agent **waits until all subagents finish**, then aggregates outbound summaries and declares **only** cross-feature edges. **Exact CLI spelling:** always `apltk architecture --help`. Ground every declaration in repo evidence; mark `TBD` when code is missing.
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Spec To Project HTML
|
|
@@ -17,7 +17,7 @@ description: >-
|
|
|
17
17
|
## Non-negotiables
|
|
18
18
|
|
|
19
19
|
- **MUST** read specs in order unless the user directs otherwise: `spec.md` → `design.md` → `contract.md` → coordination notes.
|
|
20
|
-
- **MUST** declare every change through the CLI with `--spec <spec_dir>` (exact verbs/flags: **`apltk architecture --help`**).
|
|
20
|
+
- **MUST** declare every change through the CLI with `--spec <spec_dir>` (exact verbs/flags: **`apltk architecture --help`**). Single specs write overlay YAML to `<spec_dir>/architecture_diff/atlas/` and re-render only the affected proposed-after HTML pages there. Batch member paths resolve to the shared batch-root `architecture_diff/` beside `coordination.md`, so the whole batch keeps one overlay and one rendered diff. **MUST NOT** hand-edit `architecture_diff/**/*.html` — the renderer owns those files.
|
|
21
21
|
- **MUST** obey the semantic rules from `init-project-html/SKILL.md`:
|
|
22
22
|
- Sub-module pages stay self-only — express cross-boundary interactions via **edges** (cross-feature or intra-feature), never as sub-module page prose.
|
|
23
23
|
- Feature pages stay lightweight — cross-sub-module choreography belongs in **edges**, not in `dataflow` prose that pretends to cross features.
|
|
@@ -38,7 +38,7 @@ description: >-
|
|
|
38
38
|
- **Evidence**: cite the spec passage (design subsystem entry) alongside the code path; record the citation in `meta.summary` or in sub-module purposes when relevant.
|
|
39
39
|
- **Execution**: locate the plan set → list affected feature modules → subagent fan-out → **all complete** → cross-feature edges → `render --spec` if batched → `validate --spec` → `diff` check → handover.
|
|
40
40
|
- **Quality**: macro overlay still shows every cross-feature data-row the spec requires; sub-module declarations stay self-only; `apltk architecture diff` opens cleanly with no orphan pages; no dangling edges.
|
|
41
|
-
- **Output**:
|
|
41
|
+
- **Output**: single specs touch only `<spec_dir>/architecture_diff/atlas/**` (overlay state) and `<spec_dir>/architecture_diff/**/*.html` (renderer output); batch specs write the same artifacts under the batch root beside `coordination.md`. Base `resources/project-architecture/` is **NEVER** mutated.
|
|
42
42
|
|
|
43
43
|
## Acceptance criteria (mirrors `init-project-html`)
|
|
44
44
|
|
|
@@ -92,7 +92,7 @@ List overlay files touched (or verb families used), the diff counts (`modified`
|
|
|
92
92
|
|
|
93
93
|
## Sample hints
|
|
94
94
|
|
|
95
|
-
- **Batch merge**: each member spec
|
|
95
|
+
- **Batch merge**: each member spec still calls `--spec <member_dir>`, but the CLI resolves writes to the shared batch-root overlay beside `coordination.md`; `diff` shows the batch as one combined architecture delta.
|
|
96
96
|
- **Spec shrinks scope**: prefer `submodule set --role "deprecated: ..."` before `submodule remove` so reviewers see intent.
|
|
97
97
|
- **Design-only change**: still review edges — order shifts surface as edge mutations; `validate --spec` catches dangling references.
|
|
98
98
|
|
|
@@ -3,7 +3,7 @@ interface:
|
|
|
3
3
|
short_description: "Sync the project HTML architecture atlas with active planning specs via `apltk architecture --spec`"
|
|
4
4
|
default_prompt: >-
|
|
5
5
|
Use $spec-to-project-html. Read docs/plans (spec → design → contract). Every overlay mutation: `apltk architecture --spec <spec_dir> …`. **Exact commands: `apltk architecture --help`** — do not trust long command lists in docs.
|
|
6
|
-
|
|
6
|
+
Single specs write `<spec_dir>/architecture_diff/atlas/*.yaml` + affected HTML under `<spec_dir>/architecture_diff/`. Batch member paths resolve to the shared batch-root `architecture_diff/` beside `coordination.md`, so one batch keeps one overlay + one rendered diff. Base `resources/project-architecture/` is read-only here. NEVER hand-edit `architecture_diff/**`.
|
|
7
7
|
**`apltk architecture diff`** builds a paginated viewer: scans all `docs/plans/**/architecture_diff/`, pairs overlay paths with base atlas HTML by relative path, labels modified/added/removed — use it to verify the architecture delta before hand-off.
|
|
8
8
|
**Subagents only:** dispatch ONE write-capable subagent per AFFECTED feature; each applies ALL intra-feature overlay mutations (`submodule`/`function`/`variable`/`dataflow`/`error`/`edge` within that feature). Each returns ONLY change summary + outbound boundary deltas for OTHER features.
|
|
9
9
|
**HARD GATE:** main agent MUST wait until ALL subagents finish before ANY cross-feature `edge add|remove`, overlay `meta` that stitches multiple features, or shared `actor` for cross-feature context. Then `render --spec`, `validate --spec`.
|