@equilateral_ai/mindmeld 3.5.2 → 4.0.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 (140) hide show
  1. package/hooks/session-end.js +25 -0
  2. package/hooks/session-start.js +363 -83
  3. package/hooks/session-watcher.js +585 -0
  4. package/package.json +19 -13
  5. package/scripts/init-project.js +9 -23
  6. package/src/client/dbShim.js +16 -0
  7. package/src/core/AuthManager.js +3 -2
  8. package/src/handlers/helpers/dbOperations.js +9 -46
  9. package/src/index.js +2 -217
  10. package/src/utils/piiMask.js +16 -0
  11. package/scripts/harvest.js +0 -601
  12. package/scripts/inject.js +0 -409
  13. package/scripts/mcp-bridge.js +0 -220
  14. package/scripts/repo-analyzer.js +0 -870
  15. package/src/collaboration/CollaborationPrompt.js +0 -460
  16. package/src/core/AlertEngine.js +0 -813
  17. package/src/core/AlertNotifier.js +0 -363
  18. package/src/core/CorrelationAnalyzer.js +0 -931
  19. package/src/core/CrossReferenceEngine.js +0 -624
  20. package/src/core/CurationEngine.js +0 -688
  21. package/src/core/DeprecationScheduler.js +0 -183
  22. package/src/core/LoadBearingDetector.js +0 -242
  23. package/src/core/NotificationService.js +0 -1032
  24. package/src/core/RapportOrchestrator.js +0 -632
  25. package/src/core/RelevanceDetector.js +0 -694
  26. package/src/core/StandardLifecycle.js +0 -244
  27. package/src/core/StandardsIngestion.js +0 -991
  28. package/src/core/TeamLoadBearingDetector.js +0 -431
  29. package/src/core/parsers/adrParser.js +0 -479
  30. package/src/core/parsers/cursorRulesParser.js +0 -564
  31. package/src/core/parsers/eslintParser.js +0 -439
  32. package/src/database/dbOperations.js +0 -105
  33. package/src/handlers/activity/activityGetMe.js +0 -98
  34. package/src/handlers/activity/activityGetTeam.js +0 -175
  35. package/src/handlers/admin/adminSetup.js +0 -216
  36. package/src/handlers/alerts/alertsAcknowledge.js +0 -92
  37. package/src/handlers/alerts/alertsGet.js +0 -250
  38. package/src/handlers/analytics/activitySummaryGet.js +0 -234
  39. package/src/handlers/analytics/coachingGet.js +0 -361
  40. package/src/handlers/analytics/convergenceGet.js +0 -236
  41. package/src/handlers/analytics/developerScoreGet.js +0 -137
  42. package/src/handlers/collaborators/collaboratorAdd.js +0 -200
  43. package/src/handlers/collaborators/collaboratorInvite.js +0 -219
  44. package/src/handlers/collaborators/collaboratorList.js +0 -82
  45. package/src/handlers/collaborators/collaboratorRemove.js +0 -128
  46. package/src/handlers/collaborators/inviteAccept.js +0 -122
  47. package/src/handlers/company/companyUsersDelete.js +0 -141
  48. package/src/handlers/company/companyUsersGet.js +0 -90
  49. package/src/handlers/company/companyUsersPost.js +0 -267
  50. package/src/handlers/company/companyUsersPut.js +0 -76
  51. package/src/handlers/context/contextGet.js +0 -57
  52. package/src/handlers/context/invariantsGet.js +0 -74
  53. package/src/handlers/context/loopsGet.js +0 -82
  54. package/src/handlers/context/notesCreate.js +0 -74
  55. package/src/handlers/context/purposeGet.js +0 -78
  56. package/src/handlers/correlations/correlationsDeveloperGet.js +0 -227
  57. package/src/handlers/correlations/correlationsGet.js +0 -93
  58. package/src/handlers/correlations/correlationsProjectGet.js +0 -153
  59. package/src/handlers/enterprise/controlTowerGet.js +0 -224
  60. package/src/handlers/enterprise/enterpriseAuditGet.js +0 -108
  61. package/src/handlers/enterprise/enterpriseContributorsGet.js +0 -85
  62. package/src/handlers/enterprise/enterpriseKnowledgeCategoriesGet.js +0 -53
  63. package/src/handlers/enterprise/enterpriseKnowledgeCreate.js +0 -77
  64. package/src/handlers/enterprise/enterpriseKnowledgeDelete.js +0 -71
  65. package/src/handlers/enterprise/enterpriseKnowledgeGet.js +0 -87
  66. package/src/handlers/enterprise/enterpriseKnowledgeUpdate.js +0 -122
  67. package/src/handlers/enterprise/enterpriseOnboardingComplete.js +0 -77
  68. package/src/handlers/enterprise/enterpriseOnboardingInvite.js +0 -138
  69. package/src/handlers/enterprise/enterpriseOnboardingSetup.js +0 -128
  70. package/src/handlers/enterprise/enterpriseOnboardingStatus.js +0 -88
  71. package/src/handlers/github/githubConnectionStatus.js +0 -49
  72. package/src/handlers/github/githubDiscoverPatterns.js +0 -621
  73. package/src/handlers/github/githubOAuthCallback.js +0 -178
  74. package/src/handlers/github/githubOAuthStart.js +0 -59
  75. package/src/handlers/github/githubPatternsReview.js +0 -76
  76. package/src/handlers/github/githubReposList.js +0 -105
  77. package/src/handlers/health/healthGet.js +0 -55
  78. package/src/handlers/helpers/auditLogger.js +0 -201
  79. package/src/handlers/helpers/checkSuperAdmin.js +0 -84
  80. package/src/handlers/helpers/decisionFrames.js +0 -29
  81. package/src/handlers/helpers/errorHandler.js +0 -49
  82. package/src/handlers/helpers/index.js +0 -138
  83. package/src/handlers/helpers/lambdaWrapper.js +0 -60
  84. package/src/handlers/helpers/mindmeldMcpCore.js +0 -1103
  85. package/src/handlers/helpers/predictiveCache.js +0 -51
  86. package/src/handlers/helpers/projectAccess.js +0 -88
  87. package/src/handlers/helpers/responseUtil.js +0 -55
  88. package/src/handlers/helpers/subscriptionTiers.js +0 -1168
  89. package/src/handlers/mcp/mcpHandler.js +0 -569
  90. package/src/handlers/mcp/mindmeldMcpHandler.js +0 -124
  91. package/src/handlers/mcp/mindmeldMcpStreamHandler.js +0 -342
  92. package/src/handlers/notifications/getPreferences.js +0 -84
  93. package/src/handlers/notifications/sendNotification.js +0 -170
  94. package/src/handlers/notifications/updatePreferences.js +0 -316
  95. package/src/handlers/patterns/patternEvaluatePromotionPost.js +0 -173
  96. package/src/handlers/patterns/patternUsagePost.js +0 -182
  97. package/src/handlers/patterns/patternViolationPost.js +0 -185
  98. package/src/handlers/projects/projectCreate.js +0 -248
  99. package/src/handlers/projects/projectDelete.js +0 -82
  100. package/src/handlers/projects/projectGet.js +0 -95
  101. package/src/handlers/projects/projectUpdate.js +0 -117
  102. package/src/handlers/reports/aiLeverage.js +0 -210
  103. package/src/handlers/reports/engineeringInvestment.js +0 -132
  104. package/src/handlers/reports/riskForecast.js +0 -206
  105. package/src/handlers/reports/standardsRoi.js +0 -254
  106. package/src/handlers/scheduled/analyzeCorrelations.js +0 -178
  107. package/src/handlers/scheduled/analyzeGitHistory.js +0 -510
  108. package/src/handlers/scheduled/generateAlerts.js +0 -135
  109. package/src/handlers/scheduled/maturityUpdateJob.js +0 -166
  110. package/src/handlers/scheduled/refreshActivity.js +0 -21
  111. package/src/handlers/scheduled/scanCompliance.js +0 -334
  112. package/src/handlers/sessions/sessionEndPost.js +0 -180
  113. package/src/handlers/sessions/sessionStandardsPost.js +0 -171
  114. package/src/handlers/standards/catalogGet.js +0 -185
  115. package/src/handlers/standards/catalogSync.js +0 -120
  116. package/src/handlers/standards/discoveriesGet.js +0 -89
  117. package/src/handlers/standards/projectStandardsGet.js +0 -129
  118. package/src/handlers/standards/projectStandardsPut.js +0 -151
  119. package/src/handlers/standards/standardsAuditGet.js +0 -65
  120. package/src/handlers/standards/standardsParseUpload.js +0 -149
  121. package/src/handlers/standards/standardsRelevantPost.js +0 -405
  122. package/src/handlers/standards/standardsTransition.js +0 -161
  123. package/src/handlers/stripe/addonManagePost.js +0 -240
  124. package/src/handlers/stripe/billingPortalPost.js +0 -93
  125. package/src/handlers/stripe/enterpriseCheckoutPost.js +0 -272
  126. package/src/handlers/stripe/seatsUpdatePost.js +0 -185
  127. package/src/handlers/stripe/subscriptionCancelDelete.js +0 -169
  128. package/src/handlers/stripe/subscriptionCreatePost.js +0 -221
  129. package/src/handlers/stripe/subscriptionUpdatePut.js +0 -163
  130. package/src/handlers/stripe/webhookPost.js +0 -482
  131. package/src/handlers/user/apiTokenCreate.js +0 -71
  132. package/src/handlers/user/apiTokenList.js +0 -64
  133. package/src/handlers/user/userSplashAck.js +0 -91
  134. package/src/handlers/user/userSplashGet.js +0 -211
  135. package/src/handlers/users/cognitoPostConfirmation.js +0 -186
  136. package/src/handlers/users/cognitoPreSignUp.js +0 -114
  137. package/src/handlers/users/userEntitlementsGet.js +0 -89
  138. package/src/handlers/users/userGet.js +0 -118
  139. package/src/handlers/users/userProfilePut.js +0 -77
  140. package/src/handlers/webhooks/githubWebhook.js +0 -215
