@equilateral_ai/mindmeld 3.3.0 → 3.4.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 +1 -10
- package/hooks/pre-compact.js +213 -25
- package/hooks/session-start.js +636 -42
- package/hooks/subagent-start.js +150 -0
- package/hooks/subagent-stop.js +184 -0
- package/package.json +8 -7
- package/scripts/init-project.js +74 -33
- package/scripts/mcp-bridge.js +220 -0
- package/src/core/CorrelationAnalyzer.js +157 -0
- package/src/core/LLMPatternDetector.js +198 -0
- package/src/core/RelevanceDetector.js +123 -36
- package/src/core/StandardsIngestion.js +119 -18
- package/src/handlers/activity/activityGetMe.js +1 -1
- package/src/handlers/activity/activityGetTeam.js +100 -55
- package/src/handlers/admin/adminSetup.js +216 -0
- package/src/handlers/alerts/alertsAcknowledge.js +6 -6
- package/src/handlers/alerts/alertsGet.js +11 -11
- package/src/handlers/analytics/activitySummaryGet.js +34 -35
- package/src/handlers/analytics/coachingGet.js +11 -11
- package/src/handlers/analytics/convergenceGet.js +236 -0
- package/src/handlers/analytics/developerScoreGet.js +41 -111
- package/src/handlers/collaborators/collaboratorInvite.js +1 -1
- package/src/handlers/company/companyUsersDelete.js +141 -0
- package/src/handlers/company/companyUsersGet.js +90 -0
- package/src/handlers/company/companyUsersPost.js +267 -0
- package/src/handlers/company/companyUsersPut.js +76 -0
- package/src/handlers/correlations/correlationsDeveloperGet.js +12 -12
- package/src/handlers/correlations/correlationsGet.js +8 -8
- package/src/handlers/correlations/correlationsProjectGet.js +5 -5
- package/src/handlers/enterprise/controlTowerGet.js +224 -0
- package/src/handlers/enterprise/enterpriseOnboardingSetup.js +48 -9
- package/src/handlers/enterprise/enterpriseOnboardingStatus.js +1 -3
- package/src/handlers/github/githubConnectionStatus.js +1 -1
- package/src/handlers/github/githubDiscoverPatterns.js +4 -2
- package/src/handlers/github/githubPatternsReview.js +7 -36
- package/src/handlers/health/healthGet.js +55 -0
- package/src/handlers/helpers/checkSuperAdmin.js +13 -14
- package/src/handlers/helpers/subscriptionTiers.js +27 -27
- package/src/handlers/mcp/mcpHandler.js +569 -0
- package/src/handlers/mcp/mindmeldMcpHandler.js +689 -0
- package/src/handlers/notifications/sendNotification.js +18 -18
- package/src/handlers/patterns/patternEvaluatePromotionPost.js +173 -0
- package/src/handlers/projects/projectCreate.js +124 -10
- package/src/handlers/projects/projectDelete.js +4 -4
- package/src/handlers/projects/projectGet.js +8 -8
- package/src/handlers/projects/projectUpdate.js +4 -4
- package/src/handlers/reports/aiLeverage.js +34 -30
- package/src/handlers/reports/engineeringInvestment.js +16 -16
- package/src/handlers/reports/riskForecast.js +41 -21
- package/src/handlers/reports/standardsRoi.js +101 -9
- package/src/handlers/scheduled/maturityUpdateJob.js +166 -0
- package/src/handlers/sessions/sessionStandardsPost.js +43 -7
- package/src/handlers/standards/discoveriesGet.js +93 -0
- package/src/handlers/standards/projectStandardsGet.js +2 -2
- package/src/handlers/standards/projectStandardsPut.js +2 -2
- package/src/handlers/standards/standardsRelevantPost.js +107 -12
- package/src/handlers/standards/standardsTransition.js +112 -15
- package/src/handlers/stripe/billingPortalPost.js +1 -1
- package/src/handlers/stripe/enterpriseCheckoutPost.js +2 -2
- package/src/handlers/stripe/subscriptionCreatePost.js +2 -2
- package/src/handlers/stripe/webhookPost.js +42 -14
- package/src/handlers/user/apiTokenCreate.js +71 -0
- package/src/handlers/user/apiTokenList.js +64 -0
- package/src/handlers/user/userSplashGet.js +90 -73
- package/src/handlers/users/cognitoPostConfirmation.js +37 -1
- package/src/handlers/users/cognitoPreSignUp.js +114 -0
- package/src/handlers/users/userGet.js +12 -8
- package/src/handlers/webhooks/githubWebhook.js +117 -125
- package/src/index.js +46 -51
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discoveries Get Handler
|
|
3
|
+
* Returns discovery review queue for a project
|
|
4
|
+
*
|
|
5
|
+
* GET /api/standards/discoveries?project_id=xxx&status=proposed
|
|
6
|
+
* Auth: Cognito JWT required
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse } = require('./helpers');
|
|
10
|
+
|
|
11
|
+
async function getDiscoveries({ queryStringParameters, requestContext }) {
|
|
12
|
+
try {
|
|
13
|
+
const email = requestContext.authorizer?.claims?.email || requestContext.authorizer?.jwt?.claims?.email;
|
|
14
|
+
|
|
15
|
+
if (!email) {
|
|
16
|
+
return createErrorResponse(401, 'Authentication required');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const projectId = (queryStringParameters || {}).project_id;
|
|
20
|
+
const status = (queryStringParameters || {}).status;
|
|
21
|
+
|
|
22
|
+
if (!projectId) {
|
|
23
|
+
return createErrorResponse(400, 'project_id is required');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Verify user has access to project
|
|
27
|
+
const accessResult = await executeQuery(`
|
|
28
|
+
SELECT role FROM rapport.project_collaborators
|
|
29
|
+
WHERE project_id = $1 AND email_address = $2
|
|
30
|
+
`, [projectId, email]);
|
|
31
|
+
|
|
32
|
+
if (accessResult.rowCount === 0) {
|
|
33
|
+
return createErrorResponse(403, 'Access denied to project');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Build query with optional status filter
|
|
37
|
+
let query = `
|
|
38
|
+
SELECT
|
|
39
|
+
discovery_id,
|
|
40
|
+
project_id,
|
|
41
|
+
pattern_name,
|
|
42
|
+
pattern_description,
|
|
43
|
+
confidence,
|
|
44
|
+
discovery_type,
|
|
45
|
+
status,
|
|
46
|
+
evidence,
|
|
47
|
+
reason,
|
|
48
|
+
reason_code,
|
|
49
|
+
created_at,
|
|
50
|
+
updated_at
|
|
51
|
+
FROM rapport.onboarding_discoveries
|
|
52
|
+
WHERE project_id = $1
|
|
53
|
+
`;
|
|
54
|
+
const params = [projectId];
|
|
55
|
+
|
|
56
|
+
if (status) {
|
|
57
|
+
query += ` AND status = $2`;
|
|
58
|
+
params.push(status);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
query += ` ORDER BY confidence DESC, created_at DESC`;
|
|
62
|
+
|
|
63
|
+
const result = await executeQuery(query, params);
|
|
64
|
+
|
|
65
|
+
// Map DB columns to frontend Discovery interface
|
|
66
|
+
const records = result.rows.map(row => ({
|
|
67
|
+
id: row.discovery_id,
|
|
68
|
+
project_id: row.project_id,
|
|
69
|
+
name: row.pattern_name,
|
|
70
|
+
description: row.pattern_description || '',
|
|
71
|
+
confidence: parseFloat(row.confidence) || 0,
|
|
72
|
+
source: 'github',
|
|
73
|
+
category: row.discovery_type || 'Architecture',
|
|
74
|
+
status: row.status || 'proposed',
|
|
75
|
+
rule_text: null,
|
|
76
|
+
action_type: null,
|
|
77
|
+
evidence: row.evidence || null,
|
|
78
|
+
created_at: row.created_at,
|
|
79
|
+
updated_at: row.updated_at
|
|
80
|
+
}));
|
|
81
|
+
|
|
82
|
+
return createSuccessResponse({
|
|
83
|
+
records,
|
|
84
|
+
total: result.rowCount
|
|
85
|
+
}, `Found ${result.rowCount} discoveries`);
|
|
86
|
+
|
|
87
|
+
} catch (error) {
|
|
88
|
+
console.error('Discoveries Get Error:', error);
|
|
89
|
+
return createErrorResponse(500, 'Failed to load discoveries');
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
exports.handler = wrapHandler(getDiscoveries);
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse } = require('./helpers');
|
|
10
10
|
|
|
11
|
-
async function getProjectStandards({ queryStringParameters, requestContext }) {
|
|
11
|
+
async function getProjectStandards({ queryStringParameters, pathParameters, requestContext }) {
|
|
12
12
|
try {
|
|
13
13
|
const email = requestContext.authorizer?.claims?.email || requestContext.authorizer?.jwt?.claims?.email;
|
|
14
14
|
|
|
@@ -16,7 +16,7 @@ async function getProjectStandards({ queryStringParameters, requestContext }) {
|
|
|
16
16
|
return createErrorResponse(401, 'Authentication required');
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
const projectId = (queryStringParameters || {}).project_id;
|
|
19
|
+
const projectId = pathParameters?.projectId || (queryStringParameters || {}).project_id;
|
|
20
20
|
|
|
21
21
|
if (!projectId) {
|
|
22
22
|
return createErrorResponse(400, 'projectId is required');
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse } = require('./helpers');
|
|
11
11
|
|
|
12
|
-
async function updateProjectStandards({ body, requestContext }) {
|
|
12
|
+
async function updateProjectStandards({ body, pathParameters, requestContext }) {
|
|
13
13
|
try {
|
|
14
14
|
const email = requestContext.authorizer?.claims?.email || requestContext.authorizer?.jwt?.claims?.email;
|
|
15
15
|
|
|
@@ -17,7 +17,7 @@ async function updateProjectStandards({ body, requestContext }) {
|
|
|
17
17
|
return createErrorResponse(401, 'Authentication required');
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
const projectId = (body || {}).project_id;
|
|
20
|
+
const projectId = pathParameters?.projectId || (body || {}).project_id;
|
|
21
21
|
|
|
22
22
|
if (!projectId) {
|
|
23
23
|
return createErrorResponse(400, 'projectId is required');
|
|
@@ -18,15 +18,16 @@ const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse }
|
|
|
18
18
|
*/
|
|
19
19
|
const CATEGORY_WEIGHTS = {
|
|
20
20
|
'serverless-saas-aws': 1.0,
|
|
21
|
-
'multi-agent-orchestration': 1.0,
|
|
22
21
|
'frontend-development': 1.0,
|
|
23
22
|
'database': 0.9,
|
|
24
|
-
'compliance-security': 0.8,
|
|
25
|
-
'cost-optimization': 0.7,
|
|
26
|
-
'real-time-systems': 0.8,
|
|
27
|
-
'testing': 0.6,
|
|
28
23
|
'backend': 0.9,
|
|
29
|
-
'
|
|
24
|
+
'compliance-security': 0.9,
|
|
25
|
+
'deployment': 0.8,
|
|
26
|
+
'testing': 0.7,
|
|
27
|
+
'real-time-systems': 0.7,
|
|
28
|
+
'well-architected': 0.7,
|
|
29
|
+
'cost-optimization': 0.7,
|
|
30
|
+
'multi-agent-orchestration': 0.1, // Infrastructure config, rarely needed in coding sessions
|
|
30
31
|
};
|
|
31
32
|
|
|
32
33
|
/**
|
|
@@ -54,6 +55,9 @@ function mapCharacteristicsToCategories(characteristics) {
|
|
|
54
55
|
if (characteristics.hasTests) {
|
|
55
56
|
categories.add('testing');
|
|
56
57
|
}
|
|
58
|
+
if (characteristics.hasSAM || characteristics.hasLambda || characteristics.hasAPI) {
|
|
59
|
+
categories.add('deployment');
|
|
60
|
+
}
|
|
57
61
|
|
|
58
62
|
// Always relevant
|
|
59
63
|
categories.add('compliance-security');
|
|
@@ -65,7 +69,7 @@ function mapCharacteristicsToCategories(characteristics) {
|
|
|
65
69
|
/**
|
|
66
70
|
* Rank standards by relevance score
|
|
67
71
|
*/
|
|
68
|
-
function rankStandards(standards) {
|
|
72
|
+
function rankStandards(standards, recentCategories) {
|
|
69
73
|
return standards.map(standard => {
|
|
70
74
|
let score = 0;
|
|
71
75
|
|
|
@@ -98,6 +102,23 @@ function rankStandards(standards) {
|
|
|
98
102
|
if (apCount > 0) score += 5;
|
|
99
103
|
}
|
|
100
104
|
|
|
105
|
+
// Workflow bonus — workflows are high-value procedural knowledge
|
|
106
|
+
const isWorkflow = (standard.rule && standard.rule.startsWith('WORKFLOW:'))
|
|
107
|
+
|| (Array.isArray(standard.keywords) && standard.keywords.includes('workflow'));
|
|
108
|
+
if (isWorkflow) score += 10;
|
|
109
|
+
|
|
110
|
+
// Recency bonus — boost categories the user has been working in recently
|
|
111
|
+
// Scaled by category weight to prevent feedback loops (low-weight categories
|
|
112
|
+
// that got injected shouldn't bootstrap themselves back into the top 10)
|
|
113
|
+
if (recentCategories && recentCategories[standard.category]) {
|
|
114
|
+
const usageCount = recentCategories[standard.category];
|
|
115
|
+
let rawBonus;
|
|
116
|
+
if (usageCount >= 8) rawBonus = 25;
|
|
117
|
+
else if (usageCount >= 4) rawBonus = 18;
|
|
118
|
+
else rawBonus = 10;
|
|
119
|
+
score += rawBonus * categoryWeight;
|
|
120
|
+
}
|
|
121
|
+
|
|
101
122
|
return {
|
|
102
123
|
...standard,
|
|
103
124
|
relevance_score: Math.round(score * 10) / 10
|
|
@@ -141,6 +162,25 @@ async function getRelevantStandards({ body, requestContext }) {
|
|
|
141
162
|
return createErrorResponse(401, 'Authentication required');
|
|
142
163
|
}
|
|
143
164
|
|
|
165
|
+
// Verify active subscription — no free tier access
|
|
166
|
+
const subResult = await executeQuery(`
|
|
167
|
+
SELECT c.subscription_tier, c.subscription_status
|
|
168
|
+
FROM rapport.users u
|
|
169
|
+
JOIN rapport.clients c ON u.client_id = c.client_id
|
|
170
|
+
WHERE u.email_address = $1
|
|
171
|
+
LIMIT 1
|
|
172
|
+
`, [email]);
|
|
173
|
+
|
|
174
|
+
if (subResult.rows.length > 0) {
|
|
175
|
+
const { subscription_tier, subscription_status } = subResult.rows[0];
|
|
176
|
+
if (!subscription_tier || subscription_tier === 'free') {
|
|
177
|
+
return createErrorResponse(403, 'Active MindMeld subscription required. Subscribe at app.mindmeld.dev');
|
|
178
|
+
}
|
|
179
|
+
if (subscription_status === 'canceled') {
|
|
180
|
+
return createErrorResponse(403, 'Subscription canceled. Resubscribe at app.mindmeld.dev');
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
144
184
|
// Parse request body
|
|
145
185
|
let requestBody;
|
|
146
186
|
try {
|
|
@@ -166,13 +206,43 @@ async function getRelevantStandards({ body, requestContext }) {
|
|
|
166
206
|
}, 'No relevant categories detected');
|
|
167
207
|
}
|
|
168
208
|
|
|
169
|
-
// Query
|
|
209
|
+
// Query recent session categories first (fast), then merge into categories for main query
|
|
210
|
+
const recentCategories = {};
|
|
211
|
+
try {
|
|
212
|
+
const recencyResult = await executeQuery(`
|
|
213
|
+
SELECT sp.category, COUNT(*) as usage_count
|
|
214
|
+
FROM rapport.session_standards ss
|
|
215
|
+
JOIN rapport.sessions s ON s.session_id = ss.session_id
|
|
216
|
+
JOIN rapport.standards_patterns sp ON sp.pattern_id = ss.standard_id
|
|
217
|
+
WHERE s.email_address = $1
|
|
218
|
+
AND s.started_at >= NOW() - INTERVAL '7 days'
|
|
219
|
+
GROUP BY sp.category
|
|
220
|
+
ORDER BY usage_count DESC
|
|
221
|
+
LIMIT 5
|
|
222
|
+
`, [email]);
|
|
223
|
+
for (const row of recencyResult.rows) {
|
|
224
|
+
recentCategories[row.category] = parseInt(row.usage_count, 10);
|
|
225
|
+
}
|
|
226
|
+
} catch (err) {
|
|
227
|
+
console.error('[standardsRelevant] Recency query failed:', err.message);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Merge recency categories into query — recent activity should always be represented
|
|
231
|
+
for (const category of Object.keys(recentCategories)) {
|
|
232
|
+
if (!categories.includes(category)) {
|
|
233
|
+
categories.push(category);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Query standards from all relevant categories (static + recency)
|
|
170
238
|
const result = await executeQuery(`
|
|
171
239
|
SELECT
|
|
172
240
|
pattern_id,
|
|
173
241
|
element,
|
|
242
|
+
title,
|
|
174
243
|
rule,
|
|
175
244
|
category,
|
|
245
|
+
keywords,
|
|
176
246
|
correlation,
|
|
177
247
|
maturity,
|
|
178
248
|
applicable_files,
|
|
@@ -192,16 +262,41 @@ async function getRelevantStandards({ body, requestContext }) {
|
|
|
192
262
|
correlation DESC
|
|
193
263
|
`, [categories]);
|
|
194
264
|
|
|
195
|
-
// Rank by relevance
|
|
196
|
-
let ranked = rankStandards(result.rows);
|
|
265
|
+
// Rank by relevance with recency boost
|
|
266
|
+
let ranked = rankStandards(result.rows, recentCategories);
|
|
267
|
+
|
|
268
|
+
// Deduplicate by element name (same rule can be ingested with different path prefixes)
|
|
269
|
+
const seenElements = new Set();
|
|
270
|
+
ranked = ranked.filter(standard => {
|
|
271
|
+
const key = standard.element;
|
|
272
|
+
if (seenElements.has(key)) return false;
|
|
273
|
+
seenElements.add(key);
|
|
274
|
+
return true;
|
|
275
|
+
});
|
|
197
276
|
|
|
198
277
|
// Apply preferences if provided
|
|
199
278
|
if (preferences) {
|
|
200
279
|
ranked = applyPreferences(ranked, preferences);
|
|
201
280
|
}
|
|
202
281
|
|
|
203
|
-
// Return top 10
|
|
204
|
-
|
|
282
|
+
// Return top 10 with diversity caps:
|
|
283
|
+
// - Max 2 per category (prevents single-category saturation)
|
|
284
|
+
// - Max 1 per standard title (prevents same-file rule pairs wasting slots)
|
|
285
|
+
const MAX_PER_CATEGORY = 2;
|
|
286
|
+
const MAX_PER_TITLE = 1;
|
|
287
|
+
const top = [];
|
|
288
|
+
const categoryCounts = {};
|
|
289
|
+
const titleCounts = {};
|
|
290
|
+
for (const standard of ranked) {
|
|
291
|
+
const cat = standard.category;
|
|
292
|
+
const title = standard.title || standard.element;
|
|
293
|
+
categoryCounts[cat] = (categoryCounts[cat] || 0) + 1;
|
|
294
|
+
titleCounts[title] = (titleCounts[title] || 0) + 1;
|
|
295
|
+
if (categoryCounts[cat] <= MAX_PER_CATEGORY && titleCounts[title] <= MAX_PER_TITLE) {
|
|
296
|
+
top.push(standard);
|
|
297
|
+
if (top.length >= 10) break;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
205
300
|
|
|
206
301
|
return createSuccessResponse({
|
|
207
302
|
standards: top,
|
|
@@ -1,17 +1,95 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Standards Transition Handler
|
|
3
|
-
* Executes lifecycle state transitions on standards
|
|
3
|
+
* Executes lifecycle state transitions on standards and discoveries
|
|
4
4
|
*
|
|
5
5
|
* POST /api/standards/transition
|
|
6
|
-
* Body: { standard_id, action: 'approve'|'disable'|'deprecate'|'delete'|'enable'|'cancel_deprecation', reason? }
|
|
6
|
+
* Body: { standard_id, action: 'approve'|'reject'|'observe'|'disable'|'deprecate'|'delete'|'enable'|'cancel_deprecation', reason?, reason_code? }
|
|
7
7
|
* Auth: Cognito JWT required
|
|
8
|
+
*
|
|
9
|
+
* For discovery IDs (from onboarding_discoveries table):
|
|
10
|
+
* approve → creates pattern + marks discovery approved
|
|
11
|
+
* reject → marks discovery rejected
|
|
12
|
+
* observe → marks discovery as observing (deferred review)
|
|
8
13
|
*/
|
|
9
14
|
|
|
10
|
-
const { wrapHandler, createSuccessResponse, createErrorResponse } = require('./helpers');
|
|
15
|
+
const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse } = require('./helpers');
|
|
11
16
|
const { StandardLifecycle } = require('./core/StandardLifecycle');
|
|
12
17
|
|
|
13
18
|
const lifecycle = new StandardLifecycle();
|
|
14
19
|
|
|
20
|
+
const DISCOVERY_ACTIONS = ['approve', 'reject', 'observe'];
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Handle transitions for onboarding discoveries
|
|
24
|
+
*/
|
|
25
|
+
async function transitionDiscovery(discoveryId, action, email, reason, reasonCode) {
|
|
26
|
+
const discovery = await executeQuery(`
|
|
27
|
+
SELECT discovery_id, project_id, discovery_type, pattern_name, pattern_description, confidence, evidence, status
|
|
28
|
+
FROM rapport.onboarding_discoveries
|
|
29
|
+
WHERE discovery_id = $1
|
|
30
|
+
`, [discoveryId]);
|
|
31
|
+
|
|
32
|
+
if (discovery.rowCount === 0) {
|
|
33
|
+
return null; // Not a discovery either
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const row = discovery.rows[0];
|
|
37
|
+
const oldStatus = row.status;
|
|
38
|
+
|
|
39
|
+
if (action === 'approve') {
|
|
40
|
+
// Create pattern from discovery
|
|
41
|
+
const patternId = `pat_${row.project_id}_${Date.now()}`;
|
|
42
|
+
await executeQuery(`
|
|
43
|
+
INSERT INTO rapport.patterns (
|
|
44
|
+
pattern_id, project_id, intent, constraints, outcome_criteria,
|
|
45
|
+
maturity, discovered_by, pattern_data
|
|
46
|
+
) VALUES ($1, $2, $3, $4, $5, 'provisional', $6, $7)
|
|
47
|
+
`, [
|
|
48
|
+
patternId,
|
|
49
|
+
row.project_id,
|
|
50
|
+
row.pattern_name,
|
|
51
|
+
JSON.stringify([row.pattern_description || '']),
|
|
52
|
+
JSON.stringify([`Discovered via onboarding: ${row.discovery_type}`]),
|
|
53
|
+
email,
|
|
54
|
+
JSON.stringify({
|
|
55
|
+
source: 'discovery_review',
|
|
56
|
+
discovery_type: row.discovery_type,
|
|
57
|
+
evidence: row.evidence
|
|
58
|
+
})
|
|
59
|
+
]);
|
|
60
|
+
|
|
61
|
+
await executeQuery(`
|
|
62
|
+
UPDATE rapport.onboarding_discoveries
|
|
63
|
+
SET status = 'approved', reviewed_at = NOW(), updated_at = NOW(), pattern_id = $1
|
|
64
|
+
WHERE discovery_id = $2
|
|
65
|
+
`, [patternId, discoveryId]);
|
|
66
|
+
|
|
67
|
+
return { old_state: oldStatus, new_state: 'approved', pattern_id: patternId };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (action === 'reject') {
|
|
71
|
+
await executeQuery(`
|
|
72
|
+
UPDATE rapport.onboarding_discoveries
|
|
73
|
+
SET status = 'rejected', reviewed_at = NOW(), updated_at = NOW(), reason = $1, reason_code = $2
|
|
74
|
+
WHERE discovery_id = $3
|
|
75
|
+
`, [reason || null, reasonCode || null, discoveryId]);
|
|
76
|
+
|
|
77
|
+
return { old_state: oldStatus, new_state: 'rejected' };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (action === 'observe') {
|
|
81
|
+
await executeQuery(`
|
|
82
|
+
UPDATE rapport.onboarding_discoveries
|
|
83
|
+
SET status = 'observing', updated_at = NOW()
|
|
84
|
+
WHERE discovery_id = $1
|
|
85
|
+
`, [discoveryId]);
|
|
86
|
+
|
|
87
|
+
return { old_state: oldStatus, new_state: 'observing' };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
15
93
|
async function transitionStandard({ body, requestContext }) {
|
|
16
94
|
try {
|
|
17
95
|
const email = requestContext.authorizer?.claims?.email || requestContext.authorizer?.jwt?.claims?.email;
|
|
@@ -20,7 +98,7 @@ async function transitionStandard({ body, requestContext }) {
|
|
|
20
98
|
return createErrorResponse(401, 'Authentication required');
|
|
21
99
|
}
|
|
22
100
|
|
|
23
|
-
const { standard_id: id, action, reason } = body || {};
|
|
101
|
+
const { standard_id: id, action, reason, reason_code: reasonCode } = body || {};
|
|
24
102
|
|
|
25
103
|
if (!id) {
|
|
26
104
|
return createErrorResponse(400, 'standard_id is required');
|
|
@@ -28,25 +106,44 @@ async function transitionStandard({ body, requestContext }) {
|
|
|
28
106
|
|
|
29
107
|
if (!action) {
|
|
30
108
|
return createErrorResponse(400, 'action is required', {
|
|
31
|
-
valid_actions: ['propose', 'approve', 'reject', 'disable', 'deprecate', 'delete', 'enable', 'cancel_deprecation']
|
|
109
|
+
valid_actions: ['propose', 'approve', 'reject', 'observe', 'disable', 'deprecate', 'delete', 'enable', 'cancel_deprecation']
|
|
32
110
|
});
|
|
33
111
|
}
|
|
34
112
|
|
|
35
|
-
//
|
|
36
|
-
|
|
113
|
+
// Try standard lifecycle transition first
|
|
114
|
+
try {
|
|
115
|
+
const result = await lifecycle.transition(id, action, email, reason);
|
|
116
|
+
|
|
117
|
+
return createSuccessResponse({
|
|
118
|
+
standard_id: result.standard_id,
|
|
119
|
+
old_state: result.old_state,
|
|
120
|
+
new_state: result.new_state,
|
|
121
|
+
action: result.action,
|
|
122
|
+
audit_entry: result.audit_entry
|
|
123
|
+
}, `Standard transitioned from '${result.old_state}' to '${result.new_state}'`);
|
|
124
|
+
} catch (lifecycleError) {
|
|
125
|
+
// If pattern not found and action is valid for discoveries, try discovery transition
|
|
126
|
+
if (lifecycleError.message.includes('not found') && DISCOVERY_ACTIONS.includes(action)) {
|
|
127
|
+
const discoveryResult = await transitionDiscovery(id, action, email, reason, reasonCode);
|
|
37
128
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
129
|
+
if (discoveryResult) {
|
|
130
|
+
return createSuccessResponse({
|
|
131
|
+
standard_id: id,
|
|
132
|
+
old_state: discoveryResult.old_state,
|
|
133
|
+
new_state: discoveryResult.new_state,
|
|
134
|
+
action,
|
|
135
|
+
pattern_id: discoveryResult.pattern_id || null
|
|
136
|
+
}, `Discovery transitioned from '${discoveryResult.old_state}' to '${discoveryResult.new_state}'`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Re-throw if not handled
|
|
141
|
+
throw lifecycleError;
|
|
142
|
+
}
|
|
45
143
|
|
|
46
144
|
} catch (error) {
|
|
47
145
|
console.error('Standards Transition Error:', error);
|
|
48
146
|
|
|
49
|
-
// Return user-friendly messages for known validation errors
|
|
50
147
|
if (error.message.includes('Invalid action')) {
|
|
51
148
|
return createErrorResponse(400, error.message);
|
|
52
149
|
}
|
|
@@ -67,7 +67,7 @@ async function createBillingPortal({ body: requestBody = {}, requestContext }) {
|
|
|
67
67
|
// Create billing portal session
|
|
68
68
|
const session = await stripe.billingPortal.sessions.create({
|
|
69
69
|
customer: client.stripe_customer_id,
|
|
70
|
-
return_url: returnUrl || `${process.env.APP_URL || 'https://mindmeld.dev'}/settings/billing`
|
|
70
|
+
return_url: returnUrl || `${process.env.APP_URL || 'https://app.mindmeld.dev'}/settings/billing`
|
|
71
71
|
});
|
|
72
72
|
|
|
73
73
|
return createSuccessResponse(
|
|
@@ -159,8 +159,8 @@ async function createEnterpriseCheckout({ body: requestBody = {}, requestContext
|
|
|
159
159
|
payment_method_types: ['card'],
|
|
160
160
|
customer_email: email,
|
|
161
161
|
line_items: lineItems,
|
|
162
|
-
success_url: `${process.env.APP_URL || 'https://mindmeld.dev'}/enterprise/success?session_id={CHECKOUT_SESSION_ID}`,
|
|
163
|
-
cancel_url: `${process.env.APP_URL || 'https://mindmeld.dev'}/enterprise/cancel`,
|
|
162
|
+
success_url: `${process.env.APP_URL || 'https://app.mindmeld.dev'}/enterprise/success?session_id={CHECKOUT_SESSION_ID}`,
|
|
163
|
+
cancel_url: `${process.env.APP_URL || 'https://app.mindmeld.dev'}/enterprise/cancel`,
|
|
164
164
|
metadata: {
|
|
165
165
|
client_id: user.client_id,
|
|
166
166
|
user_email: email,
|
|
@@ -118,8 +118,8 @@ async function createSubscription({ body: requestBody = {}, requestContext }) {
|
|
|
118
118
|
price: priceId,
|
|
119
119
|
quantity: tierConfig.perUser ? userCount : 1
|
|
120
120
|
}],
|
|
121
|
-
success_url: `${process.env.APP_URL || 'https://mindmeld.dev'}/
|
|
122
|
-
cancel_url: `${process.env.APP_URL || 'https://mindmeld.dev'}/
|
|
121
|
+
success_url: `${process.env.APP_URL || 'https://app.mindmeld.dev'}/dashboard?checkout=success`,
|
|
122
|
+
cancel_url: `${process.env.APP_URL || 'https://app.mindmeld.dev'}/signup`,
|
|
123
123
|
metadata: {
|
|
124
124
|
client_id: user.client_id,
|
|
125
125
|
user_email: email,
|
|
@@ -72,18 +72,22 @@ async function handleCheckoutCompleted(session) {
|
|
|
72
72
|
const { client_id, tier, user_email, enterprise_package, seat_count, addons } = session.metadata || {};
|
|
73
73
|
|
|
74
74
|
if (!client_id) {
|
|
75
|
-
console.
|
|
76
|
-
|
|
75
|
+
console.error('[webhookPost] CRITICAL: Missing client_id in checkout session metadata — subscription will not activate');
|
|
76
|
+
throw new Error('Missing client_id in checkout session metadata');
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
// Parse enterprise metadata
|
|
80
80
|
const isEnterprise = tier === 'enterprise';
|
|
81
81
|
const seatCountNum = seat_count ? parseInt(seat_count, 10) : 1;
|
|
82
|
-
|
|
82
|
+
let addonsList = [];
|
|
83
|
+
if (addons) {
|
|
84
|
+
try { addonsList = JSON.parse(addons); }
|
|
85
|
+
catch { console.warn('Failed to parse addons metadata:', addons); }
|
|
86
|
+
}
|
|
83
87
|
|
|
84
88
|
// Update client with subscription
|
|
85
89
|
if (isEnterprise) {
|
|
86
|
-
await executeQuery(`
|
|
90
|
+
const updateResult = await executeQuery(`
|
|
87
91
|
UPDATE rapport.clients
|
|
88
92
|
SET stripe_customer_id = $2,
|
|
89
93
|
stripe_subscription_id = $3,
|
|
@@ -96,6 +100,11 @@ async function handleCheckoutCompleted(session) {
|
|
|
96
100
|
WHERE client_id = $1
|
|
97
101
|
`, [client_id, session.customer, session.subscription, enterprise_package, seatCountNum, JSON.stringify(addonsList)]);
|
|
98
102
|
|
|
103
|
+
if (updateResult.rowCount === 0) {
|
|
104
|
+
console.error(`[webhookPost] CRITICAL: Client ${client_id} not found in DB — subscription paid but not activated`);
|
|
105
|
+
throw new Error(`Client ${client_id} not found — subscription activation failed`);
|
|
106
|
+
}
|
|
107
|
+
|
|
99
108
|
// Create addon entitlements if any
|
|
100
109
|
if (addonsList.length > 0) {
|
|
101
110
|
for (const addonId of addonsList) {
|
|
@@ -112,7 +121,7 @@ async function handleCheckoutCompleted(session) {
|
|
|
112
121
|
|
|
113
122
|
console.log('Enterprise checkout completed:', { client_id, enterprise_package, seat_count: seatCountNum, addons: addonsList });
|
|
114
123
|
} else {
|
|
115
|
-
await executeQuery(`
|
|
124
|
+
const updateResult = await executeQuery(`
|
|
116
125
|
UPDATE rapport.clients
|
|
117
126
|
SET stripe_customer_id = $2,
|
|
118
127
|
stripe_subscription_id = $3,
|
|
@@ -122,6 +131,11 @@ async function handleCheckoutCompleted(session) {
|
|
|
122
131
|
WHERE client_id = $1
|
|
123
132
|
`, [client_id, session.customer, session.subscription, tier || 'team']);
|
|
124
133
|
|
|
134
|
+
if (updateResult.rowCount === 0) {
|
|
135
|
+
console.error(`[webhookPost] CRITICAL: Client ${client_id} not found in DB — subscription paid but not activated`);
|
|
136
|
+
throw new Error(`Client ${client_id} not found — subscription activation failed`);
|
|
137
|
+
}
|
|
138
|
+
|
|
125
139
|
console.log('Checkout completed:', { client_id, tier, customer: session.customer });
|
|
126
140
|
}
|
|
127
141
|
|
|
@@ -160,7 +174,11 @@ async function handleSubscriptionUpdated(subscription) {
|
|
|
160
174
|
// Parse enterprise metadata
|
|
161
175
|
const isEnterprise = tier === 'enterprise';
|
|
162
176
|
const seatCountNum = seat_count ? parseInt(seat_count, 10) : null;
|
|
163
|
-
|
|
177
|
+
let addonsList = null;
|
|
178
|
+
if (addons) {
|
|
179
|
+
try { addonsList = JSON.parse(addons); }
|
|
180
|
+
catch { console.warn('Failed to parse addons metadata in subscription update:', addons); }
|
|
181
|
+
}
|
|
164
182
|
|
|
165
183
|
// Build update query based on what changed
|
|
166
184
|
if (isEnterprise) {
|
|
@@ -424,15 +442,25 @@ async function handler(event, context) {
|
|
|
424
442
|
console.error('Error processing webhook:', processError);
|
|
425
443
|
|
|
426
444
|
// Record error
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
445
|
+
try {
|
|
446
|
+
await executeQuery(`
|
|
447
|
+
UPDATE rapport.stripe_webhook_events
|
|
448
|
+
SET handled = false,
|
|
449
|
+
error = $2,
|
|
450
|
+
processed_at = CURRENT_TIMESTAMP
|
|
451
|
+
WHERE event_id = $1
|
|
452
|
+
`, [stripeEvent.id, processError.message]);
|
|
453
|
+
} catch (recordErr) {
|
|
454
|
+
console.error('Failed to record webhook error:', recordErr.message);
|
|
455
|
+
}
|
|
434
456
|
|
|
435
|
-
//
|
|
457
|
+
// Return 500 for critical failures so Stripe retries
|
|
458
|
+
// (missing client, DB connection errors, subscription not activated)
|
|
459
|
+
return {
|
|
460
|
+
statusCode: 500,
|
|
461
|
+
headers: { 'Content-Type': 'application/json' },
|
|
462
|
+
body: JSON.stringify({ error: processError.message })
|
|
463
|
+
};
|
|
436
464
|
}
|
|
437
465
|
|
|
438
466
|
return {
|