@laitszkin/apollo-toolkit 3.11.0 → 3.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/analyse-app-logs/scripts/__pycache__/filter_logs_by_time.cpython-312.pyc +0 -0
  3. package/analyse-app-logs/scripts/__pycache__/log_cli_utils.cpython-312.pyc +0 -0
  4. package/analyse-app-logs/scripts/__pycache__/search_logs.cpython-312.pyc +0 -0
  5. package/docs-to-voice/scripts/__pycache__/docs_to_voice.cpython-312.pyc +0 -0
  6. package/generate-spec/scripts/__pycache__/create-specscpython-312.pyc +0 -0
  7. package/init-project-html/SKILL.md +56 -20
  8. package/init-project-html/agents/openai.yaml +6 -6
  9. package/init-project-html/lib/atlas/assets/architecture.css +27 -6
  10. package/init-project-html/lib/atlas/assets/viewer.client.js +124 -81
  11. package/init-project-html/lib/atlas/cli.js +31 -3
  12. package/init-project-html/lib/atlas/layout.js +112 -11
  13. package/init-project-html/lib/atlas/render.js +131 -33
  14. package/init-project-html/lib/atlas/schema.js +39 -2
  15. package/init-project-html/references/TEMPLATE_SPEC.md +26 -8
  16. package/init-project-html/sample-demo/resources/project-architecture/assets/architecture.css +27 -6
  17. package/init-project-html/sample-demo/resources/project-architecture/assets/viewer.client.js +124 -81
  18. package/init-project-html/sample-demo/resources/project-architecture/atlas/features/get-invite-codes.yaml +17 -4
  19. package/init-project-html/sample-demo/resources/project-architecture/features/get-invite-codes/invite-code-generator.html +23 -7
  20. package/init-project-html/sample-demo/resources/project-architecture/features/get-invite-codes/invite-issuance-service.html +45 -13
  21. package/init-project-html/sample-demo/resources/project-architecture/features/get-invite-codes/postgresql.html +28 -10
  22. package/init-project-html/sample-demo/resources/project-architecture/features/get-invite-codes/public-api.html +33 -13
  23. package/init-project-html/sample-demo/resources/project-architecture/features/get-invite-codes/web-get-invite-ui.html +28 -10
  24. package/init-project-html/sample-demo/resources/project-architecture/features/invite-code-registration/postgresql.html +28 -10
  25. package/init-project-html/sample-demo/resources/project-architecture/features/invite-code-registration/public-api.html +28 -10
  26. package/init-project-html/sample-demo/resources/project-architecture/features/invite-code-registration/registration-service.html +38 -17
  27. package/init-project-html/sample-demo/resources/project-architecture/features/invite-code-registration/web-register-ui.html +29 -11
  28. package/init-project-html/sample-demo/resources/project-architecture/index.html +100 -76
  29. package/katex/scripts/__pycache__/render_katex.cpython-312.pyc +0 -0
  30. package/open-github-issue/scripts/__pycache__/open_github_issue.cpython-312.pyc +0 -0
  31. package/package.json +1 -1
  32. package/read-github-issue/scripts/__pycache__/find_issues.cpython-312.pyc +0 -0
  33. package/read-github-issue/scripts/__pycache__/read_issue.cpython-312.pyc +0 -0
  34. package/resolve-review-comments/scripts/__pycache__/review_threads.cpython-312.pyc +0 -0
  35. package/spec-to-project-html/SKILL.md +25 -16
  36. package/spec-to-project-html/agents/openai.yaml +5 -5
  37. package/spec-to-project-html/references/TEMPLATE_SPEC.md +2 -0
  38. package/text-to-short-video/scripts/__pycache__/enforce_video_aspect_ratio.cpython-312.pyc +0 -0
package/CHANGELOG.md CHANGED
@@ -10,6 +10,23 @@ All notable changes to this repository are documented in this file.
10
10
 
11
11
  ### Fixed
12
12
 
13
+ ## [v3.11.1] - 2026-05-11
14
+
15
+ ### Added
16
+
17
+ - `dataflow add` optional `--fn`, `--reads`, and `--writes` (comma-separated variable names): each internal step can reference a declared function and read/write declared variables in the same sub-module; `validate` rejects unknown names. The renderer draws a function pill plus reads/writes chips on sub-module internal flow diagrams.
18
+ - `init-project-html` / `spec-to-project-html`: **Acceptance criteria** for macro edges (call/return/data-row/failure) and for sub-module diagrams (function flow + variable state); Rule 2 and CLI verb table updated; `TEMPLATE_SPEC.md` documents object-shaped `dataflow` steps and new CSS hooks.
19
+ - Macro atlas UX: each sub-module rectangle is an `<a href="features/<feature>/<sub>.html">` with `aria-label`, nested SVG `<title>` tooltip, `cursor: pointer`, and clearer hover/focus styles.
20
+
21
+ ### Changed
22
+
23
+ - `layout.js`: `measureSubmodule()` computes per-node width/height from slug, kind label, and `role` so elkjs lays out boxes that fit wrapped role text (replaces fixed 240×92 and the old single-line truncation).
24
+ - `render.js`: macro SVG draws slug, kind, and multi-line role using the same `measureSubmodule()` output as layout; sub-dataflow SVG renders enriched steps with fn pill and reads/writes chips (from prior work in this release train, now validated and documented).
25
+
26
+ ### Fixed
27
+
28
+ - `viewer.client.js`: defer pointer capture until the pointer moves past a small drag threshold so clicks on macro sub-module links navigate; swallow the synthetic click after an actual drag so pan does not accidentally open a page.
29
+
13
30
  ## [v3.11.0] - 2026-05-11
14
31
 
15
32
  ### Added
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: init-project-html
3
3
  description: >-
