@equilateral_ai/mindmeld 3.2.0 → 3.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/hooks/README.md +46 -4
- package/hooks/pre-compact.js +87 -1
- package/hooks/session-end.js +292 -0
- package/hooks/session-start.js +292 -23
- package/package.json +4 -2
- package/scripts/auth-login.js +53 -0
- package/scripts/init-project.js +69 -375
- 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 +75 -75
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADR Parser - Parse Architecture Decision Records into YAML standards format
|
|
3
|
+
*
|
|
4
|
+
* Parses ADR markdown files with standard sections:
|
|
5
|
+
* Title, Status, Context, Decision, Consequences
|
|
6
|
+
*
|
|
7
|
+
* Extracts:
|
|
8
|
+
* - Rules from Decision section with action types (ALWAYS/NEVER/USE/PREFER/AVOID)
|
|
9
|
+
* - Anti-patterns from Consequences negative items
|
|
10
|
+
* - Context for applicability
|
|
11
|
+
*
|
|
12
|
+
* Returns YAML-compatible object matching equilateral-standards schema:
|
|
13
|
+
* { id, category, priority, rules, anti_patterns, context }
|
|
14
|
+
*
|
|
15
|
+
* @module parsers/adrParser
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Action keywords that indicate rule types in decision text
|
|
20
|
+
*/
|
|
21
|
+
const ACTION_PATTERNS = [
|
|
22
|
+
{ regex: /\b(?:must|shall|always|required)\b/i, action: 'ALWAYS' },
|
|
23
|
+
{ regex: /\b(?:must not|shall not|never|prohibited|forbidden)\b/i, action: 'NEVER' },
|
|
24
|
+
{ regex: /\b(?:will use|adopt|choose|selected|use)\b/i, action: 'USE' },
|
|
25
|
+
{ regex: /\b(?:prefer|should|recommended|favor)\b/i, action: 'PREFER' },
|
|
26
|
+
{ regex: /\b(?:avoid|discourage|should not|minimize)\b/i, action: 'AVOID' }
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Parse ADR markdown content into YAML-compatible standards object
|
|
31
|
+
*
|
|
32
|
+
* @param {string} content - Raw ADR markdown content
|
|
33
|
+
* @param {Object} options - Parser options
|
|
34
|
+
* @param {string} options.filename - Source filename for ID generation
|
|
35
|
+
* @param {string} options.category - Standards category (e.g., 'backend', 'database')
|
|
36
|
+
* @returns {Object} YAML-compatible standards object
|
|
37
|
+
*/
|
|
38
|
+
function parseAdr(content, options = {}) {
|
|
39
|
+
if (!content || typeof content !== 'string') {
|
|
40
|
+
throw new Error('ADR content is required and must be a string');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const filename = options.filename || 'unknown-adr';
|
|
44
|
+
const category = options.category || inferCategory(content);
|
|
45
|
+
|
|
46
|
+
const title = extractTitle(content);
|
|
47
|
+
const status = extractStatus(content);
|
|
48
|
+
const context = extractSection(content, 'Context');
|
|
49
|
+
const decision = extractSection(content, 'Decision');
|
|
50
|
+
const consequences = extractSection(content, 'Consequences');
|
|
51
|
+
|
|
52
|
+
const id = generateId(filename, title);
|
|
53
|
+
const rules = extractRules(decision);
|
|
54
|
+
const antiPatterns = extractAntiPatterns(consequences);
|
|
55
|
+
const priority = determinePriority(status, rules);
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
id,
|
|
59
|
+
category,
|
|
60
|
+
priority,
|
|
61
|
+
rules,
|
|
62
|
+
anti_patterns: antiPatterns,
|
|
63
|
+
context: {
|
|
64
|
+
title,
|
|
65
|
+
status,
|
|
66
|
+
description: context || '',
|
|
67
|
+
source_format: 'adr',
|
|
68
|
+
source_file: filename
|
|
69
|
+
},
|
|
70
|
+
tags: extractTags(content, category),
|
|
71
|
+
updated: new Date().toISOString().split('T')[0]
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Extract the ADR title from the first heading
|
|
77
|
+
*
|
|
78
|
+
* @param {string} content - ADR markdown content
|
|
79
|
+
* @returns {string} Extracted title
|
|
80
|
+
*/
|
|
81
|
+
function extractTitle(content) {
|
|
82
|
+
// ADR titles often follow pattern: "# ADR-NNN: Title" or "# Title"
|
|
83
|
+
const match = content.match(/^#\s+(?:ADR[-\s]?\d+[:\s-]+)?(.+)$/m);
|
|
84
|
+
return match ? match[1].trim() : 'Untitled ADR';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Extract the ADR status
|
|
89
|
+
*
|
|
90
|
+
* @param {string} content - ADR markdown content
|
|
91
|
+
* @returns {string} ADR status
|
|
92
|
+
*/
|
|
93
|
+
function extractStatus(content) {
|
|
94
|
+
const statusSection = extractSection(content, 'Status');
|
|
95
|
+
if (!statusSection) return 'proposed';
|
|
96
|
+
|
|
97
|
+
const statusMatch = statusSection.match(/\b(accepted|proposed|deprecated|superseded|rejected)\b/i);
|
|
98
|
+
return statusMatch ? statusMatch[1].toLowerCase() : 'proposed';
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Extract a named section from ADR markdown
|
|
103
|
+
*
|
|
104
|
+
* @param {string} content - ADR markdown content
|
|
105
|
+
* @param {string} sectionName - Name of the section to extract
|
|
106
|
+
* @returns {string|null} Section content or null
|
|
107
|
+
*/
|
|
108
|
+
function extractSection(content, sectionName) {
|
|
109
|
+
// Split content by section headers and find the matching one
|
|
110
|
+
const sectionRegex = /^(##\s+.+)$/gm;
|
|
111
|
+
const headers = [];
|
|
112
|
+
let match;
|
|
113
|
+
|
|
114
|
+
while ((match = sectionRegex.exec(content)) !== null) {
|
|
115
|
+
headers.push({ text: match[1], index: match.index });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
for (let i = 0; i < headers.length; i++) {
|
|
119
|
+
const headerText = headers[i].text.replace(/^##\s+/, '').trim();
|
|
120
|
+
if (headerText.toLowerCase() === sectionName.toLowerCase()) {
|
|
121
|
+
const startIndex = headers[i].index + headers[i].text.length;
|
|
122
|
+
const endIndex = (i + 1 < headers.length) ? headers[i + 1].index : content.length;
|
|
123
|
+
const sectionContent = content.substring(startIndex, endIndex).trim();
|
|
124
|
+
return sectionContent || null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Extract rules from the Decision section
|
|
133
|
+
* Maps decision statements to YAML rules with action types
|
|
134
|
+
*
|
|
135
|
+
* @param {string} decisionText - Content of the Decision section
|
|
136
|
+
* @returns {Array<Object>} Array of rule objects with action and rule text
|
|
137
|
+
*/
|
|
138
|
+
function extractRules(decisionText) {
|
|
139
|
+
if (!decisionText) return [];
|
|
140
|
+
|
|
141
|
+
const rules = [];
|
|
142
|
+
const lines = decisionText.split('\n');
|
|
143
|
+
|
|
144
|
+
// Process list items and standalone sentences
|
|
145
|
+
const statements = [];
|
|
146
|
+
|
|
147
|
+
for (const line of lines) {
|
|
148
|
+
const trimmed = line.trim();
|
|
149
|
+
|
|
150
|
+
// Capture bullet/numbered list items
|
|
151
|
+
const listMatch = trimmed.match(/^[-*\d]+\.?\s+(.+)$/);
|
|
152
|
+
if (listMatch) {
|
|
153
|
+
statements.push(listMatch[1].trim());
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Capture sentences that contain action keywords
|
|
158
|
+
if (trimmed.length > 15 && containsActionKeyword(trimmed)) {
|
|
159
|
+
statements.push(trimmed);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Also split paragraph text into sentences for action keyword detection
|
|
164
|
+
const paragraphText = lines
|
|
165
|
+
.filter(l => !l.trim().match(/^[-*\d]+\./))
|
|
166
|
+
.join(' ')
|
|
167
|
+
.trim();
|
|
168
|
+
|
|
169
|
+
const sentences = paragraphText.split(/(?<=[.!])\s+/);
|
|
170
|
+
for (const sentence of sentences) {
|
|
171
|
+
const cleaned = sentence.trim();
|
|
172
|
+
if (cleaned.length > 15 && containsActionKeyword(cleaned)) {
|
|
173
|
+
// Avoid duplicates with list items
|
|
174
|
+
const isDuplicate = statements.some(s =>
|
|
175
|
+
s.includes(cleaned) || cleaned.includes(s)
|
|
176
|
+
);
|
|
177
|
+
if (!isDuplicate) {
|
|
178
|
+
statements.push(cleaned);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
for (const statement of statements) {
|
|
184
|
+
const action = classifyAction(statement);
|
|
185
|
+
const ruleText = cleanRuleText(statement, action);
|
|
186
|
+
|
|
187
|
+
if (ruleText.length > 5) {
|
|
188
|
+
rules.push({
|
|
189
|
+
action,
|
|
190
|
+
rule: ruleText
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// If no specific rules extracted, create a general USE rule from the decision
|
|
196
|
+
if (rules.length === 0 && decisionText.length > 20) {
|
|
197
|
+
const summary = decisionText.split('\n')[0].trim();
|
|
198
|
+
if (summary.length > 10) {
|
|
199
|
+
rules.push({
|
|
200
|
+
action: 'USE',
|
|
201
|
+
rule: summary.replace(/^We\s+(will|have decided to)\s+/i, '').trim()
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return rules;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Extract anti-patterns from the Consequences section
|
|
211
|
+
* Negative consequences indicate what to avoid
|
|
212
|
+
*
|
|
213
|
+
* @param {string} consequencesText - Content of the Consequences section
|
|
214
|
+
* @returns {Array<string>} Array of anti-pattern descriptions
|
|
215
|
+
*/
|
|
216
|
+
function extractAntiPatterns(consequencesText) {
|
|
217
|
+
if (!consequencesText) return [];
|
|
218
|
+
|
|
219
|
+
const antiPatterns = [];
|
|
220
|
+
const lines = consequencesText.split('\n');
|
|
221
|
+
|
|
222
|
+
let inNegativeSection = false;
|
|
223
|
+
|
|
224
|
+
for (const line of lines) {
|
|
225
|
+
const trimmed = line.trim();
|
|
226
|
+
|
|
227
|
+
// Detect negative subsection headers
|
|
228
|
+
if (trimmed.match(/^###?\s*(negative|bad|risks?|downsides?|drawbacks?|disadvantages?)/i)) {
|
|
229
|
+
inNegativeSection = true;
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Detect positive subsection headers (exit negative section)
|
|
234
|
+
if (trimmed.match(/^###?\s*(positive|good|benefits?|advantages?|upsides?)/i)) {
|
|
235
|
+
inNegativeSection = false;
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Detect neutral subsection headers
|
|
240
|
+
if (trimmed.match(/^###?\s*(neutral)/i)) {
|
|
241
|
+
inNegativeSection = false;
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Capture list items in negative sections
|
|
246
|
+
if (inNegativeSection) {
|
|
247
|
+
const listMatch = trimmed.match(/^[-*]\s+(.+)$/);
|
|
248
|
+
if (listMatch && listMatch[1].length > 10) {
|
|
249
|
+
antiPatterns.push(listMatch[1].trim());
|
|
250
|
+
}
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Capture items with negative markers anywhere in consequences
|
|
255
|
+
const negativeMatch = trimmed.match(/^[-*]\s*(?:(?:Bad|Negative|Risk|Downside)[:\s]+)?(.+)$/i);
|
|
256
|
+
if (negativeMatch) {
|
|
257
|
+
const item = negativeMatch[1].trim();
|
|
258
|
+
if (isNegativeConsequence(item)) {
|
|
259
|
+
antiPatterns.push(item);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return antiPatterns;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Check if a consequence line indicates a negative outcome
|
|
269
|
+
*
|
|
270
|
+
* @param {string} text - Consequence text
|
|
271
|
+
* @returns {boolean} True if the text describes a negative consequence
|
|
272
|
+
*/
|
|
273
|
+
function isNegativeConsequence(text) {
|
|
274
|
+
const negativeIndicators = [
|
|
275
|
+
/\brisk\b/i,
|
|
276
|
+
/\bcomplexity\b/i,
|
|
277
|
+
/\bdifficult\b/i,
|
|
278
|
+
/\boverhead\b/i,
|
|
279
|
+
/\bcost\b/i,
|
|
280
|
+
/\bslower\b/i,
|
|
281
|
+
/\blimitation\b/i,
|
|
282
|
+
/\bdrawback\b/i,
|
|
283
|
+
/\bdownside\b/i,
|
|
284
|
+
/\bchalleng/i,
|
|
285
|
+
/\bbreaking\b/i,
|
|
286
|
+
/\bmigration\b/i,
|
|
287
|
+
/\blearning curve\b/i,
|
|
288
|
+
/\bcoupling\b/i,
|
|
289
|
+
/\block-in\b/i,
|
|
290
|
+
/\btechnical debt\b/i,
|
|
291
|
+
/\bneed(?:s)?\s+to\b/i,
|
|
292
|
+
/\brequire(?:s)?\s+(?:additional|extra|more)\b/i
|
|
293
|
+
];
|
|
294
|
+
|
|
295
|
+
return negativeIndicators.some(pattern => pattern.test(text));
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Check if text contains an action keyword
|
|
300
|
+
*
|
|
301
|
+
* @param {string} text - Text to check
|
|
302
|
+
* @returns {boolean} True if text contains an action keyword
|
|
303
|
+
*/
|
|
304
|
+
function containsActionKeyword(text) {
|
|
305
|
+
return ACTION_PATTERNS.some(p => p.regex.test(text));
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Classify the action type for a decision statement
|
|
310
|
+
*
|
|
311
|
+
* @param {string} statement - Decision statement
|
|
312
|
+
* @returns {string} Action type: ALWAYS, NEVER, USE, PREFER, or AVOID
|
|
313
|
+
*/
|
|
314
|
+
function classifyAction(statement) {
|
|
315
|
+
// Check NEVER before ALWAYS since "must not" contains "must"
|
|
316
|
+
for (const { regex, action } of ACTION_PATTERNS) {
|
|
317
|
+
if (action === 'NEVER' && regex.test(statement)) return 'NEVER';
|
|
318
|
+
}
|
|
319
|
+
for (const { regex, action } of ACTION_PATTERNS) {
|
|
320
|
+
if (action === 'AVOID' && regex.test(statement)) return 'AVOID';
|
|
321
|
+
}
|
|
322
|
+
for (const { regex, action } of ACTION_PATTERNS) {
|
|
323
|
+
if (action === 'ALWAYS' && regex.test(statement)) return 'ALWAYS';
|
|
324
|
+
}
|
|
325
|
+
for (const { regex, action } of ACTION_PATTERNS) {
|
|
326
|
+
if (action === 'PREFER' && regex.test(statement)) return 'PREFER';
|
|
327
|
+
}
|
|
328
|
+
for (const { regex, action } of ACTION_PATTERNS) {
|
|
329
|
+
if (action === 'USE' && regex.test(statement)) return 'USE';
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return 'USE';
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Clean rule text by removing leading action verbs for cleaner output
|
|
337
|
+
*
|
|
338
|
+
* @param {string} text - Raw rule text
|
|
339
|
+
* @param {string} action - Classified action type
|
|
340
|
+
* @returns {string} Cleaned rule text
|
|
341
|
+
*/
|
|
342
|
+
function cleanRuleText(text, action) {
|
|
343
|
+
let cleaned = text
|
|
344
|
+
.replace(/^We\s+(will|have decided to|decided to|should)\s+/i, '')
|
|
345
|
+
.replace(/^(Must|Shall|Always|Never|Should|Should not|Must not)\s+/i, '')
|
|
346
|
+
.replace(/\s*\.$/, '')
|
|
347
|
+
.trim();
|
|
348
|
+
|
|
349
|
+
// Capitalize first letter
|
|
350
|
+
if (cleaned.length > 0) {
|
|
351
|
+
cleaned = cleaned.charAt(0).toUpperCase() + cleaned.slice(1);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return cleaned;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Generate a standards-compatible ID from filename and title
|
|
359
|
+
*
|
|
360
|
+
* @param {string} filename - Source filename
|
|
361
|
+
* @param {string} title - ADR title
|
|
362
|
+
* @returns {string} Generated ID
|
|
363
|
+
*/
|
|
364
|
+
function generateId(filename, title) {
|
|
365
|
+
// Use filename slug if it looks like an ADR (e.g., "0001-use-react.md")
|
|
366
|
+
const filenameSlug = filename
|
|
367
|
+
.replace(/\.md$/i, '')
|
|
368
|
+
.replace(/[^a-z0-9]+/gi, '-')
|
|
369
|
+
.toLowerCase();
|
|
370
|
+
|
|
371
|
+
if (filenameSlug.match(/^\d+-/)) {
|
|
372
|
+
return `adr-${filenameSlug}`;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Fall back to title-based ID
|
|
376
|
+
const titleSlug = title
|
|
377
|
+
.replace(/[^a-z0-9]+/gi, '-')
|
|
378
|
+
.toLowerCase()
|
|
379
|
+
.substring(0, 60);
|
|
380
|
+
|
|
381
|
+
return `adr-${titleSlug}`;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Infer category from ADR content
|
|
386
|
+
*
|
|
387
|
+
* @param {string} content - ADR markdown content
|
|
388
|
+
* @returns {string} Inferred category
|
|
389
|
+
*/
|
|
390
|
+
function inferCategory(content) {
|
|
391
|
+
const lower = content.toLowerCase();
|
|
392
|
+
|
|
393
|
+
const categoryKeywords = {
|
|
394
|
+
'serverless-saas-aws': ['lambda', 'api gateway', 'serverless', 'aws', 'sam template', 'cloudformation'],
|
|
395
|
+
'frontend-development': ['react', 'vue', 'angular', 'frontend', 'component', 'ui', 'css', 'tsx', 'jsx'],
|
|
396
|
+
'database': ['database', 'postgresql', 'sql', 'schema', 'migration', 'query', 'orm'],
|
|
397
|
+
'backend': ['api', 'service', 'handler', 'endpoint', 'rest', 'graphql'],
|
|
398
|
+
'multi-agent-orchestration': ['agent', 'orchestration', 'llm', 'ai', 'workflow'],
|
|
399
|
+
'compliance-security': ['security', 'authentication', 'authorization', 'encryption', 'compliance', 'audit'],
|
|
400
|
+
'cost-optimization': ['cost', 'performance', 'optimization', 'scaling', 'caching'],
|
|
401
|
+
'testing': ['test', 'testing', 'coverage', 'ci/cd', 'pipeline'],
|
|
402
|
+
'deployment': ['deploy', 'infrastructure', 'docker', 'kubernetes', 'ci/cd']
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
for (const [category, keywords] of Object.entries(categoryKeywords)) {
|
|
406
|
+
const matchCount = keywords.filter(kw => lower.includes(kw)).length;
|
|
407
|
+
if (matchCount >= 2) return category;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Single keyword fallback
|
|
411
|
+
for (const [category, keywords] of Object.entries(categoryKeywords)) {
|
|
412
|
+
if (keywords.some(kw => lower.includes(kw))) return category;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return 'general';
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Determine priority based on ADR status and rule types
|
|
420
|
+
* Priority values: 10 (high), 20 (medium), 30 (low)
|
|
421
|
+
*
|
|
422
|
+
* @param {string} status - ADR status
|
|
423
|
+
* @param {Array} rules - Extracted rules
|
|
424
|
+
* @returns {number} Priority value
|
|
425
|
+
*/
|
|
426
|
+
function determinePriority(status, rules) {
|
|
427
|
+
// Deprecated/superseded ADRs are low priority
|
|
428
|
+
if (status === 'deprecated' || status === 'superseded') return 30;
|
|
429
|
+
|
|
430
|
+
// If rules contain ALWAYS or NEVER, higher priority
|
|
431
|
+
const hasEnforcedRules = rules.some(r =>
|
|
432
|
+
r.action === 'ALWAYS' || r.action === 'NEVER'
|
|
433
|
+
);
|
|
434
|
+
|
|
435
|
+
if (hasEnforcedRules && status === 'accepted') return 10;
|
|
436
|
+
if (status === 'accepted') return 20;
|
|
437
|
+
|
|
438
|
+
return 30;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Extract relevant tags from ADR content
|
|
443
|
+
*
|
|
444
|
+
* @param {string} content - ADR content
|
|
445
|
+
* @param {string} category - Assigned category
|
|
446
|
+
* @returns {Array<string>} Tags
|
|
447
|
+
*/
|
|
448
|
+
function extractTags(content, category) {
|
|
449
|
+
const tags = ['adr', category];
|
|
450
|
+
const lower = content.toLowerCase();
|
|
451
|
+
|
|
452
|
+
const tagKeywords = {
|
|
453
|
+
'architecture': /\barchitectur/i,
|
|
454
|
+
'breaking-change': /\bbreaking\s+change/i,
|
|
455
|
+
'migration': /\bmigration/i,
|
|
456
|
+
'security': /\bsecurity\b/i,
|
|
457
|
+
'performance': /\bperformance\b/i,
|
|
458
|
+
'scalability': /\bscalability\b/i,
|
|
459
|
+
'maintainability': /\bmaintainability\b/i,
|
|
460
|
+
'testing': /\btesting\b/i
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
for (const [tag, pattern] of Object.entries(tagKeywords)) {
|
|
464
|
+
if (pattern.test(lower)) {
|
|
465
|
+
tags.push(tag);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
return [...new Set(tags)];
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
module.exports = {
|
|
473
|
+
parseAdr,
|
|
474
|
+
extractTitle,
|
|
475
|
+
extractStatus,
|
|
476
|
+
extractSection,
|
|
477
|
+
extractRules,
|
|
478
|
+
extractAntiPatterns
|
|
479
|
+
};
|