project-graph-mcp 1.2.4 → 1.5.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.
@@ -1,14 +1,24 @@
1
1
  /**
2
2
  * Full Analysis - Comprehensive Code Health Report
3
3
  * Runs all analysis tools and generates a health score
4
+ *
5
+ * Uses incremental caching for per-file metrics (complexity, undocumented, jsdocConsistency).
6
+ * Cross-file metrics (dead code, similarity) always run dynamically.
4
7
  */
5
8
 
9
+ import { readFileSync, readdirSync, statSync } from 'fs';
10
+ import { join, relative, resolve } from 'path';
6
11
  import { getDeadCode } from './dead-code.js';
7
- import { getUndocumentedSummary } from './undocumented.js';
12
+ import { checkUndocumentedFile } from './undocumented.js';
8
13
  import { getSimilarFunctions } from './similar-functions.js';
9
- import { getComplexity } from './complexity.js';
14
+ import { analyzeComplexityFile } from './complexity.js';
10
15
  import { getLargeFiles } from './large-files.js';
11
16
  import { getOutdatedPatterns } from './outdated-patterns.js';
17
+ import { getTableUsage } from './db-analysis.js';
18
+ import { checkJSDocFile } from './jsdoc-checker.js';
19
+ import { readCache, writeCache, computeContentHash, isCacheValid } from './analysis-cache.js';
20
+ import { shouldExcludeDir, shouldExcludeFile, parseGitignore } from './filters.js';
21
+ import { getWorkspaceRoot } from './workspace.js';
12
22
 
