@equilateral_ai/mindmeld 3.3.1 → 3.5.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-end.js +112 -3
- package/hooks/session-start.js +635 -41
- 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/mindmeldMcpCore.js +594 -0
- package/src/handlers/helpers/subscriptionTiers.js +27 -27
- package/src/handlers/mcp/mcpHandler.js +569 -0
- package/src/handlers/mcp/mindmeldMcpHandler.js +124 -0
- package/src/handlers/mcp/mindmeldMcpStreamHandler.js +243 -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 +15 -11
- package/src/handlers/webhooks/githubWebhook.js +117 -125
- package/src/index.js +8 -5
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maturity Update Scheduled Job
|
|
3
|
+
* Auto-progresses/demotes standards maturity based on 30-day compliance evidence
|
|
4
|
+
*
|
|
5
|
+
* Schedule: Daily
|
|
6
|
+
* Auth: None (Lambda scheduled event)
|
|
7
|
+
*
|
|
8
|
+
* Maturity progression rules (based on session_standards compliance data):
|
|
9
|
+
* - >90% followed + >50 sessions → enforced
|
|
10
|
+
* - >70% followed + >20 sessions → validated
|
|
11
|
+
* - <30% followed + >20 sessions → demote to provisional
|
|
12
|
+
*
|
|
13
|
+
* Records all transitions in standards_audit_trail for traceability
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const { wrapHandler, executeQuery, createSuccessResponse } = require('./helpers');
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Main handler
|
|
20
|
+
*/
|
|
21
|
+
exports.handler = wrapHandler(async (event, context) => {
|
|
22
|
+
console.log('[MaturityUpdateJob] Starting maturity auto-progression...');
|
|
23
|
+
|
|
24
|
+
const lookbackDays = (event && event.lookbackDays) || 30;
|
|
25
|
+
const dryRun = (event && event.dryRun) || false;
|
|
26
|
+
|
|
27
|
+
const summary = {
|
|
28
|
+
startedAt: new Date().toISOString(),
|
|
29
|
+
standardsEvaluated: 0,
|
|
30
|
+
promotions: 0,
|
|
31
|
+
demotions: 0,
|
|
32
|
+
unchanged: 0,
|
|
33
|
+
transitions: [],
|
|
34
|
+
errors: []
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
// Query 30-day compliance rates per standard from session_standards
|
|
39
|
+
const complianceQuery = `
|
|
40
|
+
SELECT
|
|
41
|
+
ss.standard_id,
|
|
42
|
+
ss.standard_name,
|
|
43
|
+
COUNT(*) as total_sessions,
|
|
44
|
+
COUNT(*) FILTER (WHERE ss.followed = true) as followed_count,
|
|
45
|
+
COUNT(*) FILTER (WHERE ss.violated = true) as violated_count,
|
|
46
|
+
ROUND(
|
|
47
|
+
COUNT(*) FILTER (WHERE ss.followed = true)::decimal /
|
|
48
|
+
NULLIF(COUNT(*), 0),
|
|
49
|
+
3
|
|
50
|
+
) as follow_rate,
|
|
51
|
+
sp.maturity as current_maturity
|
|
52
|
+
FROM rapport.session_standards ss
|
|
53
|
+
LEFT JOIN rapport.standards_patterns sp ON sp.pattern_id = ss.standard_id
|
|
54
|
+
WHERE ss.shown_at > NOW() - INTERVAL '${lookbackDays} days'
|
|
55
|
+
GROUP BY ss.standard_id, ss.standard_name, sp.maturity
|
|
56
|
+
HAVING COUNT(*) >= 10
|
|
57
|
+
ORDER BY total_sessions DESC
|
|
58
|
+
`;
|
|
59
|
+
|
|
60
|
+
const complianceResult = await executeQuery(complianceQuery);
|
|
61
|
+
const standards = complianceResult.rows;
|
|
62
|
+
|
|
63
|
+
summary.standardsEvaluated = standards.length;
|
|
64
|
+
console.log(`[MaturityUpdateJob] Evaluating ${standards.length} standards with sufficient data`);
|
|
65
|
+
|
|
66
|
+
for (const standard of standards) {
|
|
67
|
+
const totalSessions = parseInt(standard.total_sessions);
|
|
68
|
+
const followRate = parseFloat(standard.follow_rate) || 0;
|
|
69
|
+
const currentMaturity = standard.current_maturity || 'provisional';
|
|
70
|
+
|
|
71
|
+
// Determine target maturity
|
|
72
|
+
let targetMaturity = currentMaturity;
|
|
73
|
+
|
|
74
|
+
if (followRate > 0.90 && totalSessions >= 50) {
|
|
75
|
+
targetMaturity = 'enforced';
|
|
76
|
+
} else if (followRate > 0.70 && totalSessions >= 20) {
|
|
77
|
+
targetMaturity = 'validated';
|
|
78
|
+
} else if (followRate < 0.30 && totalSessions >= 20) {
|
|
79
|
+
targetMaturity = 'provisional';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (targetMaturity === currentMaturity) {
|
|
83
|
+
summary.unchanged++;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Determine if this is a promotion or demotion
|
|
88
|
+
const maturityOrder = { provisional: 0, validated: 1, reinforced: 2, enforced: 3 };
|
|
89
|
+
const isPromotion = (maturityOrder[targetMaturity] || 0) > (maturityOrder[currentMaturity] || 0);
|
|
90
|
+
|
|
91
|
+
const transition = {
|
|
92
|
+
standard_id: standard.standard_id,
|
|
93
|
+
standard_name: standard.standard_name,
|
|
94
|
+
from: currentMaturity,
|
|
95
|
+
to: targetMaturity,
|
|
96
|
+
direction: isPromotion ? 'promotion' : 'demotion',
|
|
97
|
+
follow_rate: followRate,
|
|
98
|
+
total_sessions: totalSessions
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
summary.transitions.push(transition);
|
|
102
|
+
|
|
103
|
+
if (dryRun) {
|
|
104
|
+
console.log(`[MaturityUpdateJob] DRY RUN: Would ${transition.direction} ${standard.standard_id}: ${currentMaturity} → ${targetMaturity} (${(followRate * 100).toFixed(1)}% followed, ${totalSessions} sessions)`);
|
|
105
|
+
if (isPromotion) summary.promotions++;
|
|
106
|
+
else summary.demotions++;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
// Update standards_patterns maturity
|
|
112
|
+
await executeQuery(`
|
|
113
|
+
UPDATE rapport.standards_patterns
|
|
114
|
+
SET maturity = $1, last_updated = NOW()
|
|
115
|
+
WHERE pattern_id = $2
|
|
116
|
+
`, [targetMaturity, standard.standard_id]);
|
|
117
|
+
|
|
118
|
+
// Record in audit trail
|
|
119
|
+
await executeQuery(`
|
|
120
|
+
INSERT INTO rapport.standards_audit_trail (
|
|
121
|
+
standard_id, action, old_state, new_state,
|
|
122
|
+
user_email, reason, metadata, created_at
|
|
123
|
+
) VALUES ($1, $2, $3, $4, $5, $6, $7, NOW())
|
|
124
|
+
`, [
|
|
125
|
+
standard.standard_id,
|
|
126
|
+
isPromotion ? 'auto_promote' : 'auto_demote',
|
|
127
|
+
currentMaturity,
|
|
128
|
+
targetMaturity,
|
|
129
|
+
'system@mindmeld.dev',
|
|
130
|
+
`Auto-${transition.direction}: ${(followRate * 100).toFixed(1)}% follow rate across ${totalSessions} sessions (30-day window)`,
|
|
131
|
+
JSON.stringify({
|
|
132
|
+
follow_rate: followRate,
|
|
133
|
+
total_sessions: totalSessions,
|
|
134
|
+
followed_count: parseInt(standard.followed_count),
|
|
135
|
+
violated_count: parseInt(standard.violated_count),
|
|
136
|
+
lookback_days: lookbackDays
|
|
137
|
+
})
|
|
138
|
+
]);
|
|
139
|
+
|
|
140
|
+
if (isPromotion) summary.promotions++;
|
|
141
|
+
else summary.demotions++;
|
|
142
|
+
|
|
143
|
+
console.log(`[MaturityUpdateJob] ${transition.direction}: ${standard.standard_id} ${currentMaturity} → ${targetMaturity}`);
|
|
144
|
+
|
|
145
|
+
} catch (error) {
|
|
146
|
+
console.error(`[MaturityUpdateJob] Error updating ${standard.standard_id}:`, error.message);
|
|
147
|
+
summary.errors.push({
|
|
148
|
+
standard_id: standard.standard_id,
|
|
149
|
+
error: error.message
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
} catch (error) {
|
|
155
|
+
console.error('[MaturityUpdateJob] Job failed:', error);
|
|
156
|
+
summary.errors.push({ step: 'main', error: error.message });
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
summary.completedAt = new Date().toISOString();
|
|
160
|
+
console.log(`[MaturityUpdateJob] Complete: ${summary.promotions} promotions, ${summary.demotions} demotions, ${summary.unchanged} unchanged`);
|
|
161
|
+
|
|
162
|
+
return createSuccessResponse(
|
|
163
|
+
summary,
|
|
164
|
+
`Maturity update complete: ${summary.promotions} promotions, ${summary.demotions} demotions`
|
|
165
|
+
);
|
|
166
|
+
});
|
|
@@ -41,7 +41,7 @@ async function recordSessionStandards({ body: requestBody = {}, requestContext }
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
// Try to ensure session exists (upsert with minimal data)
|
|
44
|
-
//
|
|
44
|
+
// If project doesn't exist, auto-create it to prevent silent session loss
|
|
45
45
|
if (project_id && user_id) {
|
|
46
46
|
const sessionUpsertQuery = `
|
|
47
47
|
INSERT INTO rapport.sessions (
|
|
@@ -59,9 +59,42 @@ async function recordSessionStandards({ body: requestBody = {}, requestContext }
|
|
|
59
59
|
try {
|
|
60
60
|
await executeQuery(sessionUpsertQuery, [session_id, project_id, user_id]);
|
|
61
61
|
} catch (sessionError) {
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
// FK constraint on project_id — auto-create the project and retry
|
|
63
|
+
if (sessionError.code === '23503' && sessionError.constraint && sessionError.constraint.includes('project_id')) {
|
|
64
|
+
console.log(`[sessionStandardsPost] Project ${project_id} not found, auto-creating`);
|
|
65
|
+
try {
|
|
66
|
+
// Look up user's company_id
|
|
67
|
+
const entResult = await executeQuery(
|
|
68
|
+
`SELECT company_id FROM rapport.user_entitlements WHERE email_address = $1 LIMIT 1`,
|
|
69
|
+
[user_id]
|
|
70
|
+
);
|
|
71
|
+
if (entResult.rowCount > 0) {
|
|
72
|
+
const company_id = entResult.rows[0].company_id;
|
|
73
|
+
// Derive a readable project name from the project_id
|
|
74
|
+
// e.g. prj_pareidolia_main_1770727596802 → pareidolia main
|
|
75
|
+
const projectName = project_id
|
|
76
|
+
.replace(/^prj_/, '')
|
|
77
|
+
.replace(/_\d+$/, '')
|
|
78
|
+
.replace(/_/g, ' ');
|
|
79
|
+
|
|
80
|
+
await executeQuery(`
|
|
81
|
+
INSERT INTO rapport.projects (project_id, company_id, project_name, private)
|
|
82
|
+
VALUES ($1, $2, $3, false)
|
|
83
|
+
ON CONFLICT (project_id) DO NOTHING
|
|
84
|
+
`, [project_id, company_id, projectName]);
|
|
85
|
+
|
|
86
|
+
// Retry session upsert
|
|
87
|
+
await executeQuery(sessionUpsertQuery, [session_id, project_id, user_id]);
|
|
88
|
+
console.log(`[sessionStandardsPost] Auto-created project ${project_id} under ${company_id}`);
|
|
89
|
+
} else {
|
|
90
|
+
console.error(`[sessionStandardsPost] No entitlement found for ${user_id}, cannot auto-create project`);
|
|
91
|
+
}
|
|
92
|
+
} catch (autoCreateError) {
|
|
93
|
+
console.error('[sessionStandardsPost] Auto-create project failed:', autoCreateError.message);
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
console.error('[sessionStandardsPost] Session upsert failed:', sessionError.message);
|
|
97
|
+
}
|
|
65
98
|
}
|
|
66
99
|
}
|
|
67
100
|
|
|
@@ -71,6 +104,7 @@ async function recordSessionStandards({ body: requestBody = {}, requestContext }
|
|
|
71
104
|
|
|
72
105
|
for (const standard of standards) {
|
|
73
106
|
const standardId = standard.pattern_id || standard.element;
|
|
107
|
+
const standardName = standard.title || standard.element || standardId;
|
|
74
108
|
const relevanceScore = standard.relevance_score || standard.score || 0;
|
|
75
109
|
|
|
76
110
|
if (!standardId) {
|
|
@@ -82,14 +116,15 @@ async function recordSessionStandards({ body: requestBody = {}, requestContext }
|
|
|
82
116
|
INSERT INTO rapport.session_standards (
|
|
83
117
|
session_id,
|
|
84
118
|
standard_id,
|
|
119
|
+
standard_name,
|
|
85
120
|
relevance_score,
|
|
86
|
-
|
|
121
|
+
created_at
|
|
87
122
|
) VALUES (
|
|
88
|
-
$1, $2, $3, NOW()
|
|
123
|
+
$1, $2, $3, $4, NOW()
|
|
89
124
|
)
|
|
90
125
|
ON CONFLICT (session_id, standard_id) DO UPDATE SET
|
|
91
126
|
relevance_score = EXCLUDED.relevance_score,
|
|
92
|
-
|
|
127
|
+
created_at = NOW()
|
|
93
128
|
RETURNING id
|
|
94
129
|
`;
|
|
95
130
|
|
|
@@ -97,6 +132,7 @@ async function recordSessionStandards({ body: requestBody = {}, requestContext }
|
|
|
97
132
|
const result = await executeQuery(insertQuery, [
|
|
98
133
|
session_id,
|
|
99
134
|
standardId,
|
|
135
|
+
standardName,
|
|
100
136
|
relevanceScore
|
|
101
137
|
]);
|
|
102
138
|
|
|
@@ -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,
|