@equilateral_ai/mindmeld 3.1.2 → 3.3.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.
Files changed (71) hide show
  1. package/README.md +4 -4
  2. package/hooks/README.md +46 -4
  3. package/hooks/pre-compact.js +115 -12
  4. package/hooks/session-end.js +292 -0
  5. package/hooks/session-start.js +302 -25
  6. package/package.json +5 -2
  7. package/scripts/auth-login.js +53 -0
  8. package/scripts/harvest.js +59 -19
  9. package/scripts/init-project.js +134 -374
  10. package/scripts/inject.js +30 -9
  11. package/scripts/repo-analyzer.js +870 -0
  12. package/src/core/AuthManager.js +498 -0
  13. package/src/core/CrossReferenceEngine.js +624 -0
  14. package/src/core/DeprecationScheduler.js +183 -0
  15. package/src/core/LLMPatternDetector.js +218 -0
  16. package/src/core/RapportOrchestrator.js +186 -0
  17. package/src/core/RelevanceDetector.js +32 -2
  18. package/src/core/StandardLifecycle.js +244 -0
  19. package/src/core/StandardsIngestion.js +341 -28
  20. package/src/core/parsers/adrParser.js +479 -0
  21. package/src/core/parsers/cursorRulesParser.js +564 -0
  22. package/src/core/parsers/eslintParser.js +439 -0
  23. package/src/handlers/alerts/alertsAcknowledge.js +4 -3
  24. package/src/handlers/analytics/activitySummaryGet.js +235 -0
  25. package/src/handlers/analytics/coachingGet.js +361 -0
  26. package/src/handlers/analytics/developerScoreGet.js +207 -0
  27. package/src/handlers/collaborators/collaboratorAdd.js +4 -5
  28. package/src/handlers/collaborators/collaboratorInvite.js +6 -5
  29. package/src/handlers/collaborators/collaboratorList.js +3 -3
  30. package/src/handlers/collaborators/collaboratorRemove.js +5 -4
  31. package/src/handlers/correlations/correlationsDeveloperGet.js +12 -11
  32. package/src/handlers/correlations/correlationsGet.js +1 -1
  33. package/src/handlers/correlations/correlationsProjectGet.js +7 -6
  34. package/src/handlers/enterprise/enterpriseAuditGet.js +108 -0
  35. package/src/handlers/enterprise/enterpriseContributorsGet.js +85 -0
  36. package/src/handlers/enterprise/enterpriseKnowledgeCategoriesGet.js +53 -0
  37. package/src/handlers/enterprise/enterpriseKnowledgeCreate.js +77 -0
  38. package/src/handlers/enterprise/enterpriseKnowledgeDelete.js +71 -0
  39. package/src/handlers/enterprise/enterpriseKnowledgeGet.js +87 -0
  40. package/src/handlers/enterprise/enterpriseKnowledgeUpdate.js +122 -0
  41. package/src/handlers/enterprise/enterpriseOnboardingComplete.js +77 -0
  42. package/src/handlers/enterprise/enterpriseOnboardingInvite.js +138 -0
  43. package/src/handlers/enterprise/enterpriseOnboardingSetup.js +89 -0
  44. package/src/handlers/enterprise/enterpriseOnboardingStatus.js +90 -0
  45. package/src/handlers/github/githubConnectionStatus.js +1 -1
  46. package/src/handlers/github/githubDiscoverPatterns.js +264 -5
  47. package/src/handlers/github/githubOAuthCallback.js +14 -2
  48. package/src/handlers/github/githubOAuthStart.js +1 -1
  49. package/src/handlers/github/githubPatternsReview.js +1 -1
  50. package/src/handlers/github/githubReposList.js +1 -1
  51. package/src/handlers/helpers/auditLogger.js +201 -0
  52. package/src/handlers/helpers/index.js +19 -1
  53. package/src/handlers/helpers/lambdaWrapper.js +1 -1
  54. package/src/handlers/notifications/sendNotification.js +1 -1
  55. package/src/handlers/projects/projectCreate.js +28 -1
  56. package/src/handlers/projects/projectDelete.js +3 -3
  57. package/src/handlers/projects/projectUpdate.js +4 -5
  58. package/src/handlers/scheduled/analyzeCorrelations.js +3 -3
  59. package/src/handlers/scheduled/generateAlerts.js +1 -1
  60. package/src/handlers/standards/catalogGet.js +185 -0
  61. package/src/handlers/standards/catalogSync.js +120 -0
  62. package/src/handlers/standards/projectStandardsGet.js +135 -0
  63. package/src/handlers/standards/projectStandardsPut.js +131 -0
  64. package/src/handlers/standards/standardsAuditGet.js +65 -0
  65. package/src/handlers/standards/standardsParseUpload.js +153 -0
  66. package/src/handlers/standards/standardsRelevantPost.js +213 -0
  67. package/src/handlers/standards/standardsTransition.js +64 -0
  68. package/src/handlers/user/userSplashAck.js +91 -0
  69. package/src/handlers/user/userSplashGet.js +194 -0
  70. package/src/handlers/users/userProfilePut.js +77 -0
  71. package/src/index.js +37 -29