13
23
  /**
14
24
  * @typedef {Object} AnalysisResult
@@ -74,10 +84,21 @@ function calculateHealthScore(results) {
74
84
  const warningPatterns = results.outdated.stats?.bySeverity?.warning || 0;
75
85
  const outdatedPenalty = Math.min(errorPatterns * 3 + warningPatterns * 1, 10);
76
86
  score -= outdatedPenalty;
77
- if (results.outdated.redundantDeps.length > 0) {
87
+ if (results.outdated.redundantDeps?.length > 0) {
78
88
  topIssues.push(`${results.outdated.redundantDeps.length} redundant npm dependencies`);
79
89
  }
80
90
 
91
+ // JSDoc consistency penalty: -2 per error, -1 per warning (max -15)
92
+ if (results.jsdocConsistency) {
93
+ const jsdocErrors = results.jsdocConsistency.errors || 0;
94
+ const jsdocWarnings = results.jsdocConsistency.warnings || 0;
95
+ const jsdocPenalty = Math.min(jsdocErrors * 2 + jsdocWarnings * 1, 15);
96
+ score -= jsdocPenalty;
97
+ if (jsdocErrors > 0) {
98
+ topIssues.push(`${jsdocErrors} JSDoc consistency errors`);
99
+ }
100
+ }
101
+
81
102
  // Clamp score
82
103
  score = Math.max(0, Math.min(100, Math.round(score)));
83
104
 
@@ -91,8 +112,158 @@ function calculateHealthScore(results) {
91
112
  return { score, rating, topIssues: topIssues.slice(0, 5) };
92
113
  }
93
114
 
115
+ /**
116
+ * Find all JS files in directory
117
+ * @param {string} dir
118
+ * @param {string} rootDir
119
+ * @returns {string[]}
120
+ */
121
+ function findJSFiles(dir, rootDir = dir) {
122
+ if (dir === rootDir) parseGitignore(rootDir);
123
+ const files = [];
124
+ try {
125
+ for (const entry of readdirSync(dir)) {
126
+ const fullPath = join(dir, entry);
127
+ const relativePath = relative(rootDir, fullPath);
128
+ const stat = statSync(fullPath);
129
+ if (stat.isDirectory()) {
130
+ if (!shouldExcludeDir(entry, relativePath)) {
131
+ files.push(...findJSFiles(fullPath, rootDir));
132
+ }
133
+ } else if (entry.endsWith('.js') && !entry.endsWith('.css.js') && !entry.endsWith('.tpl.js')) {
134
+ if (!shouldExcludeFile(entry, relativePath)) {
135
+ files.push(fullPath);
136
+ }
137
+ }
138
+ }
139
+ } catch (e) { /* dir not found */ }
140
+ return files;
141
+ }
142
+
143
+ /**
144
+ * Run cacheable per-file analyses with cache support
145
+ * Returns aggregated complexity, undocumented, and jsdoc results
146
+ * @param {string} dir
147
+ * @param {string} contextDir
148
+ * @returns {{ complexity: Object[], undocumented: Object[], jsdocIssues: Object[], cacheStats: { hits: number, misses: number } }}
149
+ */
150
+ function runCacheableAnalyses(dir, contextDir) {
151
+ const resolvedDir = resolve(dir);
152
+ const wsRoot = getWorkspaceRoot();
153
+ const files = findJSFiles(dir);
154
+
155
+ const allComplexity = [];
156
+ const allUndocumented = [];
157
+ const allJsdocIssues = [];
158
+ let cacheHits = 0;
159
+ let cacheMisses = 0;
160
+
161
+ for (const file of files) {
162
+ const relPath = relative(resolvedDir, file);
163
+ // Cache key: workspace-relative (src/parser.js), matches graph paths
164
+ const cacheKey = relative(wsRoot, file);
165
+ let code;
166
+ try {
167
+ code = readFileSync(file, 'utf-8');
168
+ } catch (e) {
169
+ continue; // File deleted between findJSFiles and read
170
+ }
171
+ const contentHash = computeContentHash(code);
172
+
173
+ // Check cache (key: workspace-relative)
174
+ const cached = readCache(contextDir, cacheKey);
175
+
176
+ if (cached && isCacheValid(cached, cached.sig, contentHash, 'content')) {
177
+ // Cache hit — use cached results
178
+ cacheHits++;
179
+ if (cached.complexity) allComplexity.push(...cached.complexity);
180
+ if (cached.undocumented) allUndocumented.push(...cached.undocumented);
181
+ if (cached.jsdocIssues) allJsdocIssues.push(...cached.jsdocIssues);
182
+ } else {
183
+ // Cache miss — compute fresh
184
+ cacheMisses++;
185
+ const complexity = analyzeComplexityFile(code, relPath);
186
+ const undocumented = checkUndocumentedFile(code, relPath, 'tests');
187
+ const jsdocIssues = checkJSDocFile(code, relPath);
188
+
189
+ allComplexity.push(...complexity);
190
+ allUndocumented.push(...undocumented);
191
+ allJsdocIssues.push(...jsdocIssues);
192
+
193
+ // Save to cache (key: workspace-relative)
194
+ writeCache(contextDir, cacheKey, {
195
+ sig: cached?.sig || contentHash,
196
+ contentHash,
197
+ complexity,
198
+ undocumented,
199
+ jsdocIssues,
200
+ });
201
+ }
202
+ }
203
+
204
+ return {
205
+ complexity: allComplexity,
206
+ undocumented: allUndocumented,
207
+ jsdocIssues: allJsdocIssues,
208
+ cacheStats: { hits: cacheHits, misses: cacheMisses },
209
+ };
210
+ }
211
+
212
+ /**
213
+ * Aggregate complexity items into summary format
214
+ * @param {Object[]} items
215
+ * @param {number} minComplexity
216
+ * @returns {Object}
217
+ */
218
+ function aggregateComplexity(items, minComplexity = 5) {
219
+ let filtered = items.filter(i => i.complexity >= minComplexity);
220
+ filtered.sort((a, b) => b.complexity - a.complexity);
221
+
222
+ const stats = {
223
+ low: filtered.filter(i => i.rating === 'low').length,
224
+ moderate: filtered.filter(i => i.rating === 'moderate').length,
225
+ high: filtered.filter(i => i.rating === 'high').length,
226
+ critical: filtered.filter(i => i.rating === 'critical').length,
227
+ average: filtered.length > 0
228
+ ? Math.round(filtered.reduce((s, i) => s + i.complexity, 0) / filtered.length * 10) / 10
229
+ : 0,
230
+ };
231
+
232
+ return { total: filtered.length, stats, items: filtered.slice(0, 30) };
233
+ }
234
+
235
+ /**
236
+ * Aggregate undocumented items into summary format
237
+ * @param {Object[]} items
238
+ * @returns {Object}
239
+ */
240
+ function aggregateUndocumented(items) {
241
+ const byType = {
242
+ class: items.filter(i => i.type === 'class').length,
243
+ function: items.filter(i => i.type === 'function').length,
244
+ method: items.filter(i => i.type === 'method').length,
245
+ };
246
+ return { total: items.length, byType, items: items.slice(0, 20) };
247
+ }
248
+
249
+ /**
250
+ * Aggregate JSDoc issues into summary format
251
+ * @param {Object[]} issues
252
+ * @returns {{ issues: Object[], summary: Object }}
253
+ */
254
+ function aggregateJSDoc(issues) {
255
+ const errors = issues.filter(i => i.severity === 'error').length;
256
+ const warnings = issues.filter(i => i.severity === 'warning').length;
257
+ const byFile = {};
258
+ for (const issue of issues) {
259
+ byFile[issue.file] = (byFile[issue.file] || 0) + 1;
260
+ }
261
+ return { issues, summary: { total: issues.length, errors, warnings, byFile } };
262
+ }
263
+
94
264
  /**
95
265
  * Run full analysis on directory
266
+ * Uses incremental cache for per-file metrics; cross-file metrics always recompute.
96
267
  * @param {string} dir
97
268
  * @param {Object} [options]
98
269
  * @param {boolean} [options.includeItems=false] - Include individual items
@@ -100,15 +271,22 @@ function calculateHealthScore(results) {
100
271
  */
