@equilateral_ai/mindmeld 3.1.2 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/hooks/README.md +46 -4
- package/hooks/pre-compact.js +115 -12
- package/hooks/session-end.js +292 -0
- package/hooks/session-start.js +302 -25
- package/package.json +5 -2
- package/scripts/auth-login.js +53 -0
- package/scripts/harvest.js +59 -19
- package/scripts/init-project.js +134 -374
- package/scripts/inject.js +30 -9
- package/scripts/repo-analyzer.js +870 -0
- package/src/core/AuthManager.js +498 -0
- package/src/core/CrossReferenceEngine.js +624 -0
- package/src/core/DeprecationScheduler.js +183 -0
- package/src/core/LLMPatternDetector.js +218 -0
- package/src/core/RapportOrchestrator.js +186 -0
- package/src/core/RelevanceDetector.js +32 -2
- package/src/core/StandardLifecycle.js +244 -0
- package/src/core/StandardsIngestion.js +341 -28
- package/src/core/parsers/adrParser.js +479 -0
- package/src/core/parsers/cursorRulesParser.js +564 -0
- package/src/core/parsers/eslintParser.js +439 -0
- package/src/handlers/alerts/alertsAcknowledge.js +4 -3
- package/src/handlers/analytics/activitySummaryGet.js +235 -0
- package/src/handlers/analytics/coachingGet.js +361 -0
- package/src/handlers/analytics/developerScoreGet.js +207 -0
- package/src/handlers/collaborators/collaboratorAdd.js +4 -5
- package/src/handlers/collaborators/collaboratorInvite.js +6 -5
- package/src/handlers/collaborators/collaboratorList.js +3 -3
- package/src/handlers/collaborators/collaboratorRemove.js +5 -4
- package/src/handlers/correlations/correlationsDeveloperGet.js +12 -11
- package/src/handlers/correlations/correlationsGet.js +1 -1
- package/src/handlers/correlations/correlationsProjectGet.js +7 -6
- package/src/handlers/enterprise/enterpriseAuditGet.js +108 -0
- package/src/handlers/enterprise/enterpriseContributorsGet.js +85 -0
- package/src/handlers/enterprise/enterpriseKnowledgeCategoriesGet.js +53 -0
- package/src/handlers/enterprise/enterpriseKnowledgeCreate.js +77 -0
- package/src/handlers/enterprise/enterpriseKnowledgeDelete.js +71 -0
- package/src/handlers/enterprise/enterpriseKnowledgeGet.js +87 -0
- package/src/handlers/enterprise/enterpriseKnowledgeUpdate.js +122 -0
- package/src/handlers/enterprise/enterpriseOnboardingComplete.js +77 -0
- package/src/handlers/enterprise/enterpriseOnboardingInvite.js +138 -0
- package/src/handlers/enterprise/enterpriseOnboardingSetup.js +89 -0
- package/src/handlers/enterprise/enterpriseOnboardingStatus.js +90 -0
- package/src/handlers/github/githubConnectionStatus.js +1 -1
- package/src/handlers/github/githubDiscoverPatterns.js +264 -5
- package/src/handlers/github/githubOAuthCallback.js +14 -2
- package/src/handlers/github/githubOAuthStart.js +1 -1
- package/src/handlers/github/githubPatternsReview.js +1 -1
- package/src/handlers/github/githubReposList.js +1 -1
- package/src/handlers/helpers/auditLogger.js +201 -0
- package/src/handlers/helpers/index.js +19 -1
- package/src/handlers/helpers/lambdaWrapper.js +1 -1
- package/src/handlers/notifications/sendNotification.js +1 -1
- package/src/handlers/projects/projectCreate.js +28 -1
- package/src/handlers/projects/projectDelete.js +3 -3
- package/src/handlers/projects/projectUpdate.js +4 -5
- package/src/handlers/scheduled/analyzeCorrelations.js +3 -3
- package/src/handlers/scheduled/generateAlerts.js +1 -1
- package/src/handlers/standards/catalogGet.js +185 -0
- package/src/handlers/standards/catalogSync.js +120 -0
- package/src/handlers/standards/projectStandardsGet.js +135 -0
- package/src/handlers/standards/projectStandardsPut.js +131 -0
- package/src/handlers/standards/standardsAuditGet.js +65 -0
- package/src/handlers/standards/standardsParseUpload.js +153 -0
- package/src/handlers/standards/standardsRelevantPost.js +213 -0
- package/src/handlers/standards/standardsTransition.js +64 -0
- package/src/handlers/user/userSplashAck.js +91 -0
- package/src/handlers/user/userSplashGet.js +194 -0
- package/src/handlers/users/userProfilePut.js +77 -0
- package/src/index.js +37 -29
|
@@ -1,14 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* StandardsIngestion.js - Phase 1 of Rapport Standards Integration
|
|
3
3
|
*
|
|
4
|
-
* Parses and ingests .equilateral-standards/
|
|
5
|
-
* Creates standards_patterns from
|
|
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]
|
|
6
14
|
*
|
|
7
15
|
* Reference: /Users/jamesford/Source/rapport/docs/RAPPORT_STANDARDS_INTEGRATION_DESIGN.md
|
|
8
16
|
*/
|
|
9
17
|
|
|
10
18
|
const fs = require('fs').promises;
|
|
11
19
|
const path = require('path');
|
|
20
|
+
const yaml = require('js-yaml');
|
|
12
21
|
const { executeQuery } = require('../handlers/helpers/dbOperations');
|
|
13
22
|
|
|
14
23
|
class StandardsIngestion {
|
|
@@ -19,7 +28,7 @@ class StandardsIngestion {
|
|
|
19
28
|
|
|
20
29
|
/**
|
|
21
30
|
* Main ingestion entry point
|
|
22
|
-
* Discovers and parses all .equilateral-standards/ markdown
|
|
31
|
+
* Discovers and parses all .equilateral-standards/ YAML files (with markdown fallback)
|
|
23
32
|
*/
|
|
24
33
|
async ingestEquilateralStandards() {
|
|
25
34
|
console.log(`[StandardsIngestion] Starting ingestion from ${this.standardsPath}`);
|
|
@@ -86,12 +95,61 @@ class StandardsIngestion {
|
|
|
86
95
|
}
|
|
87
96
|
|
|
88
97
|
/**
|
|
89
|
-
*
|
|
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
|
|
90
148
|
*/
|
|
91
149
|
async discoverStandardFiles(categoryPath) {
|
|
92
150
|
try {
|
|
93
151
|
const entries = await fs.readdir(categoryPath, { withFileTypes: true });
|
|
94
|
-
const
|
|
152
|
+
const fileMap = new Map(); // baseName -> { yaml: path, md: path }
|
|
95
153
|
|
|
96
154
|
for (const entry of entries) {
|
|
97
155
|
const fullPath = path.join(categoryPath, entry.name);
|
|
@@ -99,9 +157,47 @@ class StandardsIngestion {
|
|
|
99
157
|
if (entry.isDirectory()) {
|
|
100
158
|
// Recursively search subdirectories
|
|
101
159
|
const subFiles = await this.discoverStandardFiles(fullPath);
|
|
102
|
-
files
|
|
103
|
-
|
|
104
|
-
|
|
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);
|
|
105
201
|
}
|
|
106
202
|
}
|
|
107
203
|
|
|
@@ -116,9 +212,164 @@ class StandardsIngestion {
|
|
|
116
212
|
}
|
|
117
213
|
|
|
118
214
|
/**
|
|
119
|
-
* Parse a
|
|
215
|
+
* Parse a standards file into Rapport pattern format
|
|
216
|
+
* Detects file type and routes to appropriate parser
|
|
120
217
|
*/
|
|
121
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
|
+
* YAML format:
|
|
231
|
+
* id: string
|
|
232
|
+
* category: string
|
|
233
|
+
* priority: 10 | 20 | 30
|
|
234
|
+
* rules:
|
|
235
|
+
* - action: ALWAYS | NEVER | USE | PREFER | AVOID
|
|
236
|
+
* rule: string
|
|
237
|
+
* applies_to: [glob patterns]
|
|
238
|
+
* anti_patterns:
|
|
239
|
+
* - string
|
|
240
|
+
*/
|
|
241
|
+
async parseYamlStandard(filepath, category) {
|
|
242
|
+
const content = await fs.readFile(filepath, 'utf-8');
|
|
243
|
+
const ext = filepath.endsWith('.yml') ? '.yml' : '.yaml';
|
|
244
|
+
const filename = path.basename(filepath, ext);
|
|
245
|
+
|
|
246
|
+
let doc;
|
|
247
|
+
try {
|
|
248
|
+
doc = yaml.load(content);
|
|
249
|
+
} catch (parseError) {
|
|
250
|
+
throw new Error(`YAML parse error: ${parseError.message}`);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (!doc || typeof doc !== 'object') {
|
|
254
|
+
throw new Error('Invalid YAML structure: expected an object');
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const patterns = [];
|
|
258
|
+
const standardId = doc.id || filename;
|
|
259
|
+
const standardCategory = doc.category || category;
|
|
260
|
+
const standardPriority = doc.priority || 30;
|
|
261
|
+
|
|
262
|
+
// Extract anti-patterns as simple strings
|
|
263
|
+
const antiPatterns = Array.isArray(doc.anti_patterns)
|
|
264
|
+
? doc.anti_patterns.map(ap => ({
|
|
265
|
+
description: typeof ap === 'string' ? ap : ap.description || 'Anti-pattern to avoid',
|
|
266
|
+
code: typeof ap === 'object' ? ap.code : null,
|
|
267
|
+
language: typeof ap === 'object' ? ap.language : null
|
|
268
|
+
}))
|
|
269
|
+
: [];
|
|
270
|
+
|
|
271
|
+
// Extract examples - now a named object in YAML format
|
|
272
|
+
const examples = [];
|
|
273
|
+
if (doc.examples && typeof doc.examples === 'object') {
|
|
274
|
+
for (const [name, code] of Object.entries(doc.examples)) {
|
|
275
|
+
examples.push({
|
|
276
|
+
description: name.replace(/_/g, ' '),
|
|
277
|
+
code: typeof code === 'string' ? code.trim() : String(code),
|
|
278
|
+
language: this.inferLanguageFromCode(code)
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Extract cost_impact - now a structured object in YAML format
|
|
284
|
+
const costImpact = doc.cost_impact && typeof doc.cost_impact === 'object'
|
|
285
|
+
? doc.cost_impact
|
|
286
|
+
: (typeof doc.cost_impact === 'string' ? { description: doc.cost_impact } : null);
|
|
287
|
+
|
|
288
|
+
// Extract tags for metadata
|
|
289
|
+
const tags = Array.isArray(doc.tags) ? doc.tags : [];
|
|
290
|
+
|
|
291
|
+
// Process each rule
|
|
292
|
+
const rules = Array.isArray(doc.rules) ? doc.rules : [];
|
|
293
|
+
|
|
294
|
+
for (let ruleIndex = 0; ruleIndex < rules.length; ruleIndex++) {
|
|
295
|
+
const rule = rules[ruleIndex];
|
|
296
|
+
if (!rule || typeof rule !== 'object') continue;
|
|
297
|
+
|
|
298
|
+
const action = rule.action || 'USE';
|
|
299
|
+
const ruleText = rule.rule || '';
|
|
300
|
+
const appliesTo = Array.isArray(rule.applies_to) ? rule.applies_to : [];
|
|
301
|
+
|
|
302
|
+
// Construct element name from action and first few words
|
|
303
|
+
const element = this.extractElement(`${action} ${ruleText}`);
|
|
304
|
+
|
|
305
|
+
// Construct full rule statement
|
|
306
|
+
const fullRule = `${action}: ${ruleText}`;
|
|
307
|
+
|
|
308
|
+
const patternId = this.generatePatternId(standardCategory, standardId, element, ruleIndex);
|
|
309
|
+
|
|
310
|
+
// Map action to maturity
|
|
311
|
+
const actionMaturityMap = {
|
|
312
|
+
'ALWAYS': 'enforced',
|
|
313
|
+
'NEVER': 'enforced',
|
|
314
|
+
'USE': 'validated',
|
|
315
|
+
'PREFER': 'validated',
|
|
316
|
+
'AVOID': 'validated'
|
|
317
|
+
};
|
|
318
|
+
const maturity = actionMaturityMap[action] || 'validated';
|
|
319
|
+
|
|
320
|
+
patterns.push({
|
|
321
|
+
pattern_id: patternId,
|
|
322
|
+
file_name: filename,
|
|
323
|
+
element: element,
|
|
324
|
+
rule: fullRule,
|
|
325
|
+
priority: standardPriority,
|
|
326
|
+
correlation: 1.0,
|
|
327
|
+
source: 'equilateral-standards',
|
|
328
|
+
maturity: maturity,
|
|
329
|
+
scope: 'organization',
|
|
330
|
+
category: standardCategory,
|
|
331
|
+
applicable_files: appliesTo.length > 0 ? appliesTo : this.extractApplicableFiles('', standardCategory),
|
|
332
|
+
anti_patterns: antiPatterns,
|
|
333
|
+
examples: examples,
|
|
334
|
+
cost_impact: costImpact,
|
|
335
|
+
tags: tags,
|
|
336
|
+
source_file: filepath,
|
|
337
|
+
title: standardId
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// If no rules found but document exists, create a single pattern from the document
|
|
342
|
+
if (patterns.length === 0 && doc.id) {
|
|
343
|
+
const patternId = this.generatePatternId(standardCategory, standardId, 'standard_pattern');
|
|
344
|
+
|
|
345
|
+
patterns.push({
|
|
346
|
+
pattern_id: patternId,
|
|
347
|
+
file_name: filename,
|
|
348
|
+
element: 'Standard Pattern',
|
|
349
|
+
rule: `Standard: ${standardId}`,
|
|
350
|
+
priority: standardPriority,
|
|
351
|
+
correlation: 1.0,
|
|
352
|
+
source: 'equilateral-standards',
|
|
353
|
+
maturity: 'validated',
|
|
354
|
+
scope: 'organization',
|
|
355
|
+
category: standardCategory,
|
|
356
|
+
applicable_files: this.extractApplicableFiles('', standardCategory),
|
|
357
|
+
anti_patterns: antiPatterns,
|
|
358
|
+
examples: examples,
|
|
359
|
+
cost_impact: costImpact,
|
|
360
|
+
tags: tags,
|
|
361
|
+
source_file: filepath,
|
|
362
|
+
title: standardId
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return { patterns };
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Parse a markdown standards file into Rapport pattern format
|
|
371
|
+
*/
|
|
372
|
+
async parseMarkdownStandard(filepath, category) {
|
|
122
373
|
const content = await fs.readFile(filepath, 'utf-8');
|
|
123
374
|
const filename = path.basename(filepath, '.md');
|
|
124
375
|
|
|
@@ -136,10 +387,21 @@ class StandardsIngestion {
|
|
|
136
387
|
for (const rule of enforcementRules) {
|
|
137
388
|
const patternId = this.generatePatternId(category, filename, rule.element);
|
|
138
389
|
|
|
390
|
+
// Map severity to priority (10=high, 20=medium, 30=low per DB constraint)
|
|
391
|
+
const priorityMap = {
|
|
392
|
+
'CRITICAL': 10,
|
|
393
|
+
'MANDATORY': 10,
|
|
394
|
+
'ENFORCED': 20,
|
|
395
|
+
'VALIDATED': 30
|
|
396
|
+
};
|
|
397
|
+
const priority = priorityMap[rule.severity] || 30;
|
|
398
|
+
|
|
139
399
|
patterns.push({
|
|
140
400
|
pattern_id: patternId,
|
|
401
|
+
file_name: filename, // Source markdown file name (without .md)
|
|
141
402
|
element: rule.element,
|
|
142
403
|
rule: rule.rule,
|
|
404
|
+
priority: priority,
|
|
143
405
|
correlation: 1.0, // Standards are mandatory
|
|
144
406
|
source: 'equilateral-standards',
|
|
145
407
|
maturity: rule.severity === 'MANDATORY' || rule.severity === 'CRITICAL' ? 'enforced' : 'validated',
|
|
@@ -417,28 +679,63 @@ class StandardsIngestion {
|
|
|
417
679
|
return blocks;
|
|
418
680
|
}
|
|
419
681
|
|
|
682
|
+
/**
|
|
683
|
+
* Infer programming language from code content
|
|
684
|
+
*/
|
|
685
|
+
inferLanguageFromCode(code) {
|
|
686
|
+
if (typeof code !== 'string') return 'text';
|
|
687
|
+
|
|
688
|
+
// YAML/SAM template patterns
|
|
689
|
+
if (code.includes('{{resolve:ssm:') || code.match(/^\s*(Environment|Resources|Properties):/m)) {
|
|
690
|
+
return 'yaml';
|
|
691
|
+
}
|
|
692
|
+
// JavaScript patterns
|
|
693
|
+
if (code.includes('async ') || code.includes('await ') || code.includes('const ') ||
|
|
694
|
+
code.includes('function ') || code.includes('=>') || code.includes('require(')) {
|
|
695
|
+
return 'javascript';
|
|
696
|
+
}
|
|
697
|
+
// SQL patterns
|
|
698
|
+
if (code.match(/\b(SELECT|INSERT|UPDATE|DELETE|CREATE|ALTER)\b/i)) {
|
|
699
|
+
return 'sql';
|
|
700
|
+
}
|
|
701
|
+
// Python patterns
|
|
702
|
+
if (code.includes('def ') || code.includes('import ') || code.match(/^\s*class\s+\w+:/m)) {
|
|
703
|
+
return 'python';
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
return 'text';
|
|
707
|
+
}
|
|
708
|
+
|
|
420
709
|
/**
|
|
421
710
|
* Extract element name from a rule statement
|
|
711
|
+
* Goal: Create a descriptive, unique element name from the rule
|
|
422
712
|
*/
|
|
423
713
|
extractElement(statement) {
|
|
424
|
-
// Remove
|
|
425
|
-
let cleaned = statement.replace(/^(ALWAYS|NEVER|DO NOT|DON'T)
|
|
426
|
-
|
|
427
|
-
// Extract
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
714
|
+
// Remove action prefix (ALWAYS, NEVER, USE, etc.)
|
|
715
|
+
let cleaned = statement.replace(/^(ALWAYS|NEVER|USE|PREFER|AVOID|DO NOT|DON'T)[:\s]+/i, '').trim();
|
|
716
|
+
|
|
717
|
+
// Extract key technical terms for better element names
|
|
718
|
+
// Pattern: "verb + object" -> extract the object/concept being acted upon
|
|
719
|
+
// Allow method calls like "client.end()" by not stopping at dots within identifiers
|
|
720
|
+
const verbObjectMatch = cleaned.match(/^(?:Use|Implement|Apply|Follow|Add|Create|Set|Cache|Validate|Configure|Enable|Disable|Fetch|Call)\s+(.+?)(?:\s+-\s+|,|$)/i);
|
|
721
|
+
if (verbObjectMatch) {
|
|
722
|
+
let object = verbObjectMatch[1].trim();
|
|
723
|
+
// Clean up trailing punctuation but keep method calls
|
|
724
|
+
object = object.replace(/\s*[-–].*$/, ''); // Remove explanatory text after dash
|
|
725
|
+
return object.substring(0, 50);
|
|
431
726
|
}
|
|
432
727
|
|
|
433
|
-
//
|
|
434
|
-
const
|
|
435
|
-
if (
|
|
436
|
-
|
|
728
|
+
// For statements starting with a noun phrase, extract more context
|
|
729
|
+
const nounPhraseMatch = cleaned.match(/^([A-Za-z][a-z]+(?:[\s.][a-z()]+){0,5})/i);
|
|
730
|
+
if (nounPhraseMatch) {
|
|
731
|
+
let phrase = nounPhraseMatch[1].trim();
|
|
732
|
+
phrase = phrase.replace(/\s*[-–].*$/, ''); // Remove explanatory text
|
|
733
|
+
return phrase.substring(0, 50);
|
|
437
734
|
}
|
|
438
735
|
|
|
439
|
-
// Fallback: first
|
|
440
|
-
const words = cleaned.split(/\s+/).slice(0,
|
|
441
|
-
return words.join(' ');
|
|
736
|
+
// Fallback: first meaningful words
|
|
737
|
+
const words = cleaned.split(/\s+/).slice(0, 4);
|
|
738
|
+
return words.join(' ').substring(0, 50);
|
|
442
739
|
}
|
|
443
740
|
|
|
444
741
|
/**
|
|
@@ -457,13 +754,20 @@ class StandardsIngestion {
|
|
|
457
754
|
|
|
458
755
|
/**
|
|
459
756
|
* Generate unique pattern ID
|
|
757
|
+
* @param {string} category - Category name
|
|
758
|
+
* @param {string} filename - Source filename
|
|
759
|
+
* @param {string} element - Element description
|
|
760
|
+
* @param {number} index - Rule index for uniqueness
|
|
460
761
|
*/
|
|
461
|
-
generatePatternId(category, filename, element) {
|
|
762
|
+
generatePatternId(category, filename, element, index = 0) {
|
|
462
763
|
const categorySlug = category.replace(/[^a-z0-9]/gi, '_');
|
|
463
764
|
const fileSlug = filename.replace(/[^a-z0-9]/gi, '_');
|
|
464
|
-
const elementSlug = element.replace(/[^a-z0-9]/gi, '_').toLowerCase().substring(0,
|
|
765
|
+
const elementSlug = element.replace(/[^a-z0-9]/gi, '_').toLowerCase().substring(0, 40);
|
|
766
|
+
|
|
767
|
+
// Include index for uniqueness when elements might collide
|
|
768
|
+
const suffix = index > 0 ? `_${index}` : '';
|
|
465
769
|
|
|
466
|
-
return `${categorySlug}_${fileSlug}_${elementSlug}`.toLowerCase();
|
|
770
|
+
return `${categorySlug}_${fileSlug}_${elementSlug}${suffix}`.toLowerCase();
|
|
467
771
|
}
|
|
468
772
|
|
|
469
773
|
/**
|
|
@@ -484,8 +788,10 @@ class StandardsIngestion {
|
|
|
484
788
|
await executeQuery(`
|
|
485
789
|
INSERT INTO rapport.standards_patterns (
|
|
486
790
|
pattern_id,
|
|
791
|
+
file_name,
|
|
487
792
|
element,
|
|
488
793
|
rule,
|
|
794
|
+
priority,
|
|
489
795
|
correlation,
|
|
490
796
|
source,
|
|
491
797
|
maturity,
|
|
@@ -495,13 +801,16 @@ class StandardsIngestion {
|
|
|
495
801
|
anti_patterns,
|
|
496
802
|
examples,
|
|
497
803
|
cost_impact,
|
|
804
|
+
keywords,
|
|
498
805
|
created_at,
|
|
499
806
|
last_updated
|
|
500
|
-
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, NOW(), NOW())
|
|
807
|
+
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, NOW(), NOW())
|
|
501
808
|
ON CONFLICT (pattern_id)
|
|
502
809
|
DO UPDATE SET
|
|
810
|
+
file_name = EXCLUDED.file_name,
|
|
503
811
|
element = EXCLUDED.element,
|
|
504
812
|
rule = EXCLUDED.rule,
|
|
813
|
+
priority = EXCLUDED.priority,
|
|
505
814
|
correlation = EXCLUDED.correlation,
|
|
506
815
|
maturity = EXCLUDED.maturity,
|
|
507
816
|
scope = EXCLUDED.scope,
|
|
@@ -510,11 +819,14 @@ class StandardsIngestion {
|
|
|
510
819
|
anti_patterns = EXCLUDED.anti_patterns,
|
|
511
820
|
examples = EXCLUDED.examples,
|
|
512
821
|
cost_impact = EXCLUDED.cost_impact,
|
|
822
|
+
keywords = EXCLUDED.keywords,
|
|
513
823
|
last_updated = NOW()
|
|
514
824
|
`, [
|
|
515
825
|
pattern.pattern_id,
|
|
826
|
+
pattern.file_name,
|
|
516
827
|
pattern.element,
|
|
517
828
|
pattern.rule,
|
|
829
|
+
pattern.priority,
|
|
518
830
|
pattern.correlation,
|
|
519
831
|
pattern.source,
|
|
520
832
|
pattern.maturity,
|
|
@@ -523,7 +835,8 @@ class StandardsIngestion {
|
|
|
523
835
|
pattern.applicable_files,
|
|
524
836
|
JSON.stringify(pattern.anti_patterns),
|
|
525
837
|
JSON.stringify(pattern.examples),
|
|
526
|
-
pattern.cost_impact ? JSON.stringify(pattern.cost_impact) : null
|
|
838
|
+
pattern.cost_impact ? JSON.stringify(pattern.cost_impact) : null,
|
|
839
|
+
pattern.tags || []
|
|
527
840
|
]);
|
|
528
841
|
|
|
529
842
|
successCount++;
|