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,698 @@
|
|
|
1
|
+
// @cap-feature(feature:F-027) Memory Accumulation Engine — detect decisions, pitfalls, patterns, hotspots from session data
|
|
2
|
+
// @cap-decision Pure logic module with no I/O — takes parsed session data as input, outputs structured memory entries. Enables dry-run and unit testing.
|
|
3
|
+
// @cap-decision Relevance-based aging with pinned escape hatch — annotations expire after N sessions without edits, but pinned:true exempts from aging.
|
|
4
|
+
// @cap-constraint Zero external dependencies — uses only Node.js built-ins.
|
|
5
|
+
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
// @cap-history(sessions:2, edits:2, since:2026-04-21, learned:2026-05-08) Frequently modified — 2 sessions, 2 edits
|
|
9
|
+
// @cap-history(sessions:2, edits:27, since:2026-04-03, learned:2026-04-03) Frequently modified — 2 sessions, 27 edits
|
|
10
|
+
const fs = require('node:fs');
|
|
11
|
+
const path = require('node:path');
|
|
12
|
+
const confidence = require('./cap-memory-confidence.cjs');
|
|
13
|
+
|
|
14
|
+
// --- Constants ---
|
|
15
|
+
|
|
16
|
+
/** Default number of sessions without edits before annotation is marked stale. */
|
|
17
|
+
const DEFAULT_STALE_THRESHOLD = 5;
|
|
18
|
+
|
|
19
|
+
/** Minimum sessions with edits for a file to qualify as a hotspot. */
|
|
20
|
+
const MIN_HOTSPOT_SESSIONS = 2;
|
|
21
|
+
|
|
22
|
+
/** Minimum successful applications for a pattern to be recorded. */
|
|
23
|
+
const MIN_PATTERN_CONFIRMATIONS = 2;
|
|
24
|
+
|
|
25
|
+
/** Regex patterns for detecting decision-related content in assistant messages.
|
|
26
|
+
* Tightened: require verb+noun combinations, not just isolated keywords. */
|
|
27
|
+
const DECISION_PATTERNS = [
|
|
28
|
+
/(?:(?:I|we) (?:decided|chose|picked|selected|went with)\b)/i,
|
|
29
|
+
/(?:decision(?:\s+(?:was|is|to))\b)/i,
|
|
30
|
+
/(?:trade-?off(?:\s+(?:between|is|was))\b)/i,
|
|
31
|
+
/(?:root cause(?:\s+(?:is|was|:))\b)/i,
|
|
32
|
+
/(?:the fix(?:\s+(?:is|was|needs|requires))\b)/i,
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
/** Regex patterns for detecting pitfall/failure content.
|
|
36
|
+
* Tightened: require action context, not just isolated words. */
|
|
37
|
+
const PITFALL_PATTERNS = [
|
|
38
|
+
/(?:(?:don't|do not|never|avoid)\s+\w{3,})/i,
|
|
39
|
+
/(?:(?:watch out|careful|gotcha|pitfall|trap)\s+(?:for|with|when|:))/i,
|
|
40
|
+
/(?:hours?\s+(?:of\s+)?debugging)/i,
|
|
41
|
+
/(?:regression\s+(?:in|from|caused|when))/i,
|
|
42
|
+
/(?:(?:this|the)\s+(?:bug|crash|failure)\s+(?:is|was|happens|occurs|caused))/i,
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
/** Regex patterns for detecting successful patterns.
|
|
46
|
+
* Tightened: require specific recommendation language. */
|
|
47
|
+
const PATTERN_PATTERNS = [
|
|
48
|
+
/(?:this (?:approach|pattern|method) (?:works?|solved|is better))/i,
|
|
49
|
+
/(?:(?:proven|reliable)\s+(?:approach|pattern|method|strategy))/i,
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
/** Feature ID regex */
|
|
53
|
+
const FEATURE_RE = /F-\d{3}/g;
|
|
54
|
+
|
|
55
|
+
// --- Types ---
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @typedef {'decision'|'pitfall'|'pattern'|'hotspot'} MemoryCategory
|
|
59
|
+
*/
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @typedef {Object} MemoryEntry
|
|
63
|
+
* @property {MemoryCategory} category
|
|
64
|
+
* @property {string|null} file - Target file path (null for cross-cutting entries)
|
|
65
|
+
* @property {string} content - Human-readable description
|
|
66
|
+
* @property {Object} metadata
|
|
67
|
+
* @property {string} metadata.source - Source session date
|
|
68
|
+
* @property {string|null} metadata.branch - Git branch
|
|
69
|
+
* @property {string[]} metadata.relatedFiles - Other files involved
|
|
70
|
+
* @property {string[]} metadata.features - Feature IDs referenced
|
|
71
|
+
* @property {boolean} metadata.pinned - Whether exempt from aging
|
|
72
|
+
* @property {number} [metadata.sessions] - Number of sessions (hotspots)
|
|
73
|
+
* @property {number} [metadata.edits] - Total edit count (hotspots)
|
|
74
|
+
* @property {string} [metadata.since] - Earliest session date (hotspots)
|
|
75
|
+
* @property {number} [metadata.confirmations] - Times confirmed (patterns)
|
|
76
|
+
*/
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* @typedef {Object} ExistingAnnotation
|
|
80
|
+
* @property {MemoryCategory} category
|
|
81
|
+
* @property {string} file
|
|
82
|
+
* @property {string} content
|
|
83
|
+
* @property {boolean} pinned
|
|
84
|
+
* @property {number} lastEditSession - Session index when file was last edited (0 = current)
|
|
85
|
+
*/
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* @typedef {Object} AccumulationResult
|
|
89
|
+
* @property {MemoryEntry[]} newEntries - New memory entries to write
|
|
90
|
+
* @property {ExistingAnnotation[]} staleEntries - Existing annotations to remove
|
|
91
|
+
* @property {MemoryEntry[]} updatedEntries - Existing annotations to update (e.g., increment counts)
|
|
92
|
+
* @property {Object} stats - Accumulation statistics
|
|
93
|
+
*/
|
|
94
|
+
|
|
95
|
+
// --- Session Analysis ---
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Extract text content from a message (mirrors F-025 helper).
|
|
99
|
+
* @param {Object} msg
|
|
100
|
+
* @returns {string}
|
|
101
|
+
*/
|
|
102
|
+
function extractText(msg) {
|
|
103
|
+
const content = msg.message?.content;
|
|
104
|
+
if (!content) return '';
|
|
105
|
+
if (typeof content === 'string') return content;
|
|
106
|
+
if (Array.isArray(content)) {
|
|
107
|
+
return content.filter(c => c.type === 'text').map(c => c.text || '').join('\n');
|
|
108
|
+
}
|
|
109
|
+
return '';
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Extract tool uses from a message.
|
|
114
|
+
* @param {Object} msg
|
|
115
|
+
* @returns {Array<{tool: string, input: Object}>}
|
|
116
|
+
*/
|
|
117
|
+
function extractTools(msg) {
|
|
118
|
+
const content = msg.message?.content;
|
|
119
|
+
if (!Array.isArray(content)) return [];
|
|
120
|
+
return content.filter(c => c.type === 'tool_use').map(c => ({ tool: c.name, input: c.input }));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Strip system tags from text.
|
|
125
|
+
* @param {string} text
|
|
126
|
+
* @returns {string}
|
|
127
|
+
*/
|
|
128
|
+
function stripTags(text) {
|
|
129
|
+
return text.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, '').trim();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Patterns that indicate conversational noise rather than real knowledge. */
|
|
133
|
+
const NOISE_PATTERNS = [
|
|
134
|
+
// Conversational openers/fillers
|
|
135
|
+
/^(Let me |Let's |I'll |I will |Now |OK|Sure|Got it|Done|Great|Perfect|Absolutely|Here's|Here is|Alright)/i,
|
|
136
|
+
// Imperative prompts from agent
|
|
137
|
+
/^(Please |Provide |Check |Run |Show |Read |Write |Create |Update |Delete |Review |Look |Schauen|Lass)/i,
|
|
138
|
+
// Markdown formatting: tables, headers, bold-prefixed lines, quotes, HR, code blocks
|
|
139
|
+
/^\||^```|^#+\s|^\*\*[A-Z]|^>\s|^---|^- \*\*/,
|
|
140
|
+
// Code/command references
|
|
141
|
+
/^`\//,
|
|
142
|
+
// Numbered lists, bullet points
|
|
143
|
+
/^\d+[\.\)]\s|^- [a-z]/,
|
|
144
|
+
// GSD/CAP command references
|
|
145
|
+
/^`?(\/gsd:|\/cap:|cap:|gsd:)/,
|
|
146
|
+
// Status/log messages
|
|
147
|
+
/^(Last scan|Shell cwd|Exit code|Session|Commit|What was generated)/i,
|
|
148
|
+
// German filler / conversational
|
|
149
|
+
/^(Aendere|Weiter mit|Dann |Jetzt |Genau|Besser|Gut |Ich baue|Soll ich|Oder )/i,
|
|
150
|
+
// Agent workflow noise
|
|
151
|
+
/^(Starting|Spawning|Loading|Checking|Analyzing|Processing|Running|Scanning)/i,
|
|
152
|
+
// Contains only markdown bold + colon (structured output, not prose)
|
|
153
|
+
/^\*\*.*:\*\*$/,
|
|
154
|
+
// Lines starting with bold (structured output headers, not decisions)
|
|
155
|
+
/^\*\*\d+-\d+/,
|
|
156
|
+
// Bullet lists that start with "- Discovers", "- Analyzes", etc. (agent workflow descriptions)
|
|
157
|
+
/^- (Discovers|Analyzes|Creates|Produces|Reads|Writes|Returns|Generates|Validates|Checks)/i,
|
|
158
|
+
// Progress reports (e.g., "Von 235 → 146 Decisions")
|
|
159
|
+
/(?:Von|From)\s+\d+\s*[→\-]\s*\d+/i,
|
|
160
|
+
// Lines containing @cap-* or @gsd-* tags (meta-discussion about the system itself)
|
|
161
|
+
/@(?:cap|gsd)-(?:feature|todo|decision|pitfall|history|pattern|risk|constraint|context|ref|api)\b/,
|
|
162
|
+
// Code identifiers / function call references (not prose)
|
|
163
|
+
/^[a-zA-Z_]\w*\.\w+\(|^[a-zA-Z_]\w*\(\)/,
|
|
164
|
+
// Ergebnis/Result summary lines
|
|
165
|
+
/^(Ergebnis|Result|Output|Nächste Schritte|Next steps):/i,
|
|
166
|
+
// Lines that are mostly special characters (ASCII art, box drawing)
|
|
167
|
+
/[─│┌┐└┘┬┴├┤╔╗╚╝]{3,}/,
|
|
168
|
+
// AC table fragments
|
|
169
|
+
/^\|\s*AC-\d/,
|
|
170
|
+
// Lines starting with file paths
|
|
171
|
+
/^`?(?:cap\/|hooks\/|bin\/|commands\/|tests\/|scripts\/|src\/)/,
|
|
172
|
+
// Sentences with trailing markdown code block markers
|
|
173
|
+
/```\s*$/,
|
|
174
|
+
// Meta-discussion about regex patterns or test data
|
|
175
|
+
/(?:Pattern|Regex|regex)\s+`/,
|
|
176
|
+
// Sentences referencing test fixtures or test sentences
|
|
177
|
+
/(?:test sentence|test data|Testfall|Test-Satz)/i,
|
|
178
|
+
// Lines ending with orphaned numbering (e.g., "...visible errors\n4.")
|
|
179
|
+
/^\d+\.$/,
|
|
180
|
+
];
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Check if a sentence is conversational noise rather than real knowledge.
|
|
184
|
+
* @param {string} text
|
|
185
|
+
* @returns {boolean}
|
|
186
|
+
*/
|
|
187
|
+
function isNoise(text) {
|
|
188
|
+
return NOISE_PATTERNS.some(p => p.test(text));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Normalize a file path for cross-session matching.
|
|
193
|
+
* Strips worktree paths and resolves to monorepo-relative path.
|
|
194
|
+
* @param {string} fp - Absolute file path
|
|
195
|
+
* @param {string|null} projectRoot - Project root to make paths relative
|
|
196
|
+
* @returns {string} Normalized path
|
|
197
|
+
*/
|
|
198
|
+
function normalizeFilePath(fp, projectRoot) {
|
|
199
|
+
if (!fp) return fp;
|
|
200
|
+
let normalized = fp;
|
|
201
|
+
// Strip worktree prefix: .claude/worktrees/<name>/ → ""
|
|
202
|
+
normalized = normalized.replace(/\.claude\/worktrees\/[^/]+\//, '');
|
|
203
|
+
// Strip /private/var/folders temp paths for worktrees
|
|
204
|
+
const worktreeMatch = normalized.match(/\/private\/var\/.*?\/([^/]+)\/(.*)/);
|
|
205
|
+
if (worktreeMatch) {
|
|
206
|
+
// Try to find the project name in the path
|
|
207
|
+
normalized = worktreeMatch[2] || normalized;
|
|
208
|
+
}
|
|
209
|
+
// Make relative to project root if provided
|
|
210
|
+
if (projectRoot && normalized.startsWith(projectRoot)) {
|
|
211
|
+
normalized = normalized.substring(projectRoot.length).replace(/^\//, '');
|
|
212
|
+
}
|
|
213
|
+
return normalized;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Analyze a single parsed session for memory-worthy content.
|
|
218
|
+
* @param {Object} parsed - { meta, messages } from parseSession
|
|
219
|
+
* @param {Object} [options]
|
|
220
|
+
* @param {boolean} [options.isDebugSession] - Whether this session was a debug session
|
|
221
|
+
* @param {string} [options.projectRoot] - Project root for path normalization
|
|
222
|
+
* @returns {{decisions: string[], pitfalls: string[], patterns: string[], editedFiles: Object<string, number>, features: Set<string>}}
|
|
223
|
+
*/
|
|
224
|
+
function analyzeSession(parsed, options = {}) {
|
|
225
|
+
const { meta, messages } = parsed;
|
|
226
|
+
const decisions = [];
|
|
227
|
+
const pitfalls = [];
|
|
228
|
+
const patterns = [];
|
|
229
|
+
const editedFiles = {}; // path -> edit count
|
|
230
|
+
const features = new Set();
|
|
231
|
+
|
|
232
|
+
for (const msg of messages) {
|
|
233
|
+
if (msg.type !== 'assistant') continue;
|
|
234
|
+
|
|
235
|
+
// File edits: collect from ALL messages including subagents
|
|
236
|
+
for (const tool of extractTools(msg)) {
|
|
237
|
+
if (tool.tool === 'Write' || tool.tool === 'Edit' || tool.tool === 'MultiEdit') {
|
|
238
|
+
const rawFp = tool.input?.file_path || tool.input?.filePath || null;
|
|
239
|
+
if (rawFp) {
|
|
240
|
+
// Skip planning/memory/config artifacts — not real source code hotspots
|
|
241
|
+
if (/\.(planning|cap)\/|memory\/|\.claude\/|SESSION\.json|MEMORY\.md|STATE\.md/.test(rawFp)) continue;
|
|
242
|
+
const fp = normalizeFilePath(rawFp, options.projectRoot || null);
|
|
243
|
+
editedFiles[fp] = (editedFiles[fp] || 0) + 1;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Text analysis: include subagent messages (they contain decisions/pitfalls too)
|
|
249
|
+
const text = stripTags(extractText(msg));
|
|
250
|
+
if (!text) continue;
|
|
251
|
+
|
|
252
|
+
// Collect feature references
|
|
253
|
+
const featureMatches = text.match(FEATURE_RE);
|
|
254
|
+
if (featureMatches) featureMatches.forEach(f => features.add(f));
|
|
255
|
+
|
|
256
|
+
// Extract sentences — skip markdown formatting artifacts
|
|
257
|
+
const sentences = text.split(/(?<=[.!?\n])\s+/);
|
|
258
|
+
for (const sentence of sentences) {
|
|
259
|
+
if (sentence.length < 40 || sentence.length > 300) continue;
|
|
260
|
+
const clean = sentence.trim();
|
|
261
|
+
if (isNoise(clean)) continue;
|
|
262
|
+
|
|
263
|
+
if (DECISION_PATTERNS.some(p => p.test(clean))) {
|
|
264
|
+
decisions.push(clean);
|
|
265
|
+
}
|
|
266
|
+
if (PITFALL_PATTERNS.some(p => p.test(clean))) {
|
|
267
|
+
pitfalls.push(clean);
|
|
268
|
+
}
|
|
269
|
+
if (PATTERN_PATTERNS.some(p => p.test(clean))) {
|
|
270
|
+
patterns.push(clean);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return {
|
|
276
|
+
decisions: [...new Set(decisions)],
|
|
277
|
+
pitfalls: [...new Set(pitfalls)],
|
|
278
|
+
patterns: [...new Set(patterns)],
|
|
279
|
+
editedFiles,
|
|
280
|
+
features,
|
|
281
|
+
date: meta?.timestamp || null,
|
|
282
|
+
branch: meta?.branch || null,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// --- Cross-Session Accumulation ---
|
|
287
|
+
|
|
288
|
+
// @cap-todo(ref:F-027:AC-2) Detect four memory categories: decisions, pitfalls, patterns, hotspots
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Accumulate memory from multiple analyzed sessions.
|
|
292
|
+
* @param {Array<ReturnType<typeof analyzeSession>>} sessionAnalyses - Results from analyzeSession for each session
|
|
293
|
+
* @param {Object} [options]
|
|
294
|
+
* @param {number} [options.staleThreshold] - Sessions without edits before stale (default: 5)
|
|
295
|
+
* @param {number} [options.minHotspotSessions] - Min sessions for hotspot (default: 2)
|
|
296
|
+
* @param {number} [options.minPatternConfirmations] - Min confirmations for pattern (default: 2)
|
|
297
|
+
* @param {ExistingAnnotation[]} [options.existingAnnotations] - Current annotations in code
|
|
298
|
+
* @returns {AccumulationResult}
|
|
299
|
+
*/
|
|
300
|
+
function accumulate(sessionAnalyses, options = {}) {
|
|
301
|
+
const staleThreshold = options.staleThreshold || DEFAULT_STALE_THRESHOLD;
|
|
302
|
+
const minHotspot = options.minHotspotSessions || MIN_HOTSPOT_SESSIONS;
|
|
303
|
+
const minPattern = options.minPatternConfirmations || MIN_PATTERN_CONFIRMATIONS;
|
|
304
|
+
const existing = options.existingAnnotations || [];
|
|
305
|
+
|
|
306
|
+
const newEntries = [];
|
|
307
|
+
const updatedEntries = [];
|
|
308
|
+
const staleEntries = [];
|
|
309
|
+
|
|
310
|
+
// --- Hotspots (AC-3): files edited across multiple sessions ---
|
|
311
|
+
const fileSessionMap = {}; // path -> { sessions: Set, totalEdits: number, earliestDate: string }
|
|
312
|
+
for (const analysis of sessionAnalyses) {
|
|
313
|
+
for (const [fp, editCount] of Object.entries(analysis.editedFiles)) {
|
|
314
|
+
if (!fileSessionMap[fp]) {
|
|
315
|
+
fileSessionMap[fp] = { sessions: new Set(), totalEdits: 0, earliestDate: analysis.date };
|
|
316
|
+
}
|
|
317
|
+
fileSessionMap[fp].sessions.add(analysis.date || 'unknown');
|
|
318
|
+
fileSessionMap[fp].totalEdits += editCount;
|
|
319
|
+
if (analysis.date && (!fileSessionMap[fp].earliestDate || analysis.date < fileSessionMap[fp].earliestDate)) {
|
|
320
|
+
fileSessionMap[fp].earliestDate = analysis.date;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
for (const [fp, data] of Object.entries(fileSessionMap)) {
|
|
326
|
+
if (data.sessions.size >= minHotspot) {
|
|
327
|
+
newEntries.push({
|
|
328
|
+
category: 'hotspot',
|
|
329
|
+
file: fp,
|
|
330
|
+
content: `Frequently modified — ${data.sessions.size} sessions, ${data.totalEdits} edits`,
|
|
331
|
+
metadata: {
|
|
332
|
+
source: [...data.sessions].sort().pop(),
|
|
333
|
+
branch: null,
|
|
334
|
+
relatedFiles: [],
|
|
335
|
+
features: [],
|
|
336
|
+
pinned: false,
|
|
337
|
+
sessions: data.sessions.size,
|
|
338
|
+
edits: data.totalEdits,
|
|
339
|
+
since: data.earliestDate ? data.earliestDate.substring(0, 10) : null,
|
|
340
|
+
},
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// --- Decisions (AC-2): collect unique decisions across sessions ---
|
|
346
|
+
const seenDecisions = new Set();
|
|
347
|
+
for (const analysis of sessionAnalyses) {
|
|
348
|
+
for (const decision of analysis.decisions) {
|
|
349
|
+
const key = decision.substring(0, 80).toLowerCase();
|
|
350
|
+
if (seenDecisions.has(key)) continue;
|
|
351
|
+
seenDecisions.add(key);
|
|
352
|
+
|
|
353
|
+
// Find which files were edited in the same session
|
|
354
|
+
const relatedFiles = Object.keys(analysis.editedFiles).slice(0, 5);
|
|
355
|
+
|
|
356
|
+
newEntries.push({
|
|
357
|
+
category: 'decision',
|
|
358
|
+
file: relatedFiles[0] || null, // primary file, or cross-cutting
|
|
359
|
+
content: decision,
|
|
360
|
+
metadata: {
|
|
361
|
+
source: analysis.date,
|
|
362
|
+
branch: analysis.branch,
|
|
363
|
+
relatedFiles,
|
|
364
|
+
features: [...analysis.features],
|
|
365
|
+
pinned: false,
|
|
366
|
+
},
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// --- Pitfalls (AC-4): only from debug sessions or explicit failure context ---
|
|
372
|
+
const seenPitfalls = new Set();
|
|
373
|
+
for (const analysis of sessionAnalyses) {
|
|
374
|
+
for (const pitfall of analysis.pitfalls) {
|
|
375
|
+
const key = pitfall.substring(0, 80).toLowerCase();
|
|
376
|
+
if (seenPitfalls.has(key)) continue;
|
|
377
|
+
seenPitfalls.add(key);
|
|
378
|
+
|
|
379
|
+
const relatedFiles = Object.keys(analysis.editedFiles).slice(0, 5);
|
|
380
|
+
|
|
381
|
+
newEntries.push({
|
|
382
|
+
category: 'pitfall',
|
|
383
|
+
file: relatedFiles[0] || null,
|
|
384
|
+
content: pitfall,
|
|
385
|
+
metadata: {
|
|
386
|
+
source: analysis.date,
|
|
387
|
+
branch: analysis.branch,
|
|
388
|
+
relatedFiles,
|
|
389
|
+
features: [...analysis.features],
|
|
390
|
+
pinned: false,
|
|
391
|
+
},
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// --- Patterns (AC-5): only when confirmed across multiple sessions ---
|
|
397
|
+
const patternCounts = {}; // normalized key -> { content, count, sessions }
|
|
398
|
+
for (const analysis of sessionAnalyses) {
|
|
399
|
+
for (const pattern of analysis.patterns) {
|
|
400
|
+
const key = pattern.substring(0, 80).toLowerCase();
|
|
401
|
+
if (!patternCounts[key]) {
|
|
402
|
+
patternCounts[key] = { content: pattern, count: 0, sessions: [], relatedFiles: new Set(), features: new Set() };
|
|
403
|
+
}
|
|
404
|
+
patternCounts[key].count++;
|
|
405
|
+
if (analysis.date) patternCounts[key].sessions.push(analysis.date);
|
|
406
|
+
Object.keys(analysis.editedFiles).forEach(f => patternCounts[key].relatedFiles.add(f));
|
|
407
|
+
analysis.features.forEach(f => patternCounts[key].features.add(f));
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
for (const [, data] of Object.entries(patternCounts)) {
|
|
412
|
+
if (data.count >= minPattern) {
|
|
413
|
+
newEntries.push({
|
|
414
|
+
category: 'pattern',
|
|
415
|
+
file: [...data.relatedFiles][0] || null,
|
|
416
|
+
content: data.content,
|
|
417
|
+
metadata: {
|
|
418
|
+
source: data.sessions.sort().pop(),
|
|
419
|
+
branch: null,
|
|
420
|
+
relatedFiles: [...data.relatedFiles].slice(0, 5),
|
|
421
|
+
features: [...data.features],
|
|
422
|
+
pinned: false,
|
|
423
|
+
confirmations: data.count,
|
|
424
|
+
},
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// --- Aging (AC-6, AC-7): check existing annotations for staleness ---
|
|
430
|
+
const currentlyEditedFiles = new Set();
|
|
431
|
+
for (const analysis of sessionAnalyses) {
|
|
432
|
+
for (const fp of Object.keys(analysis.editedFiles)) {
|
|
433
|
+
currentlyEditedFiles.add(fp);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
for (const annotation of existing) {
|
|
438
|
+
// AC-7: pinned annotations never go stale
|
|
439
|
+
if (annotation.pinned) continue;
|
|
440
|
+
|
|
441
|
+
if (annotation.lastEditSession >= staleThreshold && !currentlyEditedFiles.has(annotation.file)) {
|
|
442
|
+
staleEntries.push(annotation);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// --- Stats ---
|
|
447
|
+
const stats = {
|
|
448
|
+
sessionsAnalyzed: sessionAnalyses.length,
|
|
449
|
+
hotspots: newEntries.filter(e => e.category === 'hotspot').length,
|
|
450
|
+
decisions: newEntries.filter(e => e.category === 'decision').length,
|
|
451
|
+
pitfalls: newEntries.filter(e => e.category === 'pitfall').length,
|
|
452
|
+
patterns: newEntries.filter(e => e.category === 'pattern').length,
|
|
453
|
+
stale: staleEntries.length,
|
|
454
|
+
total: newEntries.length,
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
return { newEntries, staleEntries, updatedEntries, stats };
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// --- Code-Based Memory (primary source — code is the single source of truth) ---
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Extract memory entries from code tags (via tag scanner).
|
|
464
|
+
* Code tags are high-signal, zero-noise — they are explicit developer annotations.
|
|
465
|
+
*
|
|
466
|
+
* Learning signals (F-055): when `options.existingEntries` is supplied and
|
|
467
|
+
* `options.learningSignals !== false`, each new entry is compared against the
|
|
468
|
+
* existing set. Matches are merged as re-observations (confidence bump, evidence +1);
|
|
469
|
+
* contradictions damp the existing entry's confidence without merging.
|
|
470
|
+
*
|
|
471
|
+
* @param {Array<{type: string, file: string, line: number, metadata: Object, description: string, subtype: string|null}>} tags - Tags from cap-tag-scanner
|
|
472
|
+
* @param {Object} [options]
|
|
473
|
+
* @param {Array} [options.existingEntries] - Entries previously read from .cap/memory/*.md (for learning signals)
|
|
474
|
+
* @param {boolean} [options.learningSignals] - Default true; opt-out for callers that want raw extraction
|
|
475
|
+
* @returns {MemoryEntry[]}
|
|
476
|
+
*/
|
|
477
|
+
function accumulateFromCode(tags, options = {}) {
|
|
478
|
+
const entries = [];
|
|
479
|
+
const seen = new Set();
|
|
480
|
+
|
|
481
|
+
for (const tag of tags) {
|
|
482
|
+
// @cap-decision tags → decision entries
|
|
483
|
+
if (tag.type === 'decision') {
|
|
484
|
+
if (!tag.description || tag.description.length < 10) continue;
|
|
485
|
+
const key = tag.description.substring(0, 80).toLowerCase();
|
|
486
|
+
if (seen.has('d:' + key)) continue;
|
|
487
|
+
seen.add('d:' + key);
|
|
488
|
+
|
|
489
|
+
// @cap-todo(ac:F-055/AC-2) New entries start with confidence:0.5, evidence_count:1 (injected via initFields).
|
|
490
|
+
// @cap-feature(feature:F-091, primary:true) Source-aware initial confidence — explicit
|
|
491
|
+
// @cap-decision tags start at 0.8 so they survive the F-090 filter (threshold 0.6)
|
|
492
|
+
// on first emission instead of needing re-observation.
|
|
493
|
+
entries.push({
|
|
494
|
+
category: 'decision',
|
|
495
|
+
file: tag.file,
|
|
496
|
+
content: tag.description,
|
|
497
|
+
metadata: {
|
|
498
|
+
source: 'code',
|
|
499
|
+
branch: null,
|
|
500
|
+
relatedFiles: [tag.file],
|
|
501
|
+
features: tag.metadata?.feature ? [tag.metadata.feature] : [],
|
|
502
|
+
pinned: false,
|
|
503
|
+
line: tag.line,
|
|
504
|
+
...confidence.initFields({
|
|
505
|
+
initialConfidence: confidence.initialConfidenceForSource('cap-decision'),
|
|
506
|
+
}),
|
|
507
|
+
},
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Extract pitfalls from @cap-todo with risk: subtype
|
|
512
|
+
if (tag.type === 'todo' && tag.subtype === 'risk') {
|
|
513
|
+
const desc = tag.description.replace(/^risk:\s*/i, '');
|
|
514
|
+
if (!desc || desc.length < 10) continue;
|
|
515
|
+
const key = desc.substring(0, 80).toLowerCase();
|
|
516
|
+
if (seen.has('p:' + key)) continue;
|
|
517
|
+
seen.add('p:' + key);
|
|
518
|
+
|
|
519
|
+
// @cap-feature(feature:F-091) Explicit @cap-todo risk: subtype starts at 0.7.
|
|
520
|
+
entries.push({
|
|
521
|
+
category: 'pitfall',
|
|
522
|
+
file: tag.file,
|
|
523
|
+
content: desc,
|
|
524
|
+
metadata: {
|
|
525
|
+
source: 'code',
|
|
526
|
+
branch: null,
|
|
527
|
+
relatedFiles: [tag.file],
|
|
528
|
+
features: tag.metadata?.feature ? [tag.metadata.feature] : [],
|
|
529
|
+
pinned: false,
|
|
530
|
+
line: tag.line,
|
|
531
|
+
...confidence.initFields({
|
|
532
|
+
initialConfidence: confidence.initialConfidenceForSource('cap-todo-risk'),
|
|
533
|
+
}),
|
|
534
|
+
},
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// Extract pitfalls from standalone @cap-risk tags
|
|
539
|
+
if (tag.type === 'risk') {
|
|
540
|
+
if (!tag.description || tag.description.length < 10) continue;
|
|
541
|
+
const key = tag.description.substring(0, 80).toLowerCase();
|
|
542
|
+
if (seen.has('p:' + key)) continue;
|
|
543
|
+
seen.add('p:' + key);
|
|
544
|
+
|
|
545
|
+
// @cap-feature(feature:F-091) Standalone @cap-risk starts at 0.7.
|
|
546
|
+
entries.push({
|
|
547
|
+
category: 'pitfall',
|
|
548
|
+
file: tag.file,
|
|
549
|
+
content: tag.description,
|
|
550
|
+
metadata: {
|
|
551
|
+
source: 'code',
|
|
552
|
+
branch: null,
|
|
553
|
+
relatedFiles: [tag.file],
|
|
554
|
+
features: tag.metadata?.feature ? [tag.metadata.feature] : [],
|
|
555
|
+
pinned: false,
|
|
556
|
+
line: tag.line,
|
|
557
|
+
...confidence.initFields({
|
|
558
|
+
initialConfidence: confidence.initialConfidenceForSource('cap-risk'),
|
|
559
|
+
}),
|
|
560
|
+
},
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// Learning signals: opt-in via existingEntries presence; can be disabled explicitly.
|
|
566
|
+
const signalsEnabled = options.learningSignals !== false && Array.isArray(options.existingEntries);
|
|
567
|
+
if (!signalsEnabled) return entries;
|
|
568
|
+
|
|
569
|
+
// Work on a mutable copy so contradictions can rewrite existing entries in-place for the caller's view.
|
|
570
|
+
const existing = options.existingEntries.map((e) => ({
|
|
571
|
+
...e,
|
|
572
|
+
metadata: { ...(e.metadata || {}) },
|
|
573
|
+
}));
|
|
574
|
+
|
|
575
|
+
const result = [];
|
|
576
|
+
// Track existing-entry indices that have been damped so multiple new entries
|
|
577
|
+
// contradicting the same existing one don't emit duplicate damped records.
|
|
578
|
+
// The final damp state already lives in existing[i] (applyLearningSignals
|
|
579
|
+
// reads the mutated existing array on each iteration), so we emit each
|
|
580
|
+
// touched index exactly once at the end.
|
|
581
|
+
const dampedIndices = new Set();
|
|
582
|
+
for (const newEntry of entries) {
|
|
583
|
+
const { mergedEntry, action } = confidence.applyLearningSignals(newEntry, existing);
|
|
584
|
+
|
|
585
|
+
if (action === 'reobserved') {
|
|
586
|
+
// @cap-todo(ac:F-055/AC-4) Re-observation: bump evidence + confidence on the existing entry.
|
|
587
|
+
result.push(mergedEntry);
|
|
588
|
+
} else if (action === 'contradicted') {
|
|
589
|
+
// @cap-todo(ac:F-055/AC-5) Contradiction: damp the existing entry's confidence; keep the new entry as a separate observation.
|
|
590
|
+
const updateRec = mergedEntry._contradictedExistingUpdate;
|
|
591
|
+
if (updateRec && existing[updateRec.index]) {
|
|
592
|
+
existing[updateRec.index] = { ...existing[updateRec.index], metadata: updateRec.updatedMetadata };
|
|
593
|
+
dampedIndices.add(updateRec.index);
|
|
594
|
+
}
|
|
595
|
+
const clean = { ...mergedEntry };
|
|
596
|
+
delete clean._contradictedExistingUpdate;
|
|
597
|
+
result.push(clean);
|
|
598
|
+
} else {
|
|
599
|
+
result.push(mergedEntry);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
for (const idx of dampedIndices) result.push(existing[idx]);
|
|
604
|
+
|
|
605
|
+
return result;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// --- Convenience: Full Pipeline Input ---
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* Parse raw JSONL session files and accumulate memory.
|
|
612
|
+
* Sessions provide HOTSPOTS ONLY (edit frequency). Decisions/pitfalls come from code tags.
|
|
613
|
+
* @param {Array<{path: string, isDebugSession?: boolean}>} sessionFiles - Session file descriptors
|
|
614
|
+
* @param {Object} [options] - Options passed to accumulate()
|
|
615
|
+
* @param {string} [options.projectRoot] - Project root for file path normalization (monorepo-aware)
|
|
616
|
+
* @returns {AccumulationResult}
|
|
617
|
+
*/
|
|
618
|
+
function accumulateFromFiles(sessionFiles, options = {}) {
|
|
619
|
+
const analyses = [];
|
|
620
|
+
|
|
621
|
+
for (const sf of sessionFiles) {
|
|
622
|
+
try {
|
|
623
|
+
const lines = fs.readFileSync(sf.path, 'utf8').trim().split('\n');
|
|
624
|
+
const messages = [];
|
|
625
|
+
let meta = null;
|
|
626
|
+
|
|
627
|
+
for (const line of lines) {
|
|
628
|
+
try {
|
|
629
|
+
const obj = JSON.parse(line);
|
|
630
|
+
if (obj.sessionId && !meta) {
|
|
631
|
+
meta = { id: obj.sessionId, timestamp: obj.timestamp || null, branch: obj.gitBranch || null };
|
|
632
|
+
}
|
|
633
|
+
if (!meta?.timestamp && obj.timestamp) meta.timestamp = obj.timestamp;
|
|
634
|
+
if (obj.type === 'user' || obj.type === 'assistant') messages.push(obj);
|
|
635
|
+
} catch { /* skip malformed */ }
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
const analysis = analyzeSession(
|
|
639
|
+
{ meta: meta || { id: 'unknown', timestamp: null, branch: null }, messages },
|
|
640
|
+
{ isDebugSession: sf.isDebugSession || false, projectRoot: options.projectRoot || null }
|
|
641
|
+
);
|
|
642
|
+
// Sessions contribute only hotspots — clear noisy text-based extractions
|
|
643
|
+
analysis.decisions = [];
|
|
644
|
+
analysis.pitfalls = [];
|
|
645
|
+
analysis.patterns = [];
|
|
646
|
+
analyses.push(analysis);
|
|
647
|
+
} catch { /* skip unreadable files */ }
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
return accumulate(analyses, options);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* Format a memory entry as an annotation string (without comment prefix).
|
|
655
|
+
* @param {MemoryEntry} entry
|
|
656
|
+
* @returns {string}
|
|
657
|
+
*/
|
|
658
|
+
function formatAnnotation(entry) {
|
|
659
|
+
const tag = entry.category === 'hotspot' ? 'cap-history'
|
|
660
|
+
: entry.category === 'decision' ? 'cap-decision'
|
|
661
|
+
: entry.category === 'pitfall' ? 'cap-pitfall'
|
|
662
|
+
: 'cap-pattern';
|
|
663
|
+
|
|
664
|
+
const meta = [];
|
|
665
|
+
if (entry.metadata.sessions) meta.push(`sessions:${entry.metadata.sessions}`);
|
|
666
|
+
if (entry.metadata.edits) meta.push(`edits:${entry.metadata.edits}`);
|
|
667
|
+
if (entry.metadata.since) meta.push(`since:${entry.metadata.since}`);
|
|
668
|
+
if (entry.metadata.confirmations) meta.push(`confirmed:${entry.metadata.confirmations}`);
|
|
669
|
+
if (entry.metadata.pinned) meta.push('pinned:true');
|
|
670
|
+
if (entry.metadata.source) meta.push(`learned:${entry.metadata.source.substring(0, 10)}`);
|
|
671
|
+
|
|
672
|
+
const metaStr = meta.length > 0 ? `(${meta.join(', ')})` : '';
|
|
673
|
+
return `@${tag}${metaStr} ${entry.content}`;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
module.exports = {
|
|
677
|
+
// Core
|
|
678
|
+
analyzeSession,
|
|
679
|
+
accumulate,
|
|
680
|
+
accumulateFromCode,
|
|
681
|
+
accumulateFromFiles,
|
|
682
|
+
formatAnnotation,
|
|
683
|
+
|
|
684
|
+
// Helpers (for testing)
|
|
685
|
+
extractText,
|
|
686
|
+
extractTools,
|
|
687
|
+
stripTags,
|
|
688
|
+
isNoise,
|
|
689
|
+
normalizeFilePath,
|
|
690
|
+
|
|
691
|
+
// Constants (configurable via options, exposed for transparency)
|
|
692
|
+
DEFAULT_STALE_THRESHOLD,
|
|
693
|
+
MIN_HOTSPOT_SESSIONS,
|
|
694
|
+
MIN_PATTERN_CONFIRMATIONS,
|
|
695
|
+
DECISION_PATTERNS,
|
|
696
|
+
PITFALL_PATTERNS,
|
|
697
|
+
PATTERN_PATTERNS,
|
|
698
|
+
};
|