@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
|
@@ -0,0 +1,688 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rapport v3 - Curation Engine
|
|
3
|
+
*
|
|
4
|
+
* Purpose: Promotes validated patterns to .equilateral-standards/
|
|
5
|
+
*
|
|
6
|
+
* Promotion Criteria:
|
|
7
|
+
* - correlation >= 0.90 (90% success rate)
|
|
8
|
+
* - projectCount >= 5 (proven across 5+ projects)
|
|
9
|
+
* - developerCount >= 3 (used by 3+ developers)
|
|
10
|
+
* - sessionCount >= 10 (reinforced 10+ times)
|
|
11
|
+
* - hasMeasurableBenefit = true (cost/performance gain)
|
|
12
|
+
*
|
|
13
|
+
* Based on: /Users/jamesford/Source/rapport/docs/RAPPORT_STANDARDS_INTEGRATION_DESIGN.md
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const { executeQuery } = require('../handlers/helpers/dbOperations');
|
|
17
|
+
const { NotificationService } = require('./NotificationService');
|
|
18
|
+
|
|
19
|
+
class CurationEngine {
|
|
20
|
+
constructor(config = {}) {
|
|
21
|
+
this.config = {
|
|
22
|
+
minCorrelation: config.minCorrelation || 0.90,
|
|
23
|
+
minProjects: config.minProjects || 5,
|
|
24
|
+
minDevelopers: config.minDevelopers || 3,
|
|
25
|
+
minSessions: config.minSessions || 10,
|
|
26
|
+
requireMeasurableBenefit: config.requireMeasurableBenefit !== false,
|
|
27
|
+
...config
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// Initialize notification service
|
|
31
|
+
this.notificationService = config.notificationService || new NotificationService();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Evaluate if a pattern meets promotion criteria
|
|
36
|
+
*
|
|
37
|
+
* @param {string} patternId - Pattern identifier
|
|
38
|
+
* @returns {Promise<Object|null>} Curation candidate or null
|
|
39
|
+
*/
|
|
40
|
+
async evaluateForPromotion(patternId) {
|
|
41
|
+
try {
|
|
42
|
+
// Get pattern metrics from materialized view
|
|
43
|
+
const metrics = await this.getPatternMetrics(patternId);
|
|
44
|
+
|
|
45
|
+
if (!metrics) {
|
|
46
|
+
console.log(`[CurationEngine] Pattern ${patternId} not found in metrics`);
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Check if pattern meets all criteria
|
|
51
|
+
if (!this.meetsAllCriteria(metrics)) {
|
|
52
|
+
console.log(`[CurationEngine] Pattern ${patternId} does not meet promotion criteria`);
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Create curation candidate
|
|
57
|
+
const candidate = await this.createCurationCandidate(patternId, metrics);
|
|
58
|
+
|
|
59
|
+
console.log(`[CurationEngine] Pattern ${patternId} promoted to curation candidate`);
|
|
60
|
+
return candidate;
|
|
61
|
+
} catch (error) {
|
|
62
|
+
console.error('[CurationEngine] Error evaluating pattern for promotion:', error);
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Get pattern metrics from materialized view
|
|
69
|
+
*
|
|
70
|
+
* @param {string} patternId - Pattern identifier
|
|
71
|
+
* @returns {Promise<Object|null>} Pattern metrics
|
|
72
|
+
*/
|
|
73
|
+
async getPatternMetrics(patternId) {
|
|
74
|
+
const query = `
|
|
75
|
+
SELECT
|
|
76
|
+
pattern_id,
|
|
77
|
+
element,
|
|
78
|
+
project_count,
|
|
79
|
+
developer_count,
|
|
80
|
+
session_count,
|
|
81
|
+
success_correlation,
|
|
82
|
+
last_used
|
|
83
|
+
FROM rapport.pattern_metrics
|
|
84
|
+
WHERE pattern_id = $1
|
|
85
|
+
`;
|
|
86
|
+
|
|
87
|
+
const result = await executeQuery(query, [patternId]);
|
|
88
|
+
return result.rows.length > 0 ? result.rows[0] : null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Check if metrics meet all promotion criteria
|
|
93
|
+
*
|
|
94
|
+
* @param {Object} metrics - Pattern metrics
|
|
95
|
+
* @returns {boolean} True if all criteria met
|
|
96
|
+
*/
|
|
97
|
+
meetsAllCriteria(metrics) {
|
|
98
|
+
// PostgreSQL returns numeric values as strings - parse them
|
|
99
|
+
const correlation = parseFloat(metrics.success_correlation) || 0;
|
|
100
|
+
const projectCount = parseInt(metrics.project_count) || 0;
|
|
101
|
+
const developerCount = parseInt(metrics.developer_count) || 0;
|
|
102
|
+
const sessionCount = parseInt(metrics.session_count) || 0;
|
|
103
|
+
|
|
104
|
+
const checks = {
|
|
105
|
+
correlation: correlation >= this.config.minCorrelation,
|
|
106
|
+
projects: projectCount >= this.config.minProjects,
|
|
107
|
+
developers: developerCount >= this.config.minDevelopers,
|
|
108
|
+
sessions: sessionCount >= this.config.minSessions
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const allMet = Object.values(checks).every(check => check === true);
|
|
112
|
+
|
|
113
|
+
if (!allMet) {
|
|
114
|
+
console.log('[CurationEngine] Criteria check:', {
|
|
115
|
+
pattern: metrics.pattern_id,
|
|
116
|
+
correlation: `${correlation.toFixed(2)} (required: ${this.config.minCorrelation})`,
|
|
117
|
+
projects: `${projectCount} (required: ${this.config.minProjects})`,
|
|
118
|
+
developers: `${developerCount} (required: ${this.config.minDevelopers})`,
|
|
119
|
+
sessions: `${sessionCount} (required: ${this.config.minSessions})`,
|
|
120
|
+
passed: checks
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return allMet;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Create a curation candidate for human review
|
|
129
|
+
*
|
|
130
|
+
* @param {string} patternId - Pattern identifier
|
|
131
|
+
* @param {Object} metrics - Pattern metrics
|
|
132
|
+
* @returns {Promise<Object>} Curation candidate
|
|
133
|
+
*/
|
|
134
|
+
async createCurationCandidate(patternId, metrics) {
|
|
135
|
+
// Get pattern details
|
|
136
|
+
const patternDetails = await this.getPatternDetails(patternId);
|
|
137
|
+
|
|
138
|
+
// Infer category based on pattern characteristics
|
|
139
|
+
const category = await this.inferCategory(patternDetails);
|
|
140
|
+
|
|
141
|
+
// Gather best examples from successful usage
|
|
142
|
+
const examples = await this.gatherBestExamples(patternId);
|
|
143
|
+
|
|
144
|
+
// Gather anti-patterns from failed usage
|
|
145
|
+
const antiPatterns = await this.gatherAntiPatterns(patternId);
|
|
146
|
+
|
|
147
|
+
// Get measurable benefit data
|
|
148
|
+
const benefit = await this.getMeasurableBenefit(patternId);
|
|
149
|
+
|
|
150
|
+
// Build proposed standard
|
|
151
|
+
const proposedStandard = {
|
|
152
|
+
name: metrics.element,
|
|
153
|
+
category: category,
|
|
154
|
+
status: 'candidate',
|
|
155
|
+
evidence: {
|
|
156
|
+
correlation: parseFloat(metrics.success_correlation),
|
|
157
|
+
projectCount: metrics.project_count,
|
|
158
|
+
developerCount: metrics.developer_count,
|
|
159
|
+
sessionCount: metrics.session_count,
|
|
160
|
+
benefit: benefit
|
|
161
|
+
},
|
|
162
|
+
pattern: patternDetails,
|
|
163
|
+
examples: examples,
|
|
164
|
+
antiPatterns: antiPatterns,
|
|
165
|
+
proposedEnforcement: this.generateEnforcementRule(patternDetails)
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
// Generate standard document
|
|
169
|
+
const standardDocument = this.generateStandardDocument(proposedStandard);
|
|
170
|
+
|
|
171
|
+
// Save to curation database
|
|
172
|
+
const candidateId = await this.saveCurationCandidate(
|
|
173
|
+
patternId,
|
|
174
|
+
category,
|
|
175
|
+
proposedStandard.evidence,
|
|
176
|
+
standardDocument
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
// Notify for human review
|
|
180
|
+
await this.notifyForReview(candidateId, proposedStandard);
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
candidateId,
|
|
184
|
+
patternId,
|
|
185
|
+
...proposedStandard
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Get detailed pattern information
|
|
191
|
+
*
|
|
192
|
+
* @param {string} patternId - Pattern identifier
|
|
193
|
+
* @returns {Promise<Object>} Pattern details
|
|
194
|
+
*/
|
|
195
|
+
async getPatternDetails(patternId) {
|
|
196
|
+
const query = `
|
|
197
|
+
SELECT
|
|
198
|
+
pattern_id,
|
|
199
|
+
project_id,
|
|
200
|
+
intent,
|
|
201
|
+
constraints,
|
|
202
|
+
outcome_criteria,
|
|
203
|
+
maturity,
|
|
204
|
+
handoff_count,
|
|
205
|
+
successful_handoffs,
|
|
206
|
+
failed_handoffs,
|
|
207
|
+
discovered_by,
|
|
208
|
+
discovered_at,
|
|
209
|
+
last_used,
|
|
210
|
+
pattern_data
|
|
211
|
+
FROM rapport.patterns
|
|
212
|
+
WHERE pattern_id = $1
|
|
213
|
+
`;
|
|
214
|
+
|
|
215
|
+
const result = await executeQuery(query, [patternId]);
|
|
216
|
+
return result.rows[0];
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Infer category for the pattern based on content analysis
|
|
221
|
+
*
|
|
222
|
+
* @param {Object} pattern - Pattern details
|
|
223
|
+
* @returns {Promise<string>} Inferred category
|
|
224
|
+
*/
|
|
225
|
+
async inferCategory(pattern) {
|
|
226
|
+
const intent = pattern.intent.toLowerCase();
|
|
227
|
+
const constraints = JSON.stringify(pattern.constraints).toLowerCase();
|
|
228
|
+
const data = JSON.stringify(pattern.pattern_data).toLowerCase();
|
|
229
|
+
|
|
230
|
+
// Category detection based on keywords
|
|
231
|
+
const categories = {
|
|
232
|
+
'serverless-saas-aws': ['lambda', 'api gateway', 'sam', 'serverless', 'handler', 'aws'],
|
|
233
|
+
'database-patterns': ['database', 'query', 'postgresql', 'sql', 'schema'],
|
|
234
|
+
'multi-agent-orchestration': ['agent', 'orchestration', 'workflow', 'task', 'team'],
|
|
235
|
+
'frontend-development': ['react', 'component', 'ui', 'frontend', 'tsx', 'jsx'],
|
|
236
|
+
'compliance-security': ['security', 'compliance', 'audit', 'gdpr', 'encryption'],
|
|
237
|
+
'cost-optimization': ['cost', 'performance', 'optimization', 'billing', 'pricing']
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const allText = `${intent} ${constraints} ${data}`;
|
|
241
|
+
|
|
242
|
+
for (const [category, keywords] of Object.entries(categories)) {
|
|
243
|
+
if (keywords.some(keyword => allText.includes(keyword))) {
|
|
244
|
+
return category;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return 'general'; // Default category
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Gather best examples from successful pattern usage
|
|
253
|
+
*
|
|
254
|
+
* @param {string} patternId - Pattern identifier
|
|
255
|
+
* @returns {Promise<Array>} Best examples
|
|
256
|
+
*/
|
|
257
|
+
async gatherBestExamples(patternId) {
|
|
258
|
+
const query = `
|
|
259
|
+
SELECT
|
|
260
|
+
pu.context,
|
|
261
|
+
pu.used_at,
|
|
262
|
+
u."User_Display_Name" as user_name,
|
|
263
|
+
p.project_name
|
|
264
|
+
FROM rapport.pattern_usage pu
|
|
265
|
+
JOIN "Users" u ON pu.email_address = u."Email_Address"
|
|
266
|
+
JOIN rapport.patterns pat ON pu.pattern_id = pat.pattern_id
|
|
267
|
+
JOIN rapport.projects p ON pat.project_id = p.project_id
|
|
268
|
+
WHERE pu.pattern_id = $1
|
|
269
|
+
AND pu.success = true
|
|
270
|
+
ORDER BY pu.used_at DESC
|
|
271
|
+
LIMIT 5
|
|
272
|
+
`;
|
|
273
|
+
|
|
274
|
+
const result = await executeQuery(query, [patternId]);
|
|
275
|
+
|
|
276
|
+
return result.rows.map(row => ({
|
|
277
|
+
code: row.context.code || 'No code example available',
|
|
278
|
+
description: row.context.description || 'Successful pattern application',
|
|
279
|
+
project: row.project_name,
|
|
280
|
+
user: row.user_name,
|
|
281
|
+
timestamp: row.used_at
|
|
282
|
+
}));
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Gather anti-patterns from failed usage
|
|
287
|
+
*
|
|
288
|
+
* @param {string} patternId - Pattern identifier
|
|
289
|
+
* @returns {Promise<Array>} Anti-patterns
|
|
290
|
+
*/
|
|
291
|
+
async gatherAntiPatterns(patternId) {
|
|
292
|
+
const query = `
|
|
293
|
+
SELECT
|
|
294
|
+
pu.context,
|
|
295
|
+
pu.used_at,
|
|
296
|
+
u."User_Display_Name" as user_name
|
|
297
|
+
FROM rapport.pattern_usage pu
|
|
298
|
+
JOIN "Users" u ON pu.email_address = u."Email_Address"
|
|
299
|
+
WHERE pu.pattern_id = $1
|
|
300
|
+
AND pu.success = false
|
|
301
|
+
ORDER BY pu.used_at DESC
|
|
302
|
+
LIMIT 5
|
|
303
|
+
`;
|
|
304
|
+
|
|
305
|
+
const result = await executeQuery(query, [patternId]);
|
|
306
|
+
|
|
307
|
+
return result.rows.map(row => ({
|
|
308
|
+
description: row.context.error || 'Pattern application failed',
|
|
309
|
+
context: row.context.failureReason || 'Unknown failure reason',
|
|
310
|
+
user: row.user_name,
|
|
311
|
+
timestamp: row.used_at
|
|
312
|
+
}));
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Get measurable benefit data (cost savings, performance improvements)
|
|
317
|
+
*
|
|
318
|
+
* @param {string} patternId - Pattern identifier
|
|
319
|
+
* @returns {Promise<Object>} Measurable benefit
|
|
320
|
+
*/
|
|
321
|
+
async getMeasurableBenefit(patternId) {
|
|
322
|
+
const query = `
|
|
323
|
+
SELECT
|
|
324
|
+
pattern_data->'benefit' as benefit_data
|
|
325
|
+
FROM rapport.patterns
|
|
326
|
+
WHERE pattern_id = $1
|
|
327
|
+
`;
|
|
328
|
+
|
|
329
|
+
const result = await executeQuery(query, [patternId]);
|
|
330
|
+
|
|
331
|
+
if (result.rows.length > 0 && result.rows[0].benefit_data) {
|
|
332
|
+
return result.rows[0].benefit_data;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Default benefit if not recorded
|
|
336
|
+
return {
|
|
337
|
+
type: 'team_efficiency',
|
|
338
|
+
description: 'Proven pattern with high success rate across multiple teams',
|
|
339
|
+
quantified: false
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Generate proposed enforcement rule for the pattern
|
|
345
|
+
*
|
|
346
|
+
* @param {Object} pattern - Pattern details
|
|
347
|
+
* @returns {string} Enforcement rule
|
|
348
|
+
*/
|
|
349
|
+
generateEnforcementRule(pattern) {
|
|
350
|
+
const constraints = Array.isArray(pattern.constraints) ? pattern.constraints : [];
|
|
351
|
+
const outcomeCriteria = Array.isArray(pattern.outcome_criteria) ? pattern.outcome_criteria : [];
|
|
352
|
+
|
|
353
|
+
let enforcement = `## Enforcement Rule\n\n`;
|
|
354
|
+
enforcement += `**Intent**: ${pattern.intent}\n\n`;
|
|
355
|
+
|
|
356
|
+
if (constraints.length > 0) {
|
|
357
|
+
enforcement += `### Required Constraints\n\n`;
|
|
358
|
+
constraints.forEach((constraint, idx) => {
|
|
359
|
+
enforcement += `${idx + 1}. ${constraint}\n`;
|
|
360
|
+
});
|
|
361
|
+
enforcement += `\n`;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (outcomeCriteria.length > 0) {
|
|
365
|
+
enforcement += `### Success Criteria\n\n`;
|
|
366
|
+
outcomeCriteria.forEach((criteria, idx) => {
|
|
367
|
+
enforcement += `${idx + 1}. ${criteria}\n`;
|
|
368
|
+
});
|
|
369
|
+
enforcement += `\n`;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
enforcement += `### Validation\n\n`;
|
|
373
|
+
enforcement += `This pattern should be automatically validated during code review and CI/CD pipeline.\n`;
|
|
374
|
+
|
|
375
|
+
return enforcement;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Generate markdown standard document
|
|
380
|
+
*
|
|
381
|
+
* @param {Object} candidate - Curation candidate
|
|
382
|
+
* @returns {string} Standard document in markdown
|
|
383
|
+
*/
|
|
384
|
+
generateStandardDocument(candidate) {
|
|
385
|
+
const { name, category, evidence, pattern, examples, antiPatterns, proposedEnforcement } = candidate;
|
|
386
|
+
|
|
387
|
+
let doc = `# ${name}\n\n`;
|
|
388
|
+
doc += `**Status**: Candidate (from Rapport curation)\n`;
|
|
389
|
+
doc += `**Category**: ${category}\n`;
|
|
390
|
+
doc += `**Date**: ${new Date().toISOString()}\n\n`;
|
|
391
|
+
|
|
392
|
+
doc += `## Evidence\n\n`;
|
|
393
|
+
doc += `- **Correlation**: ${(evidence.correlation * 100).toFixed(1)}%\n`;
|
|
394
|
+
doc += `- **Projects**: ${evidence.projectCount}\n`;
|
|
395
|
+
doc += `- **Developers**: ${evidence.developerCount}\n`;
|
|
396
|
+
doc += `- **Sessions**: ${evidence.sessionCount}\n`;
|
|
397
|
+
doc += `- **Benefit**: ${evidence.benefit.description || JSON.stringify(evidence.benefit)}\n\n`;
|
|
398
|
+
|
|
399
|
+
doc += `## Pattern\n\n`;
|
|
400
|
+
doc += `**Intent**: ${pattern.intent}\n\n`;
|
|
401
|
+
|
|
402
|
+
if (pattern.constraints && pattern.constraints.length > 0) {
|
|
403
|
+
doc += `**Constraints**:\n`;
|
|
404
|
+
pattern.constraints.forEach((constraint, idx) => {
|
|
405
|
+
doc += `${idx + 1}. ${constraint}\n`;
|
|
406
|
+
});
|
|
407
|
+
doc += `\n`;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (pattern.outcome_criteria && pattern.outcome_criteria.length > 0) {
|
|
411
|
+
doc += `**Outcome Criteria**:\n`;
|
|
412
|
+
pattern.outcome_criteria.forEach((criteria, idx) => {
|
|
413
|
+
doc += `${idx + 1}. ${criteria}\n`;
|
|
414
|
+
});
|
|
415
|
+
doc += `\n`;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
doc += `## Examples\n\n`;
|
|
419
|
+
if (examples.length > 0) {
|
|
420
|
+
examples.forEach((example, idx) => {
|
|
421
|
+
doc += `### Example ${idx + 1}: ${example.description}\n\n`;
|
|
422
|
+
doc += `**Project**: ${example.project}\n`;
|
|
423
|
+
doc += `**User**: ${example.user}\n\n`;
|
|
424
|
+
doc += `\`\`\`javascript\n${example.code}\n\`\`\`\n\n`;
|
|
425
|
+
});
|
|
426
|
+
} else {
|
|
427
|
+
doc += `No examples available.\n\n`;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
doc += `## Anti-Patterns\n\n`;
|
|
431
|
+
if (antiPatterns.length > 0) {
|
|
432
|
+
antiPatterns.forEach((antiPattern, idx) => {
|
|
433
|
+
doc += `${idx + 1}. ❌ **${antiPattern.description}**\n`;
|
|
434
|
+
doc += ` - Context: ${antiPattern.context}\n`;
|
|
435
|
+
doc += ` - Observed by: ${antiPattern.user}\n\n`;
|
|
436
|
+
});
|
|
437
|
+
} else {
|
|
438
|
+
doc += `No anti-patterns recorded.\n\n`;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
doc += proposedEnforcement;
|
|
442
|
+
doc += `\n---\n\n`;
|
|
443
|
+
doc += `**Generated by**: Rapport Curation Engine\n`;
|
|
444
|
+
doc += `**Requires**: Human review before promotion to .equilateral-standards/\n`;
|
|
445
|
+
|
|
446
|
+
return doc;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Save curation candidate to database
|
|
451
|
+
*
|
|
452
|
+
* @param {string} patternId - Pattern identifier
|
|
453
|
+
* @param {string} category - Proposed category
|
|
454
|
+
* @param {Object} evidence - Evidence data
|
|
455
|
+
* @param {string} standardDocument - Generated standard document
|
|
456
|
+
* @returns {Promise<number>} Candidate ID
|
|
457
|
+
*/
|
|
458
|
+
async saveCurationCandidate(patternId, category, evidence, standardDocument) {
|
|
459
|
+
const query = `
|
|
460
|
+
INSERT INTO rapport.curation_candidates
|
|
461
|
+
(pattern_id, proposed_category, evidence, standard_document, status)
|
|
462
|
+
VALUES
|
|
463
|
+
($1, $2, $3, $4, 'pending')
|
|
464
|
+
RETURNING candidate_id
|
|
465
|
+
`;
|
|
466
|
+
|
|
467
|
+
const result = await executeQuery(query, [
|
|
468
|
+
patternId,
|
|
469
|
+
category,
|
|
470
|
+
JSON.stringify(evidence),
|
|
471
|
+
standardDocument
|
|
472
|
+
]);
|
|
473
|
+
|
|
474
|
+
return result.rows[0].candidate_id;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Notify stakeholders for human review
|
|
479
|
+
*
|
|
480
|
+
* @param {number} candidateId - Candidate ID
|
|
481
|
+
* @param {Object} candidate - Curation candidate
|
|
482
|
+
*/
|
|
483
|
+
async notifyForReview(candidateId, candidate) {
|
|
484
|
+
console.log(`\n========================================`);
|
|
485
|
+
console.log(`[CurationEngine] NEW CURATION CANDIDATE`);
|
|
486
|
+
console.log(`========================================`);
|
|
487
|
+
console.log(`Candidate ID: ${candidateId}`);
|
|
488
|
+
console.log(`Pattern: ${candidate.name}`);
|
|
489
|
+
console.log(`Category: ${candidate.category}`);
|
|
490
|
+
console.log(`Evidence:`);
|
|
491
|
+
console.log(` - Correlation: ${(candidate.evidence.correlation * 100).toFixed(1)}%`);
|
|
492
|
+
console.log(` - Projects: ${candidate.evidence.projectCount}`);
|
|
493
|
+
console.log(` - Developers: ${candidate.evidence.developerCount}`);
|
|
494
|
+
console.log(` - Sessions: ${candidate.evidence.sessionCount}`);
|
|
495
|
+
console.log(`\nAction Required: Review candidate for promotion to .equilateral-standards/`);
|
|
496
|
+
console.log(`========================================\n`);
|
|
497
|
+
|
|
498
|
+
// Send notifications to curators (admins with curation permissions)
|
|
499
|
+
try {
|
|
500
|
+
const curators = await this.getCurators();
|
|
501
|
+
|
|
502
|
+
for (const curator of curators) {
|
|
503
|
+
const preferences = await this.getUserPreferences(curator.email_address);
|
|
504
|
+
|
|
505
|
+
await this.notificationService.sendNotification({
|
|
506
|
+
type: 'curation_candidate',
|
|
507
|
+
email: curator.email_address,
|
|
508
|
+
preferences: preferences,
|
|
509
|
+
data: {
|
|
510
|
+
candidateId: candidateId,
|
|
511
|
+
patternName: candidate.name,
|
|
512
|
+
category: candidate.category,
|
|
513
|
+
evidence: candidate.evidence,
|
|
514
|
+
referenceType: 'curation_candidate',
|
|
515
|
+
referenceId: String(candidateId)
|
|
516
|
+
}
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
console.log(`[CurationEngine] Notified ${curators.length} curators`);
|
|
521
|
+
} catch (error) {
|
|
522
|
+
console.error('[CurationEngine] Failed to send notifications:', error);
|
|
523
|
+
// Don't throw - notification failure shouldn't block curation
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Get users with curation permissions (Super Admins and Admins)
|
|
529
|
+
*
|
|
530
|
+
* @returns {Promise<Array>} List of curator emails
|
|
531
|
+
*/
|
|
532
|
+
async getCurators() {
|
|
533
|
+
const query = `
|
|
534
|
+
SELECT DISTINCT u."Email_Address" as email_address
|
|
535
|
+
FROM "Users" u
|
|
536
|
+
LEFT JOIN "UserEntitlements" ue ON u."Email_Address" = ue."Email_Address"
|
|
537
|
+
WHERE u."Super_Admin" = true OR ue."Admin" = true
|
|
538
|
+
AND u."active" = true
|
|
539
|
+
`;
|
|
540
|
+
|
|
541
|
+
const result = await executeQuery(query);
|
|
542
|
+
return result.rows;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Get user notification preferences
|
|
547
|
+
*
|
|
548
|
+
* @param {string} email - User email
|
|
549
|
+
* @returns {Promise<Object>} User preferences
|
|
550
|
+
*/
|
|
551
|
+
async getUserPreferences(email) {
|
|
552
|
+
const query = `SELECT rapport.get_notification_preferences($1) as preferences`;
|
|
553
|
+
const result = await executeQuery(query, [email]);
|
|
554
|
+
return result.rows[0]?.preferences || null;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Refresh pattern metrics materialized view
|
|
559
|
+
*
|
|
560
|
+
* @returns {Promise<void>}
|
|
561
|
+
*/
|
|
562
|
+
async refreshMetrics() {
|
|
563
|
+
try {
|
|
564
|
+
await executeQuery('REFRESH MATERIALIZED VIEW rapport.pattern_metrics');
|
|
565
|
+
console.log('[CurationEngine] Pattern metrics refreshed');
|
|
566
|
+
} catch (error) {
|
|
567
|
+
console.error('[CurationEngine] Error refreshing metrics:', error);
|
|
568
|
+
throw error;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Get all pending curation candidates
|
|
574
|
+
*
|
|
575
|
+
* @returns {Promise<Array>} Pending candidates
|
|
576
|
+
*/
|
|
577
|
+
async getPendingCandidates() {
|
|
578
|
+
const query = `
|
|
579
|
+
SELECT
|
|
580
|
+
candidate_id,
|
|
581
|
+
pattern_id,
|
|
582
|
+
proposed_category,
|
|
583
|
+
evidence,
|
|
584
|
+
standard_document,
|
|
585
|
+
created_at
|
|
586
|
+
FROM rapport.curation_candidates
|
|
587
|
+
WHERE status = 'pending'
|
|
588
|
+
ORDER BY created_at DESC
|
|
589
|
+
`;
|
|
590
|
+
|
|
591
|
+
const result = await executeQuery(query);
|
|
592
|
+
return result.rows;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Approve a curation candidate
|
|
597
|
+
*
|
|
598
|
+
* @param {number} candidateId - Candidate ID
|
|
599
|
+
* @param {string} reviewedBy - Reviewer email
|
|
600
|
+
* @returns {Promise<Object>} Approved candidate
|
|
601
|
+
*/
|
|
602
|
+
async approveCandidate(candidateId, reviewedBy) {
|
|
603
|
+
const query = `
|
|
604
|
+
UPDATE rapport.curation_candidates
|
|
605
|
+
SET
|
|
606
|
+
status = 'approved',
|
|
607
|
+
reviewed_by = $2,
|
|
608
|
+
reviewed_at = NOW()
|
|
609
|
+
WHERE candidate_id = $1
|
|
610
|
+
RETURNING *
|
|
611
|
+
`;
|
|
612
|
+
|
|
613
|
+
const result = await executeQuery(query, [candidateId, reviewedBy]);
|
|
614
|
+
return result.rows[0];
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Reject a curation candidate
|
|
619
|
+
*
|
|
620
|
+
* @param {number} candidateId - Candidate ID
|
|
621
|
+
* @param {string} reviewedBy - Reviewer email
|
|
622
|
+
* @returns {Promise<Object>} Rejected candidate
|
|
623
|
+
*/
|
|
624
|
+
async rejectCandidate(candidateId, reviewedBy) {
|
|
625
|
+
const query = `
|
|
626
|
+
UPDATE rapport.curation_candidates
|
|
627
|
+
SET
|
|
628
|
+
status = 'rejected',
|
|
629
|
+
reviewed_by = $2,
|
|
630
|
+
reviewed_at = NOW()
|
|
631
|
+
WHERE candidate_id = $1
|
|
632
|
+
RETURNING *
|
|
633
|
+
`;
|
|
634
|
+
|
|
635
|
+
const result = await executeQuery(query, [candidateId, reviewedBy]);
|
|
636
|
+
return result.rows[0];
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* Evaluate all patterns for promotion
|
|
641
|
+
*
|
|
642
|
+
* @returns {Promise<Array>} Created candidates
|
|
643
|
+
*/
|
|
644
|
+
async evaluateAllPatterns() {
|
|
645
|
+
try {
|
|
646
|
+
// Refresh metrics first
|
|
647
|
+
await this.refreshMetrics();
|
|
648
|
+
|
|
649
|
+
// Get all patterns that meet criteria
|
|
650
|
+
const query = `
|
|
651
|
+
SELECT pattern_id
|
|
652
|
+
FROM rapport.pattern_metrics
|
|
653
|
+
WHERE success_correlation >= $1
|
|
654
|
+
AND project_count >= $2
|
|
655
|
+
AND developer_count >= $3
|
|
656
|
+
AND session_count >= $4
|
|
657
|
+
AND pattern_id NOT IN (
|
|
658
|
+
SELECT pattern_id
|
|
659
|
+
FROM rapport.curation_candidates
|
|
660
|
+
WHERE status IN ('pending', 'approved')
|
|
661
|
+
)
|
|
662
|
+
`;
|
|
663
|
+
|
|
664
|
+
const result = await executeQuery(query, [
|
|
665
|
+
this.config.minCorrelation,
|
|
666
|
+
this.config.minProjects,
|
|
667
|
+
this.config.minDevelopers,
|
|
668
|
+
this.config.minSessions
|
|
669
|
+
]);
|
|
670
|
+
|
|
671
|
+
const candidates = [];
|
|
672
|
+
for (const row of result.rows) {
|
|
673
|
+
const candidate = await this.evaluateForPromotion(row.pattern_id);
|
|
674
|
+
if (candidate) {
|
|
675
|
+
candidates.push(candidate);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
console.log(`[CurationEngine] Evaluated ${result.rows.length} patterns, created ${candidates.length} candidates`);
|
|
680
|
+
return candidates;
|
|
681
|
+
} catch (error) {
|
|
682
|
+
console.error('[CurationEngine] Error evaluating all patterns:', error);
|
|
683
|
+
throw error;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
module.exports = { CurationEngine };
|