cap-pro 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (275) hide show
  1. package/.claude-plugin/README.md +26 -0
  2. package/.claude-plugin/marketplace.json +24 -0
  3. package/.claude-plugin/plugin.json +24 -0
  4. package/LICENSE +21 -0
  5. package/README.ja-JP.md +834 -0
  6. package/README.ko-KR.md +823 -0
  7. package/README.md +806 -0
  8. package/README.pt-BR.md +452 -0
  9. package/README.zh-CN.md +800 -0
  10. package/agents/cap-architect.md +269 -0
  11. package/agents/cap-brainstormer.md +207 -0
  12. package/agents/cap-curator.md +276 -0
  13. package/agents/cap-debugger.md +365 -0
  14. package/agents/cap-designer.md +246 -0
  15. package/agents/cap-historian.md +464 -0
  16. package/agents/cap-migrator.md +291 -0
  17. package/agents/cap-prototyper.md +197 -0
  18. package/agents/cap-validator.md +308 -0
  19. package/bin/install.js +5433 -0
  20. package/cap/bin/cap-tools.cjs +853 -0
  21. package/cap/bin/lib/arc-scanner.cjs +344 -0
  22. package/cap/bin/lib/cap-affinity-engine.cjs +862 -0
  23. package/cap/bin/lib/cap-anchor.cjs +228 -0
  24. package/cap/bin/lib/cap-annotation-writer.cjs +340 -0
  25. package/cap/bin/lib/cap-checkpoint.cjs +434 -0
  26. package/cap/bin/lib/cap-cluster-detect.cjs +945 -0
  27. package/cap/bin/lib/cap-cluster-display.cjs +52 -0
  28. package/cap/bin/lib/cap-cluster-format.cjs +245 -0
  29. package/cap/bin/lib/cap-cluster-helpers.cjs +295 -0
  30. package/cap/bin/lib/cap-cluster-io.cjs +212 -0
  31. package/cap/bin/lib/cap-completeness.cjs +540 -0
  32. package/cap/bin/lib/cap-deps.cjs +583 -0
  33. package/cap/bin/lib/cap-design-families.cjs +332 -0
  34. package/cap/bin/lib/cap-design.cjs +966 -0
  35. package/cap/bin/lib/cap-divergence-detector.cjs +400 -0
  36. package/cap/bin/lib/cap-doctor.cjs +752 -0
  37. package/cap/bin/lib/cap-feature-map-internals.cjs +19 -0
  38. package/cap/bin/lib/cap-feature-map-migrate.cjs +335 -0
  39. package/cap/bin/lib/cap-feature-map-monorepo.cjs +885 -0
  40. package/cap/bin/lib/cap-feature-map-shard.cjs +315 -0
  41. package/cap/bin/lib/cap-feature-map.cjs +1943 -0
  42. package/cap/bin/lib/cap-fitness-score.cjs +1075 -0
  43. package/cap/bin/lib/cap-impact-analysis.cjs +652 -0
  44. package/cap/bin/lib/cap-learn-review.cjs +1072 -0
  45. package/cap/bin/lib/cap-learning-signals.cjs +627 -0
  46. package/cap/bin/lib/cap-loader.cjs +227 -0
  47. package/cap/bin/lib/cap-logger.cjs +57 -0
  48. package/cap/bin/lib/cap-memory-bridge.cjs +764 -0
  49. package/cap/bin/lib/cap-memory-confidence.cjs +452 -0
  50. package/cap/bin/lib/cap-memory-dir.cjs +987 -0
  51. package/cap/bin/lib/cap-memory-engine.cjs +698 -0
  52. package/cap/bin/lib/cap-memory-extends.cjs +398 -0
  53. package/cap/bin/lib/cap-memory-graph.cjs +790 -0
  54. package/cap/bin/lib/cap-memory-migrate.cjs +2015 -0
  55. package/cap/bin/lib/cap-memory-pin.cjs +183 -0
  56. package/cap/bin/lib/cap-memory-platform.cjs +490 -0
  57. package/cap/bin/lib/cap-memory-prune.cjs +707 -0
  58. package/cap/bin/lib/cap-memory-schema.cjs +812 -0
  59. package/cap/bin/lib/cap-migrate-tags.cjs +309 -0
  60. package/cap/bin/lib/cap-migrate.cjs +540 -0
  61. package/cap/bin/lib/cap-pattern-apply.cjs +1203 -0
  62. package/cap/bin/lib/cap-pattern-pipeline.cjs +1034 -0
  63. package/cap/bin/lib/cap-plugin-manifest.cjs +80 -0
  64. package/cap/bin/lib/cap-realtime-affinity.cjs +399 -0
  65. package/cap/bin/lib/cap-reconcile.cjs +570 -0
  66. package/cap/bin/lib/cap-research-gate.cjs +218 -0
  67. package/cap/bin/lib/cap-scope-filter.cjs +402 -0
  68. package/cap/bin/lib/cap-semantic-pipeline.cjs +1038 -0
  69. package/cap/bin/lib/cap-session-extract.cjs +987 -0
  70. package/cap/bin/lib/cap-session.cjs +445 -0
  71. package/cap/bin/lib/cap-snapshot-linkage.cjs +963 -0
  72. package/cap/bin/lib/cap-stack-docs.cjs +646 -0
  73. package/cap/bin/lib/cap-tag-observer.cjs +371 -0
  74. package/cap/bin/lib/cap-tag-scanner.cjs +1766 -0
  75. package/cap/bin/lib/cap-telemetry.cjs +466 -0
  76. package/cap/bin/lib/cap-test-audit.cjs +1438 -0
  77. package/cap/bin/lib/cap-thread-migrator.cjs +307 -0
  78. package/cap/bin/lib/cap-thread-synthesis.cjs +545 -0
  79. package/cap/bin/lib/cap-thread-tracker.cjs +519 -0
  80. package/cap/bin/lib/cap-trace.cjs +399 -0
  81. package/cap/bin/lib/cap-trust-mode.cjs +336 -0
  82. package/cap/bin/lib/cap-ui-design-editor.cjs +642 -0
  83. package/cap/bin/lib/cap-ui-mind-map.cjs +712 -0
  84. package/cap/bin/lib/cap-ui-thread-nav.cjs +693 -0
  85. package/cap/bin/lib/cap-ui.cjs +1245 -0
  86. package/cap/bin/lib/cap-upgrade.cjs +1028 -0
  87. package/cap/bin/lib/cli/arg-helpers.cjs +49 -0
  88. package/cap/bin/lib/cli/frontmatter-router.cjs +31 -0
  89. package/cap/bin/lib/cli/init-router.cjs +68 -0
  90. package/cap/bin/lib/cli/phase-router.cjs +102 -0
  91. package/cap/bin/lib/cli/state-router.cjs +61 -0
  92. package/cap/bin/lib/cli/template-router.cjs +37 -0
  93. package/cap/bin/lib/cli/uat-router.cjs +29 -0
  94. package/cap/bin/lib/cli/validation-router.cjs +26 -0
  95. package/cap/bin/lib/cli/verification-router.cjs +31 -0
  96. package/cap/bin/lib/cli/workstream-router.cjs +39 -0
  97. package/cap/bin/lib/commands.cjs +961 -0
  98. package/cap/bin/lib/config.cjs +467 -0
  99. package/cap/bin/lib/convention-reader.cjs +258 -0
  100. package/cap/bin/lib/core.cjs +1241 -0
  101. package/cap/bin/lib/feature-aggregator.cjs +423 -0
  102. package/cap/bin/lib/frontmatter.cjs +337 -0
  103. package/cap/bin/lib/init.cjs +1443 -0
  104. package/cap/bin/lib/manifest-generator.cjs +383 -0
  105. package/cap/bin/lib/milestone.cjs +253 -0
  106. package/cap/bin/lib/model-profiles.cjs +69 -0
  107. package/cap/bin/lib/monorepo-context.cjs +226 -0
  108. package/cap/bin/lib/monorepo-migrator.cjs +509 -0
  109. package/cap/bin/lib/phase.cjs +889 -0
  110. package/cap/bin/lib/profile-output.cjs +989 -0
  111. package/cap/bin/lib/profile-pipeline.cjs +540 -0
  112. package/cap/bin/lib/roadmap.cjs +330 -0
  113. package/cap/bin/lib/security.cjs +394 -0
  114. package/cap/bin/lib/session-manager.cjs +292 -0
  115. package/cap/bin/lib/skeleton-generator.cjs +179 -0
  116. package/cap/bin/lib/state.cjs +1032 -0
  117. package/cap/bin/lib/template.cjs +231 -0
  118. package/cap/bin/lib/test-detector.cjs +62 -0
  119. package/cap/bin/lib/uat.cjs +283 -0
  120. package/cap/bin/lib/verify.cjs +889 -0
  121. package/cap/bin/lib/workspace-detector.cjs +371 -0
  122. package/cap/bin/lib/workstream.cjs +492 -0
  123. package/cap/commands/gsd/workstreams.md +63 -0
  124. package/cap/references/arc-standard.md +315 -0
  125. package/cap/references/cap-agent-architecture.md +101 -0
  126. package/cap/references/cap-gitignore-template +9 -0
  127. package/cap/references/cap-zero-deps.md +158 -0
  128. package/cap/references/checkpoints.md +778 -0
  129. package/cap/references/continuation-format.md +249 -0
  130. package/cap/references/contract-test-templates.md +312 -0
  131. package/cap/references/feature-map-template.md +25 -0
  132. package/cap/references/git-integration.md +295 -0
  133. package/cap/references/git-planning-commit.md +38 -0
  134. package/cap/references/model-profiles.md +174 -0
  135. package/cap/references/phase-numbering.md +126 -0
  136. package/cap/references/planning-config.md +202 -0
  137. package/cap/references/property-test-templates.md +316 -0
  138. package/cap/references/security-test-templates.md +347 -0
  139. package/cap/references/session-template.json +8 -0
  140. package/cap/references/tdd.md +263 -0
  141. package/cap/references/user-profiling.md +681 -0
  142. package/cap/references/verification-patterns.md +612 -0
  143. package/cap/templates/UAT.md +265 -0
  144. package/cap/templates/claude-md.md +175 -0
  145. package/cap/templates/codebase/architecture.md +255 -0
  146. package/cap/templates/codebase/concerns.md +310 -0
  147. package/cap/templates/codebase/conventions.md +307 -0
  148. package/cap/templates/codebase/integrations.md +280 -0
  149. package/cap/templates/codebase/stack.md +186 -0
  150. package/cap/templates/codebase/structure.md +285 -0
  151. package/cap/templates/codebase/testing.md +480 -0
  152. package/cap/templates/config.json +44 -0
  153. package/cap/templates/context.md +352 -0
  154. package/cap/templates/continue-here.md +78 -0
  155. package/cap/templates/copilot-instructions.md +7 -0
  156. package/cap/templates/debug-subagent-prompt.md +91 -0
  157. package/cap/templates/discussion-log.md +63 -0
  158. package/cap/templates/milestone-archive.md +123 -0
  159. package/cap/templates/milestone.md +115 -0
  160. package/cap/templates/phase-prompt.md +610 -0
  161. package/cap/templates/planner-subagent-prompt.md +117 -0
  162. package/cap/templates/project.md +186 -0
  163. package/cap/templates/requirements.md +231 -0
  164. package/cap/templates/research-project/ARCHITECTURE.md +204 -0
  165. package/cap/templates/research-project/FEATURES.md +147 -0
  166. package/cap/templates/research-project/PITFALLS.md +200 -0
  167. package/cap/templates/research-project/STACK.md +120 -0
  168. package/cap/templates/research-project/SUMMARY.md +170 -0
  169. package/cap/templates/research.md +552 -0
  170. package/cap/templates/roadmap.md +202 -0
  171. package/cap/templates/state.md +176 -0
  172. package/cap/templates/summary.md +364 -0
  173. package/cap/templates/user-preferences.md +498 -0
  174. package/cap/templates/verification-report.md +322 -0
  175. package/cap/workflows/add-phase.md +112 -0
  176. package/cap/workflows/add-tests.md +351 -0
  177. package/cap/workflows/add-todo.md +158 -0
  178. package/cap/workflows/audit-milestone.md +340 -0
  179. package/cap/workflows/audit-uat.md +109 -0
  180. package/cap/workflows/autonomous.md +891 -0
  181. package/cap/workflows/check-todos.md +177 -0
  182. package/cap/workflows/cleanup.md +152 -0
  183. package/cap/workflows/complete-milestone.md +767 -0
  184. package/cap/workflows/diagnose-issues.md +231 -0
  185. package/cap/workflows/discovery-phase.md +289 -0
  186. package/cap/workflows/discuss-phase-assumptions.md +653 -0
  187. package/cap/workflows/discuss-phase.md +1049 -0
  188. package/cap/workflows/do.md +104 -0
  189. package/cap/workflows/execute-phase.md +846 -0
  190. package/cap/workflows/execute-plan.md +514 -0
  191. package/cap/workflows/fast.md +105 -0
  192. package/cap/workflows/forensics.md +265 -0
  193. package/cap/workflows/health.md +181 -0
  194. package/cap/workflows/help.md +660 -0
  195. package/cap/workflows/insert-phase.md +130 -0
  196. package/cap/workflows/list-phase-assumptions.md +178 -0
  197. package/cap/workflows/list-workspaces.md +56 -0
  198. package/cap/workflows/manager.md +362 -0
  199. package/cap/workflows/map-codebase.md +377 -0
  200. package/cap/workflows/milestone-summary.md +223 -0
  201. package/cap/workflows/new-milestone.md +486 -0
  202. package/cap/workflows/new-project.md +1250 -0
  203. package/cap/workflows/new-workspace.md +237 -0
  204. package/cap/workflows/next.md +97 -0
  205. package/cap/workflows/node-repair.md +92 -0
  206. package/cap/workflows/note.md +156 -0
  207. package/cap/workflows/pause-work.md +176 -0
  208. package/cap/workflows/plan-milestone-gaps.md +273 -0
  209. package/cap/workflows/plan-phase.md +857 -0
  210. package/cap/workflows/plant-seed.md +169 -0
  211. package/cap/workflows/pr-branch.md +129 -0
  212. package/cap/workflows/profile-user.md +449 -0
  213. package/cap/workflows/progress.md +507 -0
  214. package/cap/workflows/quick.md +757 -0
  215. package/cap/workflows/remove-phase.md +155 -0
  216. package/cap/workflows/remove-workspace.md +90 -0
  217. package/cap/workflows/research-phase.md +82 -0
  218. package/cap/workflows/resume-project.md +326 -0
  219. package/cap/workflows/review.md +228 -0
  220. package/cap/workflows/session-report.md +146 -0
  221. package/cap/workflows/settings.md +283 -0
  222. package/cap/workflows/ship.md +228 -0
  223. package/cap/workflows/stats.md +60 -0
  224. package/cap/workflows/transition.md +671 -0
  225. package/cap/workflows/ui-phase.md +298 -0
  226. package/cap/workflows/ui-review.md +161 -0
  227. package/cap/workflows/update.md +323 -0
  228. package/cap/workflows/validate-phase.md +170 -0
  229. package/cap/workflows/verify-phase.md +254 -0
  230. package/cap/workflows/verify-work.md +637 -0
  231. package/commands/cap/annotate.md +165 -0
  232. package/commands/cap/brainstorm.md +393 -0
  233. package/commands/cap/checkpoint.md +106 -0
  234. package/commands/cap/completeness.md +94 -0
  235. package/commands/cap/continue.md +72 -0
  236. package/commands/cap/debug.md +588 -0
  237. package/commands/cap/deps.md +169 -0
  238. package/commands/cap/design.md +479 -0
  239. package/commands/cap/init.md +354 -0
  240. package/commands/cap/iterate.md +249 -0
  241. package/commands/cap/learn.md +459 -0
  242. package/commands/cap/memory.md +275 -0
  243. package/commands/cap/migrate-feature-map.md +91 -0
  244. package/commands/cap/migrate-memory.md +108 -0
  245. package/commands/cap/migrate-tags.md +91 -0
  246. package/commands/cap/migrate.md +131 -0
  247. package/commands/cap/prototype.md +510 -0
  248. package/commands/cap/reconcile.md +121 -0
  249. package/commands/cap/review.md +360 -0
  250. package/commands/cap/save.md +72 -0
  251. package/commands/cap/scan.md +404 -0
  252. package/commands/cap/start.md +356 -0
  253. package/commands/cap/status.md +118 -0
  254. package/commands/cap/test-audit.md +262 -0
  255. package/commands/cap/test.md +394 -0
  256. package/commands/cap/trace.md +133 -0
  257. package/commands/cap/ui.md +167 -0
  258. package/hooks/dist/cap-check-update.js +115 -0
  259. package/hooks/dist/cap-context-monitor.js +185 -0
  260. package/hooks/dist/cap-learn-review-hook.js +114 -0
  261. package/hooks/dist/cap-learning-hook.js +192 -0
  262. package/hooks/dist/cap-memory.js +299 -0
  263. package/hooks/dist/cap-prompt-guard.js +97 -0
  264. package/hooks/dist/cap-statusline.js +157 -0
  265. package/hooks/dist/cap-tag-observer.js +115 -0
  266. package/hooks/dist/cap-version-check.js +112 -0
  267. package/hooks/dist/cap-workflow-guard.js +175 -0
  268. package/hooks/hooks.json +55 -0
  269. package/package.json +58 -0
  270. package/scripts/base64-scan.sh +262 -0
  271. package/scripts/build-hooks.js +93 -0
  272. package/scripts/cap-removal-checklist.md +202 -0
  273. package/scripts/prompt-injection-scan.sh +199 -0
  274. package/scripts/run-tests.cjs +181 -0
  275. package/scripts/secret-scan.sh +227 -0