@@ -1,991 +0,0 @@
1
- /**
2
- * StandardsIngestion.js - Phase 1 of Rapport Standards Integration
3
- *
4
- * Parses and ingests .equilateral-standards/ YAML files into Rapport database
5
- * Creates standards_patterns from rules, anti-patterns, examples, and cost impacts
6
- *
7
- * YAML Schema (equilateral-standards-yaml):
8
- * id, category, priority (10/20/30), updated
9
- * rules: [{ action: ALWAYS|NEVER|USE|PREFER|AVOID, rule: string, applies_to: [globs] }]
10
- * anti_patterns: [strings]
11
- * examples: { name: code }
12
- * cost_impact: { pattern: impact }
13
- * tags: [strings]
14
- *
15
- * Reference: /Users/jamesford/Source/rapport/docs/RAPPORT_STANDARDS_INTEGRATION_DESIGN.md
16
- */
17
-
18
- const fs = require('fs').promises;
19
- const path = require('path');
20
- const yaml = require('js-yaml');
21
- const { executeQuery } = require('../handlers/helpers/dbOperations');
22
-
23
- class StandardsIngestion {
24
- constructor(options = {}) {
25
- this.standardsPath = options.standardsPath || path.join(process.cwd(), '.equilateral-standards');
26
- this.verbose = options.verbose || false;
27
- }
28
-
29
- /**
30
- * Main ingestion entry point
31
- * Discovers and parses all .equilateral-standards/ YAML files (with markdown fallback)
32
- */
33
- async ingestEquilateralStandards() {
34
- console.log(`[StandardsIngestion] Starting ingestion from ${this.standardsPath}`);
35
-
36
- const startTime = Date.now();
37
- const patterns = [];
38
-
39
- // Define standard categories
40
- const categories = [
41
- 'serverless-saas-aws',
42
- 'multi-agent-orchestration',
43
- 'compliance-security',
44
- 'database',
45
- 'frontend-development',
46
- 'cost-optimization',
47
- 'backend',
48
- 'deployment',
49
- 'real-time-systems',
50
- 'testing',
51
- 'well-architected'
52
- ];
53
-
54
- // Discover and parse all standards files
55
- for (const category of categories) {
56
- const categoryPath = path.join(this.standardsPath, category);
57
-
58
- try {
59
- const files = await this.discoverStandardFiles(categoryPath);
60
-
61
- for (const file of files) {
62
- try {
63
- const standard = await this.parseStandard(file, category);
64
-
65
- if (standard && standard.patterns.length > 0) {
66
- patterns.push(...standard.patterns);
67
-
68
- if (this.verbose) {
69
- console.log(` ✓ Parsed ${path.basename(file)}: ${standard.patterns.length} patterns`);
70
- }
71
- }
72
- } catch (parseError) {
73
- console.warn(` ⚠ Failed to parse ${path.basename(file)}:`, parseError.message);
74
- }
75
- }
76
- } catch (categoryError) {
77
- if (categoryError.code !== 'ENOENT') {
78
- console.warn(` ⚠ Failed to process category ${category}:`, categoryError.message);
79
- }
80
- }
81
- }
82
-
83
- // Store in database
84
- const ingestedCount = await this.seedPatternDatabase(patterns);
85
-
86
- const duration = ((Date.now() - startTime) / 1000).toFixed(2);
87
-
88
- return {
89
- patternsIngested: ingestedCount,
90
- totalPatternsFound: patterns.length,
91
- categories: categories.length,
92
- timestamp: new Date().toISOString(),
93
- duration: `${duration}s`
94
- };
95
- }
96
-
97
- /**
98
- * Parse all standards from YAML files without storing in database.
99
- * Used as a fallback when the database is unavailable (e.g., local dev).
100
- * @returns {Promise<Array>} Parsed patterns array
101
- */
102
- async parseAllStandards() {
103
- const patterns = [];
104
-
105
- const categories = [
106
- 'serverless-saas-aws',
107
- 'multi-agent-orchestration',
108
- 'compliance-security',
109
- 'database',
110
- 'frontend-development',
111
- 'cost-optimization',
112
- 'backend',
113
- 'deployment',
114
- 'real-time-systems',
115
- 'testing',
116
- 'well-architected'
117
- ];
118
-
119
- for (const category of categories) {
120
- const categoryPath = path.join(this.standardsPath, category);
121
-
122
- try {
123
- const files = await this.discoverStandardFiles(categoryPath);
124
-
125
- for (const file of files) {
126
- try {
127
- const standard = await this.parseStandard(file, category);
128
- if (standard && standard.patterns.length > 0) {
129
- patterns.push(...standard.patterns);
130
- }
131
- } catch (parseError) {
132
- // Skip unparseable files
133
- }
134
- }
135
- } catch (categoryError) {
136
- if (categoryError.code !== 'ENOENT') {
137
- // Skip missing categories silently
138
- }
139
- }
140
- }
141
-
142
- return patterns;
143
- }
144
-
145
- /**
146
- * Discover all standard files in a category directory
147
- * Prefers YAML files over markdown - if both exist for the same base name, only YAML is returned
148
- */
149
- async discoverStandardFiles(categoryPath) {
150
- try {
151
- const entries = await fs.readdir(categoryPath, { withFileTypes: true });
152
- const fileMap = new Map(); // baseName -> { yaml: path, md: path }
153
-
154
- for (const entry of entries) {
155
- const fullPath = path.join(categoryPath, entry.name);
156
-
157
- if (entry.isDirectory()) {
158
- // Recursively search subdirectories
159
- const subFiles = await this.discoverStandardFiles(fullPath);
160
- // Add subdirectory files directly (they've already been deduplicated)
161
- for (const subFile of subFiles) {
162
- const subBaseName = path.basename(subFile).replace(/\.(yaml|yml|md)$/, '');
163
- const subKey = `${path.dirname(subFile)}/${subBaseName}`;
164
- if (!fileMap.has(subKey)) {
165
- fileMap.set(subKey, {});
166
- }
167
- if (subFile.endsWith('.yaml') || subFile.endsWith('.yml')) {
168
- fileMap.get(subKey).yaml = subFile;
169
- } else if (subFile.endsWith('.md')) {
170
- fileMap.get(subKey).md = subFile;
171
- }
172
- }
173
- } else if (entry.isFile()) {
174
- const isYaml = entry.name.endsWith('.yaml') || entry.name.endsWith('.yml');
175
- const isMd = entry.name.endsWith('.md');
176
-
177
- if (isYaml || isMd) {
178
- const baseName = entry.name.replace(/\.(yaml|yml|md)$/, '');
179
- const key = `${categoryPath}/${baseName}`;
180
-
181
- if (!fileMap.has(key)) {
182
- fileMap.set(key, {});
183
- }
184
-
185
- if (isYaml) {
186
- fileMap.get(key).yaml = fullPath;
187
- } else {
188
- fileMap.get(key).md = fullPath;
189
- }
190
- }
191
- }
192
- }
193
-
194
- // Build final list, preferring YAML over markdown
195
- const files = [];
196
- for (const paths of fileMap.values()) {
197
- if (paths.yaml) {
198
- files.push(paths.yaml);
199
- } else if (paths.md) {
200
- files.push(paths.md);
201
- }
202
- }
203
-
204
- return files;
205
- } catch (error) {
206
- if (error.code === 'ENOENT') {
207
- // Category doesn't exist - not an error
208
- return [];
209
- }
210
- throw error;
211
- }
212
- }
213
-
214
- /**
215
- * Parse a standards file into Rapport pattern format
216
- * Detects file type and routes to appropriate parser
217
- */
218
- async parseStandard(filepath, category) {
219
- const isYaml = filepath.endsWith('.yaml') || filepath.endsWith('.yml');
220
-
221
- if (isYaml) {
222
- return this.parseYamlStandard(filepath, category);
223
- }
224
-
225
- return this.parseMarkdownStandard(filepath, category);
226
- }
227
-
228
- /**
229
- * Parse a YAML standards file into Rapport pattern format
230
- * Supports both type: standard (v2.0) and type: workflow (v3.0)
231
- *
232
- * YAML format (standard):
233
- * id, category, priority, rules[], anti_patterns[]
234
- *
235
- * YAML format (workflow):
236
- * id, category, priority, type: workflow, trigger, steps[], preconditions[], anti_patterns[]
237
- */
238
- async parseYamlStandard(filepath, category) {
239
- const content = await fs.readFile(filepath, 'utf-8');
240
- const ext = filepath.endsWith('.yml') ? '.yml' : '.yaml';
241
- const filename = path.basename(filepath, ext);
242
-
243
- let doc;
244
- try {
245
- doc = yaml.load(content);
246
- } catch (parseError) {
247
- throw new Error(`YAML parse error: ${parseError.message}`);
248
- }
249
-
250
- if (!doc || typeof doc !== 'object') {
251
- throw new Error('Invalid YAML structure: expected an object');
252
- }
253
-
254
- // Route to workflow parser if type: workflow
255
- if (doc.type === 'workflow') {
256
- return this.parseYamlWorkflow(doc, filepath, filename, category);
257
- }
258
-
259
- const patterns = [];
260
- const standardId = doc.id || filename;
261
- const standardCategory = doc.category || category;
262
- const standardPriority = doc.priority || 30;
263
-
264
- // Extract anti-patterns as simple strings
265
- const antiPatterns = Array.isArray(doc.anti_patterns)
266
- ? doc.anti_patterns.map(ap => ({
267
- description: typeof ap === 'string' ? ap : ap.description || 'Anti-pattern to avoid',
268
- code: typeof ap === 'object' ? ap.code : null,
269
- language: typeof ap === 'object' ? ap.language : null
270
- }))
271
- : [];
272
-
273
- // Extract examples - handles both array of strings and named object formats
274
- const examples = [];
275
- if (doc.examples) {
276
- if (Array.isArray(doc.examples)) {
277
- // Array format: ["code example 1", "code example 2"]
278
- for (const example of doc.examples) {
279
- if (typeof example === 'string') {
280
- examples.push({
281
- description: 'Example',
282
- code: example.trim(),
283
- language: this.inferLanguageFromCode(example)
284
- });
285
- } else if (typeof example === 'object' && example !== null) {
286
- // Object in array: { description: "...", code: "..." }
287
- examples.push({
288
- description: example.description || 'Example',
289
- code: (example.code || '').trim(),
290
- language: example.language || this.inferLanguageFromCode(example.code || '')
291
- });
292
- }
293
- }
294
- } else if (typeof doc.examples === 'object') {
295
- // Named object format: { example_name: "code here" }
296
- for (const [name, code] of Object.entries(doc.examples)) {
297
- examples.push({
298
- description: name.replace(/_/g, ' '),
299
- code: typeof code === 'string' ? code.trim() : String(code),
300
- language: this.inferLanguageFromCode(code)
301
- });
302
- }
303
- }
304
- }
305
-
306
- // Extract cost_impact - now a structured object in YAML format
307
- const costImpact = doc.cost_impact && typeof doc.cost_impact === 'object'
308
- ? doc.cost_impact
309
- : (typeof doc.cost_impact === 'string' ? { description: doc.cost_impact } : null);
310
-
311
- // Extract tags for metadata
312
- const tags = Array.isArray(doc.tags) ? doc.tags : [];
313
-
314
- // Process each rule
315
- const rules = Array.isArray(doc.rules) ? doc.rules : [];
316
-
317
- for (let ruleIndex = 0; ruleIndex < rules.length; ruleIndex++) {
318
- const rule = rules[ruleIndex];
319
- if (!rule || typeof rule !== 'object') continue;
320
-
321
- const action = rule.action || 'USE';
322
- const ruleText = rule.rule || '';
323
- const appliesTo = Array.isArray(rule.applies_to) ? rule.applies_to : [];
324
-
325
- // Construct element name from action and first few words
326
- const element = this.extractElement(`${action} ${ruleText}`);
327
-
328
- // Construct full rule statement
329
- const fullRule = `${action}: ${ruleText}`;
330
-
331
- const patternId = this.generatePatternId(standardCategory, standardId, element, ruleIndex);
332
-
333
- // Map action to maturity
334
- const actionMaturityMap = {
335
- 'ALWAYS': 'enforced',
336
- 'NEVER': 'enforced',
337
- 'USE': 'validated',
338
- 'PREFER': 'validated',
339
- 'AVOID': 'validated'
340
- };
341
- const maturity = actionMaturityMap[action] || 'validated';
342
-
343
- patterns.push({
344
- pattern_id: patternId,
345
- file_name: filename,
346
- element: element,
347
- rule: fullRule,
348
- priority: standardPriority,
349
- correlation: 1.0,
350
- source: 'equilateral-standards',
351
- maturity: maturity,
352
- scope: 'organization',
353
- category: standardCategory,
354
- applicable_files: appliesTo.length > 0 ? appliesTo : this.extractApplicableFiles('', standardCategory),
355
- anti_patterns: antiPatterns,
356
- examples: examples,
357
- cost_impact: costImpact,
358
- tags: tags,
359
- source_file: filepath,
360
- title: standardId
361
- });
362
- }
363
-
364
- // If no rules found but document exists, create a single pattern from the document
365
- if (patterns.length === 0 && doc.id) {
366
- const patternId = this.generatePatternId(standardCategory, standardId, 'standard_pattern');
367
-
368
- patterns.push({
369
- pattern_id: patternId,
370
- file_name: filename,
371
- element: 'Standard Pattern',
372
- rule: `Standard: ${standardId}`,
373
- priority: standardPriority,
374
- correlation: 1.0,
375
- source: 'equilateral-standards',
376
- maturity: 'validated',
377
- scope: 'organization',
378
- category: standardCategory,
379
- applicable_files: this.extractApplicableFiles('', standardCategory),
380
- anti_patterns: antiPatterns,
381
- examples: examples,
382
- cost_impact: costImpact,
383
- tags: tags,
384
- source_file: filepath,
385
- title: standardId
386
- });
387
- }
388
-
389
- return { patterns };
390
- }
391
-
392
- /**
393
- * Parse a workflow YAML file into a single Rapport pattern with workflow metadata
394
- * Workflows are stored as one pattern (not exploded into per-rule patterns)
395
- * so the injection system can render them as ordered procedures.
396
- */
397
- parseYamlWorkflow(doc, filepath, filename, category) {
398
- const standardId = doc.id || filename;
399
- const standardCategory = doc.category || category;
400
- const standardPriority = doc.priority || 10; // Workflows default to high priority
401
-
402
- const antiPatterns = Array.isArray(doc.anti_patterns)
403
- ? doc.anti_patterns.map(ap => ({
404
- description: typeof ap === 'string' ? ap : ap.description || 'Anti-pattern to avoid',
405
- code: typeof ap === 'object' ? ap.code : null,
406
- language: typeof ap === 'object' ? ap.language : null
407
- }))
408
- : [];
409
-
410
- const tags = Array.isArray(doc.tags) ? doc.tags : [];
411
- const costImpact = doc.cost_impact && typeof doc.cost_impact === 'object'
412
- ? doc.cost_impact
413
- : (typeof doc.cost_impact === 'string' ? { description: doc.cost_impact } : null);
414
-
415
- const steps = Array.isArray(doc.steps) ? doc.steps : [];
416
- const preconditions = Array.isArray(doc.preconditions) ? doc.preconditions : [];
417
- const trigger = doc.trigger || '';
418
- const related = Array.isArray(doc.related) ? doc.related : [];
419
-
420
- const patternId = this.generatePatternId(standardCategory, standardId, 'workflow');
421
-
422
- // Build a readable element name from the id
423
- const element = standardId.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
424
-
425
- // The rule field contains the trigger for search/matching
426
- const rule = `WORKFLOW: ${trigger}`;
427
-
428
- // Store workflow structure in examples field (JSON-serializable)
429
- // This preserves the full workflow for the injection formatter
430
- const workflowData = {
431
- type: 'workflow',
432
- trigger: trigger,
433
- preconditions: preconditions,
434
- steps: steps.map((step, i) => ({
435
- index: i + 1,
436
- name: step.name || `Step ${i + 1}`,
437
- description: step.description || '',
438
- command: step.command || null,
439
- validation: step.validation || null,
440
- standards: Array.isArray(step.standards) ? step.standards : [],
441
- gate: step.gate === true
442
- })),
443
- related: related
444
- };
445
-
446
- return {
447
- patterns: [{
448
- pattern_id: patternId,
449
- file_name: filename,
450
- element: element,
451
- rule: rule,
452
- priority: standardPriority,
453
- correlation: 1.0,
454
- source: 'equilateral-standards',
455
- maturity: 'enforced',
456
- scope: 'organization',
457
- category: standardCategory,
458
- applicable_files: this.extractApplicableFiles('', standardCategory),
459
- anti_patterns: antiPatterns,
460
- examples: [workflowData], // Workflow data stored as first example
461
- cost_impact: costImpact,
462
- tags: [...tags, 'workflow'],
463
- source_file: filepath,
464
- title: standardId,
465
- type: 'workflow' // Flag for injection formatter
466
- }]
467
- };
468
- }
469
-
470
- /**
471
- * Parse a markdown standards file into Rapport pattern format
472
- */
473
- async parseMarkdownStandard(filepath, category) {
474
- const content = await fs.readFile(filepath, 'utf-8');
475
- const filename = path.basename(filepath, '.md');
476
-
477
- // Extract standard metadata from markdown
478
- const title = this.extractTitle(content);
479
- const enforcementRules = this.extractEnforcementRules(content);
480
- const antiPatterns = this.extractAntiPatterns(content);
481
- const examples = this.extractExamples(content);
482
- const costImpact = this.extractCostImpact(content);
483
- const applicableFiles = this.extractApplicableFiles(content, category);
484
-
485
- const patterns = [];
486
-
487
- // Create pattern for each enforcement rule
488
- for (const rule of enforcementRules) {
489
- const patternId = this.generatePatternId(category, filename, rule.element);
490
-
491
- // Map severity to priority (10=high, 20=medium, 30=low per DB constraint)
492
- const priorityMap = {
493
- 'CRITICAL': 10,
494
- 'MANDATORY': 10,
495
- 'ENFORCED': 20,
496
- 'VALIDATED': 30
497
- };
498
- const priority = priorityMap[rule.severity] || 30;
499
-
500
- patterns.push({
501
- pattern_id: patternId,
502
- file_name: filename, // Source markdown file name (without .md)
503
- element: rule.element,
504
- rule: rule.rule,
505
- priority: priority,
506
- correlation: 1.0, // Standards are mandatory
507
- source: 'equilateral-standards',
508
- maturity: rule.severity === 'MANDATORY' || rule.severity === 'CRITICAL' ? 'enforced' : 'validated',
509
- scope: 'organization',
510
- category: category,
511
- applicable_files: applicableFiles,
512
- anti_patterns: antiPatterns,
513
- examples: examples,
514
- cost_impact: costImpact,
515
- source_file: filepath,
516
- title: title
517
- });
518
- }
519
-
520
- return { patterns };
521
- }
522
-
523
- /**
524
- * Extract title from markdown (first H1)
525
- */
526
- extractTitle(content) {
527
- const match = content.match(/^#\s+(.+)$/m);
528
- return match ? match[1].trim() : 'Untitled Standard';
529
- }
530
-
531
- /**
532
- * Extract enforcement rules from markdown
533
- * Looks for sections like "MANDATORY PATTERNS", "Core Principles", etc.
534
- */
535
- extractEnforcementRules(content) {
536
- const rules = [];
537
-
538
- // Pattern 1: MANDATORY/CRITICAL sections
539
- const mandatorySection = content.match(/##?\s*⚠?️?\s*(MANDATORY|CRITICAL)[^#]*/gi);
540
- if (mandatorySection) {
541
- for (const section of mandatorySection) {
542
- const sectionRules = this.extractRulesFromSection(section, 'MANDATORY');
543
- rules.push(...sectionRules);
544
- }
545
- }
546
-
547
- // Pattern 2: "ALWAYS" and "NEVER" statements
548
- const alwaysNever = content.match(/(?:ALWAYS|NEVER)\s+([^.\n]+)/g);
549
- if (alwaysNever) {
550
- for (const statement of alwaysNever) {
551
- rules.push({
552
- element: this.extractElement(statement),
553
- rule: statement.trim(),
554
- severity: 'MANDATORY'
555
- });
556
- }
557
- }
558
-
559
- // Pattern 3: Core Principles
560
- const principleMatch = content.match(/##?\s*Core Principle[s]?[:\s]+([^#]+)/i);
561
- if (principleMatch) {
562
- rules.push({
563
- element: 'Core Principle',
564
- rule: principleMatch[1].trim(),
565
- severity: 'ENFORCED'
566
- });
567
- }
568
-
569
- return rules.length > 0 ? rules : [{
570
- element: 'Standard Pattern',
571
- rule: this.extractFirstParagraph(content),
572
- severity: 'VALIDATED'
573
- }];
574
- }
575
-
576
- /**
577
- * Extract rules from a section of content
578
- */
579
- extractRulesFromSection(section, severity) {
580
- const rules = [];
581
-
582
- // Look for numbered or bulleted lists
583
- const listItems = section.match(/^[\s]*[-*\d]+\.?\s+(.+)$/gm);
584
- if (listItems) {
585
- for (const item of listItems) {
586
- const cleaned = item.replace(/^[\s]*[-*\d]+\.?\s+/, '').trim();
587
- if (cleaned.length > 10) { // Filter out very short items
588
- rules.push({
589
- element: this.extractElement(cleaned),
590
- rule: cleaned,
591
- severity: severity
592
- });
593
- }
594
- }
595
- }
596
-
597
- // Look for bold statements
598
- const boldStatements = section.match(/\*\*(.+?)\*\*/g);
599
- if (boldStatements && rules.length === 0) {
600
- for (const statement of boldStatements) {
601
- const cleaned = statement.replace(/\*\*/g, '').trim();
602
- if (cleaned.length > 10 && !cleaned.match(/^(MANDATORY|CRITICAL|NOTE|IMPORTANT)/i)) {
603
- rules.push({
604
- element: this.extractElement(cleaned),
605
- rule: cleaned,
606
- severity: severity
607
- });
608
- }
609
- }
610
- }
611
-
612
- return rules;
613
- }
614
-
615
- /**
616
- * Extract anti-patterns from markdown
617
- * Looks for ❌ WRONG, NEVER DO THIS, Anti-Pattern sections
618
- */
619
- extractAntiPatterns(content) {
620
- const antiPatterns = [];
621
-
622
- // Pattern 1: ❌ markers
623
- const wrongPatterns = content.match(/❌[^```]*?```[^`]+```/gs);
624
- if (wrongPatterns) {
625
- for (const pattern of wrongPatterns) {
626
- const description = pattern.match(/❌\s*([^\n`]+)/);
627
- const code = pattern.match(/```(\w+)?\n([\s\S]+?)```/);
628
-
629
- if (description && code) {
630
- antiPatterns.push({
631
- description: description[1].trim(),
632
- code: code[2].trim(),
633
- language: code[1] || 'javascript'
634
- });
635
- }
636
- }
637
- }
638
-
639
- // Pattern 2: Anti-Pattern sections
640
- const antiPatternSection = content.match(/##?\s*Anti-Pattern[s]?[^#]*/gi);
641
- if (antiPatternSection) {
642
- for (const section of antiPatternSection) {
643
- const sectionPatterns = this.extractCodeBlocksFromSection(section);
644
- antiPatterns.push(...sectionPatterns.map(p => ({
645
- description: p.description || 'Anti-pattern to avoid',
646
- code: p.code,
647
- language: p.language
648
- })));
649
- }
650
- }
651
-
652
- return antiPatterns;
653
- }
654
-
655
- /**
656
- * Extract examples from markdown
657
- * Looks for ✅ CORRECT, Example sections
658
- */
659
- extractExamples(content) {
660
- const examples = [];
661
-
662
- // Pattern 1: ✅ markers
663
- const correctPatterns = content.match(/✅[^```]*?```[^`]+```/gs);
664
- if (correctPatterns) {
665
- for (const pattern of correctPatterns) {
666
- const description = pattern.match(/✅\s*([^\n`]+)/);
667
- const code = pattern.match(/```(\w+)?\n([\s\S]+?)```/);
668
-
669
- if (description && code) {
670
- examples.push({
671
- description: description[1].trim(),
672
- code: code[2].trim(),
673
- language: code[1] || 'javascript'
674
- });
675
- }
676
- }
677
- }
678
-
679
- // Pattern 2: Example sections
680
- const exampleSection = content.match(/##?\s*Example[s]?[^#]*/gi);
681
- if (exampleSection) {
682
- for (const section of exampleSection) {
683
- const sectionExamples = this.extractCodeBlocksFromSection(section);
684
- examples.push(...sectionExamples.map(p => ({
685
- description: p.description || 'Example implementation',
686
- code: p.code,
687
- language: p.language
688
- })));
689
- }
690
- }
691
-
692
- return examples;
693
- }
694
-
695
- /**
696
- * Extract cost impact information
697
- */
698
- extractCostImpact(content) {
699
- const costImpact = {};
700
-
701
- // Look for cost comparison tables
702
- const costTable = content.match(/\|[^|]*Cost[^|]*\|[\s\S]*?\n\n/i);
703
- if (costTable) {
704
- costImpact.hasTable = true;
705
- costImpact.details = costTable[0].trim();
706
- }
707
-
708
- // Look for specific cost mentions
709
- const costMentions = content.match(/\$\d+[^.\n]*/g);
710
- if (costMentions) {
711
- costImpact.mentions = costMentions.map(m => m.trim());
712
- }
713
-
714
- // Look for "Cost Impact" sections
715
- const costSection = content.match(/##?\s*Cost Impact[^#]*/i);
716
- if (costSection) {
717
- costImpact.section = costSection[0].trim();
718
- }
719
-
720
- return Object.keys(costImpact).length > 0 ? costImpact : null;
721
- }
722
-
723
- /**
724
- * Determine applicable file patterns based on category and content
725
- */
726
- extractApplicableFiles(content, category) {
727
- const patterns = [];
728
-
729
- // Category-based patterns
730
- const categoryPatterns = {
731
- 'serverless-saas-aws': ['**/*.js', '**/template.yaml', '**/handlers/**/*.js'],
732
- 'frontend-development': ['**/*.tsx', '**/*.jsx', '**/*.ts', 'src/frontend/**/*'],
733
- 'database': ['**/*.sql', '**/dbOperations.js', '**/migrations/**/*'],
734
- 'backend': ['src/backend/**/*.js', 'src/handlers/**/*.js'],
735
- 'multi-agent-orchestration': ['src/agents/**/*.js', 'src/core/**/*.js']
736
- };
737
-
738
- if (categoryPatterns[category]) {
739
- patterns.push(...categoryPatterns[category]);
740
- }
741
-
742
- // Content-based patterns
743
- if (content.includes('Lambda') || content.includes('handler')) {
744
- patterns.push('src/handlers/**/*.js');
745
- }
746
- if (content.includes('React') || content.includes('component')) {
747
- patterns.push('src/frontend/**/*.tsx', 'src/frontend/**/*.jsx');
748
- }
749
- if (content.includes('PostgreSQL') || content.includes('database')) {
750
- patterns.push('**/*dbOperations.js', '**/*.sql');
751
- }
752
-
753
- return [...new Set(patterns)]; // Remove duplicates
754
- }
755
-
756
- /**
757
- * Extract code blocks from a section
758
- */
759
- extractCodeBlocksFromSection(section) {
760
- const blocks = [];
761
- const codeBlocks = section.match(/```(\w+)?\n([\s\S]+?)```/g);
762
-
763
- if (codeBlocks) {
764
- for (const block of codeBlocks) {
765
- const match = block.match(/```(\w+)?\n([\s\S]+?)```/);
766
- if (match) {
767
- // Look for description before code block
768
- const beforeBlock = section.substring(0, section.indexOf(block));
769
- const descMatch = beforeBlock.match(/([^\n]+)\n*$/);
770
-
771
- blocks.push({
772
- description: descMatch ? descMatch[1].trim() : null,
773
- code: match[2].trim(),
774
- language: match[1] || 'javascript'
775
- });
776
- }
777
- }
778
- }
779
-
780
- return blocks;
781
- }
782
-
783
- /**
784
- * Infer programming language from code content
785
- */
786
- inferLanguageFromCode(code) {
787
- if (typeof code !== 'string') return 'text';
788
-
789
- // YAML/SAM template patterns
790
- if (code.includes('{{resolve:ssm:') || code.match(/^\s*(Environment|Resources|Properties):/m)) {
791
- return 'yaml';
792
- }
793
- // JavaScript patterns
794
- if (code.includes('async ') || code.includes('await ') || code.includes('const ') ||
795
- code.includes('function ') || code.includes('=>') || code.includes('require(')) {
796
- return 'javascript';
797
- }
798
- // SQL patterns
799
- if (code.match(/\b(SELECT|INSERT|UPDATE|DELETE|CREATE|ALTER)\b/i)) {
800
- return 'sql';
801
- }
802
- // Python patterns
803
- if (code.includes('def ') || code.includes('import ') || code.match(/^\s*class\s+\w+:/m)) {
804
- return 'python';
805
- }
806
-
807
- return 'text';
808
- }
809
-
810
- /**
811
- * Extract element name from a rule statement
812
- * Goal: Create a descriptive, unique element name from the rule
813
- */
814
- extractElement(statement) {
815
- // Remove action prefix (ALWAYS, NEVER, USE, etc.)
816
- let cleaned = statement.replace(/^(ALWAYS|NEVER|USE|PREFER|AVOID|DO NOT|DON'T)[:\s]+/i, '').trim();
817
-
818
- // Extract key technical terms for better element names
819
- // Pattern: "verb + object" -> extract the object/concept being acted upon
820
- // Allow method calls like "client.end()" by not stopping at dots within identifiers
821
- const verbObjectMatch = cleaned.match(/^(?:Use|Implement|Apply|Follow|Add|Create|Set|Cache|Validate|Configure|Enable|Disable|Fetch|Call)\s+(.+?)(?:\s+-\s+|,|$)/i);
822
- if (verbObjectMatch) {
823
- let object = verbObjectMatch[1].trim();
824
- // Clean up trailing punctuation but keep method calls
825
- object = object.replace(/\s*[-–].*$/, ''); // Remove explanatory text after dash
826
- return object.substring(0, 50);
827
- }
828
-
829
- // For statements starting with a noun phrase, extract more context
830
- const nounPhraseMatch = cleaned.match(/^([A-Za-z][a-z]+(?:[\s.][a-z()]+){0,5})/i);
831
- if (nounPhraseMatch) {
832
- let phrase = nounPhraseMatch[1].trim();
833
- phrase = phrase.replace(/\s*[-–].*$/, ''); // Remove explanatory text
834
- return phrase.substring(0, 50);
835
- }
836
-
837
- // Fallback: first meaningful words
838
- const words = cleaned.split(/\s+/).slice(0, 4);
839
- return words.join(' ').substring(0, 50);
840
- }
841
-
842
- /**
843
- * Extract first paragraph from content
844
- */
845
- extractFirstParagraph(content) {
846
- const paragraphs = content.split(/\n\n+/);
847
- for (const para of paragraphs) {
848
- const cleaned = para.replace(/^#+\s+/, '').trim();
849
- if (cleaned.length > 20 && !cleaned.startsWith('**')) {
850
- return cleaned;
851
- }
852
- }
853
- return 'Standard pattern';
854
- }
855
-
856
- /**
857
- * Generate unique pattern ID
858
- * @param {string} category - Category name
859
- * @param {string} filename - Source filename
860
- * @param {string} element - Element description
861
- * @param {number} index - Rule index for uniqueness
862
- */
863
- generatePatternId(category, filename, element, index = 0) {
864
- const categorySlug = category.replace(/[^a-z0-9]/gi, '_');
865
- const fileSlug = filename.replace(/[^a-z0-9]/gi, '_');
866
- const elementSlug = element.replace(/[^a-z0-9]/gi, '_').toLowerCase().substring(0, 40);
867
-
868
- // Include index for uniqueness when elements might collide
869
- const suffix = index > 0 ? `_${index}` : '';
870
-
871
- return `${categorySlug}_${fileSlug}_${elementSlug}${suffix}`.toLowerCase();
872
- }
873
-
874
- /**
875
- * Store patterns in database
876
- */
877
- async seedPatternDatabase(patterns) {
878
- if (patterns.length === 0) {
879
- console.log('[StandardsIngestion] No patterns to ingest');
880
- return 0;
881
- }
882
-
883
- console.log(`[StandardsIngestion] Storing ${patterns.length} patterns in database...`);
884
-
885
- let successCount = 0;
886
-
887
- for (const pattern of patterns) {
888
- try {
889
- await executeQuery(`
890
- INSERT INTO rapport.standards_patterns (
891
- pattern_id,
892
- file_name,
893
- element,
894
- rule,
895
- priority,
896
- correlation,
897
- source,
898
- maturity,
899
- scope,
900
- category,
901
- applicable_files,
902
- anti_patterns,
903
- examples,
904
- cost_impact,
905
- keywords,
906
- created_at,
907
- last_updated
908
- ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, NOW(), NOW())
909
- ON CONFLICT (pattern_id)
910
- DO UPDATE SET
911
- file_name = EXCLUDED.file_name,
912
- element = EXCLUDED.element,
913
- rule = EXCLUDED.rule,
914
- priority = EXCLUDED.priority,
915
- correlation = EXCLUDED.correlation,
916
- maturity = EXCLUDED.maturity,
917
- scope = EXCLUDED.scope,
918
- category = EXCLUDED.category,
919
- applicable_files = EXCLUDED.applicable_files,
920
- anti_patterns = EXCLUDED.anti_patterns,
921
- examples = EXCLUDED.examples,
922
- cost_impact = EXCLUDED.cost_impact,
923
- keywords = EXCLUDED.keywords,
924
- last_updated = NOW(),
925
- last_seen_at = NOW(),
926
- occurrence_count = COALESCE(rapport.standards_patterns.occurrence_count, 0) + 1
927
- `, [
928
- pattern.pattern_id,
929
- pattern.file_name,
930
- pattern.element,
931
- pattern.rule,
932
- pattern.priority,
933
- pattern.correlation,
934
- pattern.source,
935
- pattern.maturity,
936
- pattern.scope,
937
- pattern.category,
938
- pattern.applicable_files,
939
- JSON.stringify(pattern.anti_patterns),
940
- JSON.stringify(pattern.examples),
941
- pattern.cost_impact ? JSON.stringify(pattern.cost_impact) : null,
942
- pattern.tags || []
943
- ]);
944
-
945
- successCount++;
946
-
947
- if (this.verbose && successCount % 10 === 0) {
948
- console.log(` Stored ${successCount}/${patterns.length} patterns...`);
949
- }
950
- } catch (error) {
951
- console.error(` ✗ Failed to store pattern ${pattern.pattern_id}:`, error.message);
952
- }
953
- }
954
-
955
- console.log(`[StandardsIngestion] ✓ Successfully stored ${successCount} patterns`);
956
- return successCount;
957
- }
958
-
959
- /**
960
- * Check if standards need re-ingestion (for performance)
961
- */
962
- async needsIngestion() {
963
- try {
964
- const result = await executeQuery(`
965
- SELECT COUNT(*) as count, MAX(last_updated) as last_updated
966
- FROM rapport.standards_patterns
967
- WHERE source = 'equilateral-standards'
968
- `);
969
-
970
- const row = result.rows[0];
971
-
972
- // Re-ingest if no patterns exist or haven't been updated in 7 days
973
- if (row.count === '0') {
974
- return { needed: true, reason: 'No standards ingested yet' };
975
- }
976
-
977
- const lastUpdated = new Date(row.last_updated);
978
- const daysSince = (Date.now() - lastUpdated.getTime()) / (1000 * 60 * 60 * 24);
979
-
980
- if (daysSince > 7) {
981
- return { needed: true, reason: `Last updated ${daysSince.toFixed(0)} days ago` };
982
- }
983
-
984
- return { needed: false, count: row.count };
985
- } catch (error) {
986
- return { needed: true, reason: 'Database check failed' };
987
- }
988
- }
989
- }
990
-
991
- module.exports = { StandardsIngestion };