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,400 @@
|
|
|
1
|
+
// @cap-feature(feature:F-035) Detect In-Session Topic Divergence During Brainstorm — monitor message-by-message topic drift and suggest branch/stay/redirect actions
|
|
2
|
+
// @cap-decision Pure logic module with explicit I/O in finalizeSession() only — same pattern as cap-thread-tracker.cjs and cap-thread-synthesis.cjs.
|
|
3
|
+
// @cap-decision Keyword decay uses a sliding window approach rather than numeric weights — simpler to reason about, zero floating-point drift, and adequate for brainstorm-length sessions.
|
|
4
|
+
// @cap-constraint Zero external dependencies — uses only Node.js built-ins (crypto).
|
|
5
|
+
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
const crypto = require('node:crypto');
|
|
9
|
+
|
|
10
|
+
const {
|
|
11
|
+
extractKeywords,
|
|
12
|
+
computeKeywordOverlap,
|
|
13
|
+
branchThread,
|
|
14
|
+
persistThread,
|
|
15
|
+
loadIndex,
|
|
16
|
+
saveIndex,
|
|
17
|
+
addToIndex,
|
|
18
|
+
} = require('./cap-thread-tracker.cjs');
|
|
19
|
+
|
|
20
|
+
// --- Constants ---
|
|
21
|
+
|
|
22
|
+
/** Default minimum overlap ratio below which divergence is detected. */
|
|
23
|
+
const DEFAULT_DIVERGENCE_THRESHOLD = 0.15;
|
|
24
|
+
|
|
25
|
+
/** Number of recent messages whose keywords remain at full weight in the running set. */
|
|
26
|
+
const KEYWORD_DECAY_WINDOW = 5;
|
|
27
|
+
|
|
28
|
+
/** Number of consecutive below-threshold messages that signal gradual drift. */
|
|
29
|
+
const GRADUAL_DRIFT_WINDOW = 3;
|
|
30
|
+
|
|
31
|
+
// --- Types ---
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @typedef {Object} DivergenceSession
|
|
35
|
+
* @property {string} id - Session ID (e.g., "dsess-a1b2c3d4")
|
|
36
|
+
* @property {string[]} baselineKeywords - Keywords from the initial thread/prompt
|
|
37
|
+
* @property {string[]} runningKeywords - Current merged keyword set (decayed)
|
|
38
|
+
* @property {Array<{text: string, keywords: string[], timestamp: string}>} messages - All processed messages
|
|
39
|
+
* @property {Array<{threadId: string, divergencePoint: number}>} branches - Branches created during this session
|
|
40
|
+
* @property {number} threshold - Divergence threshold (overlap ratio)
|
|
41
|
+
* @property {number[]} recentOverlaps - Last N overlap ratios for gradual drift detection
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @typedef {Object} DivergenceResult
|
|
46
|
+
* @property {boolean} diverged - Whether divergence was detected
|
|
47
|
+
* @property {number} overlapRatio - Overlap ratio between message keywords and running keywords
|
|
48
|
+
* @property {'sudden'|'gradual'|'none'} divergenceType - Type of divergence detected
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @typedef {Object} ProcessResult
|
|
53
|
+
* @property {boolean} diverged - Whether divergence was detected
|
|
54
|
+
* @property {number} overlapRatio - Overlap ratio for this message
|
|
55
|
+
* @property {string[]} newKeywords - Keywords extracted from the message
|
|
56
|
+
* @property {string|null} suggestion - Brief inline suggestion string, or null if no divergence
|
|
57
|
+
*/
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @typedef {Object} ActionSuggestion
|
|
61
|
+
* @property {'branch'|'stay'|'redirect'} type - Suggested action
|
|
62
|
+
* @property {string} message - Conversational inline suggestion
|
|
63
|
+
*/
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @typedef {Object} BranchResult
|
|
67
|
+
* @property {Object} branchedThread - The newly created branch thread
|
|
68
|
+
* @property {DivergenceSession} updatedSession - Session updated with branch tracking
|
|
69
|
+
*/
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* @typedef {Object} FinalizeResult
|
|
73
|
+
* @property {number} threadsSaved - Number of threads persisted
|
|
74
|
+
* @property {boolean} indexUpdated - Whether the thread index was updated
|
|
75
|
+
*/
|
|
76
|
+
|
|
77
|
+
// --- Session ID Generation ---
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Generate a unique divergence session ID.
|
|
81
|
+
* @returns {string} Session ID in format "dsess-{8 hex chars}"
|
|
82
|
+
*/
|
|
83
|
+
function generateSessionId() {
|
|
84
|
+
return 'dsess-' + crypto.randomBytes(4).toString('hex');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// --- Session Creation ---
|
|
88
|
+
|
|
89
|
+
// @cap-todo(ac:F-035/AC-4) Initialize divergence tracking session with baseline keywords and running keyword set
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Create a new divergence tracking session.
|
|
93
|
+
* Accepts either a thread object (with keywords) or a raw prompt string.
|
|
94
|
+
* @param {Object|string} threadOrPrompt - Thread object with .keywords, or a prompt string
|
|
95
|
+
* @param {Object} [options]
|
|
96
|
+
* @param {number} [options.threshold] - Override default divergence threshold
|
|
97
|
+
* @returns {DivergenceSession}
|
|
98
|
+
*/
|
|
99
|
+
function createSession(threadOrPrompt, options = {}) {
|
|
100
|
+
const threshold = options.threshold != null ? options.threshold : DEFAULT_DIVERGENCE_THRESHOLD;
|
|
101
|
+
|
|
102
|
+
let baselineKeywords;
|
|
103
|
+
if (typeof threadOrPrompt === 'string') {
|
|
104
|
+
baselineKeywords = extractKeywords(threadOrPrompt);
|
|
105
|
+
} else if (threadOrPrompt && Array.isArray(threadOrPrompt.keywords)) {
|
|
106
|
+
baselineKeywords = [...threadOrPrompt.keywords];
|
|
107
|
+
} else {
|
|
108
|
+
baselineKeywords = [];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
id: generateSessionId(),
|
|
113
|
+
baselineKeywords,
|
|
114
|
+
runningKeywords: [...baselineKeywords],
|
|
115
|
+
messages: [],
|
|
116
|
+
branches: [],
|
|
117
|
+
threshold,
|
|
118
|
+
recentOverlaps: [],
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// --- Keyword Decay ---
|
|
123
|
+
|
|
124
|
+
// @cap-todo(ac:F-035/AC-4) Maintain running keyword set with decay — keywords not seen in recent messages lose weight
|
|
125
|
+
// @cap-decision Decay uses set membership in a sliding window rather than numeric weights. Keywords that appear in any of the last N messages stay in the running set. This avoids floating-point accumulation and is deterministic.
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Update the running keyword set by merging new keywords and decaying old ones.
|
|
129
|
+
* Keywords are kept if they appear in any message within the decay window.
|
|
130
|
+
* @param {Array<{keywords: string[]}>} messages - All session messages (used for decay window)
|
|
131
|
+
* @param {string[]} newKeywords - Keywords from the latest message
|
|
132
|
+
* @param {string[]} baselineKeywords - Original baseline keywords (always retained)
|
|
133
|
+
* @param {number} [decayWindow] - Number of recent messages to consider
|
|
134
|
+
* @returns {string[]} Updated running keyword set
|
|
135
|
+
*/
|
|
136
|
+
function updateRunningKeywords(messages, newKeywords, baselineKeywords, decayWindow = KEYWORD_DECAY_WINDOW) {
|
|
137
|
+
// Baseline keywords are always retained
|
|
138
|
+
const retained = new Set(baselineKeywords);
|
|
139
|
+
|
|
140
|
+
// Keywords from recent messages within the decay window are retained
|
|
141
|
+
const windowStart = Math.max(0, messages.length - decayWindow);
|
|
142
|
+
for (let i = windowStart; i < messages.length; i++) {
|
|
143
|
+
for (const kw of messages[i].keywords) {
|
|
144
|
+
retained.add(kw);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Add new keywords
|
|
149
|
+
for (const kw of newKeywords) {
|
|
150
|
+
retained.add(kw);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return [...retained].sort();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// --- Divergence Detection ---
|
|
157
|
+
|
|
158
|
+
// @cap-todo(ac:F-035/AC-1) Compare each new message against running keywords and detect when overlap drops below threshold
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Core divergence detection: compare message keywords against the session's running keywords.
|
|
162
|
+
* Detects both sudden drops and gradual drift.
|
|
163
|
+
* @param {DivergenceSession} session - Current session state
|
|
164
|
+
* @param {string[]} messageKeywords - Keywords extracted from the new message
|
|
165
|
+
* @returns {DivergenceResult}
|
|
166
|
+
*/
|
|
167
|
+
function detectDivergence(session, messageKeywords) {
|
|
168
|
+
if (messageKeywords.length === 0) {
|
|
169
|
+
return { diverged: false, overlapRatio: 0, divergenceType: 'none' };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (session.runningKeywords.length === 0) {
|
|
173
|
+
return { diverged: false, overlapRatio: 0, divergenceType: 'none' };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const { overlapRatio } = computeKeywordOverlap(messageKeywords, session.runningKeywords);
|
|
177
|
+
|
|
178
|
+
// Check for sudden divergence: single message drops below threshold
|
|
179
|
+
const suddenDivergence = overlapRatio < session.threshold;
|
|
180
|
+
|
|
181
|
+
// Check for gradual drift: last N overlaps all below threshold
|
|
182
|
+
const recentWithCurrent = [...session.recentOverlaps, overlapRatio];
|
|
183
|
+
const windowForGradual = recentWithCurrent.slice(-GRADUAL_DRIFT_WINDOW);
|
|
184
|
+
const gradualDrift = windowForGradual.length >= GRADUAL_DRIFT_WINDOW &&
|
|
185
|
+
windowForGradual.every(r => r < session.threshold);
|
|
186
|
+
|
|
187
|
+
if (suddenDivergence && !gradualDrift) {
|
|
188
|
+
return { diverged: true, overlapRatio, divergenceType: 'sudden' };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (gradualDrift) {
|
|
192
|
+
return { diverged: true, overlapRatio, divergenceType: 'gradual' };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return { diverged: false, overlapRatio, divergenceType: 'none' };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// --- Message Processing ---
|
|
199
|
+
|
|
200
|
+
// @cap-todo(ac:F-035/AC-1) Process each user message: extract keywords, detect divergence, update running set
|
|
201
|
+
// @cap-todo(ac:F-035/AC-4) Track topic evolution by updating running keyword set with each message
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Process a new user message within a divergence tracking session.
|
|
205
|
+
* Extracts keywords, checks for divergence, updates running keywords.
|
|
206
|
+
* Returns divergence status and an optional inline suggestion.
|
|
207
|
+
* @param {DivergenceSession} session - Current session (mutated in place)
|
|
208
|
+
* @param {string} userMessage - The user's message text
|
|
209
|
+
* @returns {ProcessResult}
|
|
210
|
+
*/
|
|
211
|
+
function processMessage(session, userMessage) {
|
|
212
|
+
const newKeywords = extractKeywords(userMessage);
|
|
213
|
+
|
|
214
|
+
// Detect divergence BEFORE updating running keywords (compare against current state)
|
|
215
|
+
const divergenceResult = detectDivergence(session, newKeywords);
|
|
216
|
+
|
|
217
|
+
// Record the message
|
|
218
|
+
session.messages.push({
|
|
219
|
+
text: userMessage,
|
|
220
|
+
keywords: newKeywords,
|
|
221
|
+
timestamp: new Date().toISOString(),
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// Update running keywords with decay
|
|
225
|
+
session.runningKeywords = updateRunningKeywords(
|
|
226
|
+
session.messages,
|
|
227
|
+
newKeywords,
|
|
228
|
+
session.baselineKeywords
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
// Track recent overlaps for gradual drift detection
|
|
232
|
+
session.recentOverlaps.push(divergenceResult.overlapRatio);
|
|
233
|
+
|
|
234
|
+
// Generate suggestion if divergence detected
|
|
235
|
+
let suggestion = null;
|
|
236
|
+
if (divergenceResult.diverged) {
|
|
237
|
+
const actionSuggestion = suggestAction(session, divergenceResult);
|
|
238
|
+
suggestion = actionSuggestion.message;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
diverged: divergenceResult.diverged,
|
|
243
|
+
overlapRatio: divergenceResult.overlapRatio,
|
|
244
|
+
newKeywords,
|
|
245
|
+
suggestion,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// --- Action Suggestion ---
|
|
250
|
+
|
|
251
|
+
// @cap-todo(ac:F-035/AC-2) When divergence detected, suggest branch/stay/redirect as brief inline suggestion
|
|
252
|
+
// @cap-todo(ac:F-035/AC-7) Divergence detection shall not interrupt conversation flow — present as brief inline suggestion
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Generate a brief, non-blocking action suggestion based on divergence.
|
|
256
|
+
* @param {DivergenceSession} session - Current session state
|
|
257
|
+
* @param {DivergenceResult} divergenceResult - Result from detectDivergence
|
|
258
|
+
* @returns {ActionSuggestion}
|
|
259
|
+
*/
|
|
260
|
+
function suggestAction(session, divergenceResult) {
|
|
261
|
+
// @cap-decision Default suggestion is 'branch' for sudden shifts, 'redirect' for gradual drift. The brainstormer agent presents this inline and lets the user decide.
|
|
262
|
+
|
|
263
|
+
if (divergenceResult.divergenceType === 'gradual') {
|
|
264
|
+
return {
|
|
265
|
+
type: 'redirect',
|
|
266
|
+
message: '[Topic drift detected] The conversation has been gradually shifting. Would you like to: (1) branch into this new direction, (2) refocus on the original topic, or (3) continue as-is?',
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Sudden divergence — suggest branching
|
|
271
|
+
return {
|
|
272
|
+
type: 'branch',
|
|
273
|
+
message: '[New topic detected] This seems like a different direction. Would you like to: (1) branch into a new thread for this topic, (2) stay on the current topic, or (3) redirect the current thread?',
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// --- Branch Execution ---
|
|
278
|
+
|
|
279
|
+
// @cap-todo(ac:F-035/AC-3) If branch chosen, call branchThread() with divergence point set to last message before topic shift
|
|
280
|
+
// @cap-todo(ac:F-035/AC-5) After branch creation, continue brainstorm on new branch while preserving parent thread state
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Execute a branch based on detected divergence.
|
|
284
|
+
* Creates a new branch thread from the parent, sets the divergence point
|
|
285
|
+
* to the last message before the topic shift.
|
|
286
|
+
* @param {string} cwd - Project root path
|
|
287
|
+
* @param {DivergenceSession} session - Current session (mutated to track branch)
|
|
288
|
+
* @param {Object} parentThread - The parent thread object (from cap-thread-tracker)
|
|
289
|
+
* @param {string} newPrompt - The divergent message/prompt that triggered the branch
|
|
290
|
+
* @returns {BranchResult}
|
|
291
|
+
*/
|
|
292
|
+
function executeBranch(cwd, session, parentThread, newPrompt) {
|
|
293
|
+
// Divergence point is the last message before the shift
|
|
294
|
+
const messageCount = session.messages.length;
|
|
295
|
+
const divergencePointIndex = Math.max(0, messageCount - 1);
|
|
296
|
+
const lastMessageBeforeShift = messageCount >= 2
|
|
297
|
+
? session.messages[messageCount - 2].text
|
|
298
|
+
: parentThread.problemStatement || 'Session start';
|
|
299
|
+
|
|
300
|
+
const divergencePointDesc = `Message ${divergencePointIndex}: "${truncate(lastMessageBeforeShift, 80)}"`;
|
|
301
|
+
|
|
302
|
+
// Create the branch thread via thread-tracker
|
|
303
|
+
const branchedThread = branchThread(parentThread, {
|
|
304
|
+
problemStatement: newPrompt,
|
|
305
|
+
divergencePoint: divergencePointDesc,
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
// Track the branch in the session
|
|
309
|
+
session.branches.push({
|
|
310
|
+
threadId: branchedThread.id,
|
|
311
|
+
divergencePoint: divergencePointIndex,
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
// Persist the branch thread and update the index
|
|
315
|
+
persistThread(cwd, branchedThread);
|
|
316
|
+
|
|
317
|
+
return {
|
|
318
|
+
branchedThread,
|
|
319
|
+
updatedSession: session,
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// --- Session Finalization ---
|
|
324
|
+
|
|
325
|
+
// @cap-todo(ac:F-035/AC-6) At brainstorm end, persist all threads (parent + branches) and update thread index with branch relationships
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Finalize a divergence session: persist the parent thread and all branches,
|
|
329
|
+
* update the thread index with branch relationships.
|
|
330
|
+
* @param {string} cwd - Project root path
|
|
331
|
+
* @param {DivergenceSession} session - The completed session
|
|
332
|
+
* @param {Object} parentThread - The parent thread to persist
|
|
333
|
+
* @returns {FinalizeResult}
|
|
334
|
+
*/
|
|
335
|
+
function finalizeSession(cwd, session, parentThread) {
|
|
336
|
+
let threadsSaved = 0;
|
|
337
|
+
|
|
338
|
+
// Persist the parent thread
|
|
339
|
+
persistThread(cwd, parentThread);
|
|
340
|
+
threadsSaved++;
|
|
341
|
+
|
|
342
|
+
// Load the current index to ensure branch relationships are recorded
|
|
343
|
+
const index = loadIndex(cwd);
|
|
344
|
+
|
|
345
|
+
// All branch threads were already persisted in executeBranch(),
|
|
346
|
+
// but we ensure the index reflects the complete branch tree
|
|
347
|
+
for (const branch of session.branches) {
|
|
348
|
+
// Verify branch is in the index (executeBranch already added it, but be defensive)
|
|
349
|
+
const inIndex = index.threads.some(t => t.id === branch.threadId);
|
|
350
|
+
if (!inIndex) {
|
|
351
|
+
// This shouldn't happen normally, but handle gracefully
|
|
352
|
+
// @cap-risk Branch thread missing from index after executeBranch — defensive re-add
|
|
353
|
+
threadsSaved++;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Save the final index state
|
|
358
|
+
saveIndex(cwd, index);
|
|
359
|
+
|
|
360
|
+
return {
|
|
361
|
+
threadsSaved,
|
|
362
|
+
indexUpdated: true,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// --- Helpers ---
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Truncate a string at a word boundary.
|
|
370
|
+
* @param {string} text
|
|
371
|
+
* @param {number} maxLen
|
|
372
|
+
* @returns {string}
|
|
373
|
+
*/
|
|
374
|
+
function truncate(text, maxLen) {
|
|
375
|
+
if (!text || text.length <= maxLen) return text || '';
|
|
376
|
+
const trimmed = text.substring(0, maxLen).trim();
|
|
377
|
+
const lastSpace = trimmed.lastIndexOf(' ');
|
|
378
|
+
if (lastSpace > maxLen * 0.4) return trimmed.substring(0, lastSpace) + '...';
|
|
379
|
+
return trimmed + '...';
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
module.exports = {
|
|
383
|
+
// Core
|
|
384
|
+
createSession,
|
|
385
|
+
processMessage,
|
|
386
|
+
detectDivergence,
|
|
387
|
+
suggestAction,
|
|
388
|
+
executeBranch,
|
|
389
|
+
finalizeSession,
|
|
390
|
+
updateRunningKeywords,
|
|
391
|
+
|
|
392
|
+
// Constants (exposed for testing and configuration)
|
|
393
|
+
DEFAULT_DIVERGENCE_THRESHOLD,
|
|
394
|
+
KEYWORD_DECAY_WINDOW,
|
|
395
|
+
GRADUAL_DRIFT_WINDOW,
|
|
396
|
+
|
|
397
|
+
// Internal (for testing)
|
|
398
|
+
generateSessionId,
|
|
399
|
+
truncate,
|
|
400
|
+
};
|