@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.
Files changed (69) hide show
  1. package/README.md +1 -10
  2. package/hooks/pre-compact.js +213 -25
  3. package/hooks/session-start.js +636 -42
  4. package/hooks/subagent-start.js +150 -0
  5. package/hooks/subagent-stop.js +184 -0
  6. package/package.json +8 -7
  7. package/scripts/init-project.js +74 -33
  8. package/scripts/mcp-bridge.js +220 -0
  9. package/src/core/CorrelationAnalyzer.js +157 -0
  10. package/src/core/LLMPatternDetector.js +198 -0
  11. package/src/core/RelevanceDetector.js +123 -36
  12. package/src/core/StandardsIngestion.js +119 -18
  13. package/src/handlers/activity/activityGetMe.js +1 -1
  14. package/src/handlers/activity/activityGetTeam.js +100 -55
  15. package/src/handlers/admin/adminSetup.js +216 -0
  16. package/src/handlers/alerts/alertsAcknowledge.js +6 -6
  17. package/src/handlers/alerts/alertsGet.js +11 -11
  18. package/src/handlers/analytics/activitySummaryGet.js +34 -35
  19. package/src/handlers/analytics/coachingGet.js +11 -11
  20. package/src/handlers/analytics/convergenceGet.js +236 -0
  21. package/src/handlers/analytics/developerScoreGet.js +41 -111
  22. package/src/handlers/collaborators/collaboratorInvite.js +1 -1
  23. package/src/handlers/company/companyUsersDelete.js +141 -0
  24. package/src/handlers/company/companyUsersGet.js +90 -0
  25. package/src/handlers/company/companyUsersPost.js +267 -0
  26. package/src/handlers/company/companyUsersPut.js +76 -0
  27. package/src/handlers/correlations/correlationsDeveloperGet.js +12 -12
  28. package/src/handlers/correlations/correlationsGet.js +8 -8
  29. package/src/handlers/correlations/correlationsProjectGet.js +5 -5
  30. package/src/handlers/enterprise/controlTowerGet.js +224 -0
  31. package/src/handlers/enterprise/enterpriseOnboardingSetup.js +48 -9
  32. package/src/handlers/enterprise/enterpriseOnboardingStatus.js +1 -3
  33. package/src/handlers/github/githubConnectionStatus.js +1 -1
  34. package/src/handlers/github/githubDiscoverPatterns.js +4 -2
  35. package/src/handlers/github/githubPatternsReview.js +7 -36
  36. package/src/handlers/health/healthGet.js +55 -0
  37. package/src/handlers/helpers/checkSuperAdmin.js +13 -14
  38. package/src/handlers/helpers/subscriptionTiers.js +27 -27
  39. package/src/handlers/mcp/mcpHandler.js +569 -0
  40. package/src/handlers/mcp/mindmeldMcpHandler.js +689 -0
  41. package/src/handlers/notifications/sendNotification.js +18 -18
  42. package/src/handlers/patterns/patternEvaluatePromotionPost.js +173 -0
  43. package/src/handlers/projects/projectCreate.js +124 -10
  44. package/src/handlers/projects/projectDelete.js +4 -4
  45. package/src/handlers/projects/projectGet.js +8 -8
  46. package/src/handlers/projects/projectUpdate.js +4 -4
  47. package/src/handlers/reports/aiLeverage.js +34 -30
  48. package/src/handlers/reports/engineeringInvestment.js +16 -16
  49. package/src/handlers/reports/riskForecast.js +41 -21
  50. package/src/handlers/reports/standardsRoi.js +101 -9
  51. package/src/handlers/scheduled/maturityUpdateJob.js +166 -0
  52. package/src/handlers/sessions/sessionStandardsPost.js +43 -7
  53. package/src/handlers/standards/discoveriesGet.js +93 -0
  54. package/src/handlers/standards/projectStandardsGet.js +2 -2
  55. package/src/handlers/standards/projectStandardsPut.js +2 -2
  56. package/src/handlers/standards/standardsRelevantPost.js +107 -12
  57. package/src/handlers/standards/standardsTransition.js +112 -15
  58. package/src/handlers/stripe/billingPortalPost.js +1 -1
  59. package/src/handlers/stripe/enterpriseCheckoutPost.js +2 -2
  60. package/src/handlers/stripe/subscriptionCreatePost.js +2 -2
  61. package/src/handlers/stripe/webhookPost.js +42 -14
  62. package/src/handlers/user/apiTokenCreate.js +71 -0
  63. package/src/handlers/user/apiTokenList.js +64 -0
  64. package/src/handlers/user/userSplashGet.js +90 -73
  65. package/src/handlers/users/cognitoPostConfirmation.js +37 -1
  66. package/src/handlers/users/cognitoPreSignUp.js +114 -0
  67. package/src/handlers/users/userGet.js +12 -8
  68. package/src/handlers/webhooks/githubWebhook.js +117 -125
  69. package/src/index.js +46 -51
