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,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
|
+
};
|