101
272
  export async function getFullAnalysis(dir, options = {}) {
102
273
  const includeItems = options.includeItems || false;
274
+ const resolvedDir = resolve(dir);
275
+ const contextDir = join(getWorkspaceRoot(), '.context');
103
276
 
104
- // Run all analyses in parallel
105
- const [deadCode, undocumented, similar, complexity, largeFiles, outdated] = await Promise.all([
106
- getDeadCode(dir),
107
- getUndocumentedSummary(dir, 'tests'),
108
- getSimilarFunctions(dir, { threshold: 70 }),
109
- getComplexity(dir, { minComplexity: 5 }),
110
- getLargeFiles(dir),
111
- getOutdatedPatterns(dir),
277
+ // Run cacheable per-file analyses (complexity, undocumented, jsdoc)
278
+ const cached = runCacheableAnalyses(dir, contextDir);
279
+ const complexity = aggregateComplexity(cached.complexity);
280
+ const undocumented = aggregateUndocumented(cached.undocumented);
281
+ const jsdocCheck = aggregateJSDoc(cached.jsdocIssues);
282
+
283
+ // Run cross-file analyses (always dynamic — NOT cacheable per-file)
284
+ const [deadCode, similar, largeFiles, outdated, dbUsage] = await Promise.all([
285
+ getDeadCode(dir).catch(() => ({ total: 0, byType: {}, items: [] })),
286
+ getSimilarFunctions(dir, { threshold: 70 }).catch(() => ({ total: 0, pairs: [] })),
287
+ getLargeFiles(dir).catch(() => ({ total: 0, stats: {}, items: [] })),
288
+ getOutdatedPatterns(dir).catch(() => ({ codePatterns: [], redundantDeps: [], stats: { totalPatterns: 0, bySeverity: {}, byPattern: {}, redundantDeps: 0 } })),
289
+ getTableUsage(dir).catch(() => ({ tables: [], totalTables: 0, totalQueries: 0 })),
112
290
  ]);
113
291
 
114
292
  // Calculate overall health
@@ -119,6 +297,7 @@ export async function getFullAnalysis(dir, options = {}) {
119
297
  complexity,
120
298
  largeFiles,
121
299
  outdated,
300
+ jsdocConsistency: jsdocCheck.summary,
122
301
  });
123
302
 
124
303
  // Build result
@@ -152,8 +331,140 @@ export async function getFullAnalysis(dir, options = {}) {
152
331
  redundantDeps: outdated.redundantDeps,
153
332
  ...(includeItems && { codePatterns: outdated.codePatterns.slice(0, 10) }),
154
333
  },
334
+ jsdocConsistency: {
335
+ total: jsdocCheck.summary.total,
336
+ errors: jsdocCheck.summary.errors,
337
+ warnings: jsdocCheck.summary.warnings,
338
+ ...(includeItems && { issues: jsdocCheck.issues.slice(0, 10) }),
339
+ },
340
+ cache: cached.cacheStats,
155
341
  overall,
156
342
  };
157
343
 
344
+ // Add DB metrics if any SQL interactions found (non-scoring)
345
+ if (dbUsage.totalTables > 0) {
346
+ result.database = {
347
+ tablesUsed: dbUsage.totalTables,
348
+ totalQueries: dbUsage.totalQueries,
349
+ tables: dbUsage.tables.map(t => ({
350
+ name: t.table,
351
+ readers: t.totalReaders,
352
+ writers: t.totalWriters,
353
+ })),
354
+ };
355
+ }
356
+
158
357
  return result;
159
358
  }
