brain-dev 0.1.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 (78) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +152 -0
  3. package/agents/brain-checker.md +33 -0
  4. package/agents/brain-debugger.md +35 -0
  5. package/agents/brain-executor.md +37 -0
  6. package/agents/brain-mapper.md +44 -0
  7. package/agents/brain-planner.md +49 -0
  8. package/agents/brain-researcher.md +47 -0
  9. package/agents/brain-synthesizer.md +43 -0
  10. package/agents/brain-verifier.md +41 -0
  11. package/bin/brain-tools.cjs +185 -0
  12. package/bin/lib/adr.cjs +283 -0
  13. package/bin/lib/agents.cjs +152 -0
  14. package/bin/lib/anti-patterns.cjs +183 -0
  15. package/bin/lib/audit.cjs +268 -0
  16. package/bin/lib/commands/adr.cjs +126 -0
  17. package/bin/lib/commands/complete.cjs +270 -0
  18. package/bin/lib/commands/config.cjs +306 -0
  19. package/bin/lib/commands/discuss.cjs +237 -0
  20. package/bin/lib/commands/execute.cjs +415 -0
  21. package/bin/lib/commands/health.cjs +103 -0
  22. package/bin/lib/commands/map.cjs +101 -0
  23. package/bin/lib/commands/new-project.cjs +885 -0
  24. package/bin/lib/commands/pause.cjs +142 -0
  25. package/bin/lib/commands/phase-manage.cjs +357 -0
  26. package/bin/lib/commands/plan.cjs +451 -0
  27. package/bin/lib/commands/progress.cjs +167 -0
  28. package/bin/lib/commands/quick.cjs +447 -0
  29. package/bin/lib/commands/resume.cjs +196 -0
  30. package/bin/lib/commands/storm.cjs +590 -0
  31. package/bin/lib/commands/verify.cjs +504 -0
  32. package/bin/lib/commands.cjs +263 -0
  33. package/bin/lib/complexity.cjs +138 -0
  34. package/bin/lib/complexity.test.cjs +108 -0
  35. package/bin/lib/config.cjs +452 -0
  36. package/bin/lib/core.cjs +62 -0
  37. package/bin/lib/detect.cjs +603 -0
  38. package/bin/lib/git.cjs +112 -0
  39. package/bin/lib/health.cjs +356 -0
  40. package/bin/lib/init.cjs +310 -0
  41. package/bin/lib/logger.cjs +100 -0
  42. package/bin/lib/platform.cjs +58 -0
  43. package/bin/lib/requirements.cjs +158 -0
  44. package/bin/lib/roadmap.cjs +228 -0
  45. package/bin/lib/security.cjs +237 -0
  46. package/bin/lib/state.cjs +353 -0
  47. package/bin/lib/templates.cjs +48 -0
  48. package/bin/templates/advocate.md +182 -0
  49. package/bin/templates/checkpoint.md +55 -0
  50. package/bin/templates/debugger.md +148 -0
  51. package/bin/templates/discuss.md +60 -0
  52. package/bin/templates/executor.md +201 -0
  53. package/bin/templates/mapper.md +129 -0
  54. package/bin/templates/plan-checker.md +134 -0
  55. package/bin/templates/planner.md +165 -0
  56. package/bin/templates/researcher.md +78 -0
  57. package/bin/templates/storm.html +376 -0
  58. package/bin/templates/synthesis.md +30 -0
  59. package/bin/templates/verifier.md +181 -0
  60. package/commands/brain/adr.md +34 -0
  61. package/commands/brain/complete.md +37 -0
  62. package/commands/brain/config.md +37 -0
  63. package/commands/brain/discuss.md +35 -0
  64. package/commands/brain/execute.md +38 -0
  65. package/commands/brain/health.md +33 -0
  66. package/commands/brain/map.md +35 -0
  67. package/commands/brain/new-project.md +38 -0
  68. package/commands/brain/pause.md +26 -0
  69. package/commands/brain/plan.md +38 -0
  70. package/commands/brain/progress.md +28 -0
  71. package/commands/brain/quick.md +51 -0
  72. package/commands/brain/resume.md +28 -0
  73. package/commands/brain/storm.md +30 -0
  74. package/commands/brain/verify.md +39 -0
  75. package/hooks/bootstrap.sh +54 -0
  76. package/hooks/post-tool-use.sh +45 -0
  77. package/hooks/statusline.sh +130 -0
  78. package/package.json +36 -0