4
- Declare the project HTML architecture atlas through `apltk architecture` CLI verbs instead of hand-writing SVG/HTML. The tool owns layout, no-overlap, DOM, CSS, and pan/zoom; the agent only declares features, sub-modules, functions, variables, dataflow steps, errors, and edges. Macro `index.html` always shows feature clusters with their sub-modules and every cross/intra-feature edge; each sub-module page describes only itself (function I/O + variables-with-business-purpose + internal dataflow + local errors); feature index pages stay lightweight. Read strategy avoids context loss: list feature modules first, then either dispatch one read-only subagent per feature and aggregate summaries before declaring, or process one feature at a time end-to-end. Anchor every declaration to repo evidence.
4
+ Declare the project HTML architecture atlas through `apltk architecture` CLI verbs instead of hand-writing SVG/HTML. Two acceptance criteria gate completion: (1) the macro diagram clearly shows feature × sub-module relationships data flow, call/return interaction logic, error handling, rollback all expressed as `--kind call|return|data-row|failure` edges; (2) each sub-module's internal diagram clearly shows function-level interactions inside it — function-to-function flow via `dataflow add --fn <declared-fn>`, variable state transitions via `--reads`/`--writes` referencing declared variables, and the resulting local data flow. The tool owns layout, CSS, and pan/zoom for both diagrams; `validate` rejects any step that references an undeclared function or variable. With subagents, each owns one feature and declares every intra-feature interaction itself; the main agent only declares cross-feature edges. Anchor every declaration to repo evidence.
5
5
  ---
6
6
 
7
7
  # Init Project HTML
@@ -19,25 +19,36 @@ description: >-
19
19
 
20
20
  The atlas state lives in `resources/project-architecture/atlas/` (`atlas.index.yaml` + per-feature YAMLs). Every change goes through `apltk architecture <verb> ...`. After any mutation, the CLI re-renders `resources/project-architecture/**/*.html` with the correct layout, CSS, pan/zoom, and ARIA — agents **MUST NOT** edit those HTML files by hand or invent additional ones.
21
21
 
22
- ### Rule 2 — Sub-module pages describe only themselves
22
+ ### Rule 2 — Sub-module pages describe only themselves; the internal-dataflow diagram is zoomable and structurally typed
23
23
 
24
24
  What the CLI emits on each sub-module page is fixed:
25
25
 
26
26
  - function I/O table (`function add --feature X --submodule Y --name fn --in ... --out ... --side ... --purpose ...`),
27
27
  - variables-with-business-purpose table (`variable add --name v --type T --scope call|tx|persist|instance|loop --purpose ...`),
28
- - internal dataflow steps (`dataflow add --step "..."`),
28
+ - internal dataflow diagram — ordered steps the renderer lays out as a numbered top-to-bottom flow inside a pan/zoom viewport with +/−/Fit controls, exactly like the macro SVG. Each step is `dataflow add --step "..." [--fn <declared-fn>] [--reads "v1,v2"] [--writes "v3,v4"]`. The renderer surfaces `--fn` as a pill at the top of the step box, `--reads` as a green chip on the bottom-left, and `--writes` as an orange chip on the bottom-right — so a single SVG simultaneously shows the function-to-function flow inside the sub-module AND the variable state transitions across the run,
29
29
  - local errors (`error add --name ErrX --when ... --means ...`).
30
30
 
31
- Cross-boundary narratives ("I call X", "Y calls me") **MUST** be expressed as macro edges (`edge add --from X/sub --to Y/sub --kind call|return|data-row|failure --label ...`), never as sub-module page prose. The CLI enforces this by design — there is no verb to add cross-boundary text to a sub-module page.
31
+ Schema rules the CLI enforces on every `dataflow add` (so the diagram never lies):
32
32
 
33
- ### Rule 3 Read order: never let the codebase wipe the context window
33
+ - `--fn <name>` must match a function declared **in the same sub-module** via `function add`. If it does not, `validate` fails — declare the function first.
34
+ - Every name in `--reads` / `--writes` must match a variable declared **in the same sub-module** via `variable add`. If it does not, `validate` fails — declare the variable first.
35
+ - `--fn` / `--reads` / `--writes` are optional but additive: omitting them is allowed for purely descriptive steps, but for any non-trivial sub-module the function-and-variable structure **MUST** be filled in so the diagram conveys the function-to-function flow and the variable state transitions.
34
36
 
35
- Real production codebases dwarf the main agent's context. Reading the whole repo before declaring pushes early details out by the end, making macro edges and sub-module declarations contradict each other. **MUST** follow one of:
37
+ Cross-boundary narratives ("I call X", "Y calls me") **MUST** be expressed as edges (`edge add --from X/sub --to Y/sub --kind call|return|data-row|failure --label ...`), never as sub-module page prose. The CLI enforces this by design — there is no verb to add cross-boundary text to a sub-module page.
36
38
 
37
- - **With subagents (preferred):** first enumerate the **feature-module list** (slug + entry + boundary resources only). Then dispatch **one read-only subagent per feature**. Each subagent deep-reads and returns ONLY a structured summary (sub-module list with kind/role; per sub-module: function I/O, variables-with-business-purpose, internal dataflow steps, errors; outbound edges to other features). The main agent collects every summary first, then drives the CLI in one pass to declare every component.
38
- - **Without subagents:** process features **one at a time** — read feature A end-to-end, then declare it via the CLI (`feature add`, `submodule add`, `function add`, `variable add`, `dataflow add`, `error add`, intra-feature `edge add`). For cross-feature edges that point at not-yet-declared features, declare the placeholder feature with `feature add --slug <future> --title <future>` first, then add the edge; subsequent passes set the real title/story. Drop A's function-level details from working memory before reading feature B.
39
+ ### Rule 3 Read order: never let the codebase wipe the context window; subagents own their own feature
39
40
 