@@ -0,0 +1,712 @@
1
+ // @cap-context CAP v5 F-066 Tag Mind-Map Visualization — graph data derivation, deterministic force layout, SVG renderer, CSS, client JS.
2
+ // @cap-context Extracted from cap-ui.cjs as part of F-068 hand-off (cap-ui.cjs was 2245 LOC). Public API stays stable via re-exports from cap-ui.cjs.
3
+ // @cap-decision(F-068/split) Extracted as a standalone module so F-068 can add cap-ui-design-editor.cjs alongside without touching unrelated code.
4
+ // @cap-decision(F-066/D1) Mind-Map renders via handrolled SVG + vanilla force-directed layout — NO D3, NO vis.js, NO cytoscape. Keeps zero-deps purity intact at source and require-graph level.
5
+ // @cap-decision(F-066/D2) buildMindMapCss / buildMindMapJs are composable strings, joined by cap-ui.cjs into the full page output.
6
+ // @cap-constraint Zero external dependencies — node builtins only (here: none; pure string/number work).
7
+
8
+ 'use strict';
9
+
10
+ // @cap-feature(feature:F-066) Tag Mind-Map Visualization — graph data derivation, deterministic force layout, SVG renderer, inline interaction JS.
11
+
12
+ // --- HTML escape (local copy; cap-ui.cjs keeps the canonical one for re-export stability) ---
13
+ // @cap-decision(F-068/split) Local escapeHtml avoids a circular require with cap-ui.cjs. Behaviour is byte-identical to cap-ui.escapeHtml.
14
+ function escapeHtml(v) {
15
+ if (v === null || v === undefined) return '';
16
+ return String(v)
17
+ .replace(/&/g, '&')
18
+ .replace(/</g, '&lt;')
19
+ .replace(/>/g, '&gt;')
20
+ .replace(/"/g, '&quot;')
21
+ .replace(/'/g, '&#39;');
22
+ }
23
+
24
+ // --- F-066 Mind-Map Visualization ------------------------------------------
25
+
26
+ // @cap-decision(F-066/D3) Graph derivation is a pure function over (featureMap, designTokens, designComponents).
27
+ // Input: parsed feature map + design IDs from DESIGN.md. Output: { nodes[], edges[] }. No I/O, no side effects.
28
+ // This makes the graph easy to test without a running server and keeps the renderer downstream.
29
+ // @cap-decision(F-066/D4) Edge kinds in v1: `depends_on` (feature -> feature) and `uses-design` (feature -> DT/DC).
30
+ // Feature-AC edges are deferred — would multiply node count by ~5x and clutter the view for limited signal.
31
+ // If a future request demands them, add them behind a graph-option flag.
32
+ // @cap-decision(F-066/D5) Nodes are typed as 'feature' | 'token' | 'component'. Classification is structural:
33
+ // feature IDs start with F-, tokens with DT-, components with DC-. No heuristics beyond the ID prefix.
34
+
35
+ /**
36
+ * @typedef {Object} MindMapNode
37
+ * @property {string} id - Stable identifier (F-001, DT-001, DC-001)
38
+ * @property {'feature'|'token'|'component'} type - Node category
39
+ * @property {string} label - Display label (usually same as id, may include short title for features)
40
+ * @property {string|null} group - Optional grouping key (e.g., feature.metadata.group) for filtering
41
+ * @property {string|null} title - Full title for hover tooltip (feature title)
42
+ * @property {string|null} state - Feature state for coloring ('planned'|'prototyped'|'tested'|'shipped'); null for non-features
43
+ */
44
+
45
+ /**
46
+ * @typedef {Object} MindMapEdge
47
+ * @property {string} from - Source node id
48
+ * @property {string} to - Target node id
49
+ * @property {'depends_on'|'uses-design'} kind - Edge category
50
+ */
51
+
52
+ /**
53
+ * @typedef {Object} MindMapGraph
54
+ * @property {MindMapNode[]} nodes
55
+ * @property {MindMapEdge[]} edges
56
+ */
57
+
58
+ // @cap-todo(ac:F-066/AC-1) Derive a graph of all @cap-* tag categories (features + design tokens + design components) from parsed state.
59
+ // @cap-todo(ac:F-066/AC-2) Node types: feature/token/component. Edge kinds: depends_on, uses-design.
60
+ /**
61
+ * Pure function: derive mind-map graph from feature map + design IDs.
62
+ * Edges are only emitted when BOTH endpoints exist as nodes — this prevents dangling
63
+ * references (e.g. a feature.dependencies entry pointing to a feature that was deleted).
64
+ * @param {{ featureMap: {features: Array<Object>}, designTokens?: string[], designComponents?: string[] }} params
65
+ * @returns {MindMapGraph}
66
+ */
67
+ function buildGraphData(params) {
68
+ const features = (params && params.featureMap && Array.isArray(params.featureMap.features))
69
+ ? params.featureMap.features
70
+ : [];
71
+ const designTokens = (params && Array.isArray(params.designTokens)) ? params.designTokens : [];
72
+ const designComponents = (params && Array.isArray(params.designComponents)) ? params.designComponents : [];
73
+
74
+ /** @type {MindMapNode[]} */
75
+ const nodes = [];
76
+ const nodeIds = new Set();
77
+
78
+ // Features first, stable order.
79
+ for (const f of features) {
80
+ if (!f || !f.id || nodeIds.has(f.id)) continue;
81
+ nodes.push({
82
+ id: f.id,
83
+ type: 'feature',
84
+ label: f.id,
85
+ group: (f.metadata && f.metadata.group) ? String(f.metadata.group) : null,
86
+ title: f.title || null,
87
+ state: f.state || 'planned',
88
+ });
89
+ nodeIds.add(f.id);
90
+ }
91
+
92
+ // Design tokens (deduped, stable order via sort for determinism across calls with permuted inputs).
93
+ const sortedTokens = [...new Set(designTokens)].sort();
94
+ for (const id of sortedTokens) {
95
+ if (nodeIds.has(id)) continue;
96
+ nodes.push({ id, type: 'token', label: id, group: 'design', title: null, state: null });
97
+ nodeIds.add(id);
98
+ }
99
+
100
+ // Design components.
101
+ const sortedComponents = [...new Set(designComponents)].sort();
102
+ for (const id of sortedComponents) {
103
+ if (nodeIds.has(id)) continue;
104
+ nodes.push({ id, type: 'component', label: id, group: 'design', title: null, state: null });
105
+ nodeIds.add(id);
106
+ }
107
+
108
+ /** @type {MindMapEdge[]} */
109
+ const edges = [];
110
+ const seenEdges = new Set();
111
+ function addEdge(from, to, kind) {
112
+ if (!nodeIds.has(from) || !nodeIds.has(to)) return;
113
+ if (from === to) return;
114
+ const key = `${from}|${to}|${kind}`;
115
+ if (seenEdges.has(key)) return;
116
+ seenEdges.add(key);
117
+ edges.push({ from, to, kind });
118
+ }
119
+
120
+ for (const f of features) {
121
+ if (!f || !f.id) continue;
122
+ for (const dep of (f.dependencies || [])) {
123
+ addEdge(f.id, String(dep).trim(), 'depends_on');
124
+ }
125
+ for (const du of (f.usesDesign || [])) {
126
+ addEdge(f.id, String(du).trim(), 'uses-design');
127
+ }
128
+ }
129
+
130
+ return { nodes, edges };
131
+ }
132
+
133
+ // @cap-decision(F-066/D6) Seeded RNG: 32-bit mulberry-style from a stable string hash. Guarantees byte-identical
134
+ // SVG output for byte-identical input — required for F-062 determinism pattern and AC-5 snapshot stability.
135
+ /**
136
+ * @param {string} str
137
+ * @returns {number} - 32-bit unsigned integer hash
138
+ */
139
+ function hashString32(str) {
140
+ let h = 2166136261 >>> 0; // FNV-1a seed
141
+ for (let i = 0; i < str.length; i++) {
142
+ h ^= str.charCodeAt(i);
143
+ h = Math.imul(h, 16777619) >>> 0;
144
+ }
145
+ return h >>> 0;
146
+ }
147
+
148
+ function mulberry32(seed) {
149
+ let s = seed >>> 0;
150
+ return function () {
151
+ s = (s + 0x6D2B79F5) >>> 0;
152
+ let t = s;
153
+ t = Math.imul(t ^ (t >>> 15), t | 1);
154
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
155
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
156
+ };
157
+ }
158
+
159
+ // @cap-todo(ac:F-066/AC-3) Handroll a deterministic force-directed layout — NO D3, NO runtime require of any external lib.
160
+ // @cap-decision(F-066/D7) Simple repulsion + spring attraction, Euler integration, damping. ~150-250 iterations converge for ≤200 nodes.
161
+ // @cap-risk Quadratic repulsion loop (O(N^2)) is fine for CAP-scale (~60-120 nodes). If the graph ever exceeds 500
162
+ // nodes, introduce spatial hashing / Barnes-Hut. For now, complexity is traded for code simplicity.
163
+ /**
164
+ * Run a deterministic force-directed layout. Same input -> same (x,y) for every node.
165
+ * @param {MindMapNode[]} nodes - Input nodes (copied — not mutated)
166
+ * @param {MindMapEdge[]} edges
167
+ * @param {{ width?: number, height?: number, iterations?: number, seed?: string }} [options]
168
+ * @returns {Array<MindMapNode & {x:number, y:number}>}
169
+ */
170
+ function runForceLayout(nodes, edges, options) {
171
+ const opts = options || {};
172
+ const width = typeof opts.width === 'number' ? opts.width : 800;
173
+ const height = typeof opts.height === 'number' ? opts.height : 600;
174
+ const iterations = typeof opts.iterations === 'number' ? opts.iterations : 200;
175
+ // Seed the RNG from a stable hash of the node IDs so layout is reproducible.
176
+ const seedSource = opts.seed || nodes.map(n => n.id).sort().join(',') || 'empty';
177
+ const rand = mulberry32(hashString32(seedSource));
178
+
179
+ const N = nodes.length;
180
+ if (N === 0) return [];
181
+
182
+ // Initial positions — pseudo-random inside the viewbox using the seeded RNG.
183
+ const positions = new Array(N);
184
+ const velocities = new Array(N);
185
+ const indexById = new Map();
186
+ for (let i = 0; i < N; i++) {
187
+ indexById.set(nodes[i].id, i);
188
+ positions[i] = { x: rand() * width, y: rand() * height };
189
+ velocities[i] = { x: 0, y: 0 };
190
+ }
191
+
192
+ // Tuning constants — empirically okay for ~10-200 nodes at 800x600.
193
+ const REPULSION = 12000; // node-node push strength
194
+ const SPRING = 0.02; // edge pull strength
195
+ const REST_LEN = 90; // preferred edge length in pixels
196
+ const DAMPING = 0.82; // velocity decay per step
197
+ const MAX_STEP = 20; // clamp per-iteration displacement
198
+
199
+ // Build quick-lookup of adjacency.
200
+ /** @type {Array<Array<number>>} */
201
+ const adjacency = new Array(N).fill(null).map(() => []);
202
+ for (const e of edges) {
203
+ const a = indexById.get(e.from);
204
+ const b = indexById.get(e.to);
205
+ if (a === undefined || b === undefined || a === b) continue;
206
+ adjacency[a].push(b);
207
+ adjacency[b].push(a);
208
+ }
209
+
210
+ for (let iter = 0; iter < iterations; iter++) {
211
+ const forces = new Array(N);
212
+ for (let i = 0; i < N; i++) forces[i] = { x: 0, y: 0 };
213
+
214
+ // Repulsion: O(N^2). Fine for CAP-scale graphs.
215
+ for (let i = 0; i < N; i++) {
216
+ for (let j = i + 1; j < N; j++) {
217
+ const dx = positions[i].x - positions[j].x;
218
+ const dy = positions[i].y - positions[j].y;
219
+ let dist2 = dx * dx + dy * dy;
220
+ if (dist2 < 0.01) dist2 = 0.01;
221
+ const dist = Math.sqrt(dist2);
222
+ const force = REPULSION / dist2;
223
+ const fx = (dx / dist) * force;
224
+ const fy = (dy / dist) * force;
225
+ forces[i].x += fx; forces[i].y += fy;
226
+ forces[j].x -= fx; forces[j].y -= fy;
227
+ }
228
+ }
229
+
230
+ // Spring attraction along edges.
231
+ for (let i = 0; i < N; i++) {
232
+ for (const j of adjacency[i]) {
233
+ if (j <= i) continue; // apply once per pair
234
+ const dx = positions[j].x - positions[i].x;
235
+ const dy = positions[j].y - positions[i].y;
236
+ const dist = Math.sqrt(dx * dx + dy * dy) || 0.01;
237
+ const disp = dist - REST_LEN;
238
+ const fx = (dx / dist) * disp * SPRING;
239
+ const fy = (dy / dist) * disp * SPRING;
240
+ forces[i].x += fx; forces[i].y += fy;
241
+ forces[j].x -= fx; forces[j].y -= fy;
242
+ }
243
+ }
244
+
245
+ // Weak centering pull so disconnected components stay on-canvas.
246
+ const cx = width / 2;
247
+ const cy = height / 2;
248
+ for (let i = 0; i < N; i++) {
249
+ forces[i].x += (cx - positions[i].x) * 0.0015;
250
+ forces[i].y += (cy - positions[i].y) * 0.0015;
251
+ }
252
+
253
+ // Integrate.
254
+ for (let i = 0; i < N; i++) {
255
+ velocities[i].x = (velocities[i].x + forces[i].x) * DAMPING;
256
+ velocities[i].y = (velocities[i].y + forces[i].y) * DAMPING;
257
+ let dx = velocities[i].x;
258
+ let dy = velocities[i].y;
259
+ // Clamp per-step displacement to avoid explosion.
260
+ if (dx > MAX_STEP) dx = MAX_STEP; else if (dx < -MAX_STEP) dx = -MAX_STEP;
261
+ if (dy > MAX_STEP) dy = MAX_STEP; else if (dy < -MAX_STEP) dy = -MAX_STEP;
262
+ positions[i].x += dx;
263
+ positions[i].y += dy;
264
+ }
265
+ }
266
+
267
+ // Scale positions into the viewbox with a margin so nodes/labels fit.
268
+ const MARGIN = 40;
269
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
270
+ for (const p of positions) {
271
+ if (p.x < minX) minX = p.x;
272
+ if (p.y < minY) minY = p.y;
273
+ if (p.x > maxX) maxX = p.x;
274
+ if (p.y > maxY) maxY = p.y;
275
+ }
276
+ const spanX = Math.max(1, maxX - minX);
277
+ const spanY = Math.max(1, maxY - minY);
278
+ const innerW = Math.max(1, width - 2 * MARGIN);
279
+ const innerH = Math.max(1, height - 2 * MARGIN);
280
+
281
+ const result = new Array(N);
282
+ for (let i = 0; i < N; i++) {
283
+ const nx = MARGIN + ((positions[i].x - minX) / spanX) * innerW;
284
+ const ny = MARGIN + ((positions[i].y - minY) / spanY) * innerH;
285
+ // Round to 2 decimal places so tiny floating-point jitter across Node versions does not break snapshot byte-identity.
286
+ result[i] = Object.assign({}, nodes[i], {
287
+ x: Math.round(nx * 100) / 100,
288
+ y: Math.round(ny * 100) / 100,
289
+ });
290
+ }
291
+ return result;
292
+ }
293
+
294
+ // @cap-todo(ac:F-066/AC-3) Render the mind-map as SVG. Pure string output, no DOM APIs needed.
295
+ // @cap-decision(F-066/D8) Edges rendered first, then nodes on top — standard z-ordering for graphs.
296
+ /**
297
+ * Render the SVG markup for the mind-map.
298
+ * @param {Array<MindMapNode & {x:number,y:number}>} layoutedNodes
299
+ * @param {MindMapEdge[]} edges
300
+ * @param {{ width?: number, height?: number }} [options]
301
+ * @returns {string} SVG markup
302
+ */
303
+ function renderMindMapSvg(layoutedNodes, edges, options) {
304
+ const opts = options || {};
305
+ const width = typeof opts.width === 'number' ? opts.width : 800;
306
+ const height = typeof opts.height === 'number' ? opts.height : 600;
307
+
308
+ if (!Array.isArray(layoutedNodes) || layoutedNodes.length === 0) {
309
+ return `<svg class="mind-map" id="cap-mind-map" viewBox="0 0 ${width} ${height}" preserveAspectRatio="xMidYMid meet" role="img" aria-label="CAP Mind-Map (empty)"><text x="${width / 2}" y="${height / 2}" text-anchor="middle" class="mind-map-empty">No features to visualize yet.</text></svg>`;
310
+ }
311
+
312
+ const byId = new Map();
313
+ for (const n of layoutedNodes) byId.set(n.id, n);
314
+
315
+ const edgeParts = [];
316
+ for (const e of edges) {
317
+ const a = byId.get(e.from);
318
+ const b = byId.get(e.to);
319
+ if (!a || !b) continue;
320
+ const cls = e.kind === 'uses-design' ? 'edge edge-uses-design' : 'edge edge-depends';
321
+ edgeParts.push(
322
+ `<line x1="${a.x}" y1="${a.y}" x2="${b.x}" y2="${b.y}" class="${cls}" data-from="${escapeHtml(e.from)}" data-to="${escapeHtml(e.to)}" data-kind="${escapeHtml(e.kind)}" />`
323
+ );
324
+ }
325
+
326
+ const nodeParts = [];
327
+ for (const n of layoutedNodes) {
328
+ const r = n.type === 'feature' ? 20 : (n.type === 'component' ? 14 : 12);
329
+ const stateClass = n.type === 'feature' ? ` node-state-${escapeHtml((n.state || 'planned').replace(/[^a-z0-9_-]/gi, ''))}` : '';
330
+ const cls = `node node-${n.type}${stateClass}`;
331
+ const group = n.group ? ` data-group="${escapeHtml(n.group)}"` : '';
332
+ const titleAttr = n.title ? ` data-title="${escapeHtml(n.title)}"` : '';
333
+ // SVG <title> child gives a native tooltip.
334
+ const titleChild = n.title ? `<title>${escapeHtml(n.id)} — ${escapeHtml(n.title)}</title>` : `<title>${escapeHtml(n.id)}</title>`;
335
+ // @cap-todo(ac:F-067/AC-1) Mind-map nodes become keyboard-reachable (tabindex=0) — tying up F-066's deferred a11y per D6.
336
+ nodeParts.push(
337
+ `<g class="${cls}" data-id="${escapeHtml(n.id)}"${group}${titleAttr} tabindex="0" role="button" aria-label="${escapeHtml(n.id)}${n.title ? ' — ' + escapeHtml(n.title) : ''}">` +
338
+ `${titleChild}` +
339
+ `<circle cx="${n.x}" cy="${n.y}" r="${r}" />` +
340
+ `<text x="${n.x}" y="${n.y + 4}" text-anchor="middle" class="node-label">${escapeHtml(n.label)}</text>` +
341
+ `</g>`
342
+ );
343
+ }
344
+
345
+ return [
346
+ `<svg class="mind-map" id="cap-mind-map" viewBox="0 0 ${width} ${height}" preserveAspectRatio="xMidYMid meet" role="img" aria-label="CAP Mind-Map">`,
347
+ `<g class="edges">`,
348
+ edgeParts.join(''),
349
+ `</g>`,
350
+ `<g class="nodes">`,
351
+ nodeParts.join(''),
352
+ `</g>`,
353
+ `</svg>`,
354
+ ].join('');
355
+ }
356
+
357
+ // @cap-todo(ac:F-066/AC-1) Mind-Map HTML section — wraps SVG with a toolbar (filter checkboxes + legend) and a help hint.
358
+ // @cap-todo(ac:F-066/AC-4) Filter UI: one checkbox per distinct group; toggling hides nodes/edges whose group is unchecked.
359
+ /**
360
+ * Build the HTML section for the mind-map. Includes SVG + interaction scaffolding.
361
+ * @param {{ graphData: MindMapGraph, options?: { width?: number, height?: number } }} params
362
+ * @returns {string} HTML section markup
363
+ */
364
+ function buildMindMapSection(params) {
365
+ const graphData = (params && params.graphData) || { nodes: [], edges: [] };
366
+ const opts = (params && params.options) || {};
367
+ const width = typeof opts.width === 'number' ? opts.width : 800;
368
+ const height = typeof opts.height === 'number' ? opts.height : 600;
369
+
370
+ const layouted = runForceLayout(graphData.nodes, graphData.edges, { width, height });
371
+ const svg = renderMindMapSvg(layouted, graphData.edges, { width, height });
372
+
373
+ // Collect unique groups present in the graph for filter UI.
374
+ const groupSet = new Set();
375
+ for (const n of graphData.nodes) {
376
+ if (n.group) groupSet.add(n.group);
377
+ }
378
+ // Always include a synthetic bucket for ungrouped feature nodes so users can toggle them.
379
+ const groups = Array.from(groupSet).sort();
380
+ const hasUngrouped = graphData.nodes.some(n => !n.group);
381
+
382
+ const groupCheckboxes = groups.map(function (g) {
383
+ return `<label class="mm-filter"><input type="checkbox" class="mm-filter-input" data-filter-group="${escapeHtml(g)}" checked> ${escapeHtml(g)}</label>`;
384
+ }).join('\n ');
385
+ const ungroupedCheckbox = hasUngrouped
386
+ ? `<label class="mm-filter"><input type="checkbox" class="mm-filter-input" data-filter-group="__ungrouped__" checked> (ungrouped)</label>`
387
+ : '';
388
+
389
+ const nodeCount = graphData.nodes.length;
390
+ const edgeCount = graphData.edges.length;
391
+ const featureCount = graphData.nodes.filter(n => n.type === 'feature').length;
392
+ const tokenCount = graphData.nodes.filter(n => n.type === 'token').length;
393
+ const componentCount = graphData.nodes.filter(n => n.type === 'component').length;
394
+
395
+ return `
396
+ <section class="cap-section" id="mind-map">
397
+ <h2>Mind-Map</h2>
398
+ <div class="mm-meta">${nodeCount} nodes (${featureCount} features, ${tokenCount} tokens, ${componentCount} components) · ${edgeCount} edges</div>
399
+ <div class="mm-toolbar">
400
+ <div class="mm-legend">
401
+ <span class="mm-swatch mm-sw-feature"></span> feature
402
+ <span class="mm-swatch mm-sw-token"></span> token
403
+ <span class="mm-swatch mm-sw-component"></span> component
404
+ <span class="mm-edge-sample mm-edge-depends"></span> depends_on
405
+ <span class="mm-edge-sample mm-edge-uses-design"></span> uses-design
406
+ </div>
407
+ <div class="mm-filters">
408
+ ${groupCheckboxes}
409
+ ${ungroupedCheckbox}
410
+ </div>
411
+ <div class="mm-hint">wheel: zoom · drag: pan · click node: focus · click empty: reset</div>
412
+ </div>
413
+ <div class="mm-viewport" id="mm-viewport">
414
+ ${svg}
415
+ </div>
416
+ </section>`;
417
+ }
418
+
419
+ // @cap-todo(ac:F-066/AC-3) Mind-Map CSS is its own string composed into buildCss(). Anti-Slop: warm neutrals + terracotta accent, no gradients.
420
+ function buildMindMapCss() {
421
+ return `
422
+ section.cap-section#mind-map { max-width: 100%; }
423
+ .mm-meta {
424
+ color: var(--fg-muted);
425
+ font-size: 12px;
426
+ margin-bottom: 6px;
427
+ }
428
+ .mm-toolbar {
429
+ display: flex;
430
+ flex-wrap: wrap;
431
+ gap: 12px 24px;
432
+ align-items: center;
433
+ padding: 8px 10px;
434
+ background: var(--bg-card);
435
+ border: 1px solid var(--border);
436
+ border-radius: 3px;
437
+ margin-bottom: 8px;
438
+ font-size: 12px;
439
+ }
440
+ .mm-legend, .mm-filters {
441
+ display: flex;
442
+ flex-wrap: wrap;
443
+ gap: 10px;
444
+ align-items: center;
445
+ color: var(--fg-muted);
446
+ }
447
+ .mm-filter { display: inline-flex; align-items: center; gap: 4px; cursor: pointer; }
448
+ .mm-filter-input { margin: 0; }
449
+ .mm-hint { color: var(--fg-muted); font-size: 11px; margin-left: auto; }
450
+ .mm-swatch {
451
+ display: inline-block;
452
+ width: 10px;
453
+ height: 10px;
454
+ border-radius: 50%;
455
+ vertical-align: middle;
456
+ margin-right: 2px;
457
+ }
458
+ .mm-sw-feature { background: var(--accent); }
459
+ .mm-sw-token { background: var(--accent-muted); }
460
+ .mm-sw-component { background: var(--state-prototyped); }
461
+ .mm-edge-sample {
462
+ display: inline-block;
463
+ width: 20px;
464
+ height: 0;
465
+ border-top: 1.5px solid var(--fg-muted);
466
+ vertical-align: middle;
467
+ margin: 0 2px;
468
+ }
469
+ .mm-edge-sample.mm-edge-depends { border-top-style: solid; border-top-color: var(--fg-muted); }
470
+ .mm-edge-sample.mm-edge-uses-design { border-top-style: dashed; border-top-color: var(--accent); }
471
+ .mm-viewport {
472
+ border: 1px solid var(--border);
473
+ background: var(--bg-card);
474
+ border-radius: 3px;
475
+ overflow: hidden;
476
+ position: relative;
477
+ touch-action: none;
478
+ }
479
+ svg.mind-map {
480
+ display: block;
481
+ width: 100%;
482
+ height: 600px;
483
+ cursor: grab;
484
+ user-select: none;
485
+ }
486
+ svg.mind-map:active { cursor: grabbing; }
487
+ svg.mind-map text.mind-map-empty {
488
+ fill: var(--fg-muted);
489
+ font-family: var(--mono);
490
+ font-size: 13px;
491
+ }
492
+ svg.mind-map g.edges line {
493
+ stroke: var(--fg-muted);
494
+ stroke-width: 1;
495
+ stroke-opacity: 0.45;
496
+ }
497
+ svg.mind-map g.edges line.edge-uses-design {
498
+ stroke: var(--accent);
499
+ stroke-dasharray: 4 3;
500
+ stroke-opacity: 0.7;
501
+ }
502
+ svg.mind-map g.nodes g.node { cursor: pointer; transition: transform 120ms ease; transform-origin: center; transform-box: fill-box; }
503
+ svg.mind-map g.nodes g.node text.node-label {
504
+ fill: var(--fg);
505
+ font-family: var(--mono);
506
+ font-size: 10px;
507
+ pointer-events: none;
508
+ }
509
+ svg.mind-map g.nodes g.node circle {
510
+ stroke: var(--border);
511
+ stroke-width: 1.5;
512
+ }
513
+ svg.mind-map g.nodes g.node-feature circle { fill: var(--accent); }
514
+ svg.mind-map g.nodes g.node-feature text.node-label { fill: var(--bg); font-weight: 600; }
515
+ svg.mind-map g.nodes g.node-feature.node-state-planned circle { fill: var(--state-planned); }
516
+ svg.mind-map g.nodes g.node-feature.node-state-prototyped circle { fill: var(--state-prototyped); }
517
+ svg.mind-map g.nodes g.node-feature.node-state-tested circle { fill: var(--state-tested); }
518
+ svg.mind-map g.nodes g.node-feature.node-state-shipped circle { fill: var(--state-shipped); }
519
+ svg.mind-map g.nodes g.node-token circle { fill: var(--accent-muted); }
520
+ svg.mind-map g.nodes g.node-component circle { fill: #d8c49b; }
521
+ svg.mind-map g.nodes g.node:hover circle { stroke: var(--accent); stroke-width: 2; }
522
+ svg.mind-map g.nodes g.node:focus { outline: none; }
523
+ svg.mind-map g.nodes g.node:focus-visible circle { stroke: var(--accent); stroke-width: 3; }
524
+ svg.mind-map g.nodes g.node.mm-dim { opacity: 0.15; }
525
+ svg.mind-map g.edges line.mm-dim { opacity: 0.08; }
526
+ svg.mind-map g.nodes g.node.mm-focused circle { stroke: var(--accent); stroke-width: 3; }
527
+ svg.mind-map g.nodes g.node.mm-neighbour circle { stroke: var(--accent-muted); stroke-width: 2.5; }
528
+ svg.mind-map g.nodes g.node.mm-hidden, svg.mind-map g.edges line.mm-hidden { display: none; }
529
+ `.trim();
530
+ }
531
+
532
+ // @cap-todo(ac:F-066/AC-4) Mind-Map client JS: zoom (wheel), pan (drag), filter (checkboxes), hover (CSS :hover), click-to-focus (neighbour highlight).
533
+ // @cap-decision(F-066/D9) Interaction via viewBox manipulation — no transforms on the SVG itself. Keeps coordinates portable across browsers.
534
+ // @cap-decision(F-066/D10) Vanilla JS, IIFE, no external libs, no ES modules. Same code path for --serve and --share.
535
+ function buildMindMapJs() {
536
+ return `
537
+ (function(){
538
+ var svg = document.getElementById('cap-mind-map');
539
+ if (!svg) return;
540
+ var viewport = document.getElementById('mm-viewport');
541
+
542
+ // --- Zoom + Pan via viewBox ---------------------------------------------
543
+ var vb = { x: 0, y: 0, w: 800, h: 600 };
544
+ var vbAttr = svg.getAttribute('viewBox');
545
+ if (vbAttr) {
546
+ var parts = vbAttr.split(/\\s+/).map(function(p){ return parseFloat(p); });
547
+ if (parts.length === 4 && parts.every(function(n){ return !isNaN(n); })) {
548
+ vb.x = parts[0]; vb.y = parts[1]; vb.w = parts[2]; vb.h = parts[3];
549
+ }
550
+ }
551
+ var baseW = vb.w, baseH = vb.h;
552
+ function applyViewBox() {
553
+ svg.setAttribute('viewBox', vb.x + ' ' + vb.y + ' ' + vb.w + ' ' + vb.h);
554
+ }
555
+
556
+ svg.addEventListener('wheel', function(e){
557
+ e.preventDefault();
558
+ var rect = svg.getBoundingClientRect();
559
+ if (rect.width <= 0 || rect.height <= 0) return;
560
+ var mx = vb.x + (e.clientX - rect.left) / rect.width * vb.w;
561
+ var my = vb.y + (e.clientY - rect.top) / rect.height * vb.h;
562
+ var factor = e.deltaY > 0 ? 1.1 : 0.9;
563
+ var nextW = vb.w * factor;
564
+ var nextH = vb.h * factor;
565
+ // Clamp to 0.25x..4x of original viewBox.
566
+ if (nextW < baseW * 0.25 || nextW > baseW * 4) return;
567
+ vb.x = mx - (mx - vb.x) * factor;
568
+ vb.y = my - (my - vb.y) * factor;
569
+ vb.w = nextW;
570
+ vb.h = nextH;
571
+ applyViewBox();
572
+ }, { passive: false });
573
+
574
+ var panning = false;
575
+ var panStart = null;
576
+ svg.addEventListener('mousedown', function(e){
577
+ if (e.button !== 0) return;
578
+ // Don't start panning when the click lands on a node — nodes handle their own click.
579
+ var targetNode = e.target && e.target.closest ? e.target.closest('g.node') : null;
580
+ if (targetNode) return;
581
+ panning = true;
582
+ panStart = { x: e.clientX, y: e.clientY, vbx: vb.x, vby: vb.y };
583
+ });
584
+ window.addEventListener('mousemove', function(e){
585
+ if (!panning || !panStart) return;
586
+ var rect = svg.getBoundingClientRect();
587
+ if (rect.width <= 0 || rect.height <= 0) return;
588
+ var dx = (e.clientX - panStart.x) / rect.width * vb.w;
589
+ var dy = (e.clientY - panStart.y) / rect.height * vb.h;
590
+ vb.x = panStart.vbx - dx;
591
+ vb.y = panStart.vby - dy;
592
+ applyViewBox();
593
+ });
594
+ window.addEventListener('mouseup', function(){ panning = false; panStart = null; });
595
+
596
+ // --- Filter by group ----------------------------------------------------
597
+ function activeGroups() {
598
+ var boxes = document.querySelectorAll('.mm-filter-input');
599
+ var active = new Set();
600
+ for (var i = 0; i < boxes.length; i++) {
601
+ if (boxes[i].checked) active.add(boxes[i].getAttribute('data-filter-group'));
602
+ }
603
+ return active;
604
+ }
605
+ function applyFilters() {
606
+ var active = activeGroups();
607
+ var nodes = svg.querySelectorAll('g.nodes > g.node');
608
+ var hiddenIds = new Set();
609
+ for (var i = 0; i < nodes.length; i++) {
610
+ var g = nodes[i].getAttribute('data-group');
611
+ var key = g ? g : '__ungrouped__';
612
+ if (!active.has(key)) {
613
+ nodes[i].classList.add('mm-hidden');
614
+ hiddenIds.add(nodes[i].getAttribute('data-id'));
615
+ } else {
616
+ nodes[i].classList.remove('mm-hidden');
617
+ }
618
+ }
619
+ var edges = svg.querySelectorAll('g.edges > line');
620
+ for (var j = 0; j < edges.length; j++) {
621
+ var from = edges[j].getAttribute('data-from');
622
+ var to = edges[j].getAttribute('data-to');
623
+ if (hiddenIds.has(from) || hiddenIds.has(to)) {
624
+ edges[j].classList.add('mm-hidden');
625
+ } else {
626
+ edges[j].classList.remove('mm-hidden');
627
+ }
628
+ }
629
+ }
630
+ var filterBoxes = document.querySelectorAll('.mm-filter-input');
631
+ for (var f = 0; f < filterBoxes.length; f++) {
632
+ filterBoxes[f].addEventListener('change', applyFilters);
633
+ }
634
+
635
+ // --- Click-to-Focus -----------------------------------------------------
636
+ function clearFocus() {
637
+ var dimmed = svg.querySelectorAll('.mm-dim, .mm-focused, .mm-neighbour');
638
+ for (var i = 0; i < dimmed.length; i++) {
639
+ dimmed[i].classList.remove('mm-dim');
640
+ dimmed[i].classList.remove('mm-focused');
641
+ dimmed[i].classList.remove('mm-neighbour');
642
+ }
643
+ }
644
+ function focusNode(id) {
645
+ if (!id) return;
646
+ clearFocus();
647
+ var neighbours = new Set();
648
+ neighbours.add(id);
649
+ var edges = svg.querySelectorAll('g.edges > line');
650
+ for (var i = 0; i < edges.length; i++) {
651
+ var from = edges[i].getAttribute('data-from');
652
+ var to = edges[i].getAttribute('data-to');
653
+ if (from === id) neighbours.add(to);
654
+ else if (to === id) neighbours.add(from);
655
+ }
656
+ var nodes = svg.querySelectorAll('g.nodes > g.node');
657
+ for (var j = 0; j < nodes.length; j++) {
658
+ var nid = nodes[j].getAttribute('data-id');
659
+ if (nid === id) nodes[j].classList.add('mm-focused');
660
+ else if (neighbours.has(nid)) nodes[j].classList.add('mm-neighbour');
661
+ else nodes[j].classList.add('mm-dim');
662
+ }
663
+ for (var k = 0; k < edges.length; k++) {
664
+ var fromE = edges[k].getAttribute('data-from');
665
+ var toE = edges[k].getAttribute('data-to');
666
+ if (fromE === id || toE === id) {
667
+ // edge stays at default opacity
668
+ } else {
669
+ edges[k].classList.add('mm-dim');
670
+ }
671
+ }
672
+ }
673
+ svg.addEventListener('click', function(e){
674
+ var node = e.target && e.target.closest ? e.target.closest('g.node') : null;
675
+ if (node) {
676
+ focusNode(node.getAttribute('data-id'));
677
+ } else {
678
+ clearFocus();
679
+ }
680
+ });
681
+
682
+ // --- Keyboard navigation (F-067/D6 — tying up F-066 deferred a11y) -----
683
+ // Enter/Space focuses the node, Escape clears focus. Tab + Shift+Tab move
684
+ // between nodes via native tabindex order.
685
+ svg.addEventListener('keydown', function(e){
686
+ if (e.key === 'Escape') {
687
+ clearFocus();
688
+ e.preventDefault();
689
+ return;
690
+ }
691
+ var node = e.target && e.target.closest ? e.target.closest('g.node') : null;
692
+ if (!node) return;
693
+ if (e.key === 'Enter' || e.key === ' ') {
694
+ focusNode(node.getAttribute('data-id'));
695
+ e.preventDefault();
696
+ }
697
+ });
698
+ })();
699
+ `.trim();
700
+ }
701
+
702
+ module.exports = {
703
+ buildGraphData,
704
+ runForceLayout,
705
+ renderMindMapSvg,
706
+ buildMindMapSection,
707
+ buildMindMapCss,
708
+ buildMindMapJs,
709
+ // Exported for internal testing (hashing determinism).
710
+ _hashString32: hashString32,
711
+ _mulberry32: mulberry32,
712
+ };