@@ -0,0 +1,283 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const { atomicWriteSync } = require('./state.cjs');
6
+
7
+ /**
8
+ * Get the next available ADR ID by scanning existing ADR files.
9
+ * @param {string} adrsDir - Path to .brain/adrs/ directory
10
+ * @returns {number} Next available ID (1 if empty or dir missing)
11
+ */
12
+ function getNextId(adrsDir) {
13
+ if (!fs.existsSync(adrsDir)) return 1;
14
+
15
+ const files = fs.readdirSync(adrsDir).filter(f => /^ADR-\d+\.md$/.test(f));
16
+ if (files.length === 0) return 1;
17
+
18
+ const ids = files.map(f => parseInt(f.match(/ADR-(\d+)\.md/)[1], 10));
19
+ return Math.max(...ids) + 1;
20
+ }
21
+
22
+ /**
23
+ * Create a new Architecture Decision Record.
24
+ * @param {string} brainDir - Path to .brain/ directory
25
+ * @param {object} options
26
+ * @param {string} options.title - ADR title
27
+ * @param {string} options.context - Why this decision came up
28
+ * @param {string} options.decision - What was chosen
29
+ * @param {string} options.alternatives - What was rejected
30
+ * @param {string} options.consequences - Impact of the decision
31
+ * @param {string} options.phase - Phase number
32
+ * @param {string} options.plan - Plan number
33
+ * @param {string} [options.deciders] - Who made the decision
34
+ * @returns {{ id: string, path: string }}
35
+ */
36
+ function createADR(brainDir, options) {
37
+ const adrsDir = path.join(brainDir, 'adrs');
38
+ fs.mkdirSync(adrsDir, { recursive: true });
39
+
40
+ const nextId = getNextId(adrsDir);
41
+ const paddedId = String(nextId).padStart(3, '0');
42
+ const adrId = `ADR-${paddedId}`;
43
+ const filePath = path.join(adrsDir, `${adrId}.md`);
44
+ const date = new Date().toISOString().slice(0, 10);
45
+
46
+ const content = [
47
+ '---',
48
+ `id: ${nextId}`,
49
+ `date: "${date}"`,
50
+ 'status: proposed',
51
+ `phase: "${options.phase}"`,
52
+ `plan: "${options.plan}"`,
53
+ `deciders: "${options.deciders || 'unknown'}"`,
54
+ 'supersedes: null',
55
+ 'superseded_by: null',
56
+ '---',
57
+ '',
58
+ `# ${adrId}: ${options.title}`,
59
+ '',
60
+ '## Context',
61
+ '',
62
+ options.context || '',
63
+ '',
64
+ '## Decision',
65
+ '',
66
+ options.decision || '',
67
+ '',
68
+ '## Alternatives',
69
+ '',
70
+ options.alternatives || '',
71
+ '',
72
+ '## Consequences',
73
+ '',
74
+ options.consequences || '',
75
+ ''
76
+ ].join('\n');
77
+
78
+ atomicWriteSync(filePath, content);
79
+
80
+ return { id: adrId, path: filePath };
81
+ }
82
+
83
+ /**
84
+ * Parse YAML-ish frontmatter from ADR file content.
85
+ * @param {string} content - File content
86
+ * @returns {object} Parsed frontmatter fields
87
+ */
88
+ function parseFrontmatter(content) {
89
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
90
+ if (!match) return {};
91
+
92
+ const fm = {};
93
+ for (const line of match[1].split('\n')) {
94
+ const colonIdx = line.indexOf(':');
95
+ if (colonIdx === -1) continue;
96
+ const key = line.slice(0, colonIdx).trim();
97
+ let value = line.slice(colonIdx + 1).trim();
98
+ // Remove quotes (preserve as string if originally quoted)
99
+ let wasQuoted = false;
100
+ if ((value.startsWith('"') && value.endsWith('"')) ||
101
+ (value.startsWith("'") && value.endsWith("'"))) {
102
+ value = value.slice(1, -1);
103
+ wasQuoted = true;
104
+ }
105
+ if (value === 'null') value = null;
106
+ else if (!wasQuoted && /^\d+$/.test(value)) value = parseInt(value, 10);
107
+ fm[key] = value;
108
+ }
109
+ return fm;
110
+ }
111
+
112
+ /**
113
+ * Extract title from first # heading in ADR content.
114
+ * @param {string} content - File content
115
+ * @returns {string} Title
116
+ */
117
+ function extractTitle(content) {
118
+ const match = content.match(/^#\s+(.+)$/m);
119
+ return match ? match[1] : 'Untitled';
120
+ }
121
+
122
+ /**
123
+ * List all ADRs, optionally filtered by phase.
124
+ * @param {string} brainDir - Path to .brain/ directory
125
+ * @param {object} [options]
126
+ * @param {string} [options.phase] - Filter by phase
127
+ * @returns {Array<{id: number, date: string, status: string, title: string, path: string}>}
128
+ */
129
+ function listADRs(brainDir, options) {
130
+ const adrsDir = path.join(brainDir, 'adrs');
131
+ if (!fs.existsSync(adrsDir)) return [];
132
+
133
+ const files = fs.readdirSync(adrsDir).filter(f => /^ADR-\d+\.md$/.test(f));
134
+ if (files.length === 0) return [];
135
+
136
+ const adrs = files.map(f => {
137
+ const filePath = path.join(adrsDir, f);
138
+ const content = fs.readFileSync(filePath, 'utf8');
139
+ const fm = parseFrontmatter(content);
140
+ return {
141
+ id: fm.id || parseInt(f.match(/ADR-(\d+)\.md/)[1], 10),
142
+ date: fm.date || '',
143
+ status: fm.status || 'unknown',
144
+ title: extractTitle(content),
145
+ phase: fm.phase || '',
146
+ path: filePath
147
+ };
148
+ });
149
+
150
+ // Sort by ID ascending
151
+ adrs.sort((a, b) => a.id - b.id);
152
+
153
+ // Filter by phase if provided
154
+ if (options && options.phase) {
155
+ return adrs.filter(a => String(a.phase) === String(options.phase));
156
+ }
157
+
158
+ return adrs;
159
+ }
160
+
161
+ /**
162
+ * Search ADRs by keyword (case-insensitive).
163
+ * @param {string} brainDir - Path to .brain/ directory
164
+ * @param {string} keyword - Search term
165
+ * @returns {Array<{id: number, date: string, status: string, title: string, path: string}>}
166
+ */
167
+ function searchADRs(brainDir, keyword) {
168
+ const adrsDir = path.join(brainDir, 'adrs');
169
+ if (!fs.existsSync(adrsDir)) return [];
170
+
171
+ const files = fs.readdirSync(adrsDir).filter(f => /^ADR-\d+\.md$/.test(f));
172
+ const lowerKeyword = keyword.toLowerCase();
173
+
174
+ const results = [];
175
+ for (const f of files) {
176
+ const filePath = path.join(adrsDir, f);
177
+ const content = fs.readFileSync(filePath, 'utf8');
178
+ if (content.toLowerCase().includes(lowerKeyword)) {
179
+ const fm = parseFrontmatter(content);
180
+ results.push({
181
+ id: fm.id || parseInt(f.match(/ADR-(\d+)\.md/)[1], 10),
182
+ date: fm.date || '',
183
+ status: fm.status || 'unknown',
184
+ title: extractTitle(content),
185
+ phase: fm.phase || '',
186
+ path: filePath
187
+ });
188
+ }
189
+ }
190
+
191
+ results.sort((a, b) => a.id - b.id);
192
+ return results;
193
+ }
194
+
195
+ /**
196
+ * Update the status of an existing ADR.
197
+ * @param {string} brainDir - Path to .brain/ directory
198
+ * @param {number} adrId - ADR numeric ID
199
+ * @param {string} newStatus - New status (proposed, accepted, deprecated, superseded)
200
+ * @param {object} [opts]
201
+ * @param {number} [opts.superseded_by] - ID of superseding ADR (when status is 'superseded')
202
+ * @param {number} [opts.supersedes] - ID of superseded ADR (when this is the new ADR)
203
+ */
204
+ function updateADRStatus(brainDir, adrId, newStatus, opts) {
205
+ const adrsDir = path.join(brainDir, 'adrs');
206
+ const paddedId = String(adrId).padStart(3, '0');
207
+ const filePath = path.join(adrsDir, `ADR-${paddedId}.md`);
208
+
209
+ if (!fs.existsSync(filePath)) {
210
+ throw new Error(`ADR-${paddedId} not found`);
211
+ }
212
+
213
+ let content = fs.readFileSync(filePath, 'utf8');
214
+
215
+ // Update status
216
+ content = content.replace(/^status:\s*.+$/m, `status: ${newStatus}`);
217
+
218
+ // Update superseded_by if provided
219
+ if (opts && opts.superseded_by != null) {
220
+ content = content.replace(/^superseded_by:\s*.+$/m, `superseded_by: ${opts.superseded_by}`);
221
+ }
222
+
223
+ // Update supersedes if provided
224
+ if (opts && opts.supersedes != null) {
225
+ content = content.replace(/^supersedes:\s*.+$/m, `supersedes: ${opts.supersedes}`);
226
+ }
227
+
228
+ atomicWriteSync(filePath, content);
229
+ }
230
+
231
+ /**
232
+ * ADR-worthy detection keywords.
233
+ */
234
+ const ADR_KEYWORDS = [
235
+ 'chose',
236
+ 'decided to use',
237
+ 'instead of',
238
+ 'alternative was',
239
+ 'trade-off',
240
+ 'trade off',
241
+ 'because of',
242
+ 'rejected approach'
243
+ ];
244
+
245
+ /**
246
+ * ADR-worthy context indicators.
247
+ */
248
+ const ADR_CONTEXTS = [
249
+ 'dependency',
250
+ 'dependencies',
251
+ 'pattern',
252
+ 'architecture',
253
+ 'architectural',
254
+ 'api contract',
255
+ 'module structure',
256
+ 'performance',
257
+ 'simplicity'
258
+ ];
259
+
260
+ /**
261
+ * Check if text contains an ADR-worthy decision.
262
+ * Requires BOTH a keyword match AND a context indicator match.
263
+ * @param {string} text - Text to check
264
+ * @returns {boolean}
265
+ */
266
+ function isADRWorthy(text) {
267
+ if (!text) return false;
268
+ const lower = text.toLowerCase();
269
+
270
+ const hasKeyword = ADR_KEYWORDS.some(kw => lower.includes(kw));
271
+ const hasContext = ADR_CONTEXTS.some(ctx => lower.includes(ctx));
272
+
273
+ return hasKeyword && hasContext;
274
+ }
275
+
276
+ module.exports = {
277
+ getNextId,
278
+ createADR,
279
+ listADRs,
280
+ searchADRs,
281
+ updateADRStatus,
282
+ isADRWorthy
283
+ };
@@ -0,0 +1,152 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Agent registry for brain orchestration.
5
+ * Defines the 7 core agents and their metadata.
6
+ * Constant registry with discovery and validation functions.
7
+ */
8
+
9
+ const MAX_AGENTS = 8;
10
+
11
+ const AGENTS = {
12
+ researcher: {
13
+ template: 'researcher',
14
+ inputs: ['phase_context', 'project_info', 'focus_areas'],
15
+ outputs: ['RESEARCH.md'],
16
+ model: 'inherit',
17
+ description: 'Researches phase domain: codebase patterns, library docs, risks, and recommendations'
18
+ },
19
+ planner: {
20
+ template: 'planner',
21
+ inputs: ['phase_context', 'research', 'context_decisions'],
22
+ outputs: ['PLAN-*.md'],
23
+ model: 'inherit',
24
+ description: 'Creates execution plans with tasks, verification criteria, and dependency graphs'
25
+ },
26
+ 'plan-checker': {
27
+ template: 'plan-checker',
28
+ modes: { advocate: { template: 'advocate', max_iterations: 2 } },
29
+ inputs: ['plan_content', 'phase_requirements', 'context_decisions'],
30
+ outputs: ['checker_result'],
31
+ model: 'inherit',
32
+ description: 'Validates plans across 8 dimensions: coverage, completeness, dependencies, ownership, scope, verification, context, testing'
33
+ },
34
+ executor: {
35
+ template: 'executor',
36
+ inputs: ['plan_content', 'summary_path'],
37
+ outputs: ['SUMMARY-*.md'],
38
+ model: 'inherit',
39
+ description: 'Executes plan tasks with per-task commits, deviation handling, and checkpoint protocol'
40
+ },
41
+ verifier: {
42
+ template: 'verifier',
43
+ inputs: ['must_haves', 'summaries'],
44
+ outputs: ['VERIFICATION.md'],
45
+ model: 'inherit',
46
+ description: 'Verifies plan outputs against must_haves with 3-level depth checks'
47
+ },
48
+ debugger: {
49
+ template: 'debugger',
50
+ inputs: ['error_context', 'task_context', 'attempted_fixes'],
51
+ outputs: ['debug_session'],
52
+ model: 'inherit',
53
+ description: 'Diagnoses and fixes failures using 4-phase method with hypothesis tracking'
54
+ },
55
+ synthesizer: {
56
+ template: 'synthesis',
57
+ inputs: ['research_dir'],
58
+ outputs: ['SUMMARY.md'],
59
+ model: 'inherit',
60
+ description: 'Combines findings from parallel research agents into a unified SUMMARY.md'
61
+ },
62
+ mapper: {
63
+ template: 'mapper',
64
+ inputs: ['focus', 'codebase_root'],
65
+ outputs: ['codebase/*.md'],
66
+ model: 'inherit',
67
+ description: 'Maps codebase across focus areas producing structured Markdown documentation',
68
+ focus: ['tech', 'arch', 'quality', 'concerns']
69
+ }
70
+ };
71
+
72
+ /**
73
+ * Get agent metadata by name.
74
+ * @param {string} name - Agent name
75
+ * @returns {object} Agent metadata with name field added
76
+ * @throws {Error} If agent name is unknown
77
+ */
78
+ function getAgent(name) {
79
+ const agent = AGENTS[name];
80
+ if (!agent) {
81
+ throw new Error(`Unknown agent: ${name}`);
82
+ }
83
+ return { name, ...agent };
84
+ }
85
+
86
+ /**
87
+ * List all registered agent names.
88
+ * @returns {string[]}
89
+ */
90
+ function listAgents() {
91
+ return Object.keys(AGENTS);
92
+ }
93
+
94
+ /**
95
+ * Validate that agent count does not exceed MAX_AGENTS.
96
+ * @returns {boolean}
97
+ */
98
+ function validateAgentCount() {
99
+ return Object.keys(AGENTS).length <= MAX_AGENTS;
100
+ }
101
+
102
+ /**
103
+ * Resolve the model for an agent using 4-level priority:
104
+ * 1. Per-agent override (brainConfig.agents.models[agentName])
105
+ * 2. Active profile preset (custom profiles > built-in PROFILES)
106
+ * 3. Global default (brainConfig.agents.model)
107
+ * 4. Fallback to 'inherit' (use session model)
108
+ *
109
+ * @param {string} agentName - Agent name
110
+ * @param {object|null} brainConfig - brain.json config object
111
+ * @returns {string} Resolved model identifier
112
+ */
113
+ function resolveModel(agentName, brainConfig) {
114
+ if (!brainConfig || !brainConfig.agents) {
115
+ return 'inherit';
116
+ }
117
+
118
+ const agentsCfg = brainConfig.agents;
119
+
120
+ // Priority 1: per-agent override
121
+ if (agentsCfg.models && agentsCfg.models[agentName]) {
122
+ return agentsCfg.models[agentName];
123
+ }
124
+
125
+ // Priority 2: active profile preset
126
+ if (agentsCfg.profile) {
127
+ const { PROFILES } = require('./config.cjs');
128
+ // Check custom profiles first (user-defined), then built-in
129
+ const profile = (agentsCfg.profiles && agentsCfg.profiles[agentsCfg.profile])
130
+ || PROFILES[agentsCfg.profile];
131
+ if (profile && profile.models && profile.models[agentName]) {
132
+ return profile.models[agentName];
133
+ }
134
+ }
135
+
136
+ // Priority 3: global default
137
+ if (agentsCfg.model) {
138
+ return agentsCfg.model;
139
+ }
140
+
141
+ // Priority 4: inherit session model
142
+ return 'inherit';
143
+ }
144
+
145
+ module.exports = {
146
+ AGENTS,
147
+ MAX_AGENTS,
148
+ getAgent,
149
+ listAgents,
150
+ validateAgentCount,
151
+ resolveModel
152
+ };
@@ -0,0 +1,183 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+
6
+ /**
7
+ * Anti-pattern detection patterns for JS/CJS source files.
8
+ * Each pattern has a name, regex, and severity level.
9
+ */
10
+ const JS_PATTERNS = [
11
+ {
12
+ name: 'Empty Function Body',
13
+ regex: /function\s+\w+\s*\([^)]*\)\s*\{\s*\}/,
14
+ severity: 'block'
15
+ },
16
+ {
17
+ name: 'TODO/FIXME Comment',
18
+ regex: /\/\/\s*(TODO|FIXME|HACK)\b/,
19
+ severity: 'block'
20
+ },
21
+ {
22
+ name: 'Log-Only Error Handler',
23
+ regex: /catch\s*\([^)]*\)\s*\{\s*console\.(log|error|warn)\([^)]*\);\s*\}/,
24
+ severity: 'warning'
25
+ },
26
+ {
27
+ name: 'Not Implemented',
28
+ regex: /throw\s+new\s+Error\s*\(\s*['"]not\s+implemented['"]\s*\)/i,
29
+ severity: 'block'
30
+ },
31
+ {
32
+ name: 'Empty Return',
33
+ regex: /return\s+(null|undefined|\[\s*\]|\{\s*\})\s*;/,
34
+ severity: 'warning'
35
+ },
36
+ {
37
+ name: 'Placeholder Content',
38
+ regex: /['"`](placeholder|coming soon|lorem ipsum|sample data|test data|dummy)['"`]/i,
39
+ severity: 'warning'
40
+ }
41
+ ];
42
+
43
+ /**
44
+ * Anti-pattern detection patterns for test files.
45
+ */
46
+ const TEST_PATTERNS = [
47
+ {
48
+ name: 'Skipped Test',
49
+ regex: /(?:it\.skip|xit|xdescribe|describe\.skip)\s*\(/,
50
+ severity: 'block'
51
+ },
52
+ {
53
+ name: 'Empty Test Body',
54
+ regex: /it\s*\(\s*['"][^'"]*['"]\s*,\s*\(\s*\)\s*=>\s*\{\s*\}\s*\)/,
55
+ severity: 'block'
56
+ }
57
+ ];
58
+
59
+ /**
60
+ * Anti-pattern detection patterns for Markdown files.
61
+ */
62
+ const MD_PATTERNS = [
63
+ {
64
+ name: 'TBD/TODO Placeholder',
65
+ regex: /^(TBD|TODO)\s*$/m,
66
+ severity: 'block'
67
+ },
68
+ {
69
+ name: 'Empty Section',
70
+ regex: /^##\s+.+\n\s*\n(?=##\s|$)/m,
71
+ severity: 'warning'
72
+ }
73
+ ];
74
+
75
+ /**
76
+ * Get the appropriate pattern set for a file path.
77
+ * @param {string} filePath - File path to determine patterns for
78
+ * @returns {Array} Pattern array for the file type
79
+ */
80
+ function getPatterns(filePath) {
81
+ const ext = path.extname(filePath).toLowerCase();
82
+ const base = path.basename(filePath).toLowerCase();
83
+
84
+ // Test files get test patterns (check before JS patterns)
85
+ if (base.includes('.test.') || base.includes('.spec.')) {
86
+ return TEST_PATTERNS;
87
+ }
88
+
89
+ // JS/CJS files
90
+ if (ext === '.js' || ext === '.cjs' || ext === '.mjs') {
91
+ return JS_PATTERNS;
92
+ }
93
+
94
+ // Markdown files
95
+ if (ext === '.md') {
96
+ return MD_PATTERNS;
97
+ }
98
+
99
+ return [];
100
+ }
101
+
102
+ /**
103
+ * Scan content string for anti-patterns.
104
+ * @param {string} content - Content to scan
105
+ * @param {string} [filePath] - Optional file path for pattern selection and findings context
106
+ * @returns {{ findings: Array<{ name: string, severity: string, file?: string, line?: number, match: string }> }}
107
+ */
108
+ function scanContent(content, filePath) {
109
+ const findings = [];
110
+ const patterns = filePath ? getPatterns(filePath) : JS_PATTERNS;
111
+
112
+ for (const pattern of patterns) {
113
+ const match = content.match(pattern.regex);
114
+ if (match) {
115
+ const finding = {
116
+ name: pattern.name,
117
+ severity: pattern.severity,
118
+ match: match[0]
119
+ };
120
+ if (filePath) {
121
+ finding.file = filePath;
122
+ }
123
+ // Calculate line number
124
+ if (content.includes('\n')) {
125
+ const beforeMatch = content.slice(0, match.index);
126
+ finding.line = (beforeMatch.match(/\n/g) || []).length + 1;
127
+ }
128
+ findings.push(finding);
129
+ }
130
+ }
131
+
132
+ return { findings };
133
+ }
134
+
135
+ /**
136
+ * Scan files for anti-patterns.
137
+ * @param {string} rootDir - Root directory
138
+ * @param {object} [options] - Scan options
139
+ * @param {string[]} [options.files] - Array of relative file paths to scan (phase-scoped)
140
+ * @returns {{ findings: object[], blockers: object[], warnings: object[] }}
141
+ */
142
+ function scanFiles(rootDir, options = {}) {
143
+ const filesToScan = options.files || [];
144
+
145
+ const allFindings = [];
146
+ const blockers = [];
147
+ const warnings = [];
148
+
149
+ for (const relPath of filesToScan) {
150
+ const fullPath = path.join(rootDir, relPath);
151
+
152
+ let content;
153
+ try {
154
+ content = fs.readFileSync(fullPath, 'utf8');
155
+ } catch {
156
+ // Skip files that don't exist or can't be read
157
+ continue;
158
+ }
159
+
160
+ const result = scanContent(content, relPath);
161
+
162
+ for (const finding of result.findings) {
163
+ allFindings.push(finding);
164
+
165
+ if (finding.severity === 'block') {
166
+ blockers.push(finding);
167
+ } else {
168
+ warnings.push(finding);
169
+ }
170
+ }
171
+ }
172
+
173
+ return { findings: allFindings, blockers, warnings };
174
+ }
175
+
176
+ module.exports = {
177
+ JS_PATTERNS,
178
+ TEST_PATTERNS,
179
+ MD_PATTERNS,
180
+ scanContent,
181
+ scanFiles,
182
+ getPatterns
183
+ };