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,790 @@
1
+ // @cap-feature(feature:F-034) Memory Graph — connected graph structure linking features, threads, decisions, pitfalls, and patterns as typed nodes with labeled edges
2
+ // @cap-decision Pure logic module with explicit I/O functions — same pattern as cap-memory-engine.cjs. Graph manipulation functions are side-effect-free; only loadGraph/saveGraph touch disk.
3
+ // @cap-decision Graph stored as single JSON file (.cap/memory/graph.json) with sorted keys and one-entry-per-line edges for merge-friendly git diffs.
4
+ // @cap-decision Nodes keyed by ID in an object (O(1) lookup) while edges stored as a sorted array (merge-friendly diffs, easy filtering).
5
+ // @cap-constraint Zero external dependencies — uses only Node.js built-ins (fs, path, crypto).
6
+
7
+ 'use strict';
8
+
9
+ const fs = require('node:fs');
10
+ const path = require('node:path');
11
+ const crypto = require('node:crypto');
12
+
13
+ // --- Constants ---
14
+
15
+ /** Graph file path relative to project root. */
16
+ const GRAPH_FILE = path.join('.cap', 'memory', 'graph.json');
17
+
18
+ /** Current graph schema version. */
19
+ const GRAPH_VERSION = '1.0.0';
20
+
21
+ // @cap-todo(ac:F-034/AC-2) Support edge types: depends_on, supersedes, conflicts_with, branched_from, informed_by, relates_to
22
+ /** Valid edge types for the memory graph. */
23
+ const EDGE_TYPES = [
24
+ 'depends_on',
25
+ 'supersedes',
26
+ 'conflicts_with',
27
+ 'branched_from',
28
+ 'informed_by',
29
+ 'relates_to',
30
+ 'affinity',
31
+ ];
32
+
33
+ // @cap-todo(ac:F-034/AC-1) Node types: feature, thread, decision, pitfall, pattern, hotspot
34
+ /** Valid node types for the memory graph. */
35
+ const NODE_TYPES = [
36
+ 'feature',
37
+ 'thread',
38
+ 'decision',
39
+ 'pitfall',
40
+ 'pattern',
41
+ 'hotspot',
42
+ ];
43
+
44
+ // --- Types ---
45
+
46
+ /**
47
+ * @typedef {'feature'|'thread'|'decision'|'pitfall'|'pattern'|'hotspot'} NodeType
48
+ */
49
+
50
+ /**
51
+ * @typedef {'depends_on'|'supersedes'|'conflicts_with'|'branched_from'|'informed_by'|'relates_to'} EdgeType
52
+ */
53
+
54
+ /**
55
+ * @typedef {Object} GraphNode
56
+ * @property {NodeType} type - Node type
57
+ * @property {string} id - Unique node ID
58
+ * @property {string} label - Human-readable label
59
+ * @property {string} createdAt - ISO timestamp
60
+ * @property {string} updatedAt - ISO timestamp
61
+ * @property {boolean} active - Whether the node is active (false = stale/removed)
62
+ * @property {Object} metadata - Arbitrary metadata
63
+ */
64
+
65
+ /**
66
+ * @typedef {Object} GraphEdge
67
+ * @property {string} source - Source node ID
68
+ * @property {string} target - Target node ID
69
+ * @property {EdgeType} type - Edge type
70
+ * @property {string} createdAt - ISO timestamp
71
+ * @property {boolean} active - Whether the edge is active
72
+ * @property {Object} metadata - Arbitrary metadata
73
+ */
74
+
75
+ /**
76
+ * @typedef {Object} MemoryGraph
77
+ * @property {string} version - Schema version
78
+ * @property {string} lastUpdated - ISO timestamp
79
+ * @property {Object<string, GraphNode>} nodes - Nodes keyed by ID
80
+ * @property {GraphEdge[]} edges - Array of edges
81
+ */
82
+
83
+ /**
84
+ * @typedef {Object} Subgraph
85
+ * @property {Object<string, GraphNode>} nodes - Subset of nodes
86
+ * @property {GraphEdge[]} edges - Subset of edges connecting returned nodes
87
+ */
88
+
89
+ // --- Core Graph Functions ---
90
+
91
+ /**
92
+ * Create an empty graph structure.
93
+ * @returns {MemoryGraph}
94
+ */
95
+ function createGraph() {
96
+ return {
97
+ version: GRAPH_VERSION,
98
+ lastUpdated: new Date().toISOString(),
99
+ nodes: {},
100
+ edges: [],
101
+ };
102
+ }
103
+
104
+ /**
105
+ * Generate a stable node ID from type and content.
106
+ * @param {NodeType} type - Node type
107
+ * @param {string} content - Content to hash
108
+ * @returns {string} Node ID in format "{type}-{8 hex chars}"
109
+ */
110
+ function generateNodeId(type, content) {
111
+ const hash = crypto.createHash('sha256')
112
+ .update(content.toLowerCase().trim())
113
+ .digest('hex')
114
+ .substring(0, 8);
115
+ return `${type}-${hash}`;
116
+ }
117
+
118
+ // @cap-todo(ac:F-034/AC-1) Maintain memory graph connecting features, threads, decisions, pitfalls, and patterns as typed nodes with labeled edges
119
+
120
+ /**
121
+ * Add or update a node in the graph.
122
+ * If a node with the same ID exists, it is updated (merged metadata, refreshed updatedAt).
123
+ * @param {MemoryGraph} graph - Graph to mutate
124
+ * @param {GraphNode} node - Node to add or update
125
+ * @returns {MemoryGraph} The mutated graph (for chaining)
126
+ */
127
+ function addNode(graph, node) {
128
+ const now = new Date().toISOString();
129
+ const existing = graph.nodes[node.id];
130
+
131
+ if (existing) {
132
+ // Update: merge metadata, refresh timestamp
133
+ existing.label = node.label || existing.label;
134
+ existing.updatedAt = now;
135
+ existing.active = node.active !== undefined ? node.active : existing.active;
136
+ existing.metadata = { ...existing.metadata, ...node.metadata };
137
+ } else {
138
+ // Insert
139
+ graph.nodes[node.id] = {
140
+ type: node.type,
141
+ id: node.id,
142
+ label: node.label || '',
143
+ createdAt: node.createdAt || now,
144
+ updatedAt: node.updatedAt || now,
145
+ active: node.active !== undefined ? node.active : true,
146
+ metadata: node.metadata || {},
147
+ };
148
+ }
149
+
150
+ graph.lastUpdated = now;
151
+ return graph;
152
+ }
153
+
154
+ // @cap-todo(ac:F-034/AC-2) Support labeled edges between nodes
155
+
156
+ /**
157
+ * Add an edge to the graph. Deduplicates by source+target+type.
158
+ * If duplicate found, updates metadata and refreshes the edge.
159
+ * @param {MemoryGraph} graph - Graph to mutate
160
+ * @param {GraphEdge} edge - Edge to add
161
+ * @returns {MemoryGraph} The mutated graph (for chaining)
162
+ */
163
+ function addEdge(graph, edge) {
164
+ const now = new Date().toISOString();
165
+
166
+ // Deduplicate by source+target+type
167
+ const existingIdx = graph.edges.findIndex(
168
+ e => e.source === edge.source && e.target === edge.target && e.type === edge.type
169
+ );
170
+
171
+ if (existingIdx >= 0) {
172
+ // Update existing edge
173
+ graph.edges[existingIdx].active = edge.active !== undefined ? edge.active : graph.edges[existingIdx].active;
174
+ graph.edges[existingIdx].metadata = { ...graph.edges[existingIdx].metadata, ...edge.metadata };
175
+ } else {
176
+ graph.edges.push({
177
+ source: edge.source,
178
+ target: edge.target,
179
+ type: edge.type,
180
+ createdAt: edge.createdAt || now,
181
+ active: edge.active !== undefined ? edge.active : true,
182
+ metadata: edge.metadata || {},
183
+ });
184
+ }
185
+
186
+ graph.lastUpdated = now;
187
+ return graph;
188
+ }
189
+
190
+ /**
191
+ * Remove a node by marking it and its edges as inactive.
192
+ * Does NOT delete — preserves historical context.
193
+ * @param {MemoryGraph} graph - Graph to mutate
194
+ * @param {string} nodeId - Node ID to remove
195
+ * @returns {MemoryGraph} The mutated graph
196
+ */
197
+ function removeNode(graph, nodeId) {
198
+ const node = graph.nodes[nodeId];
199
+ if (!node) return graph;
200
+
201
+ node.active = false;
202
+ node.updatedAt = new Date().toISOString();
203
+
204
+ // Mark all connected edges as inactive
205
+ for (const edge of graph.edges) {
206
+ if (edge.source === nodeId || edge.target === nodeId) {
207
+ edge.active = false;
208
+ }
209
+ }
210
+
211
+ graph.lastUpdated = new Date().toISOString();
212
+ return graph;
213
+ }
214
+
215
+ // @cap-todo(ac:F-034/AC-6) When node marked stale, preserve edges as inactive so historical context is not lost
216
+
217
+ /**
218
+ * Mark a node as stale (inactive) while preserving edges as inactive.
219
+ * Same as removeNode but semantically distinct — stale means aged out, not deleted.
220
+ * @param {MemoryGraph} graph - Graph to mutate
221
+ * @param {string} nodeId - Node ID to mark stale
222
+ * @returns {MemoryGraph} The mutated graph
223
+ */
224
+ function markStale(graph, nodeId) {
225
+ return removeNode(graph, nodeId);
226
+ }
227
+
228
+ // --- Query Functions ---
229
+
230
+ // @cap-todo(ac:F-034/AC-3) Graph queryable by node type and traversal depth
231
+
232
+ /**
233
+ * Query nodes by type with optional filtering.
234
+ * @param {MemoryGraph} graph - Graph to query
235
+ * @param {NodeType} nodeType - Node type to filter by
236
+ * @param {Object} [options]
237
+ * @param {boolean} [options.includeInactive=false] - Include inactive nodes
238
+ * @returns {GraphNode[]} Matching nodes
239
+ */
240
+ function queryByType(graph, nodeType, options = {}) {
241
+ const { includeInactive = false } = options;
242
+ return Object.values(graph.nodes).filter(node => {
243
+ if (node.type !== nodeType) return false;
244
+ if (!includeInactive && !node.active) return false;
245
+ return true;
246
+ });
247
+ }
248
+
249
+ /**
250
+ * Query neighbors of a node using BFS traversal up to N hops.
251
+ * Returns a subgraph containing all reachable nodes and their connecting edges.
252
+ * @param {MemoryGraph} graph - Graph to query
253
+ * @param {string} nodeId - Starting node ID
254
+ * @param {number} [depth=1] - Maximum traversal depth (hops)
255
+ * @param {Object} [options]
256
+ * @param {boolean} [options.includeInactive=false] - Traverse inactive edges
257
+ * @param {EdgeType[]} [options.edgeTypes] - Filter to specific edge types
258
+ * @param {string} [options.direction='both'] - 'outgoing', 'incoming', or 'both'
259
+ * @returns {Subgraph} Subgraph of reachable nodes and edges
260
+ */
261
+ function queryNeighbors(graph, nodeId, depth = 1, options = {}) {
262
+ const { includeInactive = false, edgeTypes, direction = 'both' } = options;
263
+
264
+ const visitedNodes = new Set();
265
+ const resultNodes = {};
266
+ const resultEdges = [];
267
+ const edgeSet = new Set(); // dedup edges by "source|target|type"
268
+
269
+ // Include the starting node
270
+ if (graph.nodes[nodeId]) {
271
+ visitedNodes.add(nodeId);
272
+ resultNodes[nodeId] = graph.nodes[nodeId];
273
+ }
274
+
275
+ // BFS
276
+ let frontier = [nodeId];
277
+
278
+ for (let d = 0; d < depth; d++) {
279
+ const nextFrontier = [];
280
+
281
+ for (const currentId of frontier) {
282
+ for (const edge of graph.edges) {
283
+ // Filter by active status
284
+ if (!includeInactive && !edge.active) continue;
285
+
286
+ // Filter by edge type
287
+ if (edgeTypes && !edgeTypes.includes(edge.type)) continue;
288
+
289
+ let neighborId = null;
290
+
291
+ if (direction === 'outgoing' || direction === 'both') {
292
+ if (edge.source === currentId) neighborId = edge.target;
293
+ }
294
+ if (direction === 'incoming' || direction === 'both') {
295
+ if (edge.target === currentId) neighborId = edge.source;
296
+ }
297
+
298
+ if (neighborId && !visitedNodes.has(neighborId) && graph.nodes[neighborId]) {
299
+ const neighborNode = graph.nodes[neighborId];
300
+ if (!includeInactive && !neighborNode.active) continue;
301
+
302
+ visitedNodes.add(neighborId);
303
+ resultNodes[neighborId] = neighborNode;
304
+ nextFrontier.push(neighborId);
305
+ }
306
+
307
+ // Collect the edge if it connects visited nodes
308
+ if (neighborId) {
309
+ const edgeKey = `${edge.source}|${edge.target}|${edge.type}`;
310
+ if (!edgeSet.has(edgeKey)) {
311
+ edgeSet.add(edgeKey);
312
+ resultEdges.push(edge);
313
+ }
314
+ }
315
+ }
316
+ }
317
+
318
+ frontier = nextFrontier;
319
+ if (frontier.length === 0) break;
320
+ }
321
+
322
+ return { nodes: resultNodes, edges: resultEdges };
323
+ }
324
+
325
+ // @cap-todo(ac:F-034/AC-5) Support temporal queries — what changed between session X and session Y via timestamps
326
+
327
+ /**
328
+ * Query nodes and edges created or updated within a date range.
329
+ * @param {MemoryGraph} graph - Graph to query
330
+ * @param {string} since - ISO timestamp (inclusive lower bound)
331
+ * @param {string} [until] - ISO timestamp (inclusive upper bound, defaults to now)
332
+ * @returns {Subgraph} Subgraph of nodes/edges within the time range
333
+ */
334
+ function queryTemporal(graph, since, until) {
335
+ const sinceTs = since || '1970-01-01T00:00:00Z';
336
+ const untilTs = until || new Date().toISOString();
337
+
338
+ const nodes = {};
339
+ const nodeIds = new Set();
340
+
341
+ for (const [id, node] of Object.entries(graph.nodes)) {
342
+ const updated = node.updatedAt || node.createdAt;
343
+ if (updated >= sinceTs && updated <= untilTs) {
344
+ nodes[id] = node;
345
+ nodeIds.add(id);
346
+ }
347
+ }
348
+
349
+ const edges = graph.edges.filter(edge => {
350
+ const ts = edge.createdAt;
351
+ return ts >= sinceTs && ts <= untilTs;
352
+ });
353
+
354
+ return { nodes, edges };
355
+ }
356
+
357
+ // --- Build/Sync Functions ---
358
+
359
+ // @cap-todo(ac:F-034/AC-7) Graph incrementally updatable — adding new session shall not require full graph reconstruction
360
+
361
+ /**
362
+ * Build a complete graph from all available memory sources.
363
+ * Used for initial graph creation or full rebuild.
364
+ * Reads memory entries (from cap-memory-engine), feature map, and thread index.
365
+ *
366
+ * @param {string} cwd - Absolute path to project root
367
+ * @param {Object} [options]
368
+ * @param {string|null} [options.appPath] - Relative app path for monorepo scoping
369
+ * @returns {MemoryGraph}
370
+ */
371
+ function buildFromMemory(cwd, options = {}) {
372
+ const graph = createGraph();
373
+ const { appPath = null } = options;
374
+
375
+ // --- Load feature map ---
376
+ // @cap-decision Lazy require to avoid circular dependencies — these modules are only needed during build/sync
377
+ const { readFeatureMap } = require('./cap-feature-map.cjs');
378
+ // @cap-todo(ac:F-081/AC-4 iter:2) Migrated to {safe: true} opt-in to preserve CLI on duplicate-ID FEATURE-MAP.
379
+ // @cap-decision(F-081/iter2) Warn on parseError; continue with partial map for read-only display.
380
+ const featureMap = readFeatureMap(cwd, appPath, { safe: true });
381
+ if (featureMap && featureMap.parseError) {
382
+ console.warn('cap: memory-graph — duplicate feature ID detected, graph uses partial map: ' + String(featureMap.parseError.message).trim());
383
+ }
384
+
385
+ for (const feature of featureMap.features || []) {
386
+ const nodeId = `feature-${feature.id.toLowerCase().replace(/-/g, '')}`;
387
+ addNode(graph, {
388
+ type: 'feature',
389
+ id: nodeId,
390
+ label: `${feature.id}: ${feature.title}`,
391
+ active: true,
392
+ metadata: {
393
+ featureId: feature.id,
394
+ state: feature.state,
395
+ acCount: (feature.acs || []).length,
396
+ files: feature.files || [],
397
+ },
398
+ });
399
+
400
+ // Add dependency edges between features
401
+ for (const dep of feature.dependencies || []) {
402
+ const depNodeId = `feature-${dep.toLowerCase().replace(/-/g, '')}`;
403
+ addEdge(graph, {
404
+ source: nodeId,
405
+ target: depNodeId,
406
+ type: 'depends_on',
407
+ metadata: {},
408
+ });
409
+ }
410
+ }
411
+
412
+ // --- Load thread index ---
413
+ const { loadIndex, loadThread } = require('./cap-thread-tracker.cjs');
414
+ const threadIndex = loadIndex(cwd);
415
+
416
+ for (const entry of threadIndex.threads || []) {
417
+ const threadNodeId = `thread-${entry.id.replace(/^thr-/, '')}`;
418
+ const thread = loadThread(cwd, entry.id);
419
+
420
+ addNode(graph, {
421
+ type: 'thread',
422
+ id: threadNodeId,
423
+ label: entry.name,
424
+ createdAt: entry.timestamp,
425
+ updatedAt: entry.timestamp,
426
+ active: true,
427
+ metadata: {
428
+ threadId: entry.id,
429
+ keywords: entry.keywords || [],
430
+ problemStatement: thread ? thread.problemStatement : '',
431
+ },
432
+ });
433
+
434
+ // Link thread to features
435
+ for (const fId of entry.featureIds || []) {
436
+ const featureNodeId = `feature-${fId.toLowerCase().replace(/-/g, '')}`;
437
+ addEdge(graph, {
438
+ source: threadNodeId,
439
+ target: featureNodeId,
440
+ type: 'informed_by',
441
+ metadata: {},
442
+ });
443
+ }
444
+
445
+ // Link branched threads
446
+ if (entry.parentThreadId) {
447
+ const parentNodeId = `thread-${entry.parentThreadId.replace(/^thr-/, '')}`;
448
+ addEdge(graph, {
449
+ source: threadNodeId,
450
+ target: parentNodeId,
451
+ type: 'branched_from',
452
+ metadata: {},
453
+ });
454
+ }
455
+ }
456
+
457
+ // --- Load memory entries from flat files ---
458
+ const memoryDir = path.join(cwd, '.cap', 'memory');
459
+ if (fs.existsSync(memoryDir)) {
460
+ _ingestMemoryFiles(graph, memoryDir);
461
+ }
462
+
463
+ return graph;
464
+ }
465
+
466
+ /**
467
+ * Ingest memory entries from the flat .cap/memory/*.md files into graph nodes.
468
+ * Parses decisions.md, pitfalls.md, patterns.md, hotspots.md.
469
+ * @param {MemoryGraph} graph - Graph to mutate
470
+ * @param {string} memoryDir - Absolute path to .cap/memory/
471
+ */
472
+ function _ingestMemoryFiles(graph, memoryDir) {
473
+ const categories = {
474
+ 'decisions.md': 'decision',
475
+ 'pitfalls.md': 'pitfall',
476
+ 'patterns.md': 'pattern',
477
+ 'hotspots.md': 'hotspot',
478
+ };
479
+
480
+ for (const [filename, category] of Object.entries(categories)) {
481
+ const filePath = path.join(memoryDir, filename);
482
+ if (!fs.existsSync(filePath)) continue;
483
+
484
+ try {
485
+ const content = fs.readFileSync(filePath, 'utf8');
486
+ const entries = _parseMarkdownEntries(content, category);
487
+
488
+ for (const entry of entries) {
489
+ const nodeId = generateNodeId(category, entry.label);
490
+ addNode(graph, {
491
+ type: category,
492
+ id: nodeId,
493
+ label: entry.label,
494
+ active: true,
495
+ metadata: {
496
+ ...entry.metadata,
497
+ sourceFile: filename,
498
+ },
499
+ });
500
+
501
+ // Link to features mentioned in metadata
502
+ for (const fId of entry.features || []) {
503
+ const featureNodeId = `feature-${fId.toLowerCase().replace(/-/g, '')}`;
504
+ if (graph.nodes[featureNodeId]) {
505
+ addEdge(graph, {
506
+ source: nodeId,
507
+ target: featureNodeId,
508
+ type: 'relates_to',
509
+ metadata: {},
510
+ });
511
+ }
512
+ }
513
+ }
514
+ } catch (_e) {
515
+ // Skip unparseable files
516
+ }
517
+ }
518
+ }
519
+
520
+ /**
521
+ * Parse markdown memory files into structured entries.
522
+ * @param {string} content - Markdown content
523
+ * @param {string} category - Memory category
524
+ * @returns {Array<{label: string, metadata: Object, features: string[]}>}
525
+ */
526
+ function _parseMarkdownEntries(content, category) {
527
+ const entries = [];
528
+
529
+ if (category === 'hotspot') {
530
+ // Parse table rows: | Rank | File | Sessions | Edits | Since |
531
+ const rowRe = /^\|\s*(?:<a id="[^"]*"><\/a>)?\s*\d+\s*\|\s*`([^`]+)`\s*\|\s*(\d+)\s*\|\s*(\d+)\s*\|\s*([^\s|]+)/gm;
532
+ let match;
533
+ while ((match = rowRe.exec(content)) !== null) {
534
+ entries.push({
535
+ label: `Hotspot: ${match[1]}`,
536
+ metadata: { file: match[1], sessions: parseInt(match[2], 10), edits: parseInt(match[3], 10), since: match[4] },
537
+ features: [],
538
+ });
539
+ }
540
+ } else {
541
+ // Parse heading entries: ### <a id="..."></a>Content
542
+ const headingRe = /^###\s+(?:<a id="[^"]*"><\/a>)?(.+?)(?:\s*\*\*\[pinned\]\*\*)?$/gm;
543
+ let match;
544
+ while ((match = headingRe.exec(content)) !== null) {
545
+ const label = match[1].trim();
546
+ // Extract features from following lines
547
+ const afterMatch = content.substring(match.index + match[0].length, match.index + match[0].length + 300);
548
+ const featureRe = /F-\d{3}/g;
549
+ const features = [];
550
+ let fMatch;
551
+ while ((fMatch = featureRe.exec(afterMatch)) !== null) {
552
+ features.push(fMatch[0]);
553
+ }
554
+ entries.push({
555
+ label,
556
+ metadata: { pinned: match[0].includes('[pinned]') },
557
+ features: [...new Set(features)],
558
+ });
559
+ }
560
+ }
561
+
562
+ return entries;
563
+ }
564
+
565
+ /**
566
+ * Incrementally update the graph with new memory entries.
567
+ * Does NOT require full rebuild — only processes the new entries.
568
+ *
569
+ * @param {MemoryGraph} graph - Existing graph to update
570
+ * @param {import('./cap-memory-engine.cjs').MemoryEntry[]} newEntries - New memory entries from accumulation
571
+ * @param {Object} [options]
572
+ * @param {string[]} [options.staleNodeIds] - Node IDs to mark as stale
573
+ * @returns {MemoryGraph} The mutated graph
574
+ */
575
+ function incrementalUpdate(graph, newEntries, options = {}) {
576
+ const { staleNodeIds = [] } = options;
577
+
578
+ // Add new entries as nodes
579
+ for (const entry of newEntries) {
580
+ const nodeId = generateNodeId(entry.category, entry.content);
581
+ addNode(graph, {
582
+ type: entry.category,
583
+ id: nodeId,
584
+ label: entry.content,
585
+ active: true,
586
+ metadata: {
587
+ source: entry.metadata.source,
588
+ file: entry.file,
589
+ relatedFiles: entry.metadata.relatedFiles || [],
590
+ pinned: entry.metadata.pinned || false,
591
+ sessions: entry.metadata.sessions,
592
+ edits: entry.metadata.edits,
593
+ },
594
+ });
595
+
596
+ // Link to features
597
+ for (const fId of entry.metadata.features || []) {
598
+ const featureNodeId = `feature-${fId.toLowerCase().replace(/-/g, '')}`;
599
+ if (graph.nodes[featureNodeId]) {
600
+ addEdge(graph, {
601
+ source: nodeId,
602
+ target: featureNodeId,
603
+ type: 'relates_to',
604
+ metadata: {},
605
+ });
606
+ }
607
+ }
608
+ }
609
+
610
+ // Mark stale nodes
611
+ for (const nodeId of staleNodeIds) {
612
+ markStale(graph, nodeId);
613
+ }
614
+
615
+ return graph;
616
+ }
617
+
618
+ // @cap-todo(ac:F-034/AC-4) Flat memory files (decisions.md, hotspots.md, patterns.md, pitfalls.md) remain as human-readable views generated from graph
619
+
620
+ /**
621
+ * Generate flat markdown view content from graph nodes.
622
+ * Returns content strings suitable for writing to .cap/memory/*.md files.
623
+ * Delegates to cap-memory-dir.cjs for actual markdown formatting.
624
+ *
625
+ * @param {MemoryGraph} graph - Graph to generate views from
626
+ * @returns {import('./cap-memory-engine.cjs').MemoryEntry[]} Memory entries suitable for writeMemoryDirectory()
627
+ */
628
+ function generateViews(graph) {
629
+ const entries = [];
630
+
631
+ for (const node of Object.values(graph.nodes)) {
632
+ if (!node.active) continue;
633
+ if (!['decision', 'pitfall', 'pattern', 'hotspot'].includes(node.type)) continue;
634
+
635
+ entries.push({
636
+ category: node.type,
637
+ file: node.metadata.file || null,
638
+ content: node.label,
639
+ metadata: {
640
+ source: node.metadata.source || node.createdAt,
641
+ branch: node.metadata.branch || null,
642
+ relatedFiles: node.metadata.relatedFiles || [],
643
+ features: _getRelatedFeatureIds(graph, node.id),
644
+ pinned: node.metadata.pinned || false,
645
+ sessions: node.metadata.sessions,
646
+ edits: node.metadata.edits,
647
+ confirmations: node.metadata.confirmations,
648
+ },
649
+ });
650
+ }
651
+
652
+ return entries;
653
+ }
654
+
655
+ /**
656
+ * Get feature IDs connected to a node via relates_to or informed_by edges.
657
+ * @param {MemoryGraph} graph
658
+ * @param {string} nodeId
659
+ * @returns {string[]}
660
+ */
661
+ function _getRelatedFeatureIds(graph, nodeId) {
662
+ const featureIds = [];
663
+ for (const edge of graph.edges) {
664
+ if (!edge.active) continue;
665
+ if (edge.source !== nodeId && edge.target !== nodeId) continue;
666
+
667
+ const otherId = edge.source === nodeId ? edge.target : edge.source;
668
+ const otherNode = graph.nodes[otherId];
669
+ if (otherNode && otherNode.type === 'feature' && otherNode.metadata.featureId) {
670
+ featureIds.push(otherNode.metadata.featureId);
671
+ }
672
+ }
673
+ return [...new Set(featureIds)];
674
+ }
675
+
676
+ // --- Serialization / I/O ---
677
+
678
+ // @cap-todo(ac:F-034/AC-8) Graph data git-committable and merge-friendly — sorted keys, one-entry-per-line JSON
679
+
680
+ /**
681
+ * Serialize graph to merge-friendly JSON string.
682
+ * - Top-level keys sorted
683
+ * - Nodes object sorted by key
684
+ * - Edges array sorted by [source, target, type]
685
+ * - 2-space indent for readability
686
+ *
687
+ * @param {MemoryGraph} graph - Graph to serialize
688
+ * @returns {string} JSON string
689
+ */
690
+ function serializeGraph(graph) {
691
+ // Sort nodes by key
692
+ const sortedNodes = {};
693
+ const nodeKeys = Object.keys(graph.nodes).sort();
694
+ for (const key of nodeKeys) {
695
+ sortedNodes[key] = graph.nodes[key];
696
+ }
697
+
698
+ // Sort edges by [source, target, type]
699
+ const sortedEdges = [...graph.edges].sort((a, b) => {
700
+ if (a.source !== b.source) return a.source.localeCompare(b.source);
701
+ if (a.target !== b.target) return a.target.localeCompare(b.target);
702
+ return a.type.localeCompare(b.type);
703
+ });
704
+
705
+ const output = {
706
+ version: graph.version,
707
+ lastUpdated: graph.lastUpdated,
708
+ nodes: sortedNodes,
709
+ edges: sortedEdges,
710
+ };
711
+
712
+ return JSON.stringify(output, null, 2) + '\n';
713
+ }
714
+
715
+ /**
716
+ * Load graph from .cap/memory/graph.json.
717
+ * Returns empty graph if file does not exist.
718
+ *
719
+ * @param {string} cwd - Absolute path to project root
720
+ * @returns {MemoryGraph}
721
+ */
722
+ function loadGraph(cwd) {
723
+ const filePath = path.join(cwd, GRAPH_FILE);
724
+ try {
725
+ if (!fs.existsSync(filePath)) return createGraph();
726
+ const content = fs.readFileSync(filePath, 'utf8');
727
+ const parsed = JSON.parse(content);
728
+ // Forward-compatible merge with defaults
729
+ return {
730
+ version: parsed.version || GRAPH_VERSION,
731
+ lastUpdated: parsed.lastUpdated || new Date().toISOString(),
732
+ nodes: parsed.nodes || {},
733
+ edges: parsed.edges || [],
734
+ };
735
+ } catch (_e) {
736
+ return createGraph();
737
+ }
738
+ }
739
+
740
+ /**
741
+ * Save graph to .cap/memory/graph.json.
742
+ * Creates directory if needed.
743
+ *
744
+ * @param {string} cwd - Absolute path to project root
745
+ * @param {MemoryGraph} graph - Graph to save
746
+ */
747
+ function saveGraph(cwd, graph) {
748
+ const filePath = path.join(cwd, GRAPH_FILE);
749
+ const dir = path.dirname(filePath);
750
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
751
+ fs.writeFileSync(filePath, serializeGraph(graph), 'utf8');
752
+ }
753
+
754
+ // --- Exports ---
755
+
756
+ module.exports = {
757
+ // Core graph operations
758
+ createGraph,
759
+ addNode,
760
+ addEdge,
761
+ removeNode,
762
+ markStale,
763
+ generateNodeId,
764
+
765
+ // Query functions
766
+ queryByType,
767
+ queryNeighbors,
768
+ queryTemporal,
769
+
770
+ // Build/sync functions
771
+ buildFromMemory,
772
+ incrementalUpdate,
773
+ generateViews,
774
+
775
+ // Serialization / I/O
776
+ serializeGraph,
777
+ loadGraph,
778
+ saveGraph,
779
+
780
+ // Constants
781
+ GRAPH_FILE,
782
+ GRAPH_VERSION,
783
+ EDGE_TYPES,
784
+ NODE_TYPES,
785
+
786
+ // Internal (for testing)
787
+ _ingestMemoryFiles,
788
+ _parseMarkdownEntries,
789
+ _getRelatedFeatureIds,
790
+ };