@@ -20,13 +20,13 @@ exports.handler = wrapHandler(async ({ requestContext, queryStringParameters })
20
20
  const period = params.period || '7d';
21
21
  const companyId = params.Company_ID;
22
22
 
23
- // Validate access - must be manager/admin (boolean columns in Tim-Combo schema)
23
+ // Validate access - must be manager/admin
24
24
  const accessCheck = await executeQuery(`
25
- SELECT ue."Company_ID"
26
- FROM "UserEntitlements" ue
27
- WHERE ue."Email_Address" = $1
28
- AND (ue."Admin" = true OR ue."Manager" = true)
29
- ${companyId ? 'AND ue."Company_ID" = $2' : ''}
25
+ SELECT ue.company_id
26
+ FROM rapport.user_entitlements ue
27
+ WHERE ue.email_address = $1
28
+ AND (ue.admin = true OR ue.manager = true)
29
+ ${companyId ? 'AND ue.company_id = $2' : ''}
30
30
  LIMIT 1
31
31
  `, companyId ? [email, companyId] : [email]);
32
32
 
@@ -34,7 +34,7 @@ exports.handler = wrapHandler(async ({ requestContext, queryStringParameters })
34
34
  return createErrorResponse(403, 'Manager or Admin access required');
35
35
  }
36
36
 
37
- const userCompanyId = companyId || accessCheck.rows[0].Company_ID;
37
+ const userCompanyId = companyId || accessCheck.rows[0].company_id;
38
38
  const periodDays = parsePeriod(period);
39
39
  const periodStart = new Date();
40
40
  periodStart.setDate(periodStart.getDate() - periodDays);
@@ -43,16 +43,16 @@ exports.handler = wrapHandler(async ({ requestContext, queryStringParameters })
43
43
  const investment = await executeQuery(`
44
44
  WITH commit_modules AS (
45
45
  SELECT
46
- COALESCE(SUBSTRING(c.file_path FROM '^([^/]+)'), 'root') as module,
46
+ c.repo_name as module,
47
47
  COUNT(*) as commit_count,
48
48
  SUM(c.lines_added) as lines_added,
49
49
  SUM(c.lines_removed) as lines_removed,
50
50
  COUNT(DISTINCT c.author_email) as contributors
51
51
  FROM rapport.commits c
52
52
  JOIN rapport.git_repositories r ON c.repo_id = r.repo_id
53
- WHERE r.Company_ID = $1
54
- AND c.committed_at >= $2
55
- GROUP BY module
53
+ WHERE r.company_id = $1
54
+ AND c.commit_timestamp >= $2
55
+ GROUP BY c.repo_name
56
56
  )
57
57
  SELECT
58
58
  module,
@@ -60,8 +60,8 @@ exports.handler = wrapHandler(async ({ requestContext, queryStringParameters })
60
60
  lines_added,
61
61
  lines_removed,
62
62
  contributors,
63
- ROUND(commit_count * 100.0 / SUM(commit_count) OVER (), 1) as commit_percentage,
64
- ROUND((lines_added + lines_removed) * 100.0 / SUM(lines_added + lines_removed) OVER (), 1) as churn_percentage
63
+ ROUND(commit_count * 100.0 / NULLIF(SUM(commit_count) OVER (), 0), 1) as commit_percentage,
64
+ ROUND((lines_added + lines_removed) * 100.0 / NULLIF(SUM(lines_added + lines_removed) OVER (), 0), 1) as churn_percentage
65
65
  FROM commit_modules
66
66
  ORDER BY commit_count DESC
67
67
  LIMIT 20
@@ -78,7 +78,7 @@ exports.handler = wrapHandler(async ({ requestContext, queryStringParameters })
78
78
  AVG(dm.compliance_score) as avg_compliance
79
79
  FROM rapport.developer_metrics dm
80
80
  JOIN rapport.git_repositories r ON dm.repo_id = r.repo_id
81
- WHERE r.Company_ID = $1
81
+ WHERE r.company_id = $1
82
82
  AND dm.period_start >= $2
83
83
  GROUP BY dm.developer_name, dm.developer_email
84
84
  ORDER BY total_commits DESC
@@ -93,8 +93,8 @@ exports.handler = wrapHandler(async ({ requestContext, queryStringParameters })
93
93
  COUNT(DISTINCT c.author_email) as contributors,
94
94
  SUM(c.lines_added) as lines_added
95
95
  FROM rapport.git_repositories r
96
- LEFT JOIN rapport.commits c ON r.repo_id = c.repo_id AND c.committed_at >= $2
97
- WHERE r.Company_ID = $1
96
+ LEFT JOIN rapport.commits c ON r.repo_id = c.repo_id AND c.commit_timestamp >= $2
97
+ WHERE r.company_id = $1
98
98
  GROUP BY r.repo_id, r.repo_name
99
99
  ORDER BY commit_count DESC
100
100
  `, [userCompanyId, periodStart]);
@@ -19,13 +19,13 @@ exports.handler = wrapHandler(async ({ requestContext, queryStringParameters })
19
19
  const params = queryStringParameters || {};
20
20
  const companyId = params.Company_ID;
21
21
 
22
- // Validate access - must be manager/admin (boolean columns in Tim-Combo schema)
22
+ // Validate access - must be manager/admin
23
23
  const accessCheck = await executeQuery(`
24
- SELECT ue."Company_ID"
25
- FROM "UserEntitlements" ue
26
- WHERE ue."Email_Address" = $1
27
- AND (ue."Admin" = true OR ue."Manager" = true)
28
- ${companyId ? 'AND ue."Company_ID" = $2' : ''}
24
+ SELECT ue.company_id
25
+ FROM rapport.user_entitlements ue
26
+ WHERE ue.email_address = $1
27
+ AND (ue.admin = true OR ue.manager = true)
28
+ ${companyId ? 'AND ue.company_id = $2' : ''}
29
29
  LIMIT 1
30
30
  `, companyId ? [email, companyId] : [email]);
31
31
 
@@ -33,7 +33,7 @@ exports.handler = wrapHandler(async ({ requestContext, queryStringParameters })
33
33
  return createErrorResponse(403, 'Manager or Admin access required');
34
34
  }
35
35
 
36
- const userCompanyId = companyId || accessCheck.rows[0].Company_ID;
36
+ const userCompanyId = companyId || accessCheck.rows[0].company_id;
37
37
 
38
38
  // Knowledge silos (bus factor analysis)
39
39
  let silos = { rows: [] };
@@ -47,7 +47,7 @@ exports.handler = wrapHandler(async ({ requestContext, queryStringParameters })
47
47
  ks.risk_level
48
48
  FROM rapport.knowledge_silos ks
49
49
  JOIN rapport.git_repositories r ON ks.repo_id = r.repo_id
50
- WHERE r."Company_ID" = $1
50
+ WHERE r.company_id = $1
51
51
  ORDER BY
52
52
  CASE ks.risk_level
53
53
  WHEN 'critical' THEN 1
@@ -74,7 +74,7 @@ exports.handler = wrapHandler(async ({ requestContext, queryStringParameters })
74
74
  pr.lines_added + pr.lines_removed as total_changes
75
75
  FROM rapport.pull_requests pr
76
76
  JOIN rapport.git_repositories r ON pr.repo_id = r.repo_id
77
- WHERE r."Company_ID" = $1
77
+ WHERE r.company_id = $1
78
78
  AND pr.status = 'open'
79
79
  AND pr.created_at < NOW() - INTERVAL '7 days'
80
80
  ORDER BY pr.created_at ASC
@@ -95,7 +95,7 @@ exports.handler = wrapHandler(async ({ requestContext, queryStringParameters })
95
95
  wp.peak_hour
96
96
  FROM rapport.working_patterns wp
97
97
  JOIN rapport.git_repositories r ON wp.repo_id = r.repo_id
98
- WHERE r."Company_ID" = $1
98
+ WHERE r.company_id = $1
99
99
  AND wp.period_start >= NOW() - INTERVAL '30 days'
100
100
  GROUP BY wp.developer_email, wp.peak_hour
101
101
  HAVING SUM(wp.after_hours_commits) > 10 OR SUM(wp.weekend_commits) > 5
@@ -114,21 +114,41 @@ exports.handler = wrapHandler(async ({ requestContext, queryStringParameters })
114
114
 
115
115
  const riskScore = Math.min(100, criticalSilos * 20 + highSilos * 10 + staleCount * 5 + burnoutCount * 5);
116
116
 
117
+ const riskLevel = riskScore >= 70 ? 'critical' : riskScore >= 40 ? 'elevated' : 'normal';
118
+
117
119
  return createSuccessResponse({
118
120
  report_type: 'risk_forecast',
119
121
  generated_at: new Date().toISOString(),
120
- risk_score: {
121
- value: riskScore,
122
- level: riskScore >= 70 ? 'critical' : riskScore >= 40 ? 'elevated' : 'normal',
123
- factors: {
124
- knowledge_silos: criticalSilos + highSilos,
125
- stale_prs: staleCount,
126
- burnout_indicators: burnoutCount
127
- }
122
+ overall_risk_score: riskScore,
123
+ risk_level: riskLevel,
124
+ summary: {
125
+ inactive_developers: 0,
126
+ high_velocity_devs: burnoutCount,
127
+ critical_silos: criticalSilos,
128
+ stale_prs: staleCount,
129
+ active_alerts: 0,
130
+ high_churn_files: 0
131
+ },
132
+ risks: {
133
+ knowledge_silos: silos.rows,
134
+ inactive_developers: [],
135
+ burnout_risk: burnoutRisk.rows.map(b => ({
136
+ email: b.developer_email,
137
+ name: b.developer_email,
138
+ commits_7d: 0,
139
+ weekend_commits: parseInt(b.weekend_commits) || 0,
140
+ after_hours: parseInt(b.after_hours_commits) || 0
141
+ })),
142
+ review_bottleneck: stalePRs.rows.map(pr => ({
143
+ repo: '',
144
+ pr_number: 0,
145
+ title: pr.title,
146
+ author: pr.author_email,
147
+ days_open: parseInt(pr.days_open) || 0
148
+ })),
149
+ high_churn_files: []
128
150
  },
129
- knowledge_silos: silos.rows,
130
- stale_pull_requests: stalePRs.rows,
131
- burnout_indicators: burnoutRisk.rows,
151
+ active_alerts: [],
132
152
  recommendations: generateRecommendations(silos.rows, stalePRs.rows, burnoutRisk.rows)
133
153
  });
134
154
  });
@@ -20,13 +20,13 @@ exports.handler = wrapHandler(async ({ requestContext, queryStringParameters })
20
20
  const period = params.period || '30d';
21
21
  const companyId = params.Company_ID;
22
22
 
23
- // Validate access - must be manager/admin (boolean columns in Tim-Combo schema)
23
+ // Validate access - must be manager/admin
24
24
  const accessCheck = await executeQuery(`
25
- SELECT ue."Company_ID"
26
- FROM "UserEntitlements" ue
27
- WHERE ue."Email_Address" = $1
28
- AND (ue."Admin" = true OR ue."Manager" = true)
29
- ${companyId ? 'AND ue."Company_ID" = $2' : ''}
25
+ SELECT ue.company_id
26
+ FROM rapport.user_entitlements ue
27
+ WHERE ue.email_address = $1
28
+ AND (ue.admin = true OR ue.manager = true)
29
+ ${companyId ? 'AND ue.company_id = $2' : ''}
30
30
  LIMIT 1
31
31
  `, companyId ? [email, companyId] : [email]);
32
32
 
@@ -34,7 +34,7 @@ exports.handler = wrapHandler(async ({ requestContext, queryStringParameters })
34
34
  return createErrorResponse(403, 'Manager or Admin access required');
35
35
  }
36
36
 
37
- const userCompanyId = companyId || accessCheck.rows[0].Company_ID;
37
+ const userCompanyId = companyId || accessCheck.rows[0].company_id;
38
38
  const periodDays = parsePeriod(period);
39
39
  const periodStart = new Date();
40
40
  periodStart.setDate(periodStart.getDate() - periodDays);
@@ -52,7 +52,7 @@ exports.handler = wrapHandler(async ({ requestContext, queryStringParameters })
52
52
  SUM(dm.anti_pattern_commits) as anti_pattern_commits
53
53
  FROM rapport.developer_metrics dm
54
54
  JOIN rapport.git_repositories r ON dm.repo_id = r.repo_id
55
- WHERE r."Company_ID" = $1
55
+ WHERE r.company_id = $1
56
56
  AND dm.period_start >= $2
57
57
  GROUP BY dm.developer_email, dm.developer_name
58
58
  HAVING SUM(dm.commit_count) > 0
@@ -91,6 +91,95 @@ exports.handler = wrapHandler(async ({ requestContext, queryStringParameters })
91
91
  const compliantCommits = complianceData.reduce((sum, r) => sum + parseInt(r.compliant_commits || 0), 0);
92
92
  const antiPatternCommits = complianceData.reduce((sum, r) => sum + parseInt(r.anti_pattern_commits || 0), 0);
93
93
 
94
+ // Get standards shown count from session_standards
95
+ let standardsShown = 0;
96
+ try {
97
+ const shownResult = await executeQuery(`
98
+ SELECT COUNT(*) as total
99
+ FROM rapport.session_standards ss
100
+ JOIN rapport.sessions s ON ss.session_id = s.session_id
101
+ JOIN rapport.projects p ON s.project_id = p.project_id
102
+ WHERE p.company_id = $1
103
+ AND s.started_at >= $2
104
+ `, [userCompanyId, periodStart]);
105
+ standardsShown = parseInt(shownResult.rows[0]?.total) || 0;
106
+ } catch (e) {
107
+ // Table might not have data
108
+ }
109
+
110
+ // Adoption trend — standards injected per day
111
+ let adoptionTrend = [];
112
+ try {
113
+ const trendResult = await executeQuery(`
114
+ SELECT
115
+ DATE(ss.created_at) as date,
116
+ COUNT(*) as standards_shown,
117
+ COUNT(DISTINCT ss.session_id) as sessions,
118
+ COUNT(CASE WHEN ss.followed = true THEN 1 END) as followed,
119
+ COUNT(CASE WHEN ss.violated = true THEN 1 END) as violated
120
+ FROM rapport.session_standards ss
121
+ JOIN rapport.sessions s ON ss.session_id = s.session_id
122
+ JOIN rapport.projects p ON s.project_id = p.project_id
123
+ WHERE p.company_id = $1
124
+ AND ss.created_at >= $2
125
+ GROUP BY DATE(ss.created_at)
126
+ ORDER BY date
127
+ `, [userCompanyId, periodStart]);
128
+ adoptionTrend = trendResult.rows;
129
+ } catch (e) {
130
+ // Table might not have data
131
+ }
132
+
133
+ // Top standards — most frequently injected
134
+ let topStandards = [];
135
+ try {
136
+ const topResult = await executeQuery(`
137
+ SELECT
138
+ ss.standard_id,
139
+ ss.standard_name,
140
+ COUNT(*) as times_shown,
141
+ COUNT(DISTINCT ss.session_id) as unique_sessions,
142
+ ROUND(AVG(ss.relevance_score), 2) as avg_relevance,
143
+ COUNT(CASE WHEN ss.followed = true THEN 1 END) as times_followed,
144
+ COUNT(CASE WHEN ss.violated = true THEN 1 END) as times_violated
145
+ FROM rapport.session_standards ss
146
+ JOIN rapport.sessions s ON ss.session_id = s.session_id
147
+ JOIN rapport.projects p ON s.project_id = p.project_id
148
+ WHERE p.company_id = $1
149
+ AND ss.created_at >= $2
150
+ GROUP BY ss.standard_id, ss.standard_name
151
+ ORDER BY times_shown DESC
152
+ LIMIT 10
153
+ `, [userCompanyId, periodStart]);
154
+ topStandards = topResult.rows;
155
+ } catch (e) {
156
+ // Table might not have data
157
+ }
158
+
159
+ // Anti-patterns detected — standards that were violated
160
+ let antiPatternsDetected = [];
161
+ try {
162
+ const violationResult = await executeQuery(`
163
+ SELECT
164
+ ss.standard_id,
165
+ ss.standard_name,
166
+ COUNT(*) as violation_count,
167
+ MAX(ss.created_at) as last_violated
168
+ FROM rapport.session_standards ss
169
+ JOIN rapport.sessions s ON ss.session_id = s.session_id
170
+ JOIN rapport.projects p ON s.project_id = p.project_id
171
+ WHERE p.company_id = $1
172
+ AND ss.created_at >= $2
173
+ AND ss.violated = true
174
+ GROUP BY ss.standard_id, ss.standard_name
175
+ ORDER BY violation_count DESC
176
+ LIMIT 10
177
+ `, [userCompanyId, periodStart]);
178
+ antiPatternsDetected = violationResult.rows;
179
+ } catch (e) {
180
+ // Table might not have data
181
+ }
182
+
94
183
  return createSuccessResponse({
95
184
  report_type: 'standards_roi',
96
185
  period: period,
@@ -103,9 +192,12 @@ exports.handler = wrapHandler(async ({ requestContext, queryStringParameters })
103
192
  compliance_rate: totalCommits > 0 ? ((compliantCommits / totalCommits) * 100).toFixed(1) : '0',
104
193
  anti_pattern_commits: antiPatternCommits,
105
194
  anti_pattern_rate: totalCommits > 0 ? ((antiPatternCommits / totalCommits) * 100).toFixed(1) : '0',
106
- patterns_configured: patterns.rows.length
195
+ standards_shown: standardsShown
107
196
  },
197
+ adoption_trend: adoptionTrend,
108
198
  compliance_by_developer: complianceData,
199
+ top_standards: topStandards,
200
+ anti_patterns_detected: antiPatternsDetected,
109
201
  configured_patterns: patterns.rows.filter(p => p.pattern_type === 'standard'),
110
202
  anti_patterns: patterns.rows.filter(p => p.pattern_type === 'anti_pattern'),
111
203
  insights: generateInsights(complianceData)
@@ -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
- // This may fail if project_id is not provided or invalid - that's OK
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
- // Session insert failed (FK constraint probably)
63
- // Continue anyway - we'll try to record standards
64
- console.error('[sessionStandardsPost] Session upsert failed:', sessionError.message);
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
- shown_at
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
- shown_at = NOW()
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