359
+
360
+ /**
361
+ * Quick health check — runs only cached per-file metrics, skips cross-file.
362
+ * @param {string} dir - Path to scan
363
+ * @returns {{healthScore: number, complexity: number, undocumented: number, jsdocIssues: number}}
364
+ */
365
+ export function getAnalysisSummaryOnly(dir) {
366
+ const contextDir = join(getWorkspaceRoot(), '.context');
367
+ const cached = runCacheableAnalyses(dir, contextDir);
368
+ const complexity = aggregateComplexity(cached.complexity);
369
+ const undocumented = aggregateUndocumented(cached.undocumented);
370
+ const jsdocCheck = aggregateJSDoc(cached.jsdocIssues);
371
+
372
+ // Reuse the same health score formula as getFullAnalysis
373
+ const overall = calculateHealthScore({
374
+ deadCode: { total: 0 },
375
+ undocumented,
376
+ similar: { total: 0 },
377
+ complexity,
378
+ largeFiles: { total: 0 },
379
+ outdated: { stats: { totalPatterns: 0 } },
380
+ jsdocConsistency: jsdocCheck.summary,
381
+ });
382
+
383
+ return {
384
+ healthScore: overall.score,
385
+ grade: overall.rating,
386
+ complexity: complexity.total,
387
+ undocumented: undocumented.total,
388
+ jsdocIssues: jsdocCheck.summary.total,
389
+ cache: cached.cacheStats,
390
+ note: 'Partial score — cross-file analyses skipped for speed. Run get_full_analysis for complete health check.',
391
+ };
392
+ }
393
+
394
+ /**
395
+ * Streaming analysis — yields results as each sub-analysis completes.
396
+ * Useful for large codebases where waiting for all analyses is too slow.
397
+ * @param {string} dir - Path to scan
398
+ * @param {Object} [options]
399
+ * @param {boolean} [options.includeItems=false]
400
+ * @returns {AsyncGenerator<{type: string, data: Object}>}
401
+ */
402
+ export async function* getFullAnalysisStreaming(dir, options = {}) {
403
+ const includeItems = options.includeItems || false;
404
+ const contextDir = join(getWorkspaceRoot(), '.context');
405
+
406
+ // Phase 1: Cached per-file analyses (fast)
407
+ const cached = runCacheableAnalyses(dir, contextDir);
408
+
409
+ const complexity = aggregateComplexity(cached.complexity);
410
+ yield { type: 'complexity', data: {
411
+ total: complexity.total,
412
+ stats: complexity.stats,
413
+ ...(includeItems && { items: complexity.items.slice(0, 10) }),
414
+ }};
415
+
416
+ const undocumented = aggregateUndocumented(cached.undocumented);
417
+ yield { type: 'undocumented', data: {
418
+ total: undocumented.total,
419
+ byType: undocumented.byType,
420
+ ...(includeItems && { items: undocumented.items.slice(0, 10) }),
421
+ }};
422
+
423
+ const jsdocCheck = aggregateJSDoc(cached.jsdocIssues);
424
+ yield { type: 'jsdocConsistency', data: {
425
+ total: jsdocCheck.summary.total,
426
+ errors: jsdocCheck.summary.errors,
427
+ warnings: jsdocCheck.summary.warnings,
428
+ ...(includeItems && { issues: jsdocCheck.issues.slice(0, 10) }),
429
+ }};
430
+
431
+ yield { type: 'cache', data: cached.cacheStats };
432
+
433
+ // Phase 2: Cross-file analyses (slow, one at a time)
434
+ try {
435
+ const deadCode = await getDeadCode(dir);
436
+ yield { type: 'deadCode', data: {
437
+ total: deadCode.total,
438
+ byType: deadCode.byType,
439
+ ...(includeItems && { items: deadCode.items.slice(0, 10) }),
440
+ }};
441
+ } catch { yield { type: 'deadCode', data: { total: 0, byType: {}, error: 'analysis failed' } }; }
442
+
443
+ try {
444
+ const similar = await getSimilarFunctions(dir, { threshold: 70 });
445
+ yield { type: 'similar', data: {
446
+ total: similar.total,
447
+ ...(includeItems && { pairs: similar.pairs.slice(0, 5) }),
448
+ }};
449
+ } catch { yield { type: 'similar', data: { total: 0, error: 'analysis failed' } }; }
450
+
451
+ try {
452
+ const largeFiles = await getLargeFiles(dir);
453
+ yield { type: 'largeFiles', data: {
454
+ total: largeFiles.total,
455
+ stats: largeFiles.stats,
456
+ ...(includeItems && { items: largeFiles.items.slice(0, 10) }),
457
+ }};
458
+ } catch { yield { type: 'largeFiles', data: { total: 0, error: 'analysis failed' } }; }
459
+
460
+ try {
461
+ const outdated = await getOutdatedPatterns(dir);
462
+ yield { type: 'outdated', data: {
463
+ totalPatterns: outdated.stats.totalPatterns,
464
+ redundantDeps: outdated.redundantDeps,
465
+ ...(includeItems && { codePatterns: outdated.codePatterns.slice(0, 10) }),
466
+ }};
467
+ } catch { yield { type: 'outdated', data: { totalPatterns: 0, error: 'analysis failed' } }; }
468
+
469
+ yield { type: 'done', data: { phases: 2, timestamp: new Date().toISOString() } };
470
+ }
@@ -20,9 +20,9 @@
20
20
  * @property {number} v - version
