@equilateral_ai/mindmeld 3.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/README.md +300 -0
- package/hooks/README.md +494 -0
- package/hooks/pre-compact.js +392 -0
- package/hooks/session-start.js +264 -0
- package/package.json +90 -0
- package/scripts/harvest.js +561 -0
- package/scripts/init-project.js +437 -0
- package/scripts/inject.js +388 -0
- package/src/collaboration/CollaborationPrompt.js +460 -0
- package/src/core/AlertEngine.js +813 -0
- package/src/core/AlertNotifier.js +363 -0
- package/src/core/CorrelationAnalyzer.js +774 -0
- package/src/core/CurationEngine.js +688 -0
- package/src/core/LLMPatternDetector.js +508 -0
- package/src/core/LoadBearingDetector.js +242 -0
- package/src/core/NotificationService.js +1032 -0
- package/src/core/PatternValidator.js +355 -0
- package/src/core/README.md +160 -0
- package/src/core/RapportOrchestrator.js +446 -0
- package/src/core/RelevanceDetector.js +577 -0
- package/src/core/StandardsIngestion.js +575 -0
- package/src/core/TeamLoadBearingDetector.js +431 -0
- package/src/database/dbOperations.js +105 -0
- package/src/handlers/activity/activityGetMe.js +98 -0
- package/src/handlers/activity/activityGetTeam.js +130 -0
- package/src/handlers/alerts/alertsAcknowledge.js +91 -0
- package/src/handlers/alerts/alertsGet.js +250 -0
- package/src/handlers/collaborators/collaboratorAdd.js +201 -0
- package/src/handlers/collaborators/collaboratorInvite.js +218 -0
- package/src/handlers/collaborators/collaboratorList.js +88 -0
- package/src/handlers/collaborators/collaboratorRemove.js +127 -0
- package/src/handlers/collaborators/inviteAccept.js +122 -0
- package/src/handlers/context/contextGet.js +57 -0
- package/src/handlers/context/invariantsGet.js +74 -0
- package/src/handlers/context/loopsGet.js +82 -0
- package/src/handlers/context/notesCreate.js +74 -0
- package/src/handlers/context/purposeGet.js +78 -0
- package/src/handlers/correlations/correlationsDeveloperGet.js +226 -0
- package/src/handlers/correlations/correlationsGet.js +93 -0
- package/src/handlers/correlations/correlationsProjectGet.js +161 -0
- package/src/handlers/github/githubConnectionStatus.js +49 -0
- package/src/handlers/github/githubDiscoverPatterns.js +364 -0
- package/src/handlers/github/githubOAuthCallback.js +166 -0
- package/src/handlers/github/githubOAuthStart.js +59 -0
- package/src/handlers/github/githubPatternsReview.js +109 -0
- package/src/handlers/github/githubReposList.js +105 -0
- package/src/handlers/helpers/checkSuperAdmin.js +85 -0
- package/src/handlers/helpers/dbOperations.js +53 -0
- package/src/handlers/helpers/errorHandler.js +49 -0
- package/src/handlers/helpers/index.js +106 -0
- package/src/handlers/helpers/lambdaWrapper.js +60 -0
- package/src/handlers/helpers/responseUtil.js +55 -0
- package/src/handlers/helpers/subscriptionTiers.js +1168 -0
- package/src/handlers/notifications/getPreferences.js +84 -0
- package/src/handlers/notifications/sendNotification.js +170 -0
- package/src/handlers/notifications/updatePreferences.js +316 -0
- package/src/handlers/patterns/patternUsagePost.js +182 -0
- package/src/handlers/patterns/patternViolationPost.js +185 -0
- package/src/handlers/projects/projectCreate.js +107 -0
- package/src/handlers/projects/projectDelete.js +82 -0
- package/src/handlers/projects/projectGet.js +95 -0
- package/src/handlers/projects/projectUpdate.js +118 -0
- package/src/handlers/reports/aiLeverage.js +206 -0
- package/src/handlers/reports/engineeringInvestment.js +132 -0
- package/src/handlers/reports/riskForecast.js +186 -0
- package/src/handlers/reports/standardsRoi.js +162 -0
- package/src/handlers/scheduled/analyzeCorrelations.js +178 -0
- package/src/handlers/scheduled/analyzeGitHistory.js +510 -0
- package/src/handlers/scheduled/generateAlerts.js +135 -0
- package/src/handlers/scheduled/refreshActivity.js +21 -0
- package/src/handlers/scheduled/scanCompliance.js +334 -0
- package/src/handlers/sessions/sessionEndPost.js +180 -0
- package/src/handlers/sessions/sessionStandardsPost.js +135 -0
- package/src/handlers/stripe/addonManagePost.js +240 -0
- package/src/handlers/stripe/billingPortalPost.js +93 -0
- package/src/handlers/stripe/enterpriseCheckoutPost.js +272 -0
- package/src/handlers/stripe/seatsUpdatePost.js +185 -0
- package/src/handlers/stripe/subscriptionCancelDelete.js +169 -0
- package/src/handlers/stripe/subscriptionCreatePost.js +221 -0
- package/src/handlers/stripe/subscriptionUpdatePut.js +163 -0
- package/src/handlers/stripe/webhookPost.js +454 -0
- package/src/handlers/users/cognitoPostConfirmation.js +150 -0
- package/src/handlers/users/userEntitlementsGet.js +89 -0
- package/src/handlers/users/userGet.js +114 -0
- package/src/handlers/webhooks/githubWebhook.js +223 -0
- package/src/index.js +969 -0
package/package.json
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@equilateral_ai/mindmeld",
|
|
3
|
+
"version": "3.0.0",
|
|
4
|
+
"description": "Intelligent standards injection for AI coding sessions - context-aware, self-documenting, scales to large codebases",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"mindmeld": "./scripts/init-project.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"src/",
|
|
11
|
+
"hooks/",
|
|
12
|
+
"scripts/init-project.js",
|
|
13
|
+
"scripts/inject.js",
|
|
14
|
+
"scripts/harvest.js",
|
|
15
|
+
"README.md"
|
|
16
|
+
],
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public"
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"test": "jest",
|
|
22
|
+
"test:unit": "jest --selectProjects unit",
|
|
23
|
+
"test:integration": "jest --selectProjects integration",
|
|
24
|
+
"test:integration:hooks": "jest test/integration/hooks.integration.test.js",
|
|
25
|
+
"test:integration:database": "jest test/integration/database.integration.test.js",
|
|
26
|
+
"test:integration:multiagent": "jest test/integration/multiAgent.integration.test.js",
|
|
27
|
+
"test:integration:performance": "jest test/integration/performance.integration.test.js --testTimeout=120000",
|
|
28
|
+
"test:integration:all": "jest test/integration/ --runInBand",
|
|
29
|
+
"test:coverage": "jest --coverage",
|
|
30
|
+
"test:ci": "jest --ci --coverage --runInBand",
|
|
31
|
+
"build": "./scripts/build-handlers.sh",
|
|
32
|
+
"deploy:dev": "./scripts/deploy-dev.sh",
|
|
33
|
+
"deploy:prod": "./scripts/deploy-prod.sh",
|
|
34
|
+
"verify:dev": "./scripts/verify-deployment.sh dev",
|
|
35
|
+
"verify:prod": "./scripts/verify-deployment.sh prod",
|
|
36
|
+
"agents": "node scripts/rapport-agents-cli.js",
|
|
37
|
+
"test:hooks": "node scripts/test-claude-hooks.js",
|
|
38
|
+
"test:session-start": "node scripts/test-claude-hooks.js --session-start",
|
|
39
|
+
"test:pre-compact": "node scripts/test-claude-hooks.js --pre-compact",
|
|
40
|
+
"test:benchmark": "node scripts/test-claude-hooks.js --benchmark",
|
|
41
|
+
"test:relevance": "node scripts/test-relevance-detection.js",
|
|
42
|
+
"test:curation": "node scripts/test-curation-engine.js",
|
|
43
|
+
"test:validation": "node scripts/test-pattern-validation.js",
|
|
44
|
+
"test:orchestrator": "node scripts/test-orchestrator.js"
|
|
45
|
+
},
|
|
46
|
+
"claudeCode": {
|
|
47
|
+
"hooks": {
|
|
48
|
+
"sessionStart": "hooks/session-start.js",
|
|
49
|
+
"preCompact": "hooks/pre-compact.js"
|
|
50
|
+
},
|
|
51
|
+
"config": {
|
|
52
|
+
"standardsPath": ".equilateral-standards",
|
|
53
|
+
"maxContextLength": 50000,
|
|
54
|
+
"maxStandards": 10,
|
|
55
|
+
"sessionStartTimeout": 500
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
"keywords": [
|
|
59
|
+
"equilateral",
|
|
60
|
+
"mindmeld",
|
|
61
|
+
"collaboration",
|
|
62
|
+
"memory",
|
|
63
|
+
"context",
|
|
64
|
+
"team-learning",
|
|
65
|
+
"knowledge-management",
|
|
66
|
+
"ai-context",
|
|
67
|
+
"claude-code",
|
|
68
|
+
"standards",
|
|
69
|
+
"patterns"
|
|
70
|
+
],
|
|
71
|
+
"author": "Jim Ford <jim@equilateral.ai>",
|
|
72
|
+
"license": "PROPRIETARY",
|
|
73
|
+
"repository": {
|
|
74
|
+
"type": "git",
|
|
75
|
+
"url": "git+https://github.com/jamesfordhq/rapport.git"
|
|
76
|
+
},
|
|
77
|
+
"homepage": "https://mindmeld.dev",
|
|
78
|
+
"dependencies": {
|
|
79
|
+
"@aws-sdk/client-bedrock-runtime": "^3.460.0",
|
|
80
|
+
"pg": "^8.11.3"
|
|
81
|
+
},
|
|
82
|
+
"devDependencies": {
|
|
83
|
+
"@aws-sdk/client-s3": "^3.460.0",
|
|
84
|
+
"@aws-sdk/client-ses": "^3.968.0",
|
|
85
|
+
"jest": "^29.7.0"
|
|
86
|
+
},
|
|
87
|
+
"engines": {
|
|
88
|
+
"node": ">=18.0.0"
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* MindMeld CLI - Manually harvest patterns from recent git history
|
|
4
|
+
*
|
|
5
|
+
* For non-Claude-Code tools (Cursor, Aider, Ollama, etc.) that don't have
|
|
6
|
+
* a pre-compact hook. Analyzes recent commits to detect patterns.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* mindmeld harvest # Analyze last 10 commits
|
|
10
|
+
* mindmeld harvest --since 7d # Last 7 days
|
|
11
|
+
* mindmeld harvest --commits 20 # Last 20 commits
|
|
12
|
+
*
|
|
13
|
+
* @equilateral_ai/mindmeld v3.0.0
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const fs = require('fs').promises;
|
|
18
|
+
const { exec } = require('child_process');
|
|
19
|
+
const { promisify } = require('util');
|
|
20
|
+
|
|
21
|
+
const execAsync = promisify(exec);
|
|
22
|
+
|
|
23
|
+
function showHarvestHelp() {
|
|
24
|
+
console.log(`
|
|
25
|
+
MindMeld harvest - Capture patterns from recent git history
|
|
26
|
+
|
|
27
|
+
Usage:
|
|
28
|
+
mindmeld harvest [options]
|
|
29
|
+
|
|
30
|
+
Options:
|
|
31
|
+
--since <period> Time period: 1d, 7d, 30d (default: 7d)
|
|
32
|
+
--commits <n> Number of commits to analyze (default: 10)
|
|
33
|
+
--path <dir> Project path (default: current directory)
|
|
34
|
+
--dry-run Show what would be captured without saving
|
|
35
|
+
--help, -h Show this help
|
|
36
|
+
|
|
37
|
+
Examples:
|
|
38
|
+
mindmeld harvest # Analyze last 7 days
|
|
39
|
+
mindmeld harvest --since 1d # Just today's commits
|
|
40
|
+
mindmeld harvest --commits 20 # Last 20 commits
|
|
41
|
+
mindmeld harvest --dry-run # Preview without saving
|
|
42
|
+
|
|
43
|
+
What it does:
|
|
44
|
+
1. Reads recent git diffs and commit messages
|
|
45
|
+
2. Detects repeated patterns (file types, naming, structure)
|
|
46
|
+
3. Identifies correction patterns (reverts, fixes, amendments)
|
|
47
|
+
4. Records detected patterns in .mindmeld/patterns/
|
|
48
|
+
5. Patterns with enough repetition get promoted to standards
|
|
49
|
+
`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Convert shorthand period (7d, 30d, 2w) to git-compatible format
|
|
54
|
+
*/
|
|
55
|
+
function parseGitSince(since) {
|
|
56
|
+
const match = since.match(/^(\d+)(d|w|m)$/);
|
|
57
|
+
if (!match) return since; // pass through if already in git format
|
|
58
|
+
const [, num, unit] = match;
|
|
59
|
+
const units = { d: 'days', w: 'weeks', m: 'months' };
|
|
60
|
+
return `${num} ${units[unit] || 'days'} ago`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Get recent git activity
|
|
65
|
+
*/
|
|
66
|
+
async function getGitHistory(projectPath, options = {}) {
|
|
67
|
+
const since = parseGitSince(options.since || '7d');
|
|
68
|
+
const maxCommits = options.commits || 10;
|
|
69
|
+
|
|
70
|
+
// Get commit log with diffs
|
|
71
|
+
try {
|
|
72
|
+
const { stdout: log } = await execAsync(
|
|
73
|
+
`git log --since="${since}" -n ${maxCommits} --format="%H|%s|%an|%ai" --stat`,
|
|
74
|
+
{ cwd: projectPath, maxBuffer: 1024 * 1024 }
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const { stdout: diff } = await execAsync(
|
|
78
|
+
`git log --since="${since}" -n ${maxCommits} -p --diff-filter=M`,
|
|
79
|
+
{ cwd: projectPath, maxBuffer: 5 * 1024 * 1024 }
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
return { log, diff };
|
|
83
|
+
} catch (error) {
|
|
84
|
+
if (error.message.includes('not a git repository')) {
|
|
85
|
+
throw new Error('not a git repository');
|
|
86
|
+
}
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Detect patterns from git history using regex analysis
|
|
93
|
+
*/
|
|
94
|
+
function detectPatterns(gitHistory) {
|
|
95
|
+
const patterns = [];
|
|
96
|
+
const { log, diff } = gitHistory;
|
|
97
|
+
|
|
98
|
+
// 1. Detect fix/correction patterns (commits that fix previous work)
|
|
99
|
+
const fixPatterns = log.match(/fix[:\s]|correct[:\s]|revert[:\s]|wrong|mistake|should be|instead of/gi) || [];
|
|
100
|
+
if (fixPatterns.length >= 2) {
|
|
101
|
+
patterns.push({
|
|
102
|
+
type: 'correction',
|
|
103
|
+
element: 'Repeated corrections detected',
|
|
104
|
+
evidence: `${fixPatterns.length} fix/correction commits in recent history`,
|
|
105
|
+
confidence: Math.min(0.9, fixPatterns.length * 0.15),
|
|
106
|
+
category: 'workflow'
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// 2. Detect file type patterns (what's being edited most)
|
|
111
|
+
const fileExtensions = {};
|
|
112
|
+
const fileMatches = log.match(/\S+\.\w+/g) || [];
|
|
113
|
+
for (const file of fileMatches) {
|
|
114
|
+
const ext = path.extname(file);
|
|
115
|
+
if (ext && ext.length < 6) {
|
|
116
|
+
fileExtensions[ext] = (fileExtensions[ext] || 0) + 1;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 3. Detect code patterns from diffs
|
|
121
|
+
const codePatterns = [];
|
|
122
|
+
|
|
123
|
+
// Connection pool vs single client
|
|
124
|
+
if (diff.includes('createPool') || diff.includes('Pool(')) {
|
|
125
|
+
codePatterns.push({
|
|
126
|
+
type: 'anti-pattern',
|
|
127
|
+
element: 'Connection pool usage detected',
|
|
128
|
+
evidence: 'createPool or Pool() found in diffs',
|
|
129
|
+
confidence: 0.7,
|
|
130
|
+
category: 'database'
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Console.log removal (cleanup pattern)
|
|
135
|
+
const consoleAdds = (diff.match(/^\+.*console\.log/gm) || []).length;
|
|
136
|
+
const consoleRemoves = (diff.match(/^-.*console\.log/gm) || []).length;
|
|
137
|
+
if (consoleRemoves > consoleAdds && consoleRemoves >= 3) {
|
|
138
|
+
codePatterns.push({
|
|
139
|
+
type: 'pattern',
|
|
140
|
+
element: 'Console.log cleanup pattern',
|
|
141
|
+
evidence: `${consoleRemoves} console.log removals vs ${consoleAdds} additions`,
|
|
142
|
+
confidence: 0.6,
|
|
143
|
+
category: 'code-quality'
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Error handling patterns
|
|
148
|
+
const tryCatchAdds = (diff.match(/^\+.*try\s*\{/gm) || []).length;
|
|
149
|
+
if (tryCatchAdds >= 3) {
|
|
150
|
+
codePatterns.push({
|
|
151
|
+
type: 'pattern',
|
|
152
|
+
element: 'Error handling additions',
|
|
153
|
+
evidence: `${tryCatchAdds} try/catch blocks added`,
|
|
154
|
+
confidence: 0.5,
|
|
155
|
+
category: 'error-handling'
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Type annotation additions (TypeScript patterns)
|
|
160
|
+
const typeAnnotations = (diff.match(/^\+.*:\s*(string|number|boolean|any|void|Promise|Array)/gm) || []).length;
|
|
161
|
+
if (typeAnnotations >= 5) {
|
|
162
|
+
codePatterns.push({
|
|
163
|
+
type: 'pattern',
|
|
164
|
+
element: 'Type annotations being added',
|
|
165
|
+
evidence: `${typeAnnotations} type annotations in recent diffs`,
|
|
166
|
+
confidence: 0.6,
|
|
167
|
+
category: 'typescript'
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Import organization patterns
|
|
172
|
+
const importChanges = (diff.match(/^\+\s*import\s/gm) || []).length;
|
|
173
|
+
if (importChanges >= 5) {
|
|
174
|
+
codePatterns.push({
|
|
175
|
+
type: 'pattern',
|
|
176
|
+
element: 'Import reorganization',
|
|
177
|
+
evidence: `${importChanges} import statements modified`,
|
|
178
|
+
confidence: 0.4,
|
|
179
|
+
category: 'code-organization'
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
patterns.push(...codePatterns);
|
|
184
|
+
|
|
185
|
+
return patterns;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Save detected patterns to .mindmeld/patterns/
|
|
190
|
+
*/
|
|
191
|
+
async function savePatterns(projectPath, patterns) {
|
|
192
|
+
const patternsDir = path.join(projectPath, '.mindmeld', 'patterns');
|
|
193
|
+
await fs.mkdir(patternsDir, { recursive: true });
|
|
194
|
+
|
|
195
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
196
|
+
const harvestFile = path.join(patternsDir, `harvest-${timestamp}.json`);
|
|
197
|
+
|
|
198
|
+
const harvest = {
|
|
199
|
+
timestamp: new Date().toISOString(),
|
|
200
|
+
source: 'manual-harvest',
|
|
201
|
+
patterns: patterns,
|
|
202
|
+
projectPath: projectPath,
|
|
203
|
+
projectName: path.basename(projectPath)
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
await fs.writeFile(harvestFile, JSON.stringify(harvest, null, 2));
|
|
207
|
+
return harvestFile;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Main harvest execution
|
|
212
|
+
*/
|
|
213
|
+
async function harvest(options = {}) {
|
|
214
|
+
const projectPath = options.path || process.cwd();
|
|
215
|
+
const dryRun = options.dryRun || false;
|
|
216
|
+
|
|
217
|
+
console.error(`[MindMeld] Harvesting patterns from: ${projectPath}`);
|
|
218
|
+
console.error(`[MindMeld] Period: last ${options.since || '7d'}, max ${options.commits || 10} commits`);
|
|
219
|
+
console.error('');
|
|
220
|
+
|
|
221
|
+
// 1. Get git history
|
|
222
|
+
let gitHistory;
|
|
223
|
+
try {
|
|
224
|
+
gitHistory = await getGitHistory(projectPath, {
|
|
225
|
+
since: options.since || '7d',
|
|
226
|
+
commits: options.commits || 10
|
|
227
|
+
});
|
|
228
|
+
} catch (error) {
|
|
229
|
+
if (error.message === 'not a git repository') {
|
|
230
|
+
console.error('[MindMeld] Not a git repository. Harvest requires git history.');
|
|
231
|
+
process.exit(1);
|
|
232
|
+
}
|
|
233
|
+
throw error;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// 2. Detect patterns
|
|
237
|
+
const patterns = detectPatterns(gitHistory);
|
|
238
|
+
|
|
239
|
+
if (patterns.length === 0) {
|
|
240
|
+
console.error('[MindMeld] No patterns detected in recent history.');
|
|
241
|
+
console.error('[MindMeld] Try a longer period: mindmeld harvest --since 30d');
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// 3. Display results
|
|
246
|
+
console.error(`[MindMeld] Detected ${patterns.length} pattern(s):\n`);
|
|
247
|
+
for (const pattern of patterns) {
|
|
248
|
+
const confidence = (pattern.confidence * 100).toFixed(0);
|
|
249
|
+
console.error(` [${pattern.type}] ${pattern.element}`);
|
|
250
|
+
console.error(` Category: ${pattern.category} | Confidence: ${confidence}%`);
|
|
251
|
+
console.error(` Evidence: ${pattern.evidence}`);
|
|
252
|
+
console.error('');
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// 4. Save (unless dry-run)
|
|
256
|
+
if (!dryRun) {
|
|
257
|
+
const savedPath = await savePatterns(projectPath, patterns);
|
|
258
|
+
console.error(`[MindMeld] Patterns saved to: ${path.relative(projectPath, savedPath)}`);
|
|
259
|
+
console.error(`[MindMeld] Patterns with 5+ detections will be promoted to provisional standards.`);
|
|
260
|
+
} else {
|
|
261
|
+
console.error('[MindMeld] Dry run - no patterns saved.');
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// 5. Try LLM-powered detection if available
|
|
265
|
+
try {
|
|
266
|
+
const llmModule = require('../src/core/LLMPatternDetector');
|
|
267
|
+
if (llmModule && process.env.MINDMELD_USE_LLM !== 'false') {
|
|
268
|
+
console.error('\n[MindMeld] LLM-powered detection available for deeper analysis.');
|
|
269
|
+
console.error('[MindMeld] Set MINDMELD_USE_LLM=true for semantic pattern detection.');
|
|
270
|
+
}
|
|
271
|
+
} catch {
|
|
272
|
+
// LLM module not available
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Promote high-confidence patterns to local staging
|
|
278
|
+
* Writes to .mindmeld/patterns/{category}/ (local, never directly to community repo)
|
|
279
|
+
* Community promotion happens via API → PR to EquilateralAgents-Community-Standards
|
|
280
|
+
*/
|
|
281
|
+
async function promotePatterns(projectPath, patterns, options = {}) {
|
|
282
|
+
const threshold = options.threshold || 0.5;
|
|
283
|
+
const standardsDir = path.join(projectPath, '.mindmeld', 'patterns');
|
|
284
|
+
|
|
285
|
+
const eligible = patterns.filter(p => p.confidence >= threshold);
|
|
286
|
+
if (eligible.length === 0) return [];
|
|
287
|
+
|
|
288
|
+
await fs.mkdir(standardsDir, { recursive: true });
|
|
289
|
+
|
|
290
|
+
const promoted = [];
|
|
291
|
+
|
|
292
|
+
for (const pattern of eligible) {
|
|
293
|
+
const categoryDir = path.join(standardsDir, pattern.category);
|
|
294
|
+
await fs.mkdir(categoryDir, { recursive: true });
|
|
295
|
+
|
|
296
|
+
const filename = pattern.element
|
|
297
|
+
.toLowerCase()
|
|
298
|
+
.replace(/[^a-z0-9]+/g, '_')
|
|
299
|
+
.replace(/^_|_$/g, '') + '.md';
|
|
300
|
+
|
|
301
|
+
const filePath = path.join(categoryDir, filename);
|
|
302
|
+
|
|
303
|
+
// Don't overwrite existing standards
|
|
304
|
+
try {
|
|
305
|
+
await fs.access(filePath);
|
|
306
|
+
continue;
|
|
307
|
+
} catch {
|
|
308
|
+
// File doesn't exist, safe to create
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const content = [
|
|
312
|
+
`# ${pattern.element}`,
|
|
313
|
+
'',
|
|
314
|
+
`**Status**: provisional`,
|
|
315
|
+
`**Category**: ${pattern.category}`,
|
|
316
|
+
`**Confidence**: ${(pattern.confidence * 100).toFixed(0)}%`,
|
|
317
|
+
`**Source**: git-harvest`,
|
|
318
|
+
`**Detected**: ${new Date().toISOString().split('T')[0]}`,
|
|
319
|
+
'',
|
|
320
|
+
'## Evidence',
|
|
321
|
+
'',
|
|
322
|
+
`- ${pattern.evidence}`,
|
|
323
|
+
'',
|
|
324
|
+
'## Guidance',
|
|
325
|
+
'',
|
|
326
|
+
pattern.type === 'anti-pattern'
|
|
327
|
+
? `- Avoid: ${pattern.element}`
|
|
328
|
+
: `- Follow: ${pattern.element}`,
|
|
329
|
+
'',
|
|
330
|
+
'---',
|
|
331
|
+
'*Provisional standard - will be promoted to solidified after 5+ consistent detections.*'
|
|
332
|
+
].join('\n');
|
|
333
|
+
|
|
334
|
+
await fs.writeFile(filePath, content);
|
|
335
|
+
promoted.push({ pattern, path: filePath });
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return promoted;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Harvest decisions from Claude Code plan files
|
|
343
|
+
* Matches plans to project by correlating session timestamps with plan file mtimes
|
|
344
|
+
*/
|
|
345
|
+
async function harvestPlans(projectPath) {
|
|
346
|
+
const os = require('os');
|
|
347
|
+
const homeDir = os.homedir();
|
|
348
|
+
const plansDir = path.join(homeDir, '.claude', 'plans');
|
|
349
|
+
const projectsDir = path.join(homeDir, '.claude', 'projects');
|
|
350
|
+
|
|
351
|
+
// Encode project path the same way Claude Code does
|
|
352
|
+
const encodedPath = projectPath.replace(/\//g, '-');
|
|
353
|
+
const sessionIndexPath = path.join(projectsDir, encodedPath, 'sessions-index.json');
|
|
354
|
+
|
|
355
|
+
// 1. Load sessions for this project
|
|
356
|
+
let sessions;
|
|
357
|
+
try {
|
|
358
|
+
const indexContent = await fs.readFile(sessionIndexPath, 'utf-8');
|
|
359
|
+
const index = JSON.parse(indexContent);
|
|
360
|
+
sessions = index.entries || [];
|
|
361
|
+
} catch {
|
|
362
|
+
return []; // No sessions found for this project
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (sessions.length === 0) return [];
|
|
366
|
+
|
|
367
|
+
// 2. Get time window from sessions (earliest created → now, since current session may not be indexed)
|
|
368
|
+
const sessionTimes = sessions.map(s => new Date(s.created).getTime());
|
|
369
|
+
const windowStart = Math.min(...sessionTimes);
|
|
370
|
+
const windowEnd = Date.now();
|
|
371
|
+
|
|
372
|
+
// 3. Find plan files within the session time window
|
|
373
|
+
let planFiles;
|
|
374
|
+
try {
|
|
375
|
+
const entries = await fs.readdir(plansDir);
|
|
376
|
+
planFiles = [];
|
|
377
|
+
for (const entry of entries) {
|
|
378
|
+
if (!entry.endsWith('.md')) continue;
|
|
379
|
+
const filePath = path.join(plansDir, entry);
|
|
380
|
+
const stat = await fs.stat(filePath);
|
|
381
|
+
const mtime = stat.mtimeMs;
|
|
382
|
+
if (mtime >= windowStart && mtime <= windowEnd) {
|
|
383
|
+
planFiles.push({ path: filePath, name: entry, mtime });
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
} catch {
|
|
387
|
+
return []; // No plans directory
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// 4. Content-match: only keep plans that reference files/terms from this project
|
|
391
|
+
const projectName = path.basename(projectPath).toLowerCase();
|
|
392
|
+
const relevantPlans = [];
|
|
393
|
+
|
|
394
|
+
for (const planFile of planFiles) {
|
|
395
|
+
try {
|
|
396
|
+
const content = await fs.readFile(planFile.path, 'utf-8');
|
|
397
|
+
const contentLower = content.toLowerCase();
|
|
398
|
+
|
|
399
|
+
// Score relevance by checking project indicators
|
|
400
|
+
let relevance = 0;
|
|
401
|
+
|
|
402
|
+
// Project name match (strongest signal)
|
|
403
|
+
if (contentLower.includes(projectName)) relevance += 5;
|
|
404
|
+
|
|
405
|
+
// Check if plan mentions files that exist in this project
|
|
406
|
+
const fileRefs = content.match(/`([^`]+\.(js|ts|json|yaml|yml|py|sql))`/g) || [];
|
|
407
|
+
for (const ref of fileRefs.slice(0, 10)) {
|
|
408
|
+
const cleanRef = ref.replace(/`/g, '');
|
|
409
|
+
try {
|
|
410
|
+
await fs.access(path.join(projectPath, cleanRef));
|
|
411
|
+
relevance += 3; // File exists in this project
|
|
412
|
+
} catch {
|
|
413
|
+
// File doesn't exist here
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Check for project-specific paths
|
|
418
|
+
if (content.includes(projectPath)) relevance += 10;
|
|
419
|
+
|
|
420
|
+
if (relevance >= 3) {
|
|
421
|
+
planFile.relevance = relevance;
|
|
422
|
+
planFile.content = content;
|
|
423
|
+
relevantPlans.push(planFile);
|
|
424
|
+
}
|
|
425
|
+
} catch {
|
|
426
|
+
// Skip unreadable
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
planFiles = relevantPlans;
|
|
431
|
+
|
|
432
|
+
if (planFiles.length === 0) return [];
|
|
433
|
+
|
|
434
|
+
// 5. Extract decisions from matched plan files
|
|
435
|
+
const decisions = [];
|
|
436
|
+
|
|
437
|
+
for (const planFile of planFiles) {
|
|
438
|
+
try {
|
|
439
|
+
const content = planFile.content || await fs.readFile(planFile.path, 'utf-8');
|
|
440
|
+
|
|
441
|
+
// Extract "Decisions" sections
|
|
442
|
+
const decisionPatterns = [
|
|
443
|
+
/## Decisions?\s*(?:Confirmed|Made)?\s*\n([\s\S]*?)(?=\n## |\n---|\Z)/gi,
|
|
444
|
+
/\| Decision \|[\s\S]*?\n((?:\|[^\n]+\n)+)/gi
|
|
445
|
+
];
|
|
446
|
+
|
|
447
|
+
for (const pattern of decisionPatterns) {
|
|
448
|
+
let match;
|
|
449
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
450
|
+
const section = match[1] || match[0];
|
|
451
|
+
// Parse table rows or bullet points
|
|
452
|
+
const lines = section.split('\n').filter(l =>
|
|
453
|
+
l.includes('|') && !l.includes('---') && !l.includes('Decision') && l.trim().length > 5
|
|
454
|
+
);
|
|
455
|
+
|
|
456
|
+
for (const line of lines) {
|
|
457
|
+
const cells = line.split('|').map(c => c.trim()).filter(c => c.length > 0);
|
|
458
|
+
if (cells.length >= 2) {
|
|
459
|
+
decisions.push({
|
|
460
|
+
type: 'decision',
|
|
461
|
+
element: cells[0].replace(/\*\*/g, ''),
|
|
462
|
+
evidence: cells[1].replace(/\*\*/g, ''),
|
|
463
|
+
confidence: 0.8,
|
|
464
|
+
category: 'architecture',
|
|
465
|
+
source: planFile.name
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Extract "Recommendation:" lines
|
|
473
|
+
const recommendations = content.match(/\*\*Recommendation:?\*\*:?\s*(.+)/gi) || [];
|
|
474
|
+
for (const rec of recommendations) {
|
|
475
|
+
const cleaned = rec.replace(/\*\*Recommendation:?\*\*:?\s*/i, '').trim();
|
|
476
|
+
if (cleaned.length > 10) {
|
|
477
|
+
decisions.push({
|
|
478
|
+
type: 'decision',
|
|
479
|
+
element: cleaned,
|
|
480
|
+
evidence: `From plan: ${planFile.name}`,
|
|
481
|
+
confidence: 0.7,
|
|
482
|
+
category: 'architecture',
|
|
483
|
+
source: planFile.name
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
} catch {
|
|
488
|
+
// Skip unreadable plans
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Deduplicate by element name (keep longest evidence)
|
|
493
|
+
const unique = {};
|
|
494
|
+
for (const d of decisions) {
|
|
495
|
+
if (!unique[d.element] || d.evidence.length > unique[d.element].evidence.length) {
|
|
496
|
+
unique[d.element] = d;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
return Object.values(unique);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Promote plan decisions to standards
|
|
505
|
+
* Always writes to .mindmeld/decisions/ (local staging)
|
|
506
|
+
* Community promotion happens via API → PR to EquilateralAgents-Community-Standards
|
|
507
|
+
*/
|
|
508
|
+
async function promoteDecisions(projectPath, decisions, options = {}) {
|
|
509
|
+
if (decisions.length === 0) return [];
|
|
510
|
+
|
|
511
|
+
const decisionsDir = path.join(projectPath, '.mindmeld', 'decisions');
|
|
512
|
+
await fs.mkdir(decisionsDir, { recursive: true });
|
|
513
|
+
|
|
514
|
+
const promoted = [];
|
|
515
|
+
|
|
516
|
+
for (const decision of decisions) {
|
|
517
|
+
const filename = decision.element
|
|
518
|
+
.toLowerCase()
|
|
519
|
+
.replace(/[^a-z0-9]+/g, '_')
|
|
520
|
+
.replace(/^_|_$/g, '')
|
|
521
|
+
.substring(0, 50) + '.md';
|
|
522
|
+
|
|
523
|
+
const filePath = path.join(decisionsDir, filename);
|
|
524
|
+
|
|
525
|
+
// Don't overwrite existing
|
|
526
|
+
try {
|
|
527
|
+
await fs.access(filePath);
|
|
528
|
+
continue;
|
|
529
|
+
} catch {
|
|
530
|
+
// Safe to create
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const content = [
|
|
534
|
+
`# ${decision.element}`,
|
|
535
|
+
'',
|
|
536
|
+
`**Status**: solidified`,
|
|
537
|
+
`**Category**: ${decision.category}`,
|
|
538
|
+
`**Confidence**: ${(decision.confidence * 100).toFixed(0)}%`,
|
|
539
|
+
`**Source**: plan-harvest`,
|
|
540
|
+
`**Detected**: ${new Date().toISOString().split('T')[0]}`,
|
|
541
|
+
'',
|
|
542
|
+
'## Decision',
|
|
543
|
+
'',
|
|
544
|
+
`- ${decision.evidence}`,
|
|
545
|
+
'',
|
|
546
|
+
'## Context',
|
|
547
|
+
'',
|
|
548
|
+
`Extracted from: ${decision.source}`,
|
|
549
|
+
'',
|
|
550
|
+
'---',
|
|
551
|
+
'*Architectural decision - do not revisit without explicit discussion.*'
|
|
552
|
+
].join('\n');
|
|
553
|
+
|
|
554
|
+
await fs.writeFile(filePath, content);
|
|
555
|
+
promoted.push({ decision, path: filePath });
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
return promoted;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
module.exports = { harvest, promotePatterns, promoteDecisions, harvestPlans, getGitHistory, detectPatterns, savePatterns, showHarvestHelp };
|