@equilateral_ai/mindmeld 3.2.0 → 3.3.1

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 (68) hide show
  1. package/README.md +4 -4
  2. package/hooks/README.md +46 -4
  3. package/hooks/pre-compact.js +87 -1
  4. package/hooks/session-end.js +292 -0
  5. package/hooks/session-start.js +292 -23
  6. package/package.json +4 -2
  7. package/scripts/auth-login.js +53 -0
  8. package/scripts/init-project.js +69 -375
  9. package/src/core/AuthManager.js +498 -0
  10. package/src/core/CrossReferenceEngine.js +624 -0
  11. package/src/core/DeprecationScheduler.js +183 -0
  12. package/src/core/LLMPatternDetector.js +218 -0
  13. package/src/core/RapportOrchestrator.js +186 -0
  14. package/src/core/RelevanceDetector.js +32 -2
  15. package/src/core/StandardLifecycle.js +244 -0
  16. package/src/core/StandardsIngestion.js +341 -28
  17. package/src/core/parsers/adrParser.js +479 -0
  18. package/src/core/parsers/cursorRulesParser.js +564 -0
  19. package/src/core/parsers/eslintParser.js +439 -0
  20. package/src/handlers/alerts/alertsAcknowledge.js +4 -3
  21. package/src/handlers/analytics/activitySummaryGet.js +235 -0
  22. package/src/handlers/analytics/coachingGet.js +361 -0
  23. package/src/handlers/analytics/developerScoreGet.js +207 -0
  24. package/src/handlers/collaborators/collaboratorAdd.js +4 -5
  25. package/src/handlers/collaborators/collaboratorInvite.js +6 -5
  26. package/src/handlers/collaborators/collaboratorList.js +3 -3
  27. package/src/handlers/collaborators/collaboratorRemove.js +5 -4
  28. package/src/handlers/correlations/correlationsDeveloperGet.js +12 -11
  29. package/src/handlers/correlations/correlationsGet.js +1 -1
  30. package/src/handlers/correlations/correlationsProjectGet.js +7 -6
  31. package/src/handlers/enterprise/enterpriseAuditGet.js +108 -0
  32. package/src/handlers/enterprise/enterpriseContributorsGet.js +85 -0
  33. package/src/handlers/enterprise/enterpriseKnowledgeCategoriesGet.js +53 -0
  34. package/src/handlers/enterprise/enterpriseKnowledgeCreate.js +77 -0
  35. package/src/handlers/enterprise/enterpriseKnowledgeDelete.js +71 -0
  36. package/src/handlers/enterprise/enterpriseKnowledgeGet.js +87 -0
  37. package/src/handlers/enterprise/enterpriseKnowledgeUpdate.js +122 -0
  38. package/src/handlers/enterprise/enterpriseOnboardingComplete.js +77 -0
  39. package/src/handlers/enterprise/enterpriseOnboardingInvite.js +138 -0
  40. package/src/handlers/enterprise/enterpriseOnboardingSetup.js +89 -0
  41. package/src/handlers/enterprise/enterpriseOnboardingStatus.js +90 -0
  42. package/src/handlers/github/githubConnectionStatus.js +1 -1
  43. package/src/handlers/github/githubDiscoverPatterns.js +264 -5
  44. package/src/handlers/github/githubOAuthCallback.js +14 -2
  45. package/src/handlers/github/githubOAuthStart.js +1 -1
  46. package/src/handlers/github/githubPatternsReview.js +1 -1
  47. package/src/handlers/github/githubReposList.js +1 -1
  48. package/src/handlers/helpers/auditLogger.js +201 -0
  49. package/src/handlers/helpers/index.js +19 -1
  50. package/src/handlers/helpers/lambdaWrapper.js +1 -1
  51. package/src/handlers/notifications/sendNotification.js +1 -1
  52. package/src/handlers/projects/projectCreate.js +28 -1
  53. package/src/handlers/projects/projectDelete.js +3 -3
  54. package/src/handlers/projects/projectUpdate.js +4 -5
  55. package/src/handlers/scheduled/analyzeCorrelations.js +3 -3
  56. package/src/handlers/scheduled/generateAlerts.js +1 -1
  57. package/src/handlers/standards/catalogGet.js +185 -0
  58. package/src/handlers/standards/catalogSync.js +120 -0
  59. package/src/handlers/standards/projectStandardsGet.js +135 -0
  60. package/src/handlers/standards/projectStandardsPut.js +131 -0
  61. package/src/handlers/standards/standardsAuditGet.js +65 -0
  62. package/src/handlers/standards/standardsParseUpload.js +153 -0
  63. package/src/handlers/standards/standardsRelevantPost.js +213 -0
  64. package/src/handlers/standards/standardsTransition.js +64 -0
  65. package/src/handlers/user/userSplashAck.js +91 -0
  66. package/src/handlers/user/userSplashGet.js +194 -0
  67. package/src/handlers/users/userProfilePut.js +77 -0
  68. package/src/index.js +75 -75
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Standards Audit Get Handler
3
+ * Returns the audit trail for a standard's lifecycle transitions
4
+ *
5
+ * GET /api/standards/audit?standard_id=xxx
6
+ * Query params: standard_id (required), cursor (string), limit (number, default 50, max 200)
7
+ * Returns: { audit_trail, total_transitions, current_state }
8
+ * Auth: Cognito JWT required
9
+ */
10
+
11
+ const { wrapHandler, createSuccessResponse, createErrorResponse } = require('./helpers');
12
+ const { StandardLifecycle } = require('./core/StandardLifecycle');
13
+
14
+ const lifecycle = new StandardLifecycle();
15
+
16
+ async function getStandardsAudit({ queryStringParameters, requestContext }) {
17
+ try {
18
+ const email = requestContext.authorizer?.claims?.email || requestContext.authorizer?.jwt?.claims?.email;
19
+
20
+ if (!email) {
21
+ return createErrorResponse(401, 'Authentication required');
22
+ }
23
+
24
+ const id = (queryStringParameters || {}).standard_id;
25
+
26
+ if (!id) {
27
+ return createErrorResponse(400, 'standard_id is required');
28
+ }
29
+
30
+ // Verify the standard exists
31
+ const currentState = await lifecycle.getCurrentState(id);
32
+
33
+ if (!currentState) {
34
+ return createErrorResponse(404, 'Standard not found', { standard_id: id });
35
+ }
36
+
37
+ // Get valid transitions from the current state
38
+ const validTransitions = lifecycle.getValidTransitions(currentState);
39
+
40
+ // Get audit history with pagination
41
+ const cursor = queryStringParameters?.cursor || null;
42
+ const limit = queryStringParameters?.limit ? parseInt(queryStringParameters.limit) : 50;
43
+
44
+ const history = await lifecycle.getHistory(id, { cursor, limit });
45
+
46
+ return createSuccessResponse({
47
+ standard_id: id,
48
+ current_state: currentState,
49
+ valid_transitions: validTransitions,
50
+ audit_trail: history.entries,
51
+ total_transitions: history.total_transitions,
52
+ pagination: {
53
+ has_more: history.has_more,
54
+ next_cursor: history.next_cursor,
55
+ limit: limit
56
+ }
57
+ }, 'Audit trail retrieved');
58
+
59
+ } catch (error) {
60
+ console.error('Standards Audit Get Error:', error);
61
+ return createErrorResponse(500, 'Failed to retrieve audit trail');
62
+ }
63
+ }
64
+
65
+ exports.handler = wrapHandler(getStandardsAudit);
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Standards Parse Upload Handler
3
+ * Parses uploaded standards files into YAML-compatible format
4
+ *
5
+ * POST /api/standards/parse-upload
6
+ * Body: { project_id, content, format: 'adr'|'eslint'|'cursorrules'|'markdown', filename }
7
+ * Auth: Cognito JWT required
8
+ *
9
+ * Uses the appropriate parser based on the specified format and returns
10
+ * parsed YAML-compatible standards ready for storage or review.
11
+ */
12
+
13
+ const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse } = require('./helpers');
14
+
15
+ const { parseAdr } = require('./core/parsers/adrParser');
16
+ const { parseEslint } = require('./core/parsers/eslintParser');
17
+ const { parseCursorRules } = require('./core/parsers/cursorRulesParser');
18
+
19
+ const SUPPORTED_FORMATS = ['adr', 'eslint', 'cursorrules', 'markdown'];
20
+
21
+ async function parseUploadStandards({ body, requestContext }) {
22
+ try {
23
+ const email = requestContext.authorizer?.claims?.email || requestContext.authorizer?.jwt?.claims?.email;
24
+
25
+ if (!email) {
26
+ return createErrorResponse(401, 'Authentication required');
27
+ }
28
+
29
+ const { project_id, content, format, filename } = body || {};
30
+
31
+ // Validate required fields
32
+ if (!project_id) {
33
+ return createErrorResponse(400, 'project_id is required');
34
+ }
35
+
36
+ if (!content) {
37
+ return createErrorResponse(400, 'content is required');
38
+ }
39
+
40
+ if (!format) {
41
+ return createErrorResponse(400, 'format is required', {
42
+ supported: SUPPORTED_FORMATS
43
+ });
44
+ }
45
+
46
+ if (!SUPPORTED_FORMATS.includes(format)) {
47
+ return createErrorResponse(400, `Unsupported format: ${format}`, {
48
+ supported: SUPPORTED_FORMATS
49
+ });
50
+ }
51
+
52
+ // Verify user has access to the project
53
+ const accessResult = await executeQuery(`
54
+ SELECT role FROM rapport.project_collaborators
55
+ WHERE project_id = $1 AND email_address = $2
56
+ `, [project_id, email]);
57
+
58
+ if (accessResult.rowCount === 0) {
59
+ return createErrorResponse(403, 'Access denied to project');
60
+ }
61
+
62
+ // Parse the content using the appropriate parser
63
+ const parserOptions = {
64
+ filename: filename || `uploaded-${format}`,
65
+ category: undefined // Let parser infer category
66
+ };
67
+
68
+ let parsed;
69
+ try {
70
+ parsed = parseContent(content, format, parserOptions);
71
+ } catch (parseError) {
72
+ return createErrorResponse(422, `Failed to parse ${format} content: ${parseError.message}`, {
73
+ format,
74
+ filename: parserOptions.filename
75
+ });
76
+ }
77
+
78
+ // Record the upload activity
79
+ await executeQuery(`
80
+ INSERT INTO rapport.activity_log (
81
+ email_address,
82
+ project_id,
83
+ activity_type,
84
+ activity_data,
85
+ created_at
86
+ ) VALUES ($1, $2, 'standards_upload', $3, NOW())
87
+ `, [
88
+ email,
89
+ project_id,
90
+ JSON.stringify({
91
+ format,
92
+ filename: filename || null,
93
+ rules_count: parsed.rules ? parsed.rules.length : 0,
94
+ anti_patterns_count: parsed.anti_patterns ? parsed.anti_patterns.length : 0,
95
+ category: parsed.category,
96
+ id: parsed.id
97
+ })
98
+ ]);
99
+
100
+ return createSuccessResponse({
101
+ project_id,
102
+ format,
103
+ filename: filename || null,
104
+ parsed,
105
+ summary: {
106
+ id: parsed.id,
107
+ category: parsed.category,
108
+ priority: parsed.priority,
109
+ rules_count: parsed.rules ? parsed.rules.length : 0,
110
+ anti_patterns_count: parsed.anti_patterns ? parsed.anti_patterns.length : 0,
111
+ tags: parsed.tags || []
112
+ }
113
+ }, `Successfully parsed ${format} content`);
114
+
115
+ } catch (error) {
116
+ console.error('Standards Parse Upload Error:', error);
117
+ return createErrorResponse(500, 'Failed to parse uploaded standards');
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Route content to the appropriate parser based on format
123
+ *
124
+ * @param {string} content - Raw content to parse
125
+ * @param {string} format - Format identifier
126
+ * @param {Object} options - Parser options
127
+ * @returns {Object} Parsed YAML-compatible standards object
128
+ */
129
+ function parseContent(content, format, options) {
130
+ switch (format) {
131
+ case 'adr':
132
+ return parseAdr(content, options);
133
+
134
+ case 'eslint':
135
+ return parseEslint(content, options);
136
+
137
+ case 'cursorrules':
138
+ return parseCursorRules(content, options);
139
+
140
+ case 'markdown':
141
+ // Markdown format uses the cursor rules parser since it handles
142
+ // generic markdown with rule sections effectively
143
+ return parseCursorRules(content, {
144
+ ...options,
145
+ filename: options.filename || 'standards.md'
146
+ });
147
+
148
+ default:
149
+ throw new Error(`Unsupported format: ${format}`);
150
+ }
151
+ }
152
+
153
+ exports.handler = wrapHandler(parseUploadStandards);
@@ -0,0 +1,213 @@
1
+ /**
2
+ * Standards Relevant Post Handler
3
+ *
4
+ * Returns top 10 relevant standards for a project based on detected characteristics.
5
+ * The session-start hook calls this endpoint with locally-detected project characteristics;
6
+ * the handler maps them to categories, queries the database, ranks results, and returns
7
+ * the most relevant standards for injection into the AI coding session.
8
+ *
9
+ * POST /api/standards/relevant
10
+ * Auth: Cognito JWT required
11
+ * Body: { characteristics, projectId?, preferences? }
12
+ */
13
+
14
+ const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse } = require('./helpers');
15
+
16
+ /**
17
+ * Category weights for relevance scoring
18
+ */
19
+ const CATEGORY_WEIGHTS = {
20
+ 'serverless-saas-aws': 1.0,
21
+ 'multi-agent-orchestration': 1.0,
22
+ 'frontend-development': 1.0,
23
+ 'database': 0.9,
24
+ 'compliance-security': 0.8,
25
+ 'cost-optimization': 0.7,
26
+ 'real-time-systems': 0.8,
27
+ 'testing': 0.6,
28
+ 'backend': 0.9,
29
+ 'well-architected': 0.7
30
+ };
31
+
32
+ /**
33
+ * Map project characteristics to relevant standard categories
34
+ */
35
+ function mapCharacteristicsToCategories(characteristics) {
36
+ const categories = new Set();
37
+
38
+ if (characteristics.hasLambda || characteristics.hasSAM) {
39
+ categories.add('serverless-saas-aws');
40
+ categories.add('cost-optimization');
41
+ }
42
+ if (characteristics.hasReact) {
43
+ categories.add('frontend-development');
44
+ }
45
+ if (characteristics.hasDatabase) {
46
+ categories.add('database');
47
+ }
48
+ if (characteristics.hasMultiAgent) {
49
+ categories.add('multi-agent-orchestration');
50
+ }
51
+ if (characteristics.hasAPI) {
52
+ categories.add('backend');
53
+ }
54
+ if (characteristics.hasTests) {
55
+ categories.add('testing');
56
+ }
57
+
58
+ // Always relevant
59
+ categories.add('compliance-security');
60
+ categories.add('well-architected');
61
+
62
+ return Array.from(categories);
63
+ }
64
+
65
+ /**
66
+ * Rank standards by relevance score
67
+ */
68
+ function rankStandards(standards) {
69
+ return standards.map(standard => {
70
+ let score = 0;
71
+
72
+ // Base score from correlation
73
+ score += (standard.correlation || 1.0) * 40;
74
+
75
+ // Maturity score
76
+ const maturityScores = { enforced: 30, validated: 20, recommended: 10, provisional: 5 };
77
+ score += maturityScores[standard.maturity] || 0;
78
+
79
+ // Category weight
80
+ const categoryWeight = CATEGORY_WEIGHTS[standard.category] || 0.5;
81
+ score += categoryWeight * 20;
82
+
83
+ // File applicability bonus
84
+ if (standard.applicable_files && standard.applicable_files.length > 0) {
85
+ score += 5;
86
+ }
87
+
88
+ // Cost impact bonus (critical patterns)
89
+ if (standard.cost_impact && standard.cost_impact.severity === 'critical') {
90
+ score += 10;
91
+ }
92
+
93
+ // Anti-patterns bonus (important to know what NOT to do)
94
+ if (standard.anti_patterns) {
95
+ const apCount = Array.isArray(standard.anti_patterns)
96
+ ? standard.anti_patterns.length
97
+ : Object.keys(standard.anti_patterns).length;
98
+ if (apCount > 0) score += 5;
99
+ }
100
+
101
+ return {
102
+ ...standard,
103
+ relevance_score: Math.round(score * 10) / 10
104
+ };
105
+ }).sort((a, b) => b.relevance_score - a.relevance_score);
106
+ }
107
+
108
+ /**
109
+ * Apply user preferences to filter standards
110
+ */
111
+ function applyPreferences(standards, preferences) {
112
+ if (!preferences) return standards;
113
+
114
+ const enabledCategories = preferences.enabled_categories || {};
115
+ const standardOverrides = preferences.standard_overrides || {};
116
+
117
+ return standards.filter(standard => {
118
+ const category = standard.category;
119
+ const standardPath = `${category}/${standard.element}`;
120
+
121
+ // Individual override takes priority
122
+ if (standardPath in standardOverrides) {
123
+ return standardOverrides[standardPath] === true;
124
+ }
125
+
126
+ // Category-level setting
127
+ if (category in enabledCategories) {
128
+ return enabledCategories[category] === true;
129
+ }
130
+
131
+ // Default: include
132
+ return true;
133
+ });
134
+ }
135
+
136
+ async function getRelevantStandards({ body, requestContext }) {
137
+ const email = requestContext.authorizer?.claims?.email
138
+ || requestContext.authorizer?.jwt?.claims?.email;
139
+
140
+ if (!email) {
141
+ return createErrorResponse(401, 'Authentication required');
142
+ }
143
+
144
+ // Parse request body
145
+ let requestBody;
146
+ try {
147
+ requestBody = typeof body === 'string' ? JSON.parse(body) : body;
148
+ } catch (e) {
149
+ return createErrorResponse(400, 'Invalid request body');
150
+ }
151
+
152
+ const { characteristics, preferences } = requestBody || {};
153
+
154
+ if (!characteristics || typeof characteristics !== 'object') {
155
+ return createErrorResponse(400, 'characteristics object is required');
156
+ }
157
+
158
+ // Map characteristics to categories
159
+ const categories = mapCharacteristicsToCategories(characteristics);
160
+
161
+ if (categories.length === 0) {
162
+ return createSuccessResponse({
163
+ standards: [],
164
+ categories: [],
165
+ total_matched: 0
166
+ }, 'No relevant categories detected');
167
+ }
168
+
169
+ // Query standards matching categories
170
+ const result = await executeQuery(`
171
+ SELECT
172
+ pattern_id,
173
+ element,
174
+ rule,
175
+ category,
176
+ correlation,
177
+ maturity,
178
+ applicable_files,
179
+ anti_patterns,
180
+ examples,
181
+ cost_impact,
182
+ source
183
+ FROM rapport.standards_patterns
184
+ WHERE category = ANY($1::varchar[])
185
+ AND maturity IN ('enforced', 'validated', 'recommended')
186
+ ORDER BY
187
+ CASE
188
+ WHEN maturity = 'enforced' THEN 1
189
+ WHEN maturity = 'validated' THEN 2
190
+ ELSE 3
191
+ END,
192
+ correlation DESC
193
+ `, [categories]);
194
+
195
+ // Rank by relevance
196
+ let ranked = rankStandards(result.rows);
197
+
198
+ // Apply preferences if provided
199
+ if (preferences) {
200
+ ranked = applyPreferences(ranked, preferences);
201
+ }
202
+
203
+ // Return top 10
204
+ const top = ranked.slice(0, 10);
205
+
206
+ return createSuccessResponse({
207
+ standards: top,
208
+ categories,
209
+ total_matched: result.rows.length
210
+ }, 'Relevant standards retrieved');
211
+ }
212
+
213
+ exports.handler = wrapHandler(getRelevantStandards);
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Standards Transition Handler
3
+ * Executes lifecycle state transitions on standards
4
+ *
5
+ * POST /api/standards/transition
6
+ * Body: { standard_id, action: 'approve'|'disable'|'deprecate'|'delete'|'enable'|'cancel_deprecation', reason? }
7
+ * Auth: Cognito JWT required
8
+ */
9
+
10
+ const { wrapHandler, createSuccessResponse, createErrorResponse } = require('./helpers');
11
+ const { StandardLifecycle } = require('./core/StandardLifecycle');
12
+
13
+ const lifecycle = new StandardLifecycle();
14
+
15
+ async function transitionStandard({ body, requestContext }) {
16
+ try {
17
+ const email = requestContext.authorizer?.claims?.email || requestContext.authorizer?.jwt?.claims?.email;
18
+
19
+ if (!email) {
20
+ return createErrorResponse(401, 'Authentication required');
21
+ }
22
+
23
+ const { standard_id: id, action, reason } = body || {};
24
+
25
+ if (!id) {
26
+ return createErrorResponse(400, 'standard_id is required');
27
+ }
28
+
29
+ if (!action) {
30
+ return createErrorResponse(400, 'action is required', {
31
+ valid_actions: ['propose', 'approve', 'reject', 'disable', 'deprecate', 'delete', 'enable', 'cancel_deprecation']
32
+ });
33
+ }
34
+
35
+ // Execute the transition
36
+ const result = await lifecycle.transition(id, action, email, reason);
37
+
38
+ return createSuccessResponse({
39
+ standard_id: result.standard_id,
40
+ old_state: result.old_state,
41
+ new_state: result.new_state,
42
+ action: result.action,
43
+ audit_entry: result.audit_entry
44
+ }, `Standard transitioned from '${result.old_state}' to '${result.new_state}'`);
45
+
46
+ } catch (error) {
47
+ console.error('Standards Transition Error:', error);
48
+
49
+ // Return user-friendly messages for known validation errors
50
+ if (error.message.includes('Invalid action')) {
51
+ return createErrorResponse(400, error.message);
52
+ }
53
+ if (error.message.includes('not found')) {
54
+ return createErrorResponse(404, error.message);
55
+ }
56
+ if (error.message.includes('Cannot perform')) {
57
+ return createErrorResponse(409, error.message);
58
+ }
59
+
60
+ return createErrorResponse(500, 'Failed to transition standard');
61
+ }
62
+ }
63
+
64
+ exports.handler = wrapHandler(transitionStandard);
@@ -0,0 +1,91 @@
1
+ /**
2
+ * User Splash Acknowledge Handler
3
+ * Records that the user has dismissed the weekly splash screen
4
+ *
5
+ * POST /api/user/splash/acknowledge
6
+ * Auth: Cognito JWT required
7
+ *
8
+ * Body: { week_start: "2026-01-27" }
9
+ * Records acknowledgment so splash does not show again this week
10
+ */
11
+
12
+ const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse, handleError } = require('./helpers');
13
+
14
+ async function acknowledgeUserSplash({ requestContext, body }) {
15
+ const Request_ID = requestContext.requestId;
16
+ const email = requestContext.authorizer?.claims?.email || requestContext.authorizer?.jwt?.claims?.email;
17
+
18
+ if (!email) {
19
+ return createErrorResponse(401, 'Authentication required');
20
+ }
21
+
22
+ const weekStart = body?.week_start;
23
+ if (!weekStart) {
24
+ return createErrorResponse(400, 'week_start is required');
25
+ }
26
+
27
+ // Validate week_start format (YYYY-MM-DD)
28
+ const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
29
+ if (!dateRegex.test(weekStart)) {
30
+ return createErrorResponse(400, 'week_start must be in YYYY-MM-DD format');
31
+ }
32
+
33
+ // Attempt to insert acknowledgment into dedicated table
34
+ try {
35
+ await executeQuery(`
36
+ INSERT INTO rapport.user_splash_acknowledgments (email_address, week_start, acknowledged_at)
37
+ VALUES ($1, $2, NOW())
38
+ ON CONFLICT (email_address, week_start) DO NOTHING
39
+ `, [email, weekStart]);
40
+
41
+ return createSuccessResponse(
42
+ { acknowledged: true, week_start: weekStart },
43
+ 'Splash acknowledged',
44
+ { Request_ID, Timestamp: new Date().toISOString() }
45
+ );
46
+ } catch (tableErr) {
47
+ // If the dedicated table does not exist, fall back to storing
48
+ // acknowledgment as metadata in the patterns table
49
+ console.log('[SplashAck] Dedicated table not available, using fallback:', tableErr.message);
50
+
51
+ try {
52
+ await executeQuery(`
53
+ INSERT INTO rapport.patterns (
54
+ pattern_type,
55
+ pattern_key,
56
+ pattern_value,
57
+ created_by,
58
+ created_at,
59
+ metadata
60
+ )
61
+ VALUES (
62
+ 'splash_ack',
63
+ $1,
64
+ $2,
65
+ $3,
66
+ NOW(),
67
+ $4
68
+ )
69
+ ON CONFLICT (pattern_type, pattern_key) WHERE pattern_type = 'splash_ack'
70
+ DO UPDATE SET
71
+ pattern_value = EXCLUDED.pattern_value,
72
+ created_at = NOW()
73
+ `, [
74
+ `splash_ack:${email}:${weekStart}`,
75
+ weekStart,
76
+ email,
77
+ JSON.stringify({ type: 'splash_acknowledgment', week_start: weekStart })
78
+ ]);
79
+
80
+ return createSuccessResponse(
81
+ { acknowledged: true, week_start: weekStart, fallback: true },
82
+ 'Splash acknowledged (fallback)',
83
+ { Request_ID, Timestamp: new Date().toISOString() }
84
+ );
85
+ } catch (fallbackErr) {
86
+ return handleError(fallbackErr);
87
+ }
88
+ }
89
+ }
90
+
91
+ exports.handler = wrapHandler(acknowledgeUserSplash);