21
21
  * @property {Object<string, string>} legend - minified name → full name
22
22
  * @property {Object<string, string>} reverseLegend - full name → minified
23
- * @property {Object} stats - { files, classes, functions }
23
+ * @property {Object} stats - { files, classes, functions, tables }
24
24
  * @property {Object<string, GraphNode>} nodes
25
- * @property {Array<[string, string, string]>} edges - [from, type, to]
25
+ * @property {Array<[string, string, string]>} edges - [from, type, to] where type is →, R→, or W→
26
26
  * @property {string[]} orphans
27
27
  * @property {Object<string, string[]>} duplicates
28
28
  * @property {string[]} files - list of parsed file paths
@@ -106,6 +106,7 @@ export function buildGraph(parsed) {
106
106
  files: (parsed.files || []).length,
107
107
  classes: classes.length,
108
108
  functions: functions.length,
109
+ tables: (parsed.tables || []).length,
109
110
  },
110
111
  nodes: {},
111
112
  edges: [],
@@ -153,6 +154,34 @@ export function buildGraph(parsed) {
153
154
  e: func.exported,
154
155
  f: func.file || undefined,
155
156
  };
157
+
158
+ // Build DB edges from function SQL reads/writes
159
+ for (const table of func.dbReads || []) {
160
+ graph.edges.push([shortName, 'R→', table]);
161
+ }
162
+ for (const table of func.dbWrites || []) {
163
+ graph.edges.push([shortName, 'W→', table]);
164
+ }
165
+ }
166
+
167
+ // Build DB edges from class SQL reads/writes
168
+ for (const cls of classes) {
169
+ const shortName = legend[cls.name];
170
+ for (const table of cls.dbReads || []) {
171
+ graph.edges.push([shortName, 'R→', table]);
172
+ }
173
+ for (const table of cls.dbWrites || []) {
174
+ graph.edges.push([shortName, 'W→', table]);
175
+ }
176
+ }
177
+
178
+ // Build table nodes from parsed SQL files
179
+ for (const table of parsed.tables || []) {
180
+ graph.nodes[table.name] = {
181
+ t: 'T',
182
+ cols: table.columns.map(c => c.name),
183
+ f: table.file || undefined,
184
+ };
156
185
  }
157
186
 
158
187
  // Detect orphans (nodes with no incoming edges)
@@ -215,6 +244,7 @@ export function createSkeleton(graph) {
215
244
  if (node.f) entry.f = node.f;
216
245
  nodes[short] = entry;
217
246
  }
247
+ // Skip Table nodes (T) — they only appear in dedicated DB tools
218
248
  }
219
249
 
220
250
  // Build exported functions grouped by file: { "file.js": ["shortName1", ...] }
@@ -13,121 +13,19 @@ export const AGENT_INSTRUCTIONS = `
13
13
  - **State Management**: Use \`this.init$\` for local state and \`this.sub()\` for reactivity.
14
14
  - **Directives**: Use \`itemize\` for lists, \`js-d-kit\` for static generation.
15
15
 
16
- ## 2. Test Annotations (@test/@expect)
17
- Universal verification checklist system. Works for **any** test type.
18
-
19
- ### Syntax
20
- \`\`\`javascript
21
- /**
22
- * Method description
23
- *
24
- * @test {type}: {description}
25
- * @expect {type}: {description}
26
- */
27
- async myMethod() { ... }
28
- \`\`\`
29
-
30
- ### @test Types by Category
31
-
32
- #### 🌐 Browser / UI
33
- | Type | Description | Example |
34
- |------|-------------|---------|
35
- | \`click\` | Click element | \`@test click: Click submit button\` |
36
- | \`key\` | Keyboard input | \`@test key: Press Enter\` |
37
- | \`drag\` | Drag and drop | \`@test drag: Drag item to list\` |
38
- | \`type\` | Text input | \`@test type: Enter email in field\` |
39
- | \`scroll\` | Scroll action | \`@test scroll: Scroll to bottom\` |
40
- | \`hover\` | Mouse hover | \`@test hover: Hover over menu\` |
41
-
42
- #### 🔌 API / Function
43
- | Type | Description | Example |
44
- |------|-------------|---------|
45
- | \`request\` | HTTP request | \`@test request: POST /api/users\` |
46
- | \`call\` | Function call | \`@test call: Call with valid params\` |
47
- | \`invoke\` | Method invoke | \`@test invoke: Trigger event\` |
48
- | \`mock\` | Mock setup | \`@test mock: Mock external service\` |
49
-
50
- #### 💻 CLI / Process
51
- | Type | Description | Example |
52
- |------|-------------|---------|
53
- | \`run\` | Run command | \`@test run: Run with --help flag\` |
54
- | \`exec\` | Execute script | \`@test exec: Execute build script\` |
55
- | \`spawn\` | Spawn process | \`@test spawn: Start server\` |
56
- | \`input\` | Stdin input | \`@test input: Enter password\` |
57
-
58
- #### 🔗 Integration / System
59
- | Type | Description | Example |
60
- |------|-------------|---------|
61
- | \`setup\` | Test setup | \`@test setup: Create test database\` |
62
- | \`action\` | Main action | \`@test action: Run migration\` |
63
- | \`teardown\` | Cleanup | \`@test teardown: Remove temp files\` |
64
- | \`wait\` | Wait condition | \`@test wait: Wait for DB connection\` |
65
-
66
- ### @expect Types by Category
67
-
68
- #### 🌐 Browser / UI
69
- | Type | Description | Example |
70
- |------|-------------|---------|
71
- | \`attr\` | Attribute check | \`@expect attr: disabled attribute set\` |
72
- | \`visual\` | Visual change | \`@expect visual: Button turns green\` |
73
- | \`element\` | Element exists | \`@expect element: Modal appears\` |
74
- | \`text\` | Text content | \`@expect text: Shows "Success"\` |
75
-
76
- #### 🔌 API / Function
77
- | Type | Description | Example |
78
- |------|-------------|---------|
79
- | \`status\` | HTTP status | \`@expect status: 201 Created\` |
80
- | \`body\` | Response body | \`@expect body: Contains user ID\` |
81
- | \`headers\` | Response headers | \`@expect headers: Content-Type JSON\` |
82
- | \`error\` | Error thrown | \`@expect error: Throws ValidationError\` |
83
-
84
- #### 💻 CLI / Process
85
- | Type | Description | Example |
86
- |------|-------------|---------|
87
- | \`output\` | Stdout content | \`@expect output: Prints version\` |
88
- | \`exitcode\` | Exit code | \`@expect exitcode: Returns 0\` |
89
- | \`file\` | File created | \`@expect file: Creates config.json\` |
90
- | \`stderr\` | Stderr content | \`@expect stderr: No errors\` |
91
-
92
- #### 🔗 Integration / System
93
- | Type | Description | Example |
94
- |------|-------------|---------|
95
- | \`state\` | State change | \`@expect state: User logged in\` |
96
- | \`log\` | Log entry | \`@expect log: Info message logged\` |
97
- | \`event\` | Event fired | \`@expect event: 'updated' emitted\` |
98
- | \`db\` | Database change | \`@expect db: Row inserted\` |
99
-
100
- ### Full Example
101
- \`\`\`javascript
102
- /**
103
- * Create new user via API
104
- *
105
- * @test request: POST /api/users with valid data
106
- * @test call: Validate email format
107
- *
108
- * @expect status: 201 Created
109
- * @expect body: Contains user ID and email
110
- * @expect db: User row created in database
111
- * @expect event: 'user.created' event emitted
112
- */
113
- async createUser(data) {
114
- // ...
115
- }
116
- \`\`\`
117
-
118
- ## 3. General Coding Rules
16
+ ## 2. General Coding Rules
119
17
  - **ESM Only**: Use \`import\` / \`export\`. No \`require\`.
120
18
  - **No Dependencies**: Avoid adding new npm packages unless critical.
121
19
  - **Comments**: Write clear JSDoc for all public methods.
122
20
  - **Async/Await**: Prefer async/await over promises.
123
21
 
124
- ## 4. MCP Tools Usage
22
+ ## 3. MCP Tools Usage
125
23
  - **Graph**: Use \`get_skeleton\` first to map the codebase.
126
24
  - **Deep Dive**: Use \`expand\` to read class details.
127
25
  - **Tests**: Use \`get_pending_tests\` to see what needs verification.
128
26
  - **Guidelines**: Use \`get_agent_instructions\` to refresh these rules.
129
27
 
130
- ## 5. Custom Rules System
28
+ ## 4. Custom Rules System
131
29
  Configurable code analysis with auto-detection.
132
30
 
133
31
  ### Available Tools