@@ -2,8 +2,8 @@
2
2
  * Project Update Handler
3
3
  * Updates project metadata
4
4
  *
5
- * PUT /api/projects/{projectId}
6
- * Body: { project_name, description, private }
5
+ * PUT /api/projects
6
+ * Body: { project_id, project_name, description, private }
7
7
  */
8
8
 
9
9
  const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse, handleError } = require('./helpers');
@@ -12,14 +12,13 @@ const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse, h
12
12
  * Update project
13
13
  * Requires owner or admin role on project
14
14
  */
15
- async function updateProject({ pathParameters = {}, body: requestBody = {}, requestContext }) {
15
+ async function updateProject({ body: requestBody = {}, requestContext }) {
16
16
  try {
17
17
  const Request_ID = requestContext.requestId;
18
18
  // REST API: requestContext.authorizer.claims.email
19
19
  // HTTP API: requestContext.authorizer.jwt.claims.email
20
20
  const email = requestContext.authorizer?.claims?.email || requestContext.authorizer?.jwt?.claims?.email;
21
- const { projectId } = pathParameters;
22
- const { project_name, description, private: isPrivate, repo_url } = requestBody;
21
+ const { project_id: projectId, project_name, description, private: isPrivate, repo_url } = requestBody;
23
22
 
24
23
  if (!projectId) {
25
24
  return createErrorResponse(400, 'projectId is required');
@@ -13,7 +13,7 @@
13
13
  */
14
14
 
15
15
  const { wrapHandler, executeQuery, createSuccessResponse } = require('./helpers');
16
- const { CorrelationAnalyzer } = require('../../core/CorrelationAnalyzer');
16
+ const { CorrelationAnalyzer } = require('./core/CorrelationAnalyzer');
17
17
 
18
18
  /**
19
19
  * Main handler - wrapped with wrapHandler for consistent error handling
@@ -105,9 +105,9 @@ async function createStrugglingDeveloperAlerts(companyId, developers) {
105
105
  SELECT 1 FROM rapport.attention_alerts
106
106
  WHERE email_address = $1
107
107
  AND alert_type = 'low_conversion'
108
- AND (status = 'active' OR created_at > NOW() - INTERVAL '${cooldownHours} hours')
108
+ AND (status = 'active' OR created_at > NOW() - $2 * INTERVAL '1 hour')
109
109
  LIMIT 1
110
- `, [dev.email]);
110
+ `, [dev.email, cooldownHours]);
111
111
 
112
112
  if (existingCheck.rows.length > 0) {
113
113
  console.log(`[AnalyzeCorrelations] Skipping alert for ${dev.email} - already exists`);
@@ -15,7 +15,7 @@
15
15
  */
16
16
 
17
17
  const { wrapHandler, createSuccessResponse, executeQuery } = require('./helpers');
18
- const { AlertEngine, setExecuteQuery } = require('../../core/AlertEngine');
18
+ const { AlertEngine, setExecuteQuery } = require('./core/AlertEngine');
19
19
 
20
20
  // Initialize AlertEngine with database connection
21
21
  setExecuteQuery(executeQuery);
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Standards Catalog Get Handler
3
+ * Returns the standards catalog manifest based on subscription tier
4
+ *
5
+ * GET /api/standards/catalog
6
+ * Auth: Cognito JWT required
7
+ * Query params:
8
+ * - catalog_id: Override catalog selection
9
+ * - tech_stack: Comma-separated list of detected technologies for recommendations
10
+ *
11
+ * Enterprise tier: Full proprietary standards (equilateral-v1)
12
+ * Other tiers: Open standards only (equilateral-open-v1)
13
+ */
14
+
15
+ const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse } = require('./helpers');
16
+
17
+ // Enterprise tiers that get full proprietary standards
18
+ const ENTERPRISE_TIERS = ['enterprise', 'enterprise_private', 'enterprise_team'];
19
+
20
+ // Tech stack to category recommendations mapping
21
+ const TECH_TO_CATEGORIES = {
22
+ // Languages/Frameworks
23
+ 'react': ['frontend-development'],
24
+ 'vue': ['frontend-development'],
25
+ 'angular': ['frontend-development'],
26
+ 'typescript': ['frontend-development'],
27
+ 'node.js': ['serverless-saas-aws'],
28
+ 'nodejs': ['serverless-saas-aws'],
29
+ 'express': ['serverless-saas-aws'],
30
+
31
+ // Infrastructure
32
+ 'aws sam': ['serverless-saas-aws'],
33
+ 'aws lambda': ['serverless-saas-aws'],
34
+ 'lambda': ['serverless-saas-aws'],
35
+ 'serverless': ['serverless-saas-aws'],
36
+ 'cloudformation': ['serverless-saas-aws'],
37
+ 'api gateway': ['serverless-saas-aws'],
38
+
39
+ // Databases
40
+ 'postgresql': ['database-patterns', 'memory-systems'],
41
+ 'postgres': ['database-patterns', 'memory-systems'],
42
+ 'dynamodb': ['serverless-saas-aws'],
43
+
44
+ // Testing
45
+ 'jest': ['frontend-development'],
46
+
47
+ // Multi-agent
48
+ 'claude': ['multi-agent-orchestration'],
49
+ 'openai': ['multi-agent-orchestration'],
50
+ 'langchain': ['multi-agent-orchestration'],
51
+ };
52
+
53
+ /**
54
+ * Get recommended categories based on detected tech stack
55
+ * @param {string} techStackParam - Comma-separated tech stack string
56
+ * @returns {string[]} - Unique list of recommended category keys
57
+ */
58
+ function getRecommendations(techStackParam) {
59
+ if (!techStackParam) return [];
60
+
61
+ const technologies = techStackParam.split(',').map(t => t.trim().toLowerCase());
62
+ const recommendations = new Set();
63
+
64
+ for (const tech of technologies) {
65
+ const categories = TECH_TO_CATEGORIES[tech];
66
+ if (categories) {
67
+ categories.forEach(cat => recommendations.add(cat));
68
+ }
69
+ }
70
+
71
+ return Array.from(recommendations);
72
+ }
73
+
74
+ async function getCatalog({ requestContext, queryStringParameters }) {
75
+ try {
76
+ const email = requestContext.authorizer?.claims?.email || requestContext.authorizer?.jwt?.claims?.email;
77
+
78
+ if (!email) {
79
+ return createErrorResponse(401, 'Authentication required');
80
+ }
81
+
82
+ // Get user's subscription tier
83
+ const userResult = await executeQuery(`
84
+ SELECT c.subscription_tier
85
+ FROM rapport.users u
86
+ LEFT JOIN rapport.clients c ON u.client_id = c.client_id
87
+ WHERE u.email_address = $1 AND u.active = true
88
+ `, [email]);
89
+
90
+ const subscriptionTier = userResult.rows[0]?.subscription_tier || 'free';
91
+ const isEnterprise = ENTERPRISE_TIERS.includes(subscriptionTier);
92
+
93
+ // Get tech stack recommendations if provided
94
+ const techStack = queryStringParameters?.tech_stack;
95
+ const recommendations = getRecommendations(techStack);
96
+
97
+ // Determine which catalog to return based on tier
98
+ // Enterprise gets full catalog, others get open standards
99
+ const catalogId = queryStringParameters?.catalog_id ||
100
+ (isEnterprise ? 'equilateral-v1' : 'equilateral-open-v1');
101
+
102
+ const result = await executeQuery(`
103
+ SELECT
104
+ catalog_id,
105
+ version,
106
+ source_url,
107
+ updated_at,
108
+ total_standards,
109
+ total_layers,
110
+ categories,
111
+ layers,
112
+ critical_alerts
113
+ FROM rapport.standards_catalog
114
+ WHERE catalog_id = $1
115
+ `, [catalogId]);
116
+
117
+ if (result.rowCount === 0) {
118
+ // Fall back to open standards if requested catalog not found
119
+ const fallbackResult = await executeQuery(`
120
+ SELECT
121
+ catalog_id,
122
+ version,
123
+ source_url,
124
+ updated_at,
125
+ total_standards,
126
+ total_layers,
127
+ categories,
128
+ layers,
129
+ critical_alerts
130
+ FROM rapport.standards_catalog
131
+ WHERE catalog_id = 'equilateral-open-v1'
132
+ `);
133
+
134
+ if (fallbackResult.rowCount === 0) {
135
+ return createErrorResponse(404, 'Standards catalog not found', {
136
+ catalog_id: catalogId,
137
+ hint: 'Run migration or sync catalog first'
138
+ });
139
+ }
140
+
141
+ const catalog = fallbackResult.rows[0];
142
+ return createSuccessResponse({
143
+ catalog_id: catalog.catalog_id,
144
+ version: catalog.version,
145
+ source_url: catalog.source_url,
146
+ updated_at: catalog.updated_at,
147
+ subscription_tier: subscriptionTier,
148
+ is_enterprise: false,
149
+ recommendations,
150
+ summary: {
151
+ total_standards: catalog.total_standards,
152
+ total_layers: catalog.total_layers
153
+ },
154
+ categories: catalog.categories,
155
+ layers: catalog.layers,
156
+ critical_alerts: catalog.critical_alerts
157
+ }, 'Open standards catalog retrieved');
158
+ }
159
+
160
+ const catalog = result.rows[0];
161
+
162
+ return createSuccessResponse({
163
+ catalog_id: catalog.catalog_id,
164
+ version: catalog.version,
165
+ source_url: catalog.source_url,
166
+ updated_at: catalog.updated_at,
167
+ subscription_tier: subscriptionTier,
168
+ is_enterprise: isEnterprise,
169
+ recommendations,
170
+ summary: {
171
+ total_standards: catalog.total_standards,
172
+ total_layers: catalog.total_layers
173
+ },
174
+ categories: catalog.categories,
175
+ layers: catalog.layers,
176
+ critical_alerts: catalog.critical_alerts
177
+ }, isEnterprise ? 'Full standards catalog retrieved' : 'Open standards catalog retrieved');
178
+
179
+ } catch (error) {
180
+ console.error('Catalog Get Error:', error);
181
+ return createErrorResponse(500, 'Failed to retrieve standards catalog');
182
+ }
183
+ }
184
+
185
+ exports.handler = wrapHandler(getCatalog);
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Standards Catalog Sync Handler
3
+ * Imports/updates the standards catalog from manifest.json
4
+ *
5
+ * POST /api/standards/catalog/sync
6
+ * Body: { manifest } - The manifest.json content
7
+ * Auth: Cognito JWT required (admin only)
8
+ */
9
+
10
+ const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse } = require('./helpers');
11
+
12
+ async function syncCatalog({ body, requestContext }) {
13
+ try {
14
+ const email = requestContext.authorizer?.claims?.email || requestContext.authorizer?.jwt?.claims?.email;
15
+
16
+ if (!email) {
17
+ return createErrorResponse(401, 'Authentication required');
18
+ }
19
+
20
+ const { manifest, catalog_id = 'equilateral-v1', source_url } = body || {};
21
+
22
+ if (!manifest) {
23
+ return createErrorResponse(400, 'manifest is required');
24
+ }
25
+
26
+ // Validate manifest structure
27
+ if (!manifest.version || !manifest.categories || !manifest.layers) {
28
+ return createErrorResponse(400, 'Invalid manifest: missing version, categories, or layers');
29
+ }
30
+
31
+ // Extract metadata
32
+ const totalStandards = manifest.metadata?.totalStandards || 0;
33
+ const totalLayers = manifest.layers?.length || 0;
34
+
35
+ // Build categories object with descriptions
36
+ const categories = {};
37
+ if (manifest.categories) {
38
+ for (const [key, description] of Object.entries(manifest.categories)) {
39
+ categories[key] = {
40
+ name: key.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase()),
41
+ description: description,
42
+ // Count files in this category from layers
43
+ standards_count: 0
44
+ };
45
+ }
46
+ }
47
+
48
+ // Count standards per category from layers
49
+ if (manifest.layers) {
50
+ for (const layer of manifest.layers) {
51
+ if (layer.files) {
52
+ for (const file of layer.files) {
53
+ const category = file.split('/')[0];
54
+ if (categories[category]) {
55
+ categories[category].standards_count++;
56
+ }
57
+ }
58
+ }
59
+ }
60
+ }
61
+
62
+ // Upsert catalog
63
+ const result = await executeQuery(`
64
+ INSERT INTO rapport.standards_catalog (
65
+ catalog_id,
66
+ version,
67
+ source_url,
68
+ manifest,
69
+ total_standards,
70
+ total_layers,
71
+ categories,
72
+ layers,
73
+ critical_alerts,
74
+ updated_at
75
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, NOW())
76
+ ON CONFLICT (catalog_id) DO UPDATE SET
77
+ version = EXCLUDED.version,
78
+ source_url = EXCLUDED.source_url,
79
+ manifest = EXCLUDED.manifest,
80
+ total_standards = EXCLUDED.total_standards,
81
+ total_layers = EXCLUDED.total_layers,
82
+ categories = EXCLUDED.categories,
83
+ layers = EXCLUDED.layers,
84
+ critical_alerts = EXCLUDED.critical_alerts,
85
+ updated_at = NOW()
86
+ RETURNING catalog_id, version, updated_at
87
+ `, [
88
+ catalog_id,
89
+ manifest.version,
90
+ source_url || manifest.metadata?.repository || null,
91
+ JSON.stringify(manifest),
92
+ totalStandards,
93
+ totalLayers,
94
+ JSON.stringify(categories),
95
+ JSON.stringify(manifest.layers),
96
+ JSON.stringify(manifest.criticalAlerts || [])
97
+ ]);
98
+
99
+ const synced = result.rows[0];
100
+
101
+ return createSuccessResponse({
102
+ catalog_id: synced.catalog_id,
103
+ version: synced.version,
104
+ updated_at: synced.updated_at,
105
+ summary: {
106
+ total_standards: totalStandards,
107
+ total_layers: totalLayers,
108
+ total_categories: Object.keys(categories).length,
109
+ critical_alerts: (manifest.criticalAlerts || []).length
110
+ },
111
+ categories: Object.keys(categories)
112
+ }, 'Standards catalog synced successfully');
113
+
114
+ } catch (error) {
115
+ console.error('Catalog Sync Error:', error);
116
+ return createErrorResponse(500, 'Failed to sync standards catalog');
117
+ }
118
+ }
119
+
120
+ exports.handler = wrapHandler(syncCatalog);
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Project Standards Get Handler
3
+ * Returns standards preferences for a project
4
+ *
5
+ * GET /api/projects/standards?project_id=xxx
6
+ * Auth: Cognito JWT required
7
+ */
8
+
9
+ const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse } = require('./helpers');
10
+
11
+ async function getProjectStandards({ 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
+
21
+ if (!projectId) {
22
+ return createErrorResponse(400, 'projectId is required');
23
+ }
24
+
25
+ // Verify user has access to project
26
+ const accessResult = await executeQuery(`
27
+ SELECT pc.role, p.project_name
28
+ FROM rapport.project_collaborators pc
29
+ JOIN rapport.projects p ON pc.project_id = p.project_id
30
+ WHERE pc.project_id = $1 AND pc.email_address = $2
31
+ `, [projectId, email]);
32
+
33
+ if (accessResult.rowCount === 0) {
34
+ return createErrorResponse(403, 'Access denied to project');
35
+ }
36
+
37
+ const projectName = accessResult.rows[0].project_name;
38
+
39
+ // Get project standards preferences
40
+ const prefsResult = await executeQuery(`
41
+ SELECT
42
+ ps.catalog_id,
43
+ ps.enabled_categories,
44
+ ps.standard_overrides,
45
+ ps.critical_overrides,
46
+ ps.modified_by,
47
+ ps.modified_at,
48
+ sc.version as catalog_version,
49
+ sc.categories as available_categories,
50
+ sc.layers as available_layers,
51
+ sc.critical_alerts,
52
+ sc.total_standards
53
+ FROM rapport.project_standards ps
54
+ JOIN rapport.standards_catalog sc ON ps.catalog_id = sc.catalog_id
55
+ WHERE ps.project_id = $1
56
+ `, [projectId]);
57
+
58
+ // If no preferences exist, return defaults with catalog info
59
+ if (prefsResult.rowCount === 0) {
60
+ const catalogResult = await executeQuery(`
61
+ SELECT
62
+ catalog_id,
63
+ version,
64
+ categories,
65
+ layers,
66
+ critical_alerts,
67
+ total_standards
68
+ FROM rapport.standards_catalog
69
+ WHERE catalog_id = 'equilateral-v1'
70
+ `);
71
+
72
+ if (catalogResult.rowCount === 0) {
73
+ return createErrorResponse(404, 'Standards catalog not found');
74
+ }
75
+
76
+ const catalog = catalogResult.rows[0];
77
+
78
+ // Default: all categories enabled
79
+ const defaultCategories = {};
80
+ if (catalog.categories) {
81
+ for (const key of Object.keys(catalog.categories)) {
82
+ defaultCategories[key] = true;
83
+ }
84
+ }
85
+
86
+ return createSuccessResponse({
87
+ project_id: projectId,
88
+ project_name: projectName,
89
+ catalog_id: catalog.catalog_id,
90
+ catalog_version: catalog.version,
91
+ has_custom_preferences: false,
92
+ preferences: {
93
+ enabled_categories: defaultCategories,
94
+ standard_overrides: {},
95
+ critical_overrides: {}
96
+ },
97
+ catalog: {
98
+ categories: catalog.categories,
99
+ layers: catalog.layers,
100
+ critical_alerts: catalog.critical_alerts,
101
+ total_standards: catalog.total_standards
102
+ }
103
+ }, 'Default standards preferences');
104
+ }
105
+
106
+ const prefs = prefsResult.rows[0];
107
+
108
+ return createSuccessResponse({
109
+ project_id: projectId,
110
+ project_name: projectName,
111
+ catalog_id: prefs.catalog_id,
112
+ catalog_version: prefs.catalog_version,
113
+ has_custom_preferences: true,
114
+ preferences: {
115
+ enabled_categories: prefs.enabled_categories || {},
116
+ standard_overrides: prefs.standard_overrides || {},
117
+ critical_overrides: prefs.critical_overrides || {}
118
+ },
119
+ catalog: {
120
+ categories: prefs.available_categories,
121
+ layers: prefs.available_layers,
122
+ critical_alerts: prefs.critical_alerts,
123
+ total_standards: prefs.total_standards
124
+ },
125
+ modified_by: prefs.modified_by,
126
+ modified_at: prefs.modified_at
127
+ }, 'Project standards preferences retrieved');
128
+
129
+ } catch (error) {
130
+ console.error('Project Standards Get Error:', error);
131
+ return createErrorResponse(500, 'Failed to retrieve project standards');
132
+ }
133
+ }
134
+
135
+ exports.handler = wrapHandler(getProjectStandards);
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Project Standards Put Handler
3
+ * Updates standards preferences for a project
4
+ *
5
+ * PUT /api/projects/standards
6
+ * Body: { project_id, enabled_categories, standard_overrides, critical_overrides }
7
+ * Auth: Cognito JWT required
8
+ */
9
+
10
+ const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse } = require('./helpers');
11
+
12
+ async function updateProjectStandards({ body, requestContext }) {
13
+ try {
14
+ const email = requestContext.authorizer?.claims?.email || requestContext.authorizer?.jwt?.claims?.email;
15
+
16
+ if (!email) {
17
+ return createErrorResponse(401, 'Authentication required');
18
+ }
19
+
20
+ const projectId = (body || {}).project_id;
21
+
22
+ if (!projectId) {
23
+ return createErrorResponse(400, 'projectId is required');
24
+ }
25
+
26
+ const {
27
+ enabled_categories,
28
+ standard_overrides,
29
+ critical_overrides,
30
+ catalog_id = 'equilateral-v1'
31
+ } = body || {};
32
+
33
+ // Verify user has admin access to project
34
+ const accessResult = await executeQuery(`
35
+ SELECT role FROM rapport.project_collaborators
36
+ WHERE project_id = $1 AND email_address = $2
37
+ `, [projectId, email]);
38
+
39
+ if (accessResult.rowCount === 0) {
40
+ return createErrorResponse(403, 'Access denied to project');
41
+ }
42
+
43
+ const role = accessResult.rows[0].role;
44
+ if (role !== 'owner' && role !== 'admin') {
45
+ return createErrorResponse(403, 'Admin access required to modify standards preferences');
46
+ }
47
+
48
+ // Verify catalog exists
49
+ const catalogResult = await executeQuery(`
50
+ SELECT catalog_id, categories FROM rapport.standards_catalog
51
+ WHERE catalog_id = $1
52
+ `, [catalog_id]);
53
+
54
+ if (catalogResult.rowCount === 0) {
55
+ return createErrorResponse(404, 'Standards catalog not found', { catalog_id });
56
+ }
57
+
58
+ const availableCategories = Object.keys(catalogResult.rows[0].categories || {});
59
+
60
+ // Validate enabled_categories keys
61
+ if (enabled_categories) {
62
+ const invalidCategories = Object.keys(enabled_categories).filter(
63
+ cat => !availableCategories.includes(cat)
64
+ );
65
+ if (invalidCategories.length > 0) {
66
+ return createErrorResponse(400, 'Invalid category names', {
67
+ invalid: invalidCategories,
68
+ valid: availableCategories
69
+ });
70
+ }
71
+ }
72
+
73
+ // Upsert project standards
74
+ const result = await executeQuery(`
75
+ INSERT INTO rapport.project_standards (
76
+ project_id,
77
+ catalog_id,
78
+ enabled_categories,
79
+ standard_overrides,
80
+ critical_overrides,
81
+ created_by,
82
+ modified_by
83
+ ) VALUES ($1, $2, $3, $4, $5, $6, $6)
84
+ ON CONFLICT (project_id, catalog_id) DO UPDATE SET
85
+ enabled_categories = COALESCE($3, rapport.project_standards.enabled_categories),
86
+ standard_overrides = COALESCE($4, rapport.project_standards.standard_overrides),
87
+ critical_overrides = COALESCE($5, rapport.project_standards.critical_overrides),
88
+ modified_by = $6,
89
+ modified_at = NOW()
90
+ RETURNING *
91
+ `, [
92
+ projectId,
93
+ catalog_id,
94
+ enabled_categories ? JSON.stringify(enabled_categories) : null,
95
+ standard_overrides ? JSON.stringify(standard_overrides) : null,
96
+ critical_overrides ? JSON.stringify(critical_overrides) : null,
97
+ email
98
+ ]);
99
+
100
+ const updated = result.rows[0];
101
+
102
+ // Count enabled standards
103
+ const enabledCats = updated.enabled_categories || {};
104
+ const enabledCount = Object.values(enabledCats).filter(v => v === true).length;
105
+ const disabledCount = Object.values(enabledCats).filter(v => v === false).length;
106
+
107
+ return createSuccessResponse({
108
+ project_id: projectId,
109
+ catalog_id: updated.catalog_id,
110
+ preferences: {
111
+ enabled_categories: updated.enabled_categories,
112
+ standard_overrides: updated.standard_overrides,
113
+ critical_overrides: updated.critical_overrides
114
+ },
115
+ summary: {
116
+ categories_enabled: enabledCount,
117
+ categories_disabled: disabledCount,
118
+ standard_overrides: Object.keys(updated.standard_overrides || {}).length,
119
+ critical_overrides: Object.keys(updated.critical_overrides || {}).length
120
+ },
121
+ modified_by: updated.modified_by,
122
+ modified_at: updated.modified_at
123
+ }, 'Standards preferences updated');
124
+
125
+ } catch (error) {
126
+ console.error('Project Standards Put Error:', error);
127
+ return createErrorResponse(500, 'Failed to update project standards');
128
+ }
129
+ }
130
+
131
+ exports.handler = wrapHandler(updateProjectStandards);