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,519 @@
|
|
|
1
|
+
// @cap-feature(feature:F-031) Conversation Thread Tracking — persist brainstorm sessions as named threads with branching and topic detection
|
|
2
|
+
// @cap-decision Pure logic module with explicit I/O functions — same pattern as cap-memory-engine.cjs. No side effects in analysis functions.
|
|
3
|
+
// @cap-decision Thread storage uses individual JSON files per thread plus a central index — enables git-friendly diffs and parallel team access.
|
|
4
|
+
// @cap-constraint Zero external dependencies — uses only Node.js built-ins (fs, path, crypto).
|
|
5
|
+
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
const fs = require('node:fs');
|
|
9
|
+
const path = require('node:path');
|
|
10
|
+
const crypto = require('node:crypto');
|
|
11
|
+
|
|
12
|
+
// --- Constants ---
|
|
13
|
+
|
|
14
|
+
/** Directory for thread storage relative to project root. */
|
|
15
|
+
const THREADS_DIR = path.join('.cap', 'memory', 'threads');
|
|
16
|
+
|
|
17
|
+
/** Thread index file relative to project root. */
|
|
18
|
+
const THREAD_INDEX_FILE = path.join('.cap', 'memory', 'thread-index.json');
|
|
19
|
+
|
|
20
|
+
/** Minimum keyword overlap ratio (0-1) to consider threads as revisiting the same topic. */
|
|
21
|
+
const REVISIT_KEYWORD_THRESHOLD = 0.25;
|
|
22
|
+
|
|
23
|
+
/** Minimum number of shared keywords required for a revisit match. */
|
|
24
|
+
const REVISIT_MIN_SHARED_KEYWORDS = 2;
|
|
25
|
+
|
|
26
|
+
/** Stop words excluded from keyword extraction. */
|
|
27
|
+
const STOP_WORDS = new Set([
|
|
28
|
+
'the', 'a', 'an', 'is', 'are', 'was', 'were', 'be', 'been', 'being',
|
|
29
|
+
'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could',
|
|
30
|
+
'should', 'may', 'might', 'shall', 'can', 'need', 'must', 'ought',
|
|
31
|
+
'and', 'but', 'or', 'nor', 'not', 'so', 'yet', 'both', 'either',
|
|
32
|
+
'neither', 'each', 'every', 'all', 'any', 'few', 'more', 'most',
|
|
33
|
+
'other', 'some', 'such', 'no', 'only', 'own', 'same', 'than',
|
|
34
|
+
'too', 'very', 'just', 'because', 'as', 'until', 'while', 'of',
|
|
35
|
+
'at', 'by', 'for', 'with', 'about', 'against', 'between', 'through',
|
|
36
|
+
'during', 'before', 'after', 'above', 'below', 'to', 'from', 'up',
|
|
37
|
+
'down', 'in', 'out', 'on', 'off', 'over', 'under', 'again',
|
|
38
|
+
'further', 'then', 'once', 'here', 'there', 'when', 'where', 'why',
|
|
39
|
+
'how', 'what', 'which', 'who', 'whom', 'this', 'that', 'these',
|
|
40
|
+
'those', 'i', 'me', 'my', 'myself', 'we', 'our', 'ours', 'you',
|
|
41
|
+
'your', 'yours', 'he', 'him', 'his', 'she', 'her', 'hers', 'it',
|
|
42
|
+
'its', 'they', 'them', 'their', 'theirs', 'also', 'into', 'if',
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
// --- Types ---
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @typedef {Object} Thread
|
|
49
|
+
* @property {string} id - Unique thread ID (e.g., "thr-a1b2c3d4")
|
|
50
|
+
* @property {string} name - Human-readable thread name (derived from problem statement)
|
|
51
|
+
* @property {string} timestamp - ISO timestamp when thread was created
|
|
52
|
+
* @property {string|null} parentThreadId - Parent thread ID if branched, null otherwise
|
|
53
|
+
* @property {string|null} divergencePoint - Description of where this thread diverged from parent
|
|
54
|
+
* @property {string} problemStatement - The problem or topic being explored
|
|
55
|
+
* @property {string} solutionShape - High-level solution direction discovered
|
|
56
|
+
* @property {string[]} boundaryDecisions - Key boundary/scope decisions made during brainstorm
|
|
57
|
+
* @property {string[]} featureIds - Feature Map entries (F-IDs) that resulted from this thread
|
|
58
|
+
* @property {string[]} keywords - Extracted problem-space keywords for topic matching
|
|
59
|
+
*/
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @typedef {Object} ThreadIndexEntry
|
|
63
|
+
* @property {string} id - Thread ID
|
|
64
|
+
* @property {string} name - Thread name
|
|
65
|
+
* @property {string} timestamp - ISO timestamp
|
|
66
|
+
* @property {string[]} featureIds - Associated feature IDs
|
|
67
|
+
* @property {string|null} parentThreadId - Parent thread ID if branched
|
|
68
|
+
* @property {string[]} keywords - Problem-space keywords
|
|
69
|
+
*/
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* @typedef {Object} ThreadIndex
|
|
73
|
+
* @property {string} version - Index schema version
|
|
74
|
+
* @property {ThreadIndexEntry[]} threads - All thread index entries
|
|
75
|
+
*/
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* @typedef {Object} RevisitMatch
|
|
79
|
+
* @property {string} threadId - Matching thread ID
|
|
80
|
+
* @property {string} threadName - Matching thread name
|
|
81
|
+
* @property {number} keywordOverlap - Number of shared keywords
|
|
82
|
+
* @property {string[]} sharedKeywords - The overlapping keywords
|
|
83
|
+
* @property {string[]} sharedFeatureIds - Overlapping feature IDs
|
|
84
|
+
* @property {number} score - Combined relevance score (0-1)
|
|
85
|
+
*/
|
|
86
|
+
|
|
87
|
+
// --- Thread ID Generation ---
|
|
88
|
+
|
|
89
|
+
// @cap-decision Thread IDs use crypto.randomBytes for uniqueness — deterministic IDs (content hash) would collide when two threads start from the same problem statement.
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Generate a unique thread ID.
|
|
93
|
+
* @returns {string} Thread ID in format "thr-{8 hex chars}"
|
|
94
|
+
*/
|
|
95
|
+
function generateThreadId() {
|
|
96
|
+
const bytes = crypto.randomBytes(4);
|
|
97
|
+
return 'thr-' + bytes.toString('hex');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// --- Keyword Extraction ---
|
|
101
|
+
|
|
102
|
+
// @cap-todo(ac:F-031/AC-3) Keyword extraction for topic revisit detection
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Extract problem-space keywords from text.
|
|
106
|
+
* Filters stop words, short words, and normalizes to lowercase.
|
|
107
|
+
* @param {string} text - Input text (problem statement, solution shape, etc.)
|
|
108
|
+
* @returns {string[]} Deduplicated sorted keywords
|
|
109
|
+
*/
|
|
110
|
+
function extractKeywords(text) {
|
|
111
|
+
if (!text || typeof text !== 'string') return [];
|
|
112
|
+
|
|
113
|
+
const words = text
|
|
114
|
+
.toLowerCase()
|
|
115
|
+
.replace(/[^a-z0-9\s-]/g, ' ')
|
|
116
|
+
.split(/\s+/)
|
|
117
|
+
.filter(w => w.length >= 3 && !STOP_WORDS.has(w));
|
|
118
|
+
|
|
119
|
+
return [...new Set(words)].sort();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// --- Thread Creation ---
|
|
123
|
+
|
|
124
|
+
// @cap-todo(ac:F-031/AC-1) Persist each brainstorm session as a named thread with unique ID, timestamp, and parent reference
|
|
125
|
+
// @cap-todo(ac:F-031/AC-2) Capture full discovery context: problem statement, solution shape, boundary decisions, feature IDs
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Create a new thread object from brainstorm session data.
|
|
129
|
+
* @param {Object} params
|
|
130
|
+
* @param {string} params.problemStatement - The problem or topic being explored
|
|
131
|
+
* @param {string} [params.solutionShape] - High-level solution direction
|
|
132
|
+
* @param {string[]} [params.boundaryDecisions] - Key boundary/scope decisions
|
|
133
|
+
* @param {string[]} [params.featureIds] - Resulting Feature Map entry IDs
|
|
134
|
+
* @param {string|null} [params.parentThreadId] - Parent thread ID if branching
|
|
135
|
+
* @param {string|null} [params.divergencePoint] - Where this diverges from parent
|
|
136
|
+
* @param {string} [params.name] - Optional human-readable name (auto-derived if omitted)
|
|
137
|
+
* @returns {Thread}
|
|
138
|
+
*/
|
|
139
|
+
function createThread(params) {
|
|
140
|
+
const {
|
|
141
|
+
problemStatement,
|
|
142
|
+
solutionShape = '',
|
|
143
|
+
boundaryDecisions = [],
|
|
144
|
+
featureIds = [],
|
|
145
|
+
parentThreadId = null,
|
|
146
|
+
divergencePoint = null,
|
|
147
|
+
name = null,
|
|
148
|
+
} = params;
|
|
149
|
+
|
|
150
|
+
const id = generateThreadId();
|
|
151
|
+
const timestamp = new Date().toISOString();
|
|
152
|
+
|
|
153
|
+
// Auto-derive name from problem statement: first 60 chars, trimmed at word boundary
|
|
154
|
+
const derivedName = name || deriveName(problemStatement);
|
|
155
|
+
|
|
156
|
+
// Extract keywords from all textual content
|
|
157
|
+
const allText = [problemStatement, solutionShape, ...boundaryDecisions].join(' ');
|
|
158
|
+
const keywords = extractKeywords(allText);
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
id,
|
|
162
|
+
name: derivedName,
|
|
163
|
+
timestamp,
|
|
164
|
+
parentThreadId,
|
|
165
|
+
divergencePoint,
|
|
166
|
+
problemStatement,
|
|
167
|
+
solutionShape,
|
|
168
|
+
boundaryDecisions,
|
|
169
|
+
featureIds,
|
|
170
|
+
keywords,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Derive a human-readable name from a problem statement.
|
|
176
|
+
* @param {string} problemStatement
|
|
177
|
+
* @returns {string}
|
|
178
|
+
*/
|
|
179
|
+
function deriveName(problemStatement) {
|
|
180
|
+
if (!problemStatement) return 'Untitled Thread';
|
|
181
|
+
const trimmed = problemStatement.substring(0, 60).trim();
|
|
182
|
+
// Trim at last word boundary if we truncated
|
|
183
|
+
if (problemStatement.length > 60) {
|
|
184
|
+
const lastSpace = trimmed.lastIndexOf(' ');
|
|
185
|
+
if (lastSpace > 20) return trimmed.substring(0, lastSpace) + '...';
|
|
186
|
+
return trimmed + '...';
|
|
187
|
+
}
|
|
188
|
+
return trimmed;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// --- Thread Branching ---
|
|
192
|
+
|
|
193
|
+
// @cap-todo(ac:F-031/AC-4) Support thread branching with parent thread ID and divergence point
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Create a branched thread from an existing parent thread.
|
|
197
|
+
* @param {Thread} parentThread - The thread to branch from
|
|
198
|
+
* @param {Object} params - Same as createThread params (minus parentThreadId/divergencePoint)
|
|
199
|
+
* @param {string} params.problemStatement - The divergent problem statement
|
|
200
|
+
* @param {string} params.divergencePoint - Description of where/why the branch diverged
|
|
201
|
+
* @param {string} [params.solutionShape]
|
|
202
|
+
* @param {string[]} [params.boundaryDecisions]
|
|
203
|
+
* @param {string[]} [params.featureIds]
|
|
204
|
+
* @param {string} [params.name]
|
|
205
|
+
* @returns {Thread}
|
|
206
|
+
*/
|
|
207
|
+
function branchThread(parentThread, params) {
|
|
208
|
+
return createThread({
|
|
209
|
+
...params,
|
|
210
|
+
parentThreadId: parentThread.id,
|
|
211
|
+
divergencePoint: params.divergencePoint || null,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// --- Topic Revisit Detection ---
|
|
216
|
+
|
|
217
|
+
// @cap-todo(ac:F-031/AC-3) Detect when a brainstorm session revisits a topic covered by an existing thread
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Compute keyword overlap between two keyword sets.
|
|
221
|
+
* @param {string[]} keywordsA
|
|
222
|
+
* @param {string[]} keywordsB
|
|
223
|
+
* @returns {{ shared: string[], overlapRatio: number }}
|
|
224
|
+
*/
|
|
225
|
+
function computeKeywordOverlap(keywordsA, keywordsB) {
|
|
226
|
+
const setB = new Set(keywordsB);
|
|
227
|
+
const shared = keywordsA.filter(k => setB.has(k));
|
|
228
|
+
const unionSize = new Set([...keywordsA, ...keywordsB]).size;
|
|
229
|
+
const overlapRatio = unionSize > 0 ? shared.length / unionSize : 0;
|
|
230
|
+
return { shared, overlapRatio };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Detect threads that revisit a given topic.
|
|
235
|
+
* Compares problem-space keywords and feature IDs against existing thread index.
|
|
236
|
+
* @param {ThreadIndex} index - Current thread index
|
|
237
|
+
* @param {Object} params
|
|
238
|
+
* @param {string} params.problemStatement - New session's problem statement
|
|
239
|
+
* @param {string[]} [params.featureIds] - Feature IDs referenced in new session
|
|
240
|
+
* @param {string} [params.solutionShape] - Solution direction text
|
|
241
|
+
* @param {number} [params.keywordThreshold] - Override keyword overlap threshold
|
|
242
|
+
* @param {number} [params.minSharedKeywords] - Override minimum shared keywords
|
|
243
|
+
* @returns {RevisitMatch[]} Matching threads sorted by relevance score descending
|
|
244
|
+
*/
|
|
245
|
+
function detectRevisits(index, params) {
|
|
246
|
+
const {
|
|
247
|
+
problemStatement,
|
|
248
|
+
featureIds = [],
|
|
249
|
+
solutionShape = '',
|
|
250
|
+
keywordThreshold = REVISIT_KEYWORD_THRESHOLD,
|
|
251
|
+
minSharedKeywords = REVISIT_MIN_SHARED_KEYWORDS,
|
|
252
|
+
} = params;
|
|
253
|
+
|
|
254
|
+
if (!index || !index.threads || index.threads.length === 0) return [];
|
|
255
|
+
|
|
256
|
+
const newKeywords = extractKeywords([problemStatement, solutionShape].join(' '));
|
|
257
|
+
if (newKeywords.length === 0 && featureIds.length === 0) return [];
|
|
258
|
+
|
|
259
|
+
const newFeatureSet = new Set(featureIds);
|
|
260
|
+
const matches = [];
|
|
261
|
+
|
|
262
|
+
for (const entry of index.threads) {
|
|
263
|
+
const { shared, overlapRatio } = computeKeywordOverlap(newKeywords, entry.keywords || []);
|
|
264
|
+
const sharedFeatureIds = (entry.featureIds || []).filter(f => newFeatureSet.has(f));
|
|
265
|
+
|
|
266
|
+
// Score: weighted combination of keyword overlap and feature ID overlap
|
|
267
|
+
const keywordScore = overlapRatio;
|
|
268
|
+
const featureScore = sharedFeatureIds.length > 0 ? 0.5 : 0;
|
|
269
|
+
const score = keywordScore * 0.6 + featureScore * 0.4;
|
|
270
|
+
|
|
271
|
+
const meetsKeywordThreshold = overlapRatio >= keywordThreshold && shared.length >= minSharedKeywords;
|
|
272
|
+
const hasFeatureOverlap = sharedFeatureIds.length > 0;
|
|
273
|
+
|
|
274
|
+
if (meetsKeywordThreshold || hasFeatureOverlap) {
|
|
275
|
+
matches.push({
|
|
276
|
+
threadId: entry.id,
|
|
277
|
+
threadName: entry.name,
|
|
278
|
+
keywordOverlap: shared.length,
|
|
279
|
+
sharedKeywords: shared,
|
|
280
|
+
sharedFeatureIds,
|
|
281
|
+
score,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Sort by score descending
|
|
287
|
+
matches.sort((a, b) => b.score - a.score);
|
|
288
|
+
return matches;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// --- Thread Index Management ---
|
|
292
|
+
|
|
293
|
+
// @cap-todo(ac:F-031/AC-5) Store thread metadata in .cap/memory/thread-index.json
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Create an empty thread index.
|
|
297
|
+
* @returns {ThreadIndex}
|
|
298
|
+
*/
|
|
299
|
+
function createEmptyIndex() {
|
|
300
|
+
return {
|
|
301
|
+
version: '1.0.0',
|
|
302
|
+
threads: [],
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Add a thread to the index.
|
|
308
|
+
* @param {ThreadIndex} index - Current index (mutated in place)
|
|
309
|
+
* @param {Thread} thread - Thread to add
|
|
310
|
+
* @returns {ThreadIndex} The updated index
|
|
311
|
+
*/
|
|
312
|
+
function addToIndex(index, thread) {
|
|
313
|
+
// Remove existing entry with same ID (idempotent upsert)
|
|
314
|
+
index.threads = index.threads.filter(t => t.id !== thread.id);
|
|
315
|
+
|
|
316
|
+
index.threads.push({
|
|
317
|
+
id: thread.id,
|
|
318
|
+
name: thread.name,
|
|
319
|
+
timestamp: thread.timestamp,
|
|
320
|
+
featureIds: thread.featureIds,
|
|
321
|
+
parentThreadId: thread.parentThreadId,
|
|
322
|
+
keywords: thread.keywords,
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
return index;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Remove a thread from the index by ID.
|
|
330
|
+
* @param {ThreadIndex} index
|
|
331
|
+
* @param {string} threadId
|
|
332
|
+
* @returns {ThreadIndex}
|
|
333
|
+
*/
|
|
334
|
+
function removeFromIndex(index, threadId) {
|
|
335
|
+
index.threads = index.threads.filter(t => t.id !== threadId);
|
|
336
|
+
return index;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// --- File I/O ---
|
|
340
|
+
|
|
341
|
+
// @cap-todo(ac:F-031/AC-6) Thread data shall be git-committable (not gitignored)
|
|
342
|
+
// @cap-decision Threads stored as individual JSON files — each thread is a single atomic file, enabling clean git diffs and minimal merge conflicts.
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Load the thread index from disk.
|
|
346
|
+
* @param {string} projectRoot - Absolute path to project root
|
|
347
|
+
* @returns {ThreadIndex}
|
|
348
|
+
*/
|
|
349
|
+
function loadIndex(projectRoot) {
|
|
350
|
+
const indexPath = path.join(projectRoot, THREAD_INDEX_FILE);
|
|
351
|
+
try {
|
|
352
|
+
if (!fs.existsSync(indexPath)) return createEmptyIndex();
|
|
353
|
+
const content = fs.readFileSync(indexPath, 'utf8');
|
|
354
|
+
const parsed = JSON.parse(content);
|
|
355
|
+
// Merge with defaults for forward compatibility
|
|
356
|
+
return { ...createEmptyIndex(), ...parsed };
|
|
357
|
+
} catch (_e) {
|
|
358
|
+
return createEmptyIndex();
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Save the thread index to disk.
|
|
364
|
+
* @param {string} projectRoot - Absolute path to project root
|
|
365
|
+
* @param {ThreadIndex} index
|
|
366
|
+
*/
|
|
367
|
+
function saveIndex(projectRoot, index) {
|
|
368
|
+
const indexPath = path.join(projectRoot, THREAD_INDEX_FILE);
|
|
369
|
+
const dir = path.dirname(indexPath);
|
|
370
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
371
|
+
// @cap-decision Sorted keys and 2-space indent for git-friendly diffs
|
|
372
|
+
fs.writeFileSync(indexPath, JSON.stringify(index, null, 2) + '\n', 'utf8');
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Load a single thread from disk.
|
|
377
|
+
* @param {string} projectRoot - Absolute path to project root
|
|
378
|
+
* @param {string} threadId - Thread ID
|
|
379
|
+
* @returns {Thread|null} Thread object or null if not found
|
|
380
|
+
*/
|
|
381
|
+
function loadThread(projectRoot, threadId) {
|
|
382
|
+
const threadPath = path.join(projectRoot, THREADS_DIR, `${threadId}.json`);
|
|
383
|
+
try {
|
|
384
|
+
if (!fs.existsSync(threadPath)) return null;
|
|
385
|
+
const content = fs.readFileSync(threadPath, 'utf8');
|
|
386
|
+
return JSON.parse(content);
|
|
387
|
+
} catch (_e) {
|
|
388
|
+
return null;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Save a single thread to disk.
|
|
394
|
+
* @param {string} projectRoot - Absolute path to project root
|
|
395
|
+
* @param {Thread} thread
|
|
396
|
+
*/
|
|
397
|
+
function saveThread(projectRoot, thread) {
|
|
398
|
+
const threadsDir = path.join(projectRoot, THREADS_DIR);
|
|
399
|
+
if (!fs.existsSync(threadsDir)) fs.mkdirSync(threadsDir, { recursive: true });
|
|
400
|
+
const threadPath = path.join(threadsDir, `${thread.id}.json`);
|
|
401
|
+
fs.writeFileSync(threadPath, JSON.stringify(thread, null, 2) + '\n', 'utf8');
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Delete a thread from disk and remove from index.
|
|
406
|
+
* @param {string} projectRoot - Absolute path to project root
|
|
407
|
+
* @param {string} threadId - Thread ID to delete
|
|
408
|
+
* @returns {boolean} True if thread was deleted
|
|
409
|
+
*/
|
|
410
|
+
function deleteThread(projectRoot, threadId) {
|
|
411
|
+
const threadPath = path.join(projectRoot, THREADS_DIR, `${threadId}.json`);
|
|
412
|
+
const existed = fs.existsSync(threadPath);
|
|
413
|
+
if (existed) fs.unlinkSync(threadPath);
|
|
414
|
+
|
|
415
|
+
const index = loadIndex(projectRoot);
|
|
416
|
+
removeFromIndex(index, threadId);
|
|
417
|
+
saveIndex(projectRoot, index);
|
|
418
|
+
|
|
419
|
+
return existed;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// --- High-Level Convenience Functions ---
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Persist a brainstorm thread: save thread file + update index.
|
|
426
|
+
* Single call for the common case.
|
|
427
|
+
* @param {string} projectRoot - Absolute path to project root
|
|
428
|
+
* @param {Thread} thread - Thread to persist
|
|
429
|
+
* @returns {{ thread: Thread, index: ThreadIndex }}
|
|
430
|
+
*/
|
|
431
|
+
function persistThread(projectRoot, thread) {
|
|
432
|
+
saveThread(projectRoot, thread);
|
|
433
|
+
const index = loadIndex(projectRoot);
|
|
434
|
+
addToIndex(index, thread);
|
|
435
|
+
saveIndex(projectRoot, index);
|
|
436
|
+
return { thread, index };
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// @cap-todo(ac:F-031/AC-7) cap-brainstormer shall check thread index at session start and surface relevant prior threads
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Check for relevant prior threads before starting a new brainstorm session.
|
|
443
|
+
* Returns matching threads and their full data for the brainstormer agent to surface.
|
|
444
|
+
* @param {string} projectRoot - Absolute path to project root
|
|
445
|
+
* @param {Object} params
|
|
446
|
+
* @param {string} params.problemStatement - The new session's problem statement
|
|
447
|
+
* @param {string[]} [params.featureIds] - Feature IDs referenced
|
|
448
|
+
* @param {string} [params.solutionShape] - Solution direction text
|
|
449
|
+
* @returns {{ matches: RevisitMatch[], threads: Thread[] }} Matches with full thread data
|
|
450
|
+
*/
|
|
451
|
+
function checkPriorThreads(projectRoot, params) {
|
|
452
|
+
const index = loadIndex(projectRoot);
|
|
453
|
+
const matches = detectRevisits(index, params);
|
|
454
|
+
|
|
455
|
+
// Load full thread data for each match
|
|
456
|
+
const threads = [];
|
|
457
|
+
for (const match of matches) {
|
|
458
|
+
const thread = loadThread(projectRoot, match.threadId);
|
|
459
|
+
if (thread) threads.push(thread);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
return { matches, threads };
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* List all threads, optionally filtered by feature ID.
|
|
467
|
+
* @param {string} projectRoot - Absolute path to project root
|
|
468
|
+
* @param {Object} [options]
|
|
469
|
+
* @param {string} [options.featureId] - Filter by feature ID
|
|
470
|
+
* @returns {ThreadIndexEntry[]}
|
|
471
|
+
*/
|
|
472
|
+
function listThreads(projectRoot, options = {}) {
|
|
473
|
+
const index = loadIndex(projectRoot);
|
|
474
|
+
let threads = index.threads;
|
|
475
|
+
|
|
476
|
+
if (options.featureId) {
|
|
477
|
+
threads = threads.filter(t => t.featureIds && t.featureIds.includes(options.featureId));
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Sort by timestamp descending (most recent first)
|
|
481
|
+
return [...threads].sort((a, b) => (b.timestamp || '').localeCompare(a.timestamp || ''));
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
module.exports = {
|
|
485
|
+
// Core
|
|
486
|
+
createThread,
|
|
487
|
+
branchThread,
|
|
488
|
+
detectRevisits,
|
|
489
|
+
extractKeywords,
|
|
490
|
+
computeKeywordOverlap,
|
|
491
|
+
deriveName,
|
|
492
|
+
|
|
493
|
+
// Index management
|
|
494
|
+
createEmptyIndex,
|
|
495
|
+
addToIndex,
|
|
496
|
+
removeFromIndex,
|
|
497
|
+
|
|
498
|
+
// File I/O
|
|
499
|
+
loadIndex,
|
|
500
|
+
saveIndex,
|
|
501
|
+
loadThread,
|
|
502
|
+
saveThread,
|
|
503
|
+
deleteThread,
|
|
504
|
+
persistThread,
|
|
505
|
+
|
|
506
|
+
// High-level
|
|
507
|
+
checkPriorThreads,
|
|
508
|
+
listThreads,
|
|
509
|
+
|
|
510
|
+
// Constants (exposed for testing and configuration)
|
|
511
|
+
THREADS_DIR,
|
|
512
|
+
THREAD_INDEX_FILE,
|
|
513
|
+
REVISIT_KEYWORD_THRESHOLD,
|
|
514
|
+
REVISIT_MIN_SHARED_KEYWORDS,
|
|
515
|
+
STOP_WORDS,
|
|
516
|
+
|
|
517
|
+
// Internal (for testing)
|
|
518
|
+
generateThreadId,
|
|
519
|
+
};
|