40
- Both paths share one invariant: at any moment the main agent only holds *current-feature details + cross-feature boundary notes*. Run `apltk architecture validate` after the last feature to catch dangling edges, unknown endpoints, or missing required fields.
41
+ Real production codebases dwarf the main agent's context. Reading the whole repo before declaring pushes early details out by the end, making macro edges and sub-module declarations contradict each other. The split below also draws a hard responsibility line between the worker and the orchestrator. **MUST** follow one of:
42
+
43
+ - **With subagents (preferred):** first enumerate the **feature-module list** (slug + entry + boundary resources only). Then dispatch **one (write-capable) subagent per feature**. Each subagent:
44
+ - deep-reads its own feature,
45
+ - declares **everything intra-feature** itself via `apltk architecture ... --feature <slug>` (sub-modules; per sub-module functions / variables / internal dataflow / errors; **every intra-feature edge** — function calls between sub-modules, error edges, rollback / compensation edges; variable state transitions are captured as ordered `dataflow add` steps on each sub-module),
46
+ - returns ONLY a structured summary of (i) the sub-module list and (ii) the **outbound boundaries** the main agent needs to wire (calls / data-rows / failure edges to other features' sub-modules, including direction and the consuming/producing endpoints).
47
+
48
+ The main agent never re-reads source for that feature. It collects every subagent's outbound-boundary summary and declares **only the cross-feature edges** (and any cross-feature actors / meta) itself, then runs `apltk architecture validate`.
49
+ - **Without subagents:** process features **one at a time** — read feature A end-to-end, declare its sub-modules, functions, variables, internal dataflow, errors, and intra-feature edges via the CLI, then move on. For cross-feature edges that point at not-yet-declared features, declare the placeholder feature with `feature add --slug <future> --title <future>` first, then add the edge; subsequent passes set the real title/story. Drop A's function-level details from working memory before reading feature B.
50
+
51
+ Both paths share one invariant: at any moment the agent doing the deep read only holds *that feature's details + cross-feature boundary notes*. The main agent's working set, in subagent mode, is **boundaries only**. Run `apltk architecture validate` after the last feature to catch dangling edges, unknown endpoints, or missing required fields.
41
52
 
42
53
  ### Rule 4 — Evidence over invention
43
54
 
@@ -51,6 +62,22 @@ Both paths share one invariant: at any moment the main agent only holds *current
51
62
  - **Quality**: macro SVG carries every cross-feature data-row edge that exists in the system; sub-module declarations are self-only; pan/zoom + Fit work in the rendered HTML; `apltk architecture validate` returns OK.
52
63
  - **Output**: `resources/project-architecture/atlas/` (YAML state) + `resources/project-architecture/**/*.html` (renderer output) — both managed by the CLI.
53
64
 
65
+ ## Acceptance criteria
66
+
67
+ The atlas is only complete when both of the following are demonstrably true on the rendered HTML (open `resources/project-architecture/index.html` and one representative sub-module page to verify):
68
+
69
+ 1. **Macro diagram clearly shows the relationships between features and sub-modules**, including:
70
+ - **Data flow** — every cross-feature DB row, queue topic, or cache key hand-off is a `--kind data-row` edge between the producing and consuming sub-modules (not free-form prose).
71
+ - **Interaction logic** — synchronous request/response paths are paired `--kind call` (outbound) and `--kind return` (response) edges with a label that names the call site or response shape.
72
+ - **Error handling and rollback** — every failure / compensation / retry path that crosses a sub-module boundary is a `--kind failure` edge with a label that names what is being rolled back or compensated.
73
+ - No interaction is captured *only* in sub-module page prose — if it crosses a sub-module boundary it MUST appear as an edge so the macro SVG carries it.
74
+ 2. **Each sub-module's internal diagram clearly shows the function-level interactions inside the sub-module**, including:
75
+ - **Function-to-function flow** — every step that does non-trivial work carries `--fn <declared-fn>`. The sequence of pills along the steps reveals which function is active at each point in the flow.
76
+ - **Variable state transitions during the run** — every variable that is read or mutated by a step is declared via `variable add`, and that step lists it in `--reads` (the value flows in) or `--writes` (the value is created/updated). Reading the steps top-to-bottom traces each variable's life cycle.
77
+ - **Local data flow** — the ordered step boxes (with reads/writes chips) read as a directed flowchart, ending at the sub-module boundary (returning a value, persisting a row, emitting an event).
78
+
79
+ `apltk architecture validate` MUST return OK before reporting completion — it enforces both criteria's structural pieces (unknown sub-modules on edges, unknown functions/variables referenced by dataflow steps).
80
+
54
81
  ## How to use `apltk architecture`
55
82
 
56
83
  Each verb is a single mutation; the CLI auto-renders after every change. Pass `--no-render` when batching mutations. Global flags: `--project <root>`, `--spec <spec_dir>` (writes to overlay; see `spec-to-project-html`), `--no-render`, `--no-open`.
@@ -63,7 +90,7 @@ Each verb is a single mutation; the CLI auto-renders after every change. Pass `-
63
90
  | `apltk architecture submodule add --feature <slug> --slug <kebab> --kind ui\|api\|service\|db\|pure-fn\|queue\|external --role "..."` | Declare a sub-module under a feature. |
64
91
  | `apltk architecture function add --feature X --submodule Y --name fn --in "T1, T2" --out "R \| ErrX" --side pure\|io\|write\|tx\|lock\|network --purpose "..."` | Add a function I/O row. |
65
92
  | `apltk architecture variable add --feature X --submodule Y --name v --type T --scope call\|tx\|persist\|instance\|loop --purpose "..."` | Add a variable-with-business-purpose row. |
66
- | `apltk architecture dataflow add --feature X --submodule Y --step "..." [--at N]` | Append (or insert at index `N`) an internal dataflow step. |
93
+ | `apltk architecture dataflow add --feature X --submodule Y --step "..." [--fn <declared-fn>] [--reads "v1,v2"] [--writes "v3,v4"] [--at N]` | Append (or insert at index `N`) an internal dataflow step. `--fn` must match a previously declared `function`; every name in `--reads` / `--writes` must match a previously declared `variable` in the same sub-module — the renderer surfaces them as a function pill + reads/writes chips so the diagram shows function-to-function flow and variable state transitions. |
67
94
  | `apltk architecture dataflow reorder --feature X --submodule Y --from i --to j` | Move a step within the same sub-module. |
68
95
  | `apltk architecture error add --feature X --submodule Y --name ErrCode --when "..." --means "..."` | Add a local error row. |
69
96
  | `apltk architecture edge add --from <feature>[/sub] --to <feature>[/sub] --kind call\|return\|data-row\|failure --label "..." [--id <stable>]` | Add an edge. Intra-feature edges (both endpoints in the same feature with sub-modules) land in the feature YAML; cross-feature edges land in `atlas.index.yaml`. |
@@ -86,25 +113,32 @@ Scan the shipped source for **user-visible capabilities** (each one = one featur
86
113
 
87
114
  ### 2) Branch the deep-read by environment (Rule 3)
88
115
 
89
- #### 2A) With subagents (preferred)
116
+ #### 2A) With subagents (preferred) — workers own their feature; main agent owns the macro seams
90
117
 
91
- Dispatch one read-only subagent per feature. Require this summary template (no source-code excerpts):
118
+ Dispatch one **write-capable** subagent per feature, plus the main agent for the macro layer. Hand each subagent: its feature slug, the feature-module list from step 1 (so it knows other features' slugs for outbound edges), and access to `apltk architecture`. Each subagent **declares its own feature end-to-end** and only reports outbound boundaries upward:
92
119
 
93
- > **Feature `<slug>` summary**
94
- > - Sub-module list: one row per `<sub-module-slug>` (kind: ui / api / service / db / pure-fn / queue / external; one-line role).
95
- > - Per sub-module: function signatures (in / out / side / purpose), variables-with-business-purpose, internal dataflow steps, errors raised.
96
- > - **Outbound boundaries**: call edges to other features' sub-modules; data-row edges through shared DB tables / topics / cache keys with another feature; mark direction.
120
+ > **Feature `<slug>` subagent contract**
121
+ > - Read every sub-module of this feature.
122
+ > - Run `feature add --slug <slug> --title "..." --story "..." --no-render` (or `feature set` if a placeholder already exists).
123
+ > - For every sub-module: `submodule add` with the right `--kind`, then add its `function`, `variable`, `dataflow` (ordered these steps carry the variable state transitions through the flow), and `error` rows.
124
+ > - For every intra-feature interaction between sub-modules — function calls, returns, error propagation, rollback / compensation — emit `edge add --from <slug>/<a> --to <slug>/<b> --kind call|return|data-row|failure --label "..."`. Failure / rollback flows belong here: model them as additional `--kind failure` edges or as ordered `dataflow add` steps on the affected sub-modules, whichever maps to the real code path.
125
+ > - Run `apltk architecture validate` (scoped to this feature) before returning.
126
+ > - **Return ONLY**: (i) sub-module list (slug + kind + one-line role), (ii) outbound boundaries to *other* features' sub-modules (direction + edge kind + suggested label). No source excerpts, no function bodies.
97
127
 
98
- Main agent collects every summary, then drives the CLI in one pass:
128
+ Main agent after every subagent returns declares **only** macro-level state from the aggregated outbound boundaries:
99
129
 
100
130
  ```bash
101
131
  apltk architecture meta set --title "..." --summary "..." --no-render
102
- apltk architecture feature add --slug <slug> --title "..." --story "..." --no-render
103
- # repeat submodule / function / variable / dataflow / error / edge add for the whole atlas
132
+ # only when an actor is shared across multiple features:
133
+ apltk architecture actor add --id <id> --label "..." --no-render
134
+ # one edge per cross-feature interaction reported by the subagents:
135
+ apltk architecture edge add --from <featA>/<subA> --to <featB>/<subB> --kind call|return|data-row|failure --label "..." --no-render
104
136
  apltk architecture render
105
137
  apltk architecture validate
106
138
  ```
107
139
 
140
+ The main agent **MUST NOT** re-declare a subagent's intra-feature components, and **MUST NOT** open source files for any feature it delegated.
141
+
108
142
  #### 2B) Without subagents — feature-by-feature read-declare loop
109
143
 
110
144
  Process the list from step 1 one feature at a time (topological hint: read-from data sources and pure helpers first, user-facing entries last). Per feature:
@@ -126,8 +160,10 @@ Report: feature count, sub-module count, macro edge counts (call / return / data
126
160
  ## Sample hints
127
161
 
128
162
  - Multiple SQL paths on `service ↔ db` → call `edge add` once per SQL path so the macro shows them as separate edges.
129
- - Retry loops between `service ↔ generator(pure)` → call/return pair in the macro plus a dataflow step in the service describing the retry budget.
163
+ - Retry loops between `service ↔ generator(pure)` → call/return pair in the macro plus a `dataflow add` step in the service with `--fn issueCode --writes "code"` describing the retry budget; a follow-up step `--fn issueCode --reads "code" --writes "row"` then captures the persistence attempt.
164
+ - Variable state transitions inside one function (e.g. `code` is computed, persisted, then returned) → emit one `dataflow add` per logical mutation, with `--fn` constant and `--reads` / `--writes` listing every variable that changes shape at that point. The sub-module page chips trace each variable's life cycle top-to-bottom.
130
165
  - Cross-feature DB hand-off (A writes, B reads) → declare both sides' `INSERT_*` / `SELECT_*` functions on the `db` sub-module, then `edge add --kind data-row` from producer feature/submodule to consumer feature/submodule.
166
+ - Rollback / compensation across sub-modules → `edge add --kind failure --label "rollback on dup"`; rollback *within* a single sub-module belongs in `dataflow add` (with `--fn` set to whichever function owns the cleanup).
131
167
  - Third-party systems → declare as `--kind external` sub-modules; the trust boundary becomes visible because the renderer styles them differently.
132
168
 
133
169
  ## References
@@ -3,11 +3,11 @@ interface:
3
3
  short_description: "Declare the project HTML architecture atlas through `apltk architecture` CLI verbs"
4
4
  default_prompt: >-
5
5
  Use $init-project-html. Its `SKILL.md` is the authoritative rulebook; `references/TEMPLATE_SPEC.md` is a component-schema cheat sheet.
6
- NEVER hand-author files under `resources/project-architecture/**/*.html`. The atlas state lives in YAML under `resources/project-architecture/atlas/` and every mutation goes through `apltk architecture <verb> ...`; the CLI owns layout, no-overlap, DOM, CSS, ARIA, and pan/zoom.
7
- Read strategy to avoid context loss on large codebases. STEP 1: enumerate the feature-module list (slug + entry + boundary resources) WITHOUT diving into function bodies.
6
+ NEVER hand-author files under `resources/project-architecture/**/*.html`. The atlas state lives in YAML under `resources/project-architecture/atlas/` and every mutation goes through `apltk architecture <verb> ...`; the CLI owns layout, no-overlap, DOM, CSS, ARIA, and pan/zoom — both the macro SVG and each sub-module's internal-dataflow diagram are zoomable.
7
+ Read strategy to avoid context loss AND to enforce a hard responsibility split. STEP 1: enumerate the feature-module list (slug + entry + boundary resources) WITHOUT diving into function bodies.
8
8
  STEP 2: branch by environment.
9
- (2A, preferred) if subagents are available, dispatch ONE read-only subagent per feature; each returns ONLY a structured summary (sub-modules with kind/role; per sub-module: function I/O, variables-with-business-purpose, internal dataflow steps, errors raised; outbound edges to other features). The main agent collects every summary first, then runs the CLI in one batched pass.
10
- (2B, no subagents) process features ONE AT A TIME — deep-read feature A, IMMEDIATELY drive the CLI (`feature add`, `submodule add` x N, `function add` / `variable add` / `dataflow add` / `error add` rows, intra-feature `edge add`), drop A's function-level details from working memory, then move to feature B. For cross-feature edges pointing at a not-yet-declared feature, declare a placeholder with `feature add --slug <future> --title <future>` first and refine its title/story on a later pass.
9
+ (2A, preferred) if subagents are available, dispatch ONE write-capable subagent per feature. Each subagent deep-reads ITS OWN feature and declares EVERY intra-feature interaction itself via `apltk architecture ... --feature <slug>`: `submodule add` (with the right `--kind`), `function add` / `variable add` / `dataflow add` (ordered — these steps carry the variable state transitions through the flow) / `error add` rows on every sub-module, and **every intra-feature edge** between its sub-modules (function calls, returns, error propagation, rollback / compensation — model failure / rollback paths as `--kind failure` edges and/or as ordered `dataflow add` steps). The subagent returns ONLY (i) sub-module list (slug + kind + one-line role) and (ii) outbound boundaries to other features' sub-modules (direction + edge kind + suggested label). The main agent never re-reads source for a delegated feature and never re-declares its components — it batches ONLY cross-feature `edge add` (and any shared `actor` / `meta`) from the aggregated outbound summaries, then runs `apltk architecture render` and `apltk architecture validate`.
10
+ (2B, no subagents) process features ONE AT A TIME — deep-read feature A, IMMEDIATELY drive the CLI (`feature add`, `submodule add` x N, `function add` / `variable add` / `dataflow add` / `error add` rows, intra-feature `edge add` including failure / rollback edges), drop A's function-level details from working memory, then move to feature B. For cross-feature edges pointing at a not-yet-declared feature, declare a placeholder with `feature add --slug <future> --title <future>` first and refine its title/story on a later pass.
11
11
  CLI verbs to declare components (always kebab-case slugs; pass `--no-render` to batch then call `apltk architecture render` once at the end):
12
12
  `apltk architecture meta set --title "..." --summary "..."`,
13
13
  `apltk architecture actor add --id <kebab> --label "..."`,
@@ -15,8 +15,8 @@ interface:
15
15
  `apltk architecture submodule add --feature X --slug Y --kind ui|api|service|db|pure-fn|queue|external --role "..."`,
16
16
  `apltk architecture function add --feature X --submodule Y --name fn --in "T1, T2" --out "R | ErrX" --side pure|io|write|tx|lock|network --purpose "..."`,
17
17
  `apltk architecture variable add --feature X --submodule Y --name v --type T --scope call|tx|persist|instance|loop --purpose "..."`,
18
- `apltk architecture dataflow add --feature X --submodule Y --step "..." [--at N]`,
18
+ `apltk architecture dataflow add --feature X --submodule Y --step "..." [--fn <declared-fn>] [--reads "v1,v2"] [--writes "v3,v4"] [--at N]` (use `--fn` to surface function-to-function flow inside the sub-module; use `--reads` / `--writes` to surface variable state transitions — every name MUST match a `function`/`variable` already declared in the SAME sub-module or `validate` fails),
19
19
  `apltk architecture error add --feature X --submodule Y --name ErrCode --when "..." --means "..."`,
20
20
  `apltk architecture edge add --from <feature>[/sub] --to <feature>[/sub] --kind call|return|data-row|failure --label "..." [--id <stable>]`.
21
21
  Intra-feature edges (both endpoints in the same feature with sub-modules) land in the feature YAML; cross-feature edges land in `atlas.index.yaml`. After the last mutation run `apltk architecture validate` — must return OK; resolve every reported error before reporting completion.
22
- Each sub-module page describes ONLY itself (`sub-io` function I/O + `sub-vars` variables-with-business-purpose + `sub-dataflow` internal flow + `sub-errors` local errors) — express any cross-boundary interaction as a macro edge, never as sub-module page prose. Anchor every declaration to a concrete path / symbol / SQL / config; mark TBD when evidence is missing; record scanned roots and deliberate omissions in `meta.summary`. Report which read strategy (2A or 2B) was actually used and the location of the rendered atlas (`resources/project-architecture/index.html`).
22
+ Each sub-module page describes ONLY itself (`sub-io` function I/O + `sub-vars` variables-with-business-purpose + `sub-dataflow` zoomable internal flow with fn pill + reads/writes chips + `sub-errors` local errors) — express any cross-boundary interaction as an edge, never as sub-module page prose. Two acceptance criteria gate completion: (1) the macro diagram clearly shows feature×sub-module relationships — data flow, call/return interaction logic, error handling, rollback — all as `--kind call|return|data-row|failure` edges (no cross-boundary path expressed only as prose); (2) every non-trivial sub-module's internal diagram clearly shows function-level interactions — `dataflow add --fn <declared-fn>` for function-to-function flow + `--reads` / `--writes` for variable state transitions through the run. Anchor every declaration to a concrete path / symbol / SQL / config; mark TBD when evidence is missing; record scanned roots and deliberate omissions in `meta.summary`. Report which read strategy (2A or 2B) was actually used and the location of the rendered atlas (`resources/project-architecture/index.html`).
@@ -74,11 +74,17 @@ p { line-height: 1.55; color: var(--text); }
74
74
  /* ---- SVG macro ---- */
75
75
  .m-cluster__bg { fill: rgba(15, 23, 42, 0.55); stroke: var(--border); stroke-width: 1; }
76
76
  .m-cluster__title { font-family: ui-sans-serif, system-ui, sans-serif; font-size: 14px; fill: var(--text); font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase; }
77
- .m-node rect { fill: var(--panel-soft); stroke: var(--border); stroke-width: 1; transition: stroke 120ms ease; }
78
- .m-node:hover rect { stroke: var(--accent); }
77
+ .m-node { cursor: pointer; }
78
+ .m-node rect { fill: var(--panel-soft); stroke: var(--border); stroke-width: 1; transition: stroke 120ms ease, fill 120ms ease; }
79
+ .m-node:hover rect,
80
+ .m-node:focus rect,
81
+ .m-node:focus-visible rect { stroke: var(--accent); stroke-width: 1.6; fill: rgba(56, 189, 248, 0.08); }
82
+ .m-node:focus { outline: none; }
79
83
  .m-node__title { font-size: 13px; font-weight: 600; fill: var(--text); }
80
84
  .m-node__kind { font-size: 11px; fill: var(--muted); }
81
85
  .m-node__role { font-size: 11px; fill: var(--muted); }
86
+ .m-node:hover .m-node__title,
87
+ .m-node:focus .m-node__title { fill: var(--accent); }
82
88
 
83
89
  .m-node--ui rect { stroke: var(--kind-ui); }
84
90
  .m-node--api rect { stroke: var(--kind-api); }
@@ -133,8 +139,23 @@ p { line-height: 1.55; color: var(--text); }
133
139
 
134
140
  .sub-section__empty { color: var(--muted); font-style: italic; font-size: 13px; }
135
141
 
136
- .sub-dataflow__svg { width: 100%; max-width: 480px; }
137
- .sub-dataflow__step rect { fill: var(--panel); stroke: var(--border); }
138
- .sub-dataflow__step text { fill: var(--text); font-size: 13px; }
139
- .sub-dataflow__arrow { stroke: var(--muted); stroke-width: 1.4; }
142
+ .sub-dataflow__canvas { position: relative; background: var(--panel); border: 1px solid var(--border); border-radius: 12px; padding: 16px; }
143
+ .sub-dataflow__toolbar { position: absolute; top: 16px; right: 16px; display: flex; gap: 4px; z-index: 2; }
144
+ .sub-dataflow__toolbar button { background: var(--panel-soft); color: var(--text); border: 1px solid var(--border); padding: 4px 12px; border-radius: 6px; cursor: pointer; font-size: 13px; }
145
+ .sub-dataflow__toolbar button:hover { border-color: var(--accent); color: var(--accent); }
146
+ .sub-dataflow__viewport { width: 100%; max-height: 60vh; overflow: hidden; border-radius: 8px; background: #0b1220; }
147
+ .sub-dataflow__viewport.is-grabbing { cursor: grabbing; }
148
+ .sub-dataflow__viewport:not(.is-grabbing) { cursor: grab; }
149
+ .sub-dataflow__svg { width: 100%; height: auto; max-height: 60vh; display: block; user-select: none; touch-action: none; }
150
+ .sub-dataflow__badge { fill: var(--panel-soft); stroke: var(--accent); stroke-width: 1.4; }
151
+ .sub-dataflow__badge-text { font-family: ui-sans-serif, system-ui, sans-serif; font-size: 13px; font-weight: 600; fill: var(--accent); }
152
+ .sub-dataflow__step .sub-dataflow__box { fill: var(--panel-soft); stroke: var(--border); stroke-width: 1; }
153
+ .sub-dataflow__step:hover .sub-dataflow__box { stroke: var(--accent); }
154
+ .sub-dataflow__text { font-family: ui-sans-serif, system-ui, sans-serif; font-size: 14px; fill: var(--text); }
155
+ .sub-dataflow__arrow { stroke: var(--muted); stroke-width: 1.6; }
156
+ .sub-dataflow__fn-bg { fill: rgba(56, 189, 248, 0.12); stroke: var(--accent); stroke-width: 1; }
157
+ .sub-dataflow__fn-text { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11px; font-weight: 600; fill: var(--accent); letter-spacing: 0.02em; }
158
+ .sub-dataflow__chip { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11px; font-weight: 500; }
159
+ .sub-dataflow__chip--reads { fill: var(--kind-service); }
160
+ .sub-dataflow__chip--writes { fill: var(--kind-db); }
140
161
  .sub-dataflow__empty { color: var(--muted); font-style: italic; }
@@ -1,93 +1,136 @@
1
- /* viewer.client.js — pan/zoom for the macro atlas SVG. No deps; wires
2
- * onto any element marked [data-pan-zoom-viewport] containing one
3
- * [data-atlas-svg] SVG. Mouse wheel zooms around the cursor; drag pans;
4
- * toolbar buttons handle +/-/Fit; keyboard arrows pan. */
1
+ /* viewer.client.js — pan/zoom for every atlas SVG on the page.
2
+ *
3
+ * Each `[data-pan-zoom-viewport]` element gets its own state and
4
+ * handlers, so a single page can host the macro atlas SVG AND
5
+ * sub-module-internal dataflow SVGs simultaneously. Toolbar buttons
6
+ * (`[data-pan-zoom="zoom-in|zoom-out|fit"]`) are scoped to the
7
+ * containing `[data-pan-zoom-container]` (falls back to the viewport's
8
+ * direct parent), so multiple toolbars on one page do not collide.
9
+ * Keyboard shortcuts (`←` / `→` / `↑` / `↓` / `+` / `−` / `0`) drive
10
+ * the page's first viewport — the "primary" diagram of that page
11
+ * (macro SVG on `index.html`; sub-dataflow SVG on a sub-module page).
12
+ */
5
13
 
6
14
  (function () {
7
15
  'use strict';
8
16
 
9
- const viewport = document.querySelector('[data-pan-zoom-viewport]');
10
- if (!viewport) return;
11
- const svg = viewport.querySelector('[data-atlas-svg]');
12
- if (!svg) return;
17
+ const viewports = Array.from(document.querySelectorAll('[data-pan-zoom-viewport]'));
18
+ const controllers = viewports.map(setupViewport).filter(Boolean);
19
+ if (controllers.length === 0) return;
13
20
 
14
- const initial = svg.getAttribute('viewBox');
15
- if (!initial) return;
16
- const [ix, iy, iw, ih] = initial.split(/\s+/).map(Number);
17
- const state = { x: ix, y: iy, w: iw, h: ih };
18
-
19
- function apply() {
20
- svg.setAttribute('viewBox', `${state.x} ${state.y} ${state.w} ${state.h}`);
21
- }
21
+ const primary = controllers[0];
22
+ document.addEventListener('keydown', function (evt) {
23
+ if (evt.target && (evt.target.tagName === 'INPUT' || evt.target.tagName === 'TEXTAREA')) return;
24
+ if (evt.key === 'ArrowLeft') { primary.pan(-1, 0); }
25
+ else if (evt.key === 'ArrowRight') { primary.pan(1, 0); }
26
+ else if (evt.key === 'ArrowUp') { primary.pan(0, -1); }
27
+ else if (evt.key === 'ArrowDown') { primary.pan(0, 1); }
28
+ else if (evt.key === '+' || evt.key === '=') { primary.zoom(1 / 1.2); }
29
+ else if (evt.key === '-' || evt.key === '_') { primary.zoom(1.2); }
30
+ else if (evt.key === '0') { primary.fit(); }
31
+ });
22
32
 
23
- function zoom(factor, cx, cy) {
24
- const newW = Math.max(40, Math.min(state.w * factor, iw * 8));
25
- const newH = newW * (state.h / state.w);
26
- if (cx == null) { cx = state.x + state.w / 2; cy = state.y + state.h / 2; }
27
- state.x = cx - (cx - state.x) * (newW / state.w);
28
- state.y = cy - (cy - state.y) * (newH / state.h);
29
- state.w = newW;
30
- state.h = newH;
31
- apply();
32
- }
33
+ function setupViewport(viewport) {
34
+ const svg = viewport.querySelector('[data-atlas-svg]');
35
+ if (!svg) return null;
36
+ const initial = svg.getAttribute('viewBox');
37
+ if (!initial) return null;
38
+ const [ix, iy, iw, ih] = initial.split(/\s+/).map(Number);
39
+ const state = { x: ix, y: iy, w: iw, h: ih };
33
40
 
34
- function clientToSvg(evt) {
35
- const rect = svg.getBoundingClientRect();
36
- const xRatio = (evt.clientX - rect.left) / rect.width;
37
- const yRatio = (evt.clientY - rect.top) / rect.height;
38
- return { x: state.x + xRatio * state.w, y: state.y + yRatio * state.h };
39
- }
41
+ function apply() {
42
+ svg.setAttribute('viewBox', `${state.x} ${state.y} ${state.w} ${state.h}`);
43
+ }
44
+ function fit() {
45
+ state.x = ix; state.y = iy; state.w = iw; state.h = ih;
46
+ apply();
47
+ }
48
+ function zoom(factor, cx, cy) {
49
+ const newW = Math.max(40, Math.min(state.w * factor, iw * 8));
50
+ const newH = newW * (state.h / state.w);
51
+ if (cx == null) { cx = state.x + state.w / 2; cy = state.y + state.h / 2; }
52
+ state.x = cx - (cx - state.x) * (newW / state.w);
53
+ state.y = cy - (cy - state.y) * (newH / state.h);
54
+ state.w = newW;
55
+ state.h = newH;
56
+ apply();
57
+ }
58
+ function pan(dirX, dirY) {
59
+ const stepX = state.w * 0.08;
60
+ const stepY = state.h * 0.08;
61
+ state.x += dirX * stepX;
62
+ state.y += dirY * stepY;
63
+ apply();
64
+ }
65
+ function clientToSvg(evt) {
66
+ const rect = svg.getBoundingClientRect();
67
+ const xRatio = (evt.clientX - rect.left) / rect.width;
68
+ const yRatio = (evt.clientY - rect.top) / rect.height;
69
+ return { x: state.x + xRatio * state.w, y: state.y + yRatio * state.h };
70
+ }
40
71
 
41
- viewport.addEventListener('wheel', function (evt) {
42
- if (!evt.ctrlKey && !evt.metaKey && Math.abs(evt.deltaY) < 4 && Math.abs(evt.deltaX) < 4) return;
43
- evt.preventDefault();
44
- const factor = evt.deltaY > 0 ? 1.1 : 1 / 1.1;
45
- const pt = clientToSvg(evt);
46
- zoom(factor, pt.x, pt.y);
47
- }, { passive: false });
72
+ viewport.addEventListener('wheel', function (evt) {
73
+ if (!evt.ctrlKey && !evt.metaKey && Math.abs(evt.deltaY) < 4 && Math.abs(evt.deltaX) < 4) return;
74
+ evt.preventDefault();
75
+ const factor = evt.deltaY > 0 ? 1.1 : 1 / 1.1;
76
+ const pt = clientToSvg(evt);
77
+ zoom(factor, pt.x, pt.y);
78
+ }, { passive: false });
48
79
 
49
- let dragging = null;
50
- viewport.addEventListener('pointerdown', function (evt) {
51
- if (evt.button !== 0) return;
52
- dragging = { x: evt.clientX, y: evt.clientY };
53
- viewport.classList.add('is-grabbing');
54
- viewport.setPointerCapture(evt.pointerId);
55
- });
56
- viewport.addEventListener('pointermove', function (evt) {
57
- if (!dragging) return;
58
- const rect = svg.getBoundingClientRect();
59
- const dx = ((evt.clientX - dragging.x) / rect.width) * state.w;
60
- const dy = ((evt.clientY - dragging.y) / rect.height) * state.h;
61
- state.x -= dx;
62
- state.y -= dy;
63
- dragging = { x: evt.clientX, y: evt.clientY };
64
- apply();
65
- });
66
- function endDrag(evt) {
67
- if (!dragging) return;
68
- dragging = null;
69
- viewport.classList.remove('is-grabbing');
70
- try { viewport.releasePointerCapture(evt.pointerId); } catch (e) { /* ignore */ }
71
- }
72
- viewport.addEventListener('pointerup', endDrag);
73
- viewport.addEventListener('pointercancel', endDrag);
74
- viewport.addEventListener('pointerleave', endDrag);
80
+ // Drag-pan: defer the pointer capture (and the "is-grabbing" class)
81
+ // until the pointer actually moves past a small threshold. Without
82
+ // this, a single click on an SVG <a> (sub-module link) would be
83
+ // captured by the viewport and never reach the link.
84
+ const DRAG_THRESHOLD_PX = 4;
85
+ let pending = null;
86
+ let dragging = null;
87
+ viewport.addEventListener('pointerdown', function (evt) {
88
+ if (evt.button !== 0) return;
89
+ pending = { x: evt.clientX, y: evt.clientY, pointerId: evt.pointerId };
90
+ });
91
+ viewport.addEventListener('pointermove', function (evt) {
92
+ if (!dragging && pending && pending.pointerId === evt.pointerId) {
93
+ const dx = evt.clientX - pending.x;
94
+ const dy = evt.clientY - pending.y;
95
+ if (Math.hypot(dx, dy) < DRAG_THRESHOLD_PX) return;
96
+ dragging = { x: pending.x, y: pending.y, pointerId: pending.pointerId };
97
+ pending = null;
98
+ viewport.classList.add('is-grabbing');
99
+ try { viewport.setPointerCapture(dragging.pointerId); } catch (e) { /* ignore */ }
100
+ }
101
+ if (!dragging || dragging.pointerId !== evt.pointerId) return;
102
+ evt.preventDefault();
103
+ const rect = svg.getBoundingClientRect();
104
+ const dx = ((evt.clientX - dragging.x) / rect.width) * state.w;
105
+ const dy = ((evt.clientY - dragging.y) / rect.height) * state.h;
106
+ state.x -= dx;
107
+ state.y -= dy;
108
+ dragging.x = evt.clientX;
109
+ dragging.y = evt.clientY;
110
+ apply();
111
+ });
112
+ function endDrag(evt) {
113
+ pending = null;
114
+ if (!dragging) return;
115
+ const draggedId = dragging.pointerId;
116
+ dragging = null;
117
+ viewport.classList.remove('is-grabbing');
118
+ try { viewport.releasePointerCapture(draggedId); } catch (e) { /* ignore */ }
119
+ // Suppress the synthetic click that would follow a drag-release
120
+ // on top of a sub-module <a>; only the no-movement case should
121
+ // navigate.
122
+ const swallow = (e) => { e.preventDefault(); e.stopPropagation(); };
123
+ viewport.addEventListener('click', swallow, { capture: true, once: true });
124
+ }
125
+ viewport.addEventListener('pointerup', endDrag);
126
+ viewport.addEventListener('pointercancel', endDrag);
127
+ viewport.addEventListener('pointerleave', endDrag);
75
128
 
76
- document.addEventListener('keydown', function (evt) {
77
- if (evt.target && (evt.target.tagName === 'INPUT' || evt.target.tagName === 'TEXTAREA')) return;
78
- const step = state.w * 0.08;
79
- if (evt.key === 'ArrowLeft') { state.x -= step; apply(); }
80
- else if (evt.key === 'ArrowRight') { state.x += step; apply(); }
81
- else if (evt.key === 'ArrowUp') { state.y -= step; apply(); }
82
- else if (evt.key === 'ArrowDown') { state.y += step; apply(); }
83
- else if (evt.key === '+' || evt.key === '=') { zoom(1 / 1.2); }
84
- else if (evt.key === '-' || evt.key === '_') { zoom(1.2); }
85
- else if (evt.key === '0') { state.x = ix; state.y = iy; state.w = iw; state.h = ih; apply(); }
86
- });
129
+ const container = viewport.closest('[data-pan-zoom-container]') || viewport.parentElement || document;
130
+ container.querySelectorAll('[data-pan-zoom="zoom-in"]').forEach((btn) => btn.addEventListener('click', () => zoom(1 / 1.2)));
131
+ container.querySelectorAll('[data-pan-zoom="zoom-out"]').forEach((btn) => btn.addEventListener('click', () => zoom(1.2)));
132
+ container.querySelectorAll('[data-pan-zoom="fit"]').forEach((btn) => btn.addEventListener('click', fit));
87
133
 
88
- document.querySelectorAll('[data-pan-zoom="zoom-in"]').forEach((btn) => btn.addEventListener('click', () => zoom(1 / 1.2)));
89
- document.querySelectorAll('[data-pan-zoom="zoom-out"]').forEach((btn) => btn.addEventListener('click', () => zoom(1.2)));
90
- document.querySelectorAll('[data-pan-zoom="fit"]').forEach((btn) => btn.addEventListener('click', () => {
91
- state.x = ix; state.y = iy; state.w = iw; state.h = ih; apply();
92
- }));
134
+ return { zoom, fit, pan };
135
+ }
93
136
  })();
@@ -77,6 +77,8 @@ Examples:
77
77
  apltk architecture feature add --slug register --title "User registration" --story "..."
78
78
  apltk architecture submodule add --feature register --slug api --kind api --role "HTTP endpoint"
79
79
  apltk architecture function add --feature register --submodule api --name handlePost --side network --purpose "..."
80
+ apltk architecture variable add --feature register --submodule api --name token --type "string" --scope call --purpose "..."
81
+ apltk architecture dataflow add --feature register --submodule api --step "Validate body" --fn handlePost --reads "body" --writes "token"
80
82
  apltk architecture --spec docs/plans/2026-05-11/add-2fa submodule set --feature register --slug api --role "..."
81
83
  apltk architecture validate
82
84
  apltk architecture diff
@@ -449,13 +451,14 @@ async function verbDataflow(action, flags, projectRoot) {
449
451
  sub.dataflow = sub.dataflow || [];
450
452
  if (action === 'add') {
451
453
  const step = String(requireFlag(flags, 'step'));
454
+ const item = buildDataflowItem(step, flags);
452
455
  const atRaw = flags.at;
453
456
  if (atRaw !== undefined) {
454
457
  const at = Number(atRaw);
455
458
  if (!Number.isFinite(at) || at < 0) throw new Error('--at must be a non-negative integer');
456
- sub.dataflow.splice(at, 0, step);
459
+ sub.dataflow.splice(at, 0, item);
457
460
  } else {
458
- sub.dataflow.push(step);
461
+ sub.dataflow.push(item);
459
462
  }
460
463
  } else if (action === 'remove') {
461
464
  if (flags.at !== undefined) {
@@ -464,7 +467,7 @@ async function verbDataflow(action, flags, projectRoot) {
464
467
  sub.dataflow.splice(at, 1);
465
468
  } else {
466
469
  const step = String(requireFlag(flags, 'step'));
467
- sub.dataflow = sub.dataflow.filter((s) => s !== step);
470
+ sub.dataflow = sub.dataflow.filter((s) => stepText(s) !== step);
468
471
  }
469
472
  } else if (action === 'reorder') {
470
473
  const from = Number(requireFlag(flags, 'from'));
@@ -481,6 +484,31 @@ async function verbDataflow(action, flags, projectRoot) {
481
484
  });
482
485
  }
483
486
 
487
+ function stepText(item) {
488
+ return typeof item === 'string' ? item : (item && typeof item.step === 'string' ? item.step : '');
489
+ }
490
+
491
+ function parseNameList(raw) {
492
+ if (raw === undefined || raw === null) return undefined;
493
+ return String(raw)
494
+ .split(',')
495
+ .map((s) => s.trim())
496
+ .filter(Boolean);
497
+ }
498
+
499
+ function buildDataflowItem(step, flags) {
500
+ const fn = flags.fn === undefined ? undefined : String(flags.fn).trim();
501
+ const reads = parseNameList(flags.reads);
502
+ const writes = parseNameList(flags.writes);
503
+ const annotated = (fn && fn.length > 0) || (reads && reads.length > 0) || (writes && writes.length > 0);
504
+ if (!annotated) return step;
505
+ const item = { step };
506
+ if (fn) item.fn = fn;
507
+ if (reads && reads.length > 0) item.reads = reads;
508
+ if (writes && writes.length > 0) item.writes = writes;
509
+ return item;
510
+ }
511
+
484
512
  async function verbError(action, flags, projectRoot) {
485
513
  const featureSlug = String(requireFlag(flags, 'feature'));
486
514
  const subSlug = String(requireFlag(flags, 'submodule'));