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.
- package/.claude-plugin/README.md +26 -0
- package/.claude-plugin/marketplace.json +24 -0
- package/.claude-plugin/plugin.json +24 -0
- package/LICENSE +21 -0
- package/README.ja-JP.md +834 -0
- package/README.ko-KR.md +823 -0
- package/README.md +806 -0
- package/README.pt-BR.md +452 -0
- package/README.zh-CN.md +800 -0
- package/agents/cap-architect.md +269 -0
- package/agents/cap-brainstormer.md +207 -0
- package/agents/cap-curator.md +276 -0
- package/agents/cap-debugger.md +365 -0
- package/agents/cap-designer.md +246 -0
- package/agents/cap-historian.md +464 -0
- package/agents/cap-migrator.md +291 -0
- package/agents/cap-prototyper.md +197 -0
- package/agents/cap-validator.md +308 -0
- package/bin/install.js +5433 -0
- package/cap/bin/cap-tools.cjs +853 -0
- package/cap/bin/lib/arc-scanner.cjs +344 -0
- package/cap/bin/lib/cap-affinity-engine.cjs +862 -0
- package/cap/bin/lib/cap-anchor.cjs +228 -0
- package/cap/bin/lib/cap-annotation-writer.cjs +340 -0
- package/cap/bin/lib/cap-checkpoint.cjs +434 -0
- package/cap/bin/lib/cap-cluster-detect.cjs +945 -0
- package/cap/bin/lib/cap-cluster-display.cjs +52 -0
- package/cap/bin/lib/cap-cluster-format.cjs +245 -0
- package/cap/bin/lib/cap-cluster-helpers.cjs +295 -0
- package/cap/bin/lib/cap-cluster-io.cjs +212 -0
- package/cap/bin/lib/cap-completeness.cjs +540 -0
- package/cap/bin/lib/cap-deps.cjs +583 -0
- package/cap/bin/lib/cap-design-families.cjs +332 -0
- package/cap/bin/lib/cap-design.cjs +966 -0
- package/cap/bin/lib/cap-divergence-detector.cjs +400 -0
- package/cap/bin/lib/cap-doctor.cjs +752 -0
- package/cap/bin/lib/cap-feature-map-internals.cjs +19 -0
- package/cap/bin/lib/cap-feature-map-migrate.cjs +335 -0
- package/cap/bin/lib/cap-feature-map-monorepo.cjs +885 -0
- package/cap/bin/lib/cap-feature-map-shard.cjs +315 -0
- package/cap/bin/lib/cap-feature-map.cjs +1943 -0
- package/cap/bin/lib/cap-fitness-score.cjs +1075 -0
- package/cap/bin/lib/cap-impact-analysis.cjs +652 -0
- package/cap/bin/lib/cap-learn-review.cjs +1072 -0
- package/cap/bin/lib/cap-learning-signals.cjs +627 -0
- package/cap/bin/lib/cap-loader.cjs +227 -0
- package/cap/bin/lib/cap-logger.cjs +57 -0
- package/cap/bin/lib/cap-memory-bridge.cjs +764 -0
- package/cap/bin/lib/cap-memory-confidence.cjs +452 -0
- package/cap/bin/lib/cap-memory-dir.cjs +987 -0
- package/cap/bin/lib/cap-memory-engine.cjs +698 -0
- package/cap/bin/lib/cap-memory-extends.cjs +398 -0
- package/cap/bin/lib/cap-memory-graph.cjs +790 -0
- package/cap/bin/lib/cap-memory-migrate.cjs +2015 -0
- package/cap/bin/lib/cap-memory-pin.cjs +183 -0
- package/cap/bin/lib/cap-memory-platform.cjs +490 -0
- package/cap/bin/lib/cap-memory-prune.cjs +707 -0
- package/cap/bin/lib/cap-memory-schema.cjs +812 -0
- package/cap/bin/lib/cap-migrate-tags.cjs +309 -0
- package/cap/bin/lib/cap-migrate.cjs +540 -0
- package/cap/bin/lib/cap-pattern-apply.cjs +1203 -0
- package/cap/bin/lib/cap-pattern-pipeline.cjs +1034 -0
- package/cap/bin/lib/cap-plugin-manifest.cjs +80 -0
- package/cap/bin/lib/cap-realtime-affinity.cjs +399 -0
- package/cap/bin/lib/cap-reconcile.cjs +570 -0
- package/cap/bin/lib/cap-research-gate.cjs +218 -0
- package/cap/bin/lib/cap-scope-filter.cjs +402 -0
- package/cap/bin/lib/cap-semantic-pipeline.cjs +1038 -0
- package/cap/bin/lib/cap-session-extract.cjs +987 -0
- package/cap/bin/lib/cap-session.cjs +445 -0
- package/cap/bin/lib/cap-snapshot-linkage.cjs +963 -0
- package/cap/bin/lib/cap-stack-docs.cjs +646 -0
- package/cap/bin/lib/cap-tag-observer.cjs +371 -0
- package/cap/bin/lib/cap-tag-scanner.cjs +1766 -0
- package/cap/bin/lib/cap-telemetry.cjs +466 -0
- package/cap/bin/lib/cap-test-audit.cjs +1438 -0
- package/cap/bin/lib/cap-thread-migrator.cjs +307 -0
- package/cap/bin/lib/cap-thread-synthesis.cjs +545 -0
- package/cap/bin/lib/cap-thread-tracker.cjs +519 -0
- package/cap/bin/lib/cap-trace.cjs +399 -0
- package/cap/bin/lib/cap-trust-mode.cjs +336 -0
- package/cap/bin/lib/cap-ui-design-editor.cjs +642 -0
- package/cap/bin/lib/cap-ui-mind-map.cjs +712 -0
- package/cap/bin/lib/cap-ui-thread-nav.cjs +693 -0
- package/cap/bin/lib/cap-ui.cjs +1245 -0
- package/cap/bin/lib/cap-upgrade.cjs +1028 -0
- package/cap/bin/lib/cli/arg-helpers.cjs +49 -0
- package/cap/bin/lib/cli/frontmatter-router.cjs +31 -0
- package/cap/bin/lib/cli/init-router.cjs +68 -0
- package/cap/bin/lib/cli/phase-router.cjs +102 -0
- package/cap/bin/lib/cli/state-router.cjs +61 -0
- package/cap/bin/lib/cli/template-router.cjs +37 -0
- package/cap/bin/lib/cli/uat-router.cjs +29 -0
- package/cap/bin/lib/cli/validation-router.cjs +26 -0
- package/cap/bin/lib/cli/verification-router.cjs +31 -0
- package/cap/bin/lib/cli/workstream-router.cjs +39 -0
- package/cap/bin/lib/commands.cjs +961 -0
- package/cap/bin/lib/config.cjs +467 -0
- package/cap/bin/lib/convention-reader.cjs +258 -0
- package/cap/bin/lib/core.cjs +1241 -0
- package/cap/bin/lib/feature-aggregator.cjs +423 -0
- package/cap/bin/lib/frontmatter.cjs +337 -0
- package/cap/bin/lib/init.cjs +1443 -0
- package/cap/bin/lib/manifest-generator.cjs +383 -0
- package/cap/bin/lib/milestone.cjs +253 -0
- package/cap/bin/lib/model-profiles.cjs +69 -0
- package/cap/bin/lib/monorepo-context.cjs +226 -0
- package/cap/bin/lib/monorepo-migrator.cjs +509 -0
- package/cap/bin/lib/phase.cjs +889 -0
- package/cap/bin/lib/profile-output.cjs +989 -0
- package/cap/bin/lib/profile-pipeline.cjs +540 -0
- package/cap/bin/lib/roadmap.cjs +330 -0
- package/cap/bin/lib/security.cjs +394 -0
- package/cap/bin/lib/session-manager.cjs +292 -0
- package/cap/bin/lib/skeleton-generator.cjs +179 -0
- package/cap/bin/lib/state.cjs +1032 -0
- package/cap/bin/lib/template.cjs +231 -0
- package/cap/bin/lib/test-detector.cjs +62 -0
- package/cap/bin/lib/uat.cjs +283 -0
- package/cap/bin/lib/verify.cjs +889 -0
- package/cap/bin/lib/workspace-detector.cjs +371 -0
- package/cap/bin/lib/workstream.cjs +492 -0
- package/cap/commands/gsd/workstreams.md +63 -0
- package/cap/references/arc-standard.md +315 -0
- package/cap/references/cap-agent-architecture.md +101 -0
- package/cap/references/cap-gitignore-template +9 -0
- package/cap/references/cap-zero-deps.md +158 -0
- package/cap/references/checkpoints.md +778 -0
- package/cap/references/continuation-format.md +249 -0
- package/cap/references/contract-test-templates.md +312 -0
- package/cap/references/feature-map-template.md +25 -0
- package/cap/references/git-integration.md +295 -0
- package/cap/references/git-planning-commit.md +38 -0
- package/cap/references/model-profiles.md +174 -0
- package/cap/references/phase-numbering.md +126 -0
- package/cap/references/planning-config.md +202 -0
- package/cap/references/property-test-templates.md +316 -0
- package/cap/references/security-test-templates.md +347 -0
- package/cap/references/session-template.json +8 -0
- package/cap/references/tdd.md +263 -0
- package/cap/references/user-profiling.md +681 -0
- package/cap/references/verification-patterns.md +612 -0
- package/cap/templates/UAT.md +265 -0
- package/cap/templates/claude-md.md +175 -0
- package/cap/templates/codebase/architecture.md +255 -0
- package/cap/templates/codebase/concerns.md +310 -0
- package/cap/templates/codebase/conventions.md +307 -0
- package/cap/templates/codebase/integrations.md +280 -0
- package/cap/templates/codebase/stack.md +186 -0
- package/cap/templates/codebase/structure.md +285 -0
- package/cap/templates/codebase/testing.md +480 -0
- package/cap/templates/config.json +44 -0
- package/cap/templates/context.md +352 -0
- package/cap/templates/continue-here.md +78 -0
- package/cap/templates/copilot-instructions.md +7 -0
- package/cap/templates/debug-subagent-prompt.md +91 -0
- package/cap/templates/discussion-log.md +63 -0
- package/cap/templates/milestone-archive.md +123 -0
- package/cap/templates/milestone.md +115 -0
- package/cap/templates/phase-prompt.md +610 -0
- package/cap/templates/planner-subagent-prompt.md +117 -0
- package/cap/templates/project.md +186 -0
- package/cap/templates/requirements.md +231 -0
- package/cap/templates/research-project/ARCHITECTURE.md +204 -0
- package/cap/templates/research-project/FEATURES.md +147 -0
- package/cap/templates/research-project/PITFALLS.md +200 -0
- package/cap/templates/research-project/STACK.md +120 -0
- package/cap/templates/research-project/SUMMARY.md +170 -0
- package/cap/templates/research.md +552 -0
- package/cap/templates/roadmap.md +202 -0
- package/cap/templates/state.md +176 -0
- package/cap/templates/summary.md +364 -0
- package/cap/templates/user-preferences.md +498 -0
- package/cap/templates/verification-report.md +322 -0
- package/cap/workflows/add-phase.md +112 -0
- package/cap/workflows/add-tests.md +351 -0
- package/cap/workflows/add-todo.md +158 -0
- package/cap/workflows/audit-milestone.md +340 -0
- package/cap/workflows/audit-uat.md +109 -0
- package/cap/workflows/autonomous.md +891 -0
- package/cap/workflows/check-todos.md +177 -0
- package/cap/workflows/cleanup.md +152 -0
- package/cap/workflows/complete-milestone.md +767 -0
- package/cap/workflows/diagnose-issues.md +231 -0
- package/cap/workflows/discovery-phase.md +289 -0
- package/cap/workflows/discuss-phase-assumptions.md +653 -0
- package/cap/workflows/discuss-phase.md +1049 -0
- package/cap/workflows/do.md +104 -0
- package/cap/workflows/execute-phase.md +846 -0
- package/cap/workflows/execute-plan.md +514 -0
- package/cap/workflows/fast.md +105 -0
- package/cap/workflows/forensics.md +265 -0
- package/cap/workflows/health.md +181 -0
- package/cap/workflows/help.md +660 -0
- package/cap/workflows/insert-phase.md +130 -0
- package/cap/workflows/list-phase-assumptions.md +178 -0
- package/cap/workflows/list-workspaces.md +56 -0
- package/cap/workflows/manager.md +362 -0
- package/cap/workflows/map-codebase.md +377 -0
- package/cap/workflows/milestone-summary.md +223 -0
- package/cap/workflows/new-milestone.md +486 -0
- package/cap/workflows/new-project.md +1250 -0
- package/cap/workflows/new-workspace.md +237 -0
- package/cap/workflows/next.md +97 -0
- package/cap/workflows/node-repair.md +92 -0
- package/cap/workflows/note.md +156 -0
- package/cap/workflows/pause-work.md +176 -0
- package/cap/workflows/plan-milestone-gaps.md +273 -0
- package/cap/workflows/plan-phase.md +857 -0
- package/cap/workflows/plant-seed.md +169 -0
- package/cap/workflows/pr-branch.md +129 -0
- package/cap/workflows/profile-user.md +449 -0
- package/cap/workflows/progress.md +507 -0
- package/cap/workflows/quick.md +757 -0
- package/cap/workflows/remove-phase.md +155 -0
- package/cap/workflows/remove-workspace.md +90 -0
- package/cap/workflows/research-phase.md +82 -0
- package/cap/workflows/resume-project.md +326 -0
- package/cap/workflows/review.md +228 -0
- package/cap/workflows/session-report.md +146 -0
- package/cap/workflows/settings.md +283 -0
- package/cap/workflows/ship.md +228 -0
- package/cap/workflows/stats.md +60 -0
- package/cap/workflows/transition.md +671 -0
- package/cap/workflows/ui-phase.md +298 -0
- package/cap/workflows/ui-review.md +161 -0
- package/cap/workflows/update.md +323 -0
- package/cap/workflows/validate-phase.md +170 -0
- package/cap/workflows/verify-phase.md +254 -0
- package/cap/workflows/verify-work.md +637 -0
- package/commands/cap/annotate.md +165 -0
- package/commands/cap/brainstorm.md +393 -0
- package/commands/cap/checkpoint.md +106 -0
- package/commands/cap/completeness.md +94 -0
- package/commands/cap/continue.md +72 -0
- package/commands/cap/debug.md +588 -0
- package/commands/cap/deps.md +169 -0
- package/commands/cap/design.md +479 -0
- package/commands/cap/init.md +354 -0
- package/commands/cap/iterate.md +249 -0
- package/commands/cap/learn.md +459 -0
- package/commands/cap/memory.md +275 -0
- package/commands/cap/migrate-feature-map.md +91 -0
- package/commands/cap/migrate-memory.md +108 -0
- package/commands/cap/migrate-tags.md +91 -0
- package/commands/cap/migrate.md +131 -0
- package/commands/cap/prototype.md +510 -0
- package/commands/cap/reconcile.md +121 -0
- package/commands/cap/review.md +360 -0
- package/commands/cap/save.md +72 -0
- package/commands/cap/scan.md +404 -0
- package/commands/cap/start.md +356 -0
- package/commands/cap/status.md +118 -0
- package/commands/cap/test-audit.md +262 -0
- package/commands/cap/test.md +394 -0
- package/commands/cap/trace.md +133 -0
- package/commands/cap/ui.md +167 -0
- package/hooks/dist/cap-check-update.js +115 -0
- package/hooks/dist/cap-context-monitor.js +185 -0
- package/hooks/dist/cap-learn-review-hook.js +114 -0
- package/hooks/dist/cap-learning-hook.js +192 -0
- package/hooks/dist/cap-memory.js +299 -0
- package/hooks/dist/cap-prompt-guard.js +97 -0
- package/hooks/dist/cap-statusline.js +157 -0
- package/hooks/dist/cap-tag-observer.js +115 -0
- package/hooks/dist/cap-version-check.js +112 -0
- package/hooks/dist/cap-workflow-guard.js +175 -0
- package/hooks/hooks.json +55 -0
- package/package.json +58 -0
- package/scripts/base64-scan.sh +262 -0
- package/scripts/build-hooks.js +93 -0
- package/scripts/cap-removal-checklist.md +202 -0
- package/scripts/prompt-injection-scan.sh +199 -0
- package/scripts/run-tests.cjs +181 -0
- 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, '<')
|
|
19
|
+
.replace(/>/g, '>')
|
|
20
|
+
.replace(/"/g, '"')
|
|
21
|
+
.replace(/'/g, ''');
|
|
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
|
+
};
|