code-auditor-mcp 3.4.17 → 3.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.
Files changed (186) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +94 -0
  3. package/README.md +5 -5
  4. package/dist/analyzers/applicability.d.ts +13 -0
  5. package/dist/analyzers/applicability.d.ts.map +1 -1
  6. package/dist/analyzers/applicability.js +37 -0
  7. package/dist/analyzers/applicability.js.map +1 -1
  8. package/dist/analyzers/cross-language/APIContractAnalyzer.d.ts.map +1 -1
  9. package/dist/analyzers/cross-language/APIContractAnalyzer.js +23 -2
  10. package/dist/analyzers/cross-language/APIContractAnalyzer.js.map +1 -1
  11. package/dist/analyzers/cross-language/DependencyGraphBuilder.d.ts +52 -9
  12. package/dist/analyzers/cross-language/DependencyGraphBuilder.d.ts.map +1 -1
  13. package/dist/analyzers/cross-language/DependencyGraphBuilder.js +228 -55
  14. package/dist/analyzers/cross-language/DependencyGraphBuilder.js.map +1 -1
  15. package/dist/analyzers/cross-language/SchemaValidator.d.ts +19 -12
  16. package/dist/analyzers/cross-language/SchemaValidator.d.ts.map +1 -1
  17. package/dist/analyzers/cross-language/SchemaValidator.js +78 -111
  18. package/dist/analyzers/cross-language/SchemaValidator.js.map +1 -1
  19. package/dist/analyzers/crossDomain/CrossDomainAnalyzer.d.ts +0 -6
  20. package/dist/analyzers/crossDomain/CrossDomainAnalyzer.d.ts.map +1 -1
  21. package/dist/analyzers/crossDomain/CrossDomainAnalyzer.js +85 -73
  22. package/dist/analyzers/crossDomain/CrossDomainAnalyzer.js.map +1 -1
  23. package/dist/analyzers/documentationAnalyzer.js +5 -5
  24. package/dist/analyzers/documentationAnalyzer.js.map +1 -1
  25. package/dist/analyzers/invariantsAnalyzer.d.ts.map +1 -1
  26. package/dist/analyzers/invariantsAnalyzer.js +1 -0
  27. package/dist/analyzers/invariantsAnalyzer.js.map +1 -1
  28. package/dist/analyzers/provenance.d.ts +7 -9
  29. package/dist/analyzers/provenance.d.ts.map +1 -1
  30. package/dist/analyzers/provenance.js +88 -124
  31. package/dist/analyzers/provenance.js.map +1 -1
  32. package/dist/analyzers/reactAnalyzer.d.ts.map +1 -1
  33. package/dist/analyzers/reactAnalyzer.js +120 -21
  34. package/dist/analyzers/reactAnalyzer.js.map +1 -1
  35. package/dist/analyzers/ruleRegistry.d.ts +21 -0
  36. package/dist/analyzers/ruleRegistry.d.ts.map +1 -1
  37. package/dist/analyzers/ruleRegistry.js +61 -12
  38. package/dist/analyzers/ruleRegistry.js.map +1 -1
  39. package/dist/analyzers/universal/UniversalConventionsAnalyzer.d.ts.map +1 -1
  40. package/dist/analyzers/universal/UniversalConventionsAnalyzer.js +60 -24
  41. package/dist/analyzers/universal/UniversalConventionsAnalyzer.js.map +1 -1
  42. package/dist/analyzers/universal/UniversalDRYAnalyzer.d.ts.map +1 -1
  43. package/dist/analyzers/universal/UniversalDRYAnalyzer.js +5 -2
  44. package/dist/analyzers/universal/UniversalDRYAnalyzer.js.map +1 -1
  45. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.d.ts.map +1 -1
  46. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.js +173 -56
  47. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.js.map +1 -1
  48. package/dist/analyzers/universal/UniversalDocumentationAnalyzer.d.ts +2 -1
  49. package/dist/analyzers/universal/UniversalDocumentationAnalyzer.d.ts.map +1 -1
  50. package/dist/analyzers/universal/UniversalDocumentationAnalyzer.js +23 -12
  51. package/dist/analyzers/universal/UniversalDocumentationAnalyzer.js.map +1 -1
  52. package/dist/analyzers/universal/UniversalSOLIDAnalyzer.d.ts +11 -1
  53. package/dist/analyzers/universal/UniversalSOLIDAnalyzer.d.ts.map +1 -1
  54. package/dist/analyzers/universal/UniversalSOLIDAnalyzer.js +60 -22
  55. package/dist/analyzers/universal/UniversalSOLIDAnalyzer.js.map +1 -1
  56. package/dist/analyzers/universal/UniversalStylesAnalyzer.d.ts +2 -3
  57. package/dist/analyzers/universal/UniversalStylesAnalyzer.d.ts.map +1 -1
  58. package/dist/analyzers/universal/UniversalStylesAnalyzer.js +258 -87
  59. package/dist/analyzers/universal/UniversalStylesAnalyzer.js.map +1 -1
  60. package/dist/analyzers/universal/schema/codeAnalysis.d.ts.map +1 -1
  61. package/dist/analyzers/universal/schema/codeAnalysis.js +7 -1
  62. package/dist/analyzers/universal/schema/codeAnalysis.js.map +1 -1
  63. package/dist/analyzers/universal/schema/jsonSchema.js +7 -11
  64. package/dist/analyzers/universal/schema/jsonSchema.js.map +1 -1
  65. package/dist/applyDataDirEnv.js +1 -1
  66. package/dist/applyDataDirEnv.js.map +1 -1
  67. package/dist/auditRouter.d.ts +41 -0
  68. package/dist/auditRouter.d.ts.map +1 -0
  69. package/dist/auditRouter.js +185 -0
  70. package/dist/auditRouter.js.map +1 -0
  71. package/dist/auditRunner.d.ts.map +1 -1
  72. package/dist/auditRunner.js +97 -58
  73. package/dist/auditRunner.js.map +1 -1
  74. package/dist/cli.js +247 -23
  75. package/dist/cli.js.map +1 -1
  76. package/dist/codeIndexDB.d.ts.map +1 -1
  77. package/dist/codeIndexDB.js +36 -2
  78. package/dist/codeIndexDB.js.map +1 -1
  79. package/dist/componentScanner.d.ts.map +1 -1
  80. package/dist/componentScanner.js +7 -2
  81. package/dist/componentScanner.js.map +1 -1
  82. package/dist/config/configLoader.d.ts +9 -0
  83. package/dist/config/configLoader.d.ts.map +1 -1
  84. package/dist/config/configLoader.js +30 -2
  85. package/dist/config/configLoader.js.map +1 -1
  86. package/dist/config/defaults.d.ts.map +1 -1
  87. package/dist/config/defaults.js +10 -3
  88. package/dist/config/defaults.js.map +1 -1
  89. package/dist/dataPaths.d.ts +8 -1
  90. package/dist/dataPaths.d.ts.map +1 -1
  91. package/dist/dataPaths.js +57 -5
  92. package/dist/dataPaths.js.map +1 -1
  93. package/dist/functionScanner.d.ts.map +1 -1
  94. package/dist/functionScanner.js +1 -17
  95. package/dist/functionScanner.js.map +1 -1
  96. package/dist/grammars/tree-sitter-scss.wasm +0 -0
  97. package/dist/invariants/ruleEngine.d.ts +8 -0
  98. package/dist/invariants/ruleEngine.d.ts.map +1 -1
  99. package/dist/invariants/ruleEngine.js +35 -16
  100. package/dist/invariants/ruleEngine.js.map +1 -1
  101. package/dist/languages/LanguageOrchestrator.d.ts +10 -11
  102. package/dist/languages/LanguageOrchestrator.d.ts.map +1 -1
  103. package/dist/languages/LanguageOrchestrator.js +135 -101
  104. package/dist/languages/LanguageOrchestrator.js.map +1 -1
  105. package/dist/languages/RuntimeManager.d.ts +28 -1
  106. package/dist/languages/RuntimeManager.d.ts.map +1 -1
  107. package/dist/languages/RuntimeManager.js +160 -49
  108. package/dist/languages/RuntimeManager.js.map +1 -1
  109. package/dist/languages/go/GoAdapter.d.ts +2 -0
  110. package/dist/languages/go/GoAdapter.d.ts.map +1 -1
  111. package/dist/languages/go/GoAdapter.js +15 -22
  112. package/dist/languages/go/GoAdapter.js.map +1 -1
  113. package/dist/languages/go/analyzer +0 -0
  114. package/dist/languages/go/analyzer-src/analyzer.go +510 -0
  115. package/dist/languages/go/analyzer-src/go.mod +3 -0
  116. package/dist/languages/go/analyzer-src/indexer.go +268 -0
  117. package/dist/languages/go/analyzer-src/parser.go +461 -0
  118. package/dist/languages/go/analyzer-src/solid.go +395 -0
  119. package/dist/languages/go/analyzer-src/testconventions.go +25 -0
  120. package/dist/languages/go/analyzer-src/types.go +148 -0
  121. package/dist/languages/go/go.mod +7 -0
  122. package/dist/languages/go/main.go +203 -0
  123. package/dist/languages/testConventions.d.ts +35 -0
  124. package/dist/languages/testConventions.d.ts.map +1 -0
  125. package/dist/languages/testConventions.js +51 -0
  126. package/dist/languages/testConventions.js.map +1 -0
  127. package/dist/languages/typescript/TreeSitterTypeScriptAdapter.d.ts +3 -0
  128. package/dist/languages/typescript/TreeSitterTypeScriptAdapter.d.ts.map +1 -1
  129. package/dist/languages/typescript/TreeSitterTypeScriptAdapter.js +25 -18
  130. package/dist/languages/typescript/TreeSitterTypeScriptAdapter.js.map +1 -1
  131. package/dist/mcp-tools/whitelistTools.d.ts +1 -1
  132. package/dist/mcp-tools/workflowGuide.js +3 -3
  133. package/dist/mcp-tools/workflowGuide.js.map +1 -1
  134. package/dist/mcp-tools-shared.d.ts +0 -3
  135. package/dist/mcp-tools-shared.d.ts.map +1 -1
  136. package/dist/mcp-tools-shared.js +24 -128
  137. package/dist/mcp-tools-shared.js.map +1 -1
  138. package/dist/mcp.js +18 -12
  139. package/dist/mcp.js.map +1 -1
  140. package/dist/mcpAuditJobs.d.ts.map +1 -1
  141. package/dist/mcpAuditJobs.js +13 -0
  142. package/dist/mcpAuditJobs.js.map +1 -1
  143. package/dist/mcpAutoIndex.js +1 -1
  144. package/dist/mcpAutoIndex.js.map +1 -1
  145. package/dist/nextFile.d.ts +21 -0
  146. package/dist/nextFile.d.ts.map +1 -0
  147. package/dist/nextFile.js +51 -0
  148. package/dist/nextFile.js.map +1 -0
  149. package/dist/nextFileIncremental.d.ts +141 -0
  150. package/dist/nextFileIncremental.d.ts.map +1 -0
  151. package/dist/nextFileIncremental.js +314 -0
  152. package/dist/nextFileIncremental.js.map +1 -0
  153. package/dist/pipeline.d.ts.map +1 -1
  154. package/dist/pipeline.js +22 -0
  155. package/dist/pipeline.js.map +1 -1
  156. package/dist/pipelineAdapters.d.ts +17 -0
  157. package/dist/pipelineAdapters.d.ts.map +1 -1
  158. package/dist/pipelineAdapters.js +800 -44
  159. package/dist/pipelineAdapters.js.map +1 -1
  160. package/dist/reporting/jsonReportGenerator.d.ts.map +1 -1
  161. package/dist/reporting/jsonReportGenerator.js +11 -30
  162. package/dist/reporting/jsonReportGenerator.js.map +1 -1
  163. package/dist/styles/styleIndexer.d.ts +7 -0
  164. package/dist/styles/styleIndexer.d.ts.map +1 -1
  165. package/dist/styles/styleIndexer.js +44 -61
  166. package/dist/styles/styleIndexer.js.map +1 -1
  167. package/dist/types/crossLanguage.d.ts +1 -0
  168. package/dist/types/crossLanguage.d.ts.map +1 -1
  169. package/dist/types.d.ts +27 -4
  170. package/dist/types.d.ts.map +1 -1
  171. package/dist/types.js.map +1 -1
  172. package/dist/utils/fileDiscovery.d.ts +11 -2
  173. package/dist/utils/fileDiscovery.d.ts.map +1 -1
  174. package/dist/utils/fileDiscovery.js +23 -5
  175. package/dist/utils/fileDiscovery.js.map +1 -1
  176. package/dist/utils/reactDetection.d.ts.map +1 -1
  177. package/dist/utils/reactDetection.js +59 -44
  178. package/dist/utils/reactDetection.js.map +1 -1
  179. package/package.json +31 -27
  180. package/plugin/.claude-plugin/plugin.json +1 -1
  181. package/plugin/README.md +6 -6
  182. package/plugin/hooks/hooks.json +4 -0
  183. package/plugin/scripts/hook-audit.sh +0 -0
  184. package/plugin/scripts/hook-self-audit.sh +107 -0
  185. package/plugin/skills/code-auditor/SKILL-RULE-KINDS.md +55 -7
  186. package/plugin/skills/code-auditor/SKILL.md +22 -23
@@ -18,6 +18,7 @@
18
18
  import { createHash } from 'crypto';
19
19
  import path from 'path';
20
20
  import { RULE_REGISTRY } from './analyzers/ruleRegistry.js';
21
+ import { TYPESCRIPT_EXTENSIONS, JAVASCRIPT_EXTENSIONS, getLanguageFromPath } from './utils/fileDiscovery.js';
21
22
  import { walkAST, isExported, getLineAndColumn, calculateComplexity, getFunctionBody, } from './languages/adapterBridge.js';
22
23
  import { buildImportMap, extractFunctionCalls, } from './utils/dependencyExtractor.js';
23
24
  import { isReactComponent, detectComponentType, getComponentName, } from './utils/reactDetection.js';
@@ -130,23 +131,6 @@ export function createDocumentationVisitor() {
130
131
  // to mine — even on a cold run with no prior `index sync`.
131
132
  // The function_calls table is rebuilt post-pipeline after functions rows have
132
133
  // their auto-increment IDs assigned.
133
- function getLanguageFromPath(filePath) {
134
- const ext = filePath.substring(filePath.lastIndexOf('.')).toLowerCase();
135
- switch (ext) {
136
- case '.ts':
137
- case '.tsx':
138
- case '.mts':
139
- case '.cts':
140
- return 'typescript';
141
- case '.js':
142
- case '.jsx':
143
- case '.mjs':
144
- case '.cjs':
145
- return 'javascript';
146
- default:
147
- return 'unknown';
148
- }
149
- }
150
134
  function computeContentHash(body, signature) {
151
135
  const normalized = (body ?? '').replace(/\s+/g, ' ').trim() + '|' + (signature ?? '').trim();
152
136
  return createHash('sha256').update(normalized).digest('hex');
@@ -479,18 +463,23 @@ export function createReactVisitor() {
479
463
  return [];
480
464
  const violations = [];
481
465
  try {
482
- const { checkCircularDependencies, checkErrorBoundaryUsage, checkRawElements, } = await import('./analyzers/reactAnalyzer.js');
466
+ const { checkCircularDependencies, checkErrorBoundaryUsage, checkRawElements, DEFAULT_REACT_CONFIG, } = await import('./analyzers/reactAnalyzer.js');
467
+ // Merge with defaults — mirrors the visitor's
468
+ // `{ ...DEFAULT_REACT_CONFIG, ...context.config }` so a partial config
469
+ // still yields every required field, and so the `react.*` flags set in
470
+ // .codeauditor.json reach the cross-component checks.
471
+ const cfg = { ...DEFAULT_REACT_CONFIG, ...(config ?? {}) };
483
472
  // Build component tree and check for circular dependencies
484
473
  const { buildComponentTree } = await import('./componentScanner.js');
485
474
  const tree = buildComponentTree(scanResults);
486
475
  violations.push(...checkCircularDependencies(tree));
487
476
  // Check for missing error boundaries
488
- if (config.requireErrorBoundaries !== false) {
477
+ if (cfg.requireErrorBoundaries !== false) {
489
478
  violations.push(...checkErrorBoundaryUsage(scanResults));
490
479
  }
491
480
  // Check for raw element usage (Spec 10 R4)
492
- if (config.rawElementCheck !== false) {
493
- violations.push(...checkRawElements(scanResults, config));
481
+ if (cfg.rawElementCheck !== false) {
482
+ violations.push(...checkRawElements(scanResults, cfg));
494
483
  }
495
484
  }
496
485
  catch {
@@ -535,6 +524,145 @@ export function createStylesCssVisitor() {
535
524
  category: 'style',
536
525
  };
537
526
  }
527
+ /**
528
+ * Styles source visitor — extracts CSS-in-JS declarations and class usage from
529
+ * the TS/JS ASTs stage 1 already parsed. This replaces the re-parse the style
530
+ * indexer used to do (adapter.parse on every TS/JS file): stage 1 parses every
531
+ * TS/JS file for the other visitors regardless, so a second parse here only
532
+ * duplicated the ~55ms that remained in the scoped short-circuit gate.
533
+ *
534
+ * Extraction is unconditional — the visitor runs on every in-scope TS/JS file,
535
+ * emitting a fact (with a content hash) even when it yields no declarations.
536
+ * The reducer uses that fact to (a) drop stale rows for files that *lost* their
537
+ * styles and (b) skip re-writing files whose stored rows are already current.
538
+ */
539
+ export function createStylesSourceVisitor() {
540
+ return {
541
+ name: 'styles-source',
542
+ stage: 'visitor',
543
+ extensions: [...TYPESCRIPT_EXTENSIONS, ...JAVASCRIPT_EXTENSIONS],
544
+ getRuleIds: () => [],
545
+ async visit(ast, adapter, context, sourceCode) {
546
+ // Lazy-load to avoid circular dependency issues at module load time.
547
+ const { extractDeclarations } = await import('./styles/styleExtractor.js');
548
+ const { extractClassUsage } = await import('./styles/styleIndexer.js');
549
+ const { loadTailwindConfig } = await import('./styles/tailwindConfigLoader.js');
550
+ const tsAst = ast;
551
+ const tsAdapter = adapter;
552
+ const filePath = context.filePath;
553
+ // Memoized per projectRoot (the style-index sync already called it this
554
+ // run), so this is a cache hit — never a re-walk of the project tree.
555
+ const tailwindTokens = loadTailwindConfig(context.projectRoot).tokens;
556
+ const declarations = extractDeclarations(filePath, tsAdapter, sourceCode, tsAst, tailwindTokens);
557
+ const classUsage = extractClassUsage(filePath, sourceCode);
558
+ const contentHash = createHash('sha256').update(sourceCode).digest('hex');
559
+ return {
560
+ violations: [],
561
+ facts: {
562
+ [filePath]: { declarations, classUsage, contentHash },
563
+ },
564
+ };
565
+ },
566
+ defaultConfig: {},
567
+ description: 'Extracts CSS-in-JS declarations and class usage from TS/JS ASTs',
568
+ category: 'style',
569
+ };
570
+ }
571
+ /**
572
+ * Insert TS/JS CSS-in-JS facts (from the styles-source visitor) into the
573
+ * style_* tables. Mirrors the TS/JS branch the style indexer used to run
574
+ * (Spec 10), but with the parse removed — stage 1 already parsed these files.
575
+ *
576
+ * The content-hash skip is a DB-WRITE guard, not a parse guard: stage 1 parses
577
+ * every TS/JS file regardless and extraction is unconditional here, so the hash
578
+ * exists only to avoid re-writing a file whose stored rows are already current.
579
+ * This is the intended semantics — do not "restore" it back to a parse-time skip.
580
+ *
581
+ * Returns true when any file contributed a declaration or class-usage row (the
582
+ * scoped short-circuit signal, equivalent to the old `contributingFiles`).
583
+ */
584
+ function insertSourceStyleFacts(indexHandle, sourceFacts, isScoped) {
585
+ if (!sourceFacts)
586
+ return false;
587
+ const entries = Object.entries(sourceFacts);
588
+ if (entries.length === 0)
589
+ return false;
590
+ const rawDb = indexHandle.rawDb;
591
+ const stmts = rawDb
592
+ ? {
593
+ getHash: rawDb.prepare('SELECT content_hash FROM style_declarations WHERE file_path = ? LIMIT 1'),
594
+ delDecl: rawDb.prepare('DELETE FROM style_declarations WHERE file_path = ?'),
595
+ delUsage: rawDb.prepare('DELETE FROM style_class_usage WHERE file_path = ?'),
596
+ delTok: rawDb.prepare('DELETE FROM style_tokens WHERE file_path = ?'),
597
+ delUnread: rawDb.prepare('DELETE FROM style_unread_sources WHERE file_path = ?'),
598
+ delClass: rawDb.prepare('DELETE FROM style_defined_classes WHERE file_path = ?'),
599
+ insDecl: rawDb.prepare('INSERT INTO style_declarations (property, raw_value, normalized_value, mechanism, file_path, line, context, variant_context, token_ref, content_hash) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'),
600
+ insClass: rawDb.prepare('INSERT OR IGNORE INTO style_defined_classes (class_name, file_path) VALUES (?, ?)'),
601
+ insUsage: rawDb.prepare('INSERT INTO style_class_usage (class_name, file_path, line, mechanism, unresolvable) VALUES (?, ?, ?, ?, ?)'),
602
+ }
603
+ : null;
604
+ let contributed = false;
605
+ const insertOne = (filePath, facts) => {
606
+ if (facts.declarations.length > 0 || facts.classUsage.length > 0)
607
+ contributed = true;
608
+ // Full-run content-hash skip: leave already-current rows untouched. Scoped
609
+ // runs always re-write, matching the old style indexer's `if (!scoped)` guard.
610
+ if (!isScoped) {
611
+ const stored = stmts
612
+ ? stmts.getHash.get(filePath)
613
+ : indexHandle.query('SELECT content_hash FROM style_declarations WHERE file_path = ? LIMIT 1', [filePath])[0];
614
+ if (stored?.content_hash === facts.contentHash)
615
+ return;
616
+ }
617
+ // Delete stale rows — all five tables, mirroring the old deleteFileEntries
618
+ // so a file that *lost* its styles also loses its stale unread-sources row.
619
+ if (stmts) {
620
+ stmts.delDecl.run(filePath);
621
+ stmts.delUsage.run(filePath);
622
+ stmts.delTok.run(filePath);
623
+ stmts.delUnread.run(filePath);
624
+ stmts.delClass.run(filePath);
625
+ }
626
+ else {
627
+ indexHandle.run('DELETE FROM style_declarations WHERE file_path = ?', [filePath]);
628
+ indexHandle.run('DELETE FROM style_class_usage WHERE file_path = ?', [filePath]);
629
+ indexHandle.run('DELETE FROM style_tokens WHERE file_path = ?', [filePath]);
630
+ indexHandle.run('DELETE FROM style_unread_sources WHERE file_path = ?', [filePath]);
631
+ indexHandle.run('DELETE FROM style_defined_classes WHERE file_path = ?', [filePath]);
632
+ }
633
+ // Insert declarations (and their defined-class catalog entries).
634
+ for (const decl of facts.declarations) {
635
+ if (stmts) {
636
+ stmts.insDecl.run(decl.property, decl.rawValue, decl.normalizedValue ? JSON.stringify(decl.normalizedValue) : null, decl.mechanism, decl.filePath, decl.line, decl.context, decl.variantContext, decl.tokenRef, facts.contentHash);
637
+ }
638
+ else {
639
+ indexHandle.run('INSERT INTO style_declarations (property, raw_value, normalized_value, mechanism, file_path, line, context, variant_context, token_ref, content_hash) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', [decl.property, decl.rawValue, decl.normalizedValue ? JSON.stringify(decl.normalizedValue) : null, decl.mechanism, decl.filePath, decl.line, decl.context, decl.variantContext, decl.tokenRef, facts.contentHash]);
640
+ }
641
+ if (decl.context) {
642
+ for (const m of decl.context.matchAll(/\.([a-zA-Z0-9_-]+)/g)) {
643
+ if (stmts)
644
+ stmts.insClass.run(m[1], decl.filePath);
645
+ else
646
+ indexHandle.run('INSERT OR IGNORE INTO style_defined_classes (class_name, file_path) VALUES (?, ?)', [m[1], decl.filePath]);
647
+ }
648
+ }
649
+ }
650
+ // Insert class usage.
651
+ for (const cu of facts.classUsage) {
652
+ if (stmts)
653
+ stmts.insUsage.run(cu.className, cu.filePath, cu.line, cu.mechanism, cu.unresolvable ? 1 : 0);
654
+ else
655
+ indexHandle.run('INSERT INTO style_class_usage (class_name, file_path, line, mechanism, unresolvable) VALUES (?, ?, ?, ?, ?)', [cu.className, cu.filePath, cu.line, cu.mechanism, cu.unresolvable ? 1 : 0]);
656
+ }
657
+ };
658
+ if (stmts)
659
+ rawDb.transaction(() => { for (const [fp, f] of entries)
660
+ insertOne(fp, f); })();
661
+ else
662
+ for (const [fp, f] of entries)
663
+ insertOne(fp, f);
664
+ return contributed;
665
+ }
538
666
  /**
539
667
  * Styles reducer — runs all style detectors on existing style_* tables.
540
668
  * Uses the analyzer.analyze([]) pattern: passes an empty file list so the
@@ -553,32 +681,114 @@ export function createStylesReducer() {
553
681
  async reduce(_allFacts, context) {
554
682
  if (!context.indexHandle)
555
683
  return { violations: [], facts: {} };
684
+ const indexHandle = context.indexHandle;
556
685
  try {
557
686
  // ── Insert CSS visitor facts into style_* tables ──────────────────────
558
687
  const cssFacts = _allFacts['styles-css'];
688
+ // ── Source (TS/JS CSS-in-JS) visitor facts ───────────────────────────
689
+ const sourceFacts = _allFacts['styles-source'];
690
+ // ── Insert source facts, then decide whether to short-circuit ────────
691
+ // The short-circuit fires when a scoped (changed/path-filtered) run has no
692
+ // style-bearing work: no in-scope .css/.scss facts, no TS/JS file that
693
+ // contributed a declaration/class-usage, and no markup file the style sync
694
+ // contributed. Skipping the full-corpus query + O(n²) detectors removes the
695
+ // ~590ms styles-reducer cost from the scoped gate.
696
+ //
697
+ // Source facts are inserted BEFORE the short-circuit so a scoped file that
698
+ // *lost* its styles still has its stale rows dropped (the insertion always
699
+ // runs; the analyzer is what gets skipped). `styles-*` facts are `{}` (empty
700
+ // object) when nothing matched (pipeline.ts seeds every visitor with an empty
701
+ // object), so test key-count rather than truthiness. `styleContributingFiles`
702
+ // is `undefined` when the style sync did not run — reducers must NOT
703
+ // short-circuit on `undefined`.
704
+ const hasCssFacts = !!cssFacts && Object.keys(cssFacts).length > 0;
705
+ const styleSyncContributedNothing = Array.isArray(context.styleContributingFiles) &&
706
+ context.styleContributingFiles.length === 0;
707
+ const sourceContributed = insertSourceStyleFacts(indexHandle, sourceFacts, context.isScoped ?? false);
559
708
  if (cssFacts) {
560
- const run = context.indexHandle.run.bind(context.indexHandle);
561
- for (const [filePath, facts] of Object.entries(cssFacts)) {
562
- // Delete old entries for this file (replaces the styleIndexer path)
563
- run('DELETE FROM style_declarations WHERE file_path = ?', [filePath]);
564
- run('DELETE FROM style_tokens WHERE file_path = ?', [filePath]);
565
- run('DELETE FROM style_class_usage WHERE file_path = ?', [filePath]);
566
- // Compute content hash for the file
567
- const contentStr = JSON.stringify({ declarations: facts.declarations.length, tokens: facts.tokens.length, classUsage: facts.classUsage.length });
568
- const contentHash = createHash('sha256').update(contentStr).digest('hex').slice(0, 16);
569
- // Insert declarations
570
- for (const decl of facts.declarations) {
571
- run('INSERT INTO style_declarations (property, raw_value, normalized_value, mechanism, file_path, line, context, variant_context, token_ref, content_hash) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', [decl.property, decl.rawValue, JSON.stringify(decl.normalizedValue), decl.mechanism, decl.filePath, decl.line, decl.context, decl.variantContext, decl.tokenRef, contentHash]);
709
+ // Prepare each statement once and run the whole re-insertion in a
710
+ // single transaction. The prior code went through `indexHandle.run`,
711
+ // which re-`prepare()`s the SQL on every call AND auto-commits per
712
+ // statement measured at 345ms for 5 files / ~13k declarations on
713
+ // recall-protocol. Prepare-once + one transaction collapses that.
714
+ const rawDb = context.indexHandle.rawDb;
715
+ const stmts = rawDb
716
+ ? {
717
+ delDecl: rawDb.prepare('DELETE FROM style_declarations WHERE file_path = ?'),
718
+ delTok: rawDb.prepare('DELETE FROM style_tokens WHERE file_path = ?'),
719
+ delUsage: rawDb.prepare('DELETE FROM style_class_usage WHERE file_path = ?'),
720
+ delClass: rawDb.prepare('DELETE FROM style_defined_classes WHERE file_path = ?'),
721
+ insDecl: rawDb.prepare('INSERT INTO style_declarations (property, raw_value, normalized_value, mechanism, file_path, line, context, variant_context, token_ref, content_hash) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'),
722
+ insClass: rawDb.prepare('INSERT OR IGNORE INTO style_defined_classes (class_name, file_path) VALUES (?, ?)'),
723
+ insTok: rawDb.prepare('INSERT INTO style_tokens (name, value, file_path, mechanism) VALUES (?, ?, ?, ?)'),
724
+ insUsage: rawDb.prepare('INSERT INTO style_class_usage (class_name, file_path, line, mechanism, unresolvable) VALUES (?, ?, ?, ?, ?)'),
572
725
  }
573
- // Insert tokens
574
- for (const tok of facts.tokens) {
575
- run('INSERT INTO style_tokens (name, value, file_path, mechanism) VALUES (?, ?, ?, ?)', [tok.name, tok.value, tok.filePath, tok.mechanism]);
576
- }
577
- // Insert class usage
578
- for (const cu of facts.classUsage) {
579
- run('INSERT INTO style_class_usage (class_name, file_path, line, mechanism, unresolvable) VALUES (?, ?, ?, ?, ?)', [cu.className, cu.filePath, cu.line, cu.mechanism, cu.unresolvable ? 1 : 0]);
726
+ : null;
727
+ const insertAll = () => {
728
+ for (const [filePath, facts] of Object.entries(cssFacts)) {
729
+ // Delete old entries for this file (replaces the styleIndexer path)
730
+ if (stmts) {
731
+ stmts.delDecl.run(filePath);
732
+ stmts.delTok.run(filePath);
733
+ stmts.delUsage.run(filePath);
734
+ stmts.delClass.run(filePath);
735
+ }
736
+ else {
737
+ indexHandle.run('DELETE FROM style_declarations WHERE file_path = ?', [filePath]);
738
+ indexHandle.run('DELETE FROM style_tokens WHERE file_path = ?', [filePath]);
739
+ indexHandle.run('DELETE FROM style_class_usage WHERE file_path = ?', [filePath]);
740
+ indexHandle.run('DELETE FROM style_defined_classes WHERE file_path = ?', [filePath]);
741
+ }
742
+ // Compute content hash for the file
743
+ const contentStr = JSON.stringify({ declarations: facts.declarations.length, tokens: facts.tokens.length, classUsage: facts.classUsage.length });
744
+ const contentHash = createHash('sha256').update(contentStr).digest('hex').slice(0, 16);
745
+ // Insert declarations
746
+ for (const decl of facts.declarations) {
747
+ if (stmts) {
748
+ stmts.insDecl.run(decl.property, decl.rawValue, JSON.stringify(decl.normalizedValue), decl.mechanism, decl.filePath, decl.line, decl.context, decl.variantContext, decl.tokenRef, contentHash);
749
+ }
750
+ else {
751
+ indexHandle.run('INSERT INTO style_declarations (property, raw_value, normalized_value, mechanism, file_path, line, context, variant_context, token_ref, content_hash) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', [decl.property, decl.rawValue, JSON.stringify(decl.normalizedValue), decl.mechanism, decl.filePath, decl.line, decl.context, decl.variantContext, decl.tokenRef, contentHash]);
752
+ }
753
+ // Populate the defined-class catalog (Spec 45) from any class
754
+ // selectors in the rule's context so styles/undefined-class can
755
+ // resolve names via `style_defined_classes` instead of a
756
+ // full-corpus regex scan over every declaration.
757
+ if (decl.context) {
758
+ for (const m of decl.context.matchAll(/\.([a-zA-Z0-9_-]+)/g)) {
759
+ if (stmts)
760
+ stmts.insClass.run(m[1], decl.filePath);
761
+ else
762
+ indexHandle.run('INSERT OR IGNORE INTO style_defined_classes (class_name, file_path) VALUES (?, ?)', [m[1], decl.filePath]);
763
+ }
764
+ }
765
+ }
766
+ // Insert tokens
767
+ for (const tok of facts.tokens) {
768
+ if (stmts)
769
+ stmts.insTok.run(tok.name, tok.value, tok.filePath, tok.mechanism);
770
+ else
771
+ indexHandle.run('INSERT INTO style_tokens (name, value, file_path, mechanism) VALUES (?, ?, ?, ?)', [tok.name, tok.value, tok.filePath, tok.mechanism]);
772
+ }
773
+ // Insert class usage
774
+ for (const cu of facts.classUsage) {
775
+ if (stmts)
776
+ stmts.insUsage.run(cu.className, cu.filePath, cu.line, cu.mechanism, cu.unresolvable ? 1 : 0);
777
+ else
778
+ indexHandle.run('INSERT INTO style_class_usage (class_name, file_path, line, mechanism, unresolvable) VALUES (?, ?, ?, ?, ?)', [cu.className, cu.filePath, cu.line, cu.mechanism, cu.unresolvable ? 1 : 0]);
779
+ }
580
780
  }
581
- }
781
+ };
782
+ if (rawDb)
783
+ rawDb.transaction(insertAll)();
784
+ else
785
+ insertAll();
786
+ }
787
+ // Short-circuit on non-style-bearing scoped runs — the source and CSS
788
+ // facts above are already written (so stale rows are dropped); only the
789
+ // analyzer is skipped when nothing style-bearing changed.
790
+ if (context.isScoped && !hasCssFacts && !sourceContributed && styleSyncContributedNothing) {
791
+ return { violations: [], facts: {} };
582
792
  }
583
793
  // ── Run the analyzer (queries the now-populated style_* tables) ───────
584
794
  const { UniversalStylesAnalyzer } = await import('./analyzers/universal/UniversalStylesAnalyzer.js');
@@ -623,7 +833,12 @@ export function createConventionsReducer() {
623
833
  .filter(([, data]) => data?.exports?.length)
624
834
  .map(([filePath, data]) => [filePath, data.exports]))
625
835
  : undefined;
626
- const config = { ...context.config, indexHandle: context.indexHandle, projectRoot: context.projectRoot, readSource: context.readSource, exportsMap };
836
+ // Scoped/diff audits: thread the in-scope file set so the analyzer scopes
837
+ // its function/file queries to only the changed files. `_infra.files` are
838
+ // absolute; `functions.file_path` is also absolute (function-index visitor
839
+ // stores `context.filePath`), so the IN clause matches directly.
840
+ const scopedFiles = context.isScoped ? (context.config.files ?? []) : undefined;
841
+ const config = { ...context.config, indexHandle: context.indexHandle, projectRoot: context.projectRoot, readSource: context.readSource, exportsMap, scopedFiles, isScoped: context.isScoped };
627
842
  const result = await analyzer.analyze([], config);
628
843
  // Count the INPUT the conventions analyzer reads (the function index the
629
844
  // function-index visitor populated), not the derived `conventions` table it
@@ -661,7 +876,11 @@ export function createCrossDomainReducer() {
661
876
  try {
662
877
  const { CrossDomainAnalyzer } = await import('./analyzers/crossDomain/CrossDomainAnalyzer.js');
663
878
  const analyzer = new CrossDomainAnalyzer();
664
- const config = { ...context.config, indexHandle: context.indexHandle, projectRoot: context.projectRoot };
879
+ // Thread the in-scope file set (absolute, from _infra.files) so detectors
880
+ // anchor to changed files on diff audits; full audits keep the root-LIKE
881
+ // scope (resolveFileScope falls back when isScoped is false).
882
+ const scopedFiles = context.isScoped ? (context.config.files ?? []) : undefined;
883
+ const config = { ...context.config, indexHandle: context.indexHandle, projectRoot: context.projectRoot, scopedFiles, isScoped: context.isScoped };
665
884
  const result = await analyzer.analyze([], config);
666
885
  // Count DB rows consumed across primary cross-domain tables
667
886
  let factsConsumed = 0;
@@ -689,6 +908,543 @@ export function createCrossDomainReducer() {
689
908
  category: 'architecture',
690
909
  };
691
910
  }
911
+ // ── Cross-language entity extraction + Stage-4 reducers ──────────────────────
912
+ //
913
+ // The three cross-language analyzers (SchemaValidator, APIContractAnalyzer,
914
+ // DependencyGraphBuilder) are corpus-wide: they compare schemas/endpoints/cycles
915
+ // across files and languages. They run as Stage-4 derived reducers over a single
916
+ // shared entity-extraction visitor, so each analyzer's rule IDs map to its own
917
+ // result row (coverage resolves by RULE_REGISTRY analyzer name — one reducer per
918
+ // analyzer, not one reducer fanning out to three names).
919
+ //
920
+ // Scope-awareness ("Stage 4, and measure the gate"): the corpus-wide graph/cycle
921
+ // work is only meaningful on a full audit. On a scoped/diff run each reducer
922
+ // short-circuits with `notRunReason` BEFORE any corpus-wide work — the gate is in
923
+ // the design, not retrofitted after the budget blows. This is the shape that made
924
+ // the styles reducer 587ms; it must never run over a partial corpus.
925
+ // ── Raw-node helpers (tree-sitter raw nodes are `any` here — strict is off) ──
926
+ function clRawField(raw, field) {
927
+ return typeof raw?.childForFieldName === 'function' ? raw.childForFieldName(field) : null;
928
+ }
929
+ function clRawNamedChild(raw, type) {
930
+ return raw?.namedChildren?.find((c) => c.type === type) ?? null;
931
+ }
932
+ /** A function body node — `statement_block` (TS/JS) or `block` (Go). */
933
+ function clRawBody(raw) {
934
+ return raw?.namedChildren?.find((c) => c.type === 'statement_block' || c.type === 'block') ?? null;
935
+ }
936
+ function clSignatureText(raw, sourceCode) {
937
+ if (!raw)
938
+ return '';
939
+ const body = clRawBody(raw);
940
+ const end = body ? body.startIndex : raw.endIndex;
941
+ return sourceCode.slice(raw.startIndex, end).trim();
942
+ }
943
+ function clBodyText(raw, sourceCode) {
944
+ const body = clRawBody(raw);
945
+ return body ? sourceCode.slice(body.startIndex, body.endIndex) : '';
946
+ }
947
+ /** Prefer the `name` field (works across TS + Go), fall back to identifier children. */
948
+ function clNodeName(raw) {
949
+ const field = clRawField(raw, 'name');
950
+ if (field?.text)
951
+ return field.text;
952
+ const id = clRawNamedChild(raw, 'identifier') ?? clRawNamedChild(raw, 'property_identifier');
953
+ return id?.text;
954
+ }
955
+ function clEntityId(filePath, type, name, line) {
956
+ return `${filePath}::${type}::${name}::${line}`;
957
+ }
958
+ function clIsExportedGo(name) {
959
+ return name.length > 0 && name[0] === name[0].toUpperCase();
960
+ }
961
+ /** Collect callee names from a function's body (TS `call_expression` / Go `call_expression`). */
962
+ function clCollectCallees(raw) {
963
+ const names = new Set();
964
+ const walk = (node) => {
965
+ if (!node)
966
+ return;
967
+ if (node.type === 'call_expression') {
968
+ const fn = typeof node.childForFieldName === 'function'
969
+ ? node.childForFieldName('function')
970
+ : null;
971
+ const callee = fn ?? node.namedChildren?.[0];
972
+ const text = callee?.text?.trim();
973
+ if (text && /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*$/.test(text)) {
974
+ names.add(text);
975
+ }
976
+ }
977
+ for (const child of node.namedChildren ?? [])
978
+ walk(child);
979
+ };
980
+ walk(raw);
981
+ return [...names];
982
+ }
983
+ function clMakeFunction(filePath, lang, name, line, signature, purpose, params, callees, exported, complexity) {
984
+ return {
985
+ id: clEntityId(filePath, 'function', name, line),
986
+ name,
987
+ language: lang,
988
+ file: filePath,
989
+ type: 'function',
990
+ signature,
991
+ parameters: params,
992
+ startLine: line,
993
+ visibility: exported ? 'public' : 'private',
994
+ complexity,
995
+ calls: [],
996
+ calledBy: [],
997
+ purpose,
998
+ context: '',
999
+ searchTokens: [name.toLowerCase()],
1000
+ metadata: { callees },
1001
+ };
1002
+ }
1003
+ // ── TS/JS extraction ─────────────────────────────────────────────────────────
1004
+ function clExtractTSParams(raw) {
1005
+ const params = [];
1006
+ const formal = clRawNamedChild(raw, 'formal_parameters');
1007
+ for (const p of formal?.namedChildren ?? []) {
1008
+ if (p.type !== 'required_parameter' && p.type !== 'optional_parameter')
1009
+ continue;
1010
+ const name = clRawField(p, 'name')?.text ?? clRawNamedChild(p, 'identifier')?.text;
1011
+ if (!name)
1012
+ continue;
1013
+ const type = clRawField(p, 'type')?.text?.trim();
1014
+ params.push({ name, type, optional: p.type === 'optional_parameter', language: 'typescript' });
1015
+ }
1016
+ return params;
1017
+ }
1018
+ function clExtractTSInterfaceParams(raw) {
1019
+ const params = [];
1020
+ const walk = (node) => {
1021
+ if (!node)
1022
+ return;
1023
+ if (node.type === 'property_signature') {
1024
+ const name = clRawField(node, 'name')?.text ?? clRawNamedChild(node, 'property_identifier')?.text;
1025
+ if (!name)
1026
+ return;
1027
+ const type = clRawField(node, 'type')?.text?.trim();
1028
+ const optional = node.children?.some((c) => c.type === '?') ?? false;
1029
+ params.push({ name, type, optional, language: 'typescript' });
1030
+ }
1031
+ for (const child of node.namedChildren ?? [])
1032
+ walk(child);
1033
+ };
1034
+ walk(raw);
1035
+ return params;
1036
+ }
1037
+ function clExtractTSEntities(root, filePath, sourceCode, lang, out) {
1038
+ walkAST(root, (node) => {
1039
+ const raw = node.raw;
1040
+ if (node.type === 'function_declaration' || node.type === 'method_definition') {
1041
+ const name = clNodeName(raw);
1042
+ if (!name)
1043
+ return;
1044
+ let displayName = name;
1045
+ if (node.type === 'method_definition') {
1046
+ let p = raw?.parent;
1047
+ while (p) {
1048
+ if (p.type === 'class_declaration') {
1049
+ const cn = clNodeName(p);
1050
+ if (cn)
1051
+ displayName = `${cn}.${name}`;
1052
+ break;
1053
+ }
1054
+ p = p.parent;
1055
+ }
1056
+ }
1057
+ const line = (raw?.startPosition?.row ?? 0) + 1;
1058
+ out.push(clMakeFunction(filePath, lang, displayName, line, clSignatureText(raw, sourceCode) || raw?.text || '', clBodyText(raw, sourceCode), clExtractTSParams(raw), clCollectCallees(raw), isExported(node), calculateComplexity(node)));
1059
+ return;
1060
+ }
1061
+ if (node.type === 'variable_declarator') {
1062
+ const arrow = clRawNamedChild(raw, 'arrow_function');
1063
+ if (!arrow)
1064
+ return;
1065
+ const name = clRawNamedChild(raw, 'identifier')?.text;
1066
+ if (!name)
1067
+ return;
1068
+ const line = (raw?.startPosition?.row ?? 0) + 1;
1069
+ out.push(clMakeFunction(filePath, lang, name, line, clSignatureText(arrow, sourceCode) || arrow?.text || '', clBodyText(arrow, sourceCode), [], clCollectCallees(arrow), isExported(node), calculateComplexity(node)));
1070
+ return;
1071
+ }
1072
+ if (node.type === 'interface_declaration') {
1073
+ const name = clNodeName(raw);
1074
+ if (!name)
1075
+ return;
1076
+ const line = (raw?.startPosition?.row ?? 0) + 1;
1077
+ out.push({
1078
+ id: clEntityId(filePath, 'interface', name, line),
1079
+ name,
1080
+ language: lang,
1081
+ file: filePath,
1082
+ type: 'interface',
1083
+ signature: raw?.text ?? '',
1084
+ parameters: clExtractTSInterfaceParams(raw),
1085
+ startLine: line,
1086
+ visibility: isExported(node) ? 'public' : 'private',
1087
+ calls: [],
1088
+ calledBy: [],
1089
+ purpose: '',
1090
+ context: '',
1091
+ searchTokens: [name.toLowerCase()],
1092
+ metadata: {},
1093
+ });
1094
+ return;
1095
+ }
1096
+ });
1097
+ }
1098
+ // ── Go extraction ────────────────────────────────────────────────────────────
1099
+ function clGoReceiverType(receiver) {
1100
+ for (const child of receiver?.namedChildren ?? []) {
1101
+ if (child.type === 'parameter_declaration') {
1102
+ const typeNode = clRawField(child, 'type');
1103
+ if (typeNode)
1104
+ return typeNode.text.replace(/^\*/, '');
1105
+ }
1106
+ }
1107
+ return undefined;
1108
+ }
1109
+ function clExtractGoParams(raw) {
1110
+ const params = [];
1111
+ const paramList = clRawField(raw, 'parameters');
1112
+ for (const p of paramList?.namedChildren ?? []) {
1113
+ if (p.type !== 'parameter_declaration')
1114
+ continue;
1115
+ const name = clRawField(p, 'name')?.text ?? clRawField(p, 'type')?.text;
1116
+ const type = clRawField(p, 'type')?.text?.trim();
1117
+ if (!name && !type)
1118
+ continue;
1119
+ params.push({ name: name ?? type ?? '<unknown>', type, optional: false, language: 'go' });
1120
+ }
1121
+ return params;
1122
+ }
1123
+ function clExtractGoStructFields(structNode) {
1124
+ const fields = [];
1125
+ for (const f of structNode?.namedChildren ?? []) {
1126
+ if (f.type !== 'field_declaration')
1127
+ continue;
1128
+ const name = clRawField(f, 'name')?.text;
1129
+ if (!name)
1130
+ continue;
1131
+ const type = clRawField(f, 'type')?.text?.trim();
1132
+ const tag = clRawField(f, 'tag')?.text;
1133
+ fields.push({ name, type, isExported: clIsExportedGo(name), tag });
1134
+ }
1135
+ return fields;
1136
+ }
1137
+ function clExtractGoEntities(root, filePath, sourceCode, out) {
1138
+ walkAST(root, (node) => {
1139
+ const raw = node.raw;
1140
+ if (node.type === 'function_declaration') {
1141
+ const name = clNodeName(raw);
1142
+ if (!name)
1143
+ return;
1144
+ const receiver = clRawField(raw, 'receiver');
1145
+ const receiverType = receiver ? clGoReceiverType(receiver) : undefined;
1146
+ const displayName = receiverType ? `${receiverType}.${name}` : name;
1147
+ const line = (raw?.startPosition?.row ?? 0) + 1;
1148
+ out.push(clMakeFunction(filePath, 'go', displayName, line, clSignatureText(raw, sourceCode) || raw?.text || '', clBodyText(raw, sourceCode), clExtractGoParams(raw), clCollectCallees(raw), clIsExportedGo(name), calculateComplexity(node)));
1149
+ return;
1150
+ }
1151
+ if (node.type === 'type_declaration') {
1152
+ for (const spec of raw?.namedChildren ?? []) {
1153
+ if (spec.type !== 'type_spec')
1154
+ continue;
1155
+ const name = clRawField(spec, 'name')?.text;
1156
+ if (!name)
1157
+ continue;
1158
+ const typeNode = clRawField(spec, 'type');
1159
+ const line = (spec?.startPosition?.row ?? 0) + 1;
1160
+ if (typeNode?.type === 'struct_type') {
1161
+ out.push({
1162
+ id: clEntityId(filePath, 'struct', name, line),
1163
+ name,
1164
+ language: 'go',
1165
+ file: filePath,
1166
+ type: 'struct',
1167
+ signature: spec?.text ?? '',
1168
+ parameters: [],
1169
+ startLine: line,
1170
+ visibility: clIsExportedGo(name) ? 'public' : 'private',
1171
+ calls: [],
1172
+ calledBy: [],
1173
+ purpose: '',
1174
+ context: '',
1175
+ searchTokens: [name.toLowerCase()],
1176
+ metadata: { fields: clExtractGoStructFields(typeNode) },
1177
+ });
1178
+ }
1179
+ else if (typeNode?.type === 'interface_type') {
1180
+ out.push({
1181
+ id: clEntityId(filePath, 'interface', name, line),
1182
+ name,
1183
+ language: 'go',
1184
+ file: filePath,
1185
+ type: 'interface',
1186
+ signature: spec?.text ?? '',
1187
+ parameters: [],
1188
+ startLine: line,
1189
+ visibility: clIsExportedGo(name) ? 'public' : 'private',
1190
+ calls: [],
1191
+ calledBy: [],
1192
+ purpose: '',
1193
+ context: '',
1194
+ searchTokens: [name.toLowerCase()],
1195
+ metadata: {},
1196
+ });
1197
+ }
1198
+ }
1199
+ return;
1200
+ }
1201
+ });
1202
+ }
1203
+ // ── Shared entity visitor (Stage 2) ──────────────────────────────────────────
1204
+ export function createCrossLanguageEntityVisitor() {
1205
+ return {
1206
+ name: 'cross-language-entities',
1207
+ stage: 'visitor',
1208
+ extensions: ['.ts', '.tsx', '.js', '.jsx', '.mts', '.cts', '.mjs', '.cjs', '.go'],
1209
+ getRuleIds: () => [],
1210
+ async visit(ast, _adapter, context, sourceCode) {
1211
+ const filePath = context.filePath;
1212
+ if (!filePath)
1213
+ return { violations: [], facts: {} };
1214
+ const lang = getLanguageFromPath(filePath);
1215
+ if (lang === 'unknown')
1216
+ return { violations: [], facts: {} };
1217
+ const root = ast.root;
1218
+ const entities = [];
1219
+ if (lang === 'go') {
1220
+ clExtractGoEntities(root, filePath, sourceCode, entities);
1221
+ }
1222
+ else {
1223
+ clExtractTSEntities(root, filePath, sourceCode, lang, entities);
1224
+ }
1225
+ return {
1226
+ violations: [],
1227
+ facts: { [filePath]: { entities } },
1228
+ };
1229
+ },
1230
+ defaultConfig: {},
1231
+ description: 'Extracts cross-language entities (functions, structs, interfaces) for the cross-language analyzers',
1232
+ category: 'infrastructure',
1233
+ };
1234
+ }
1235
+ // ── Shared flatten + reference building ──────────────────────────────────────
1236
+ function clFlattenEntities(allFacts) {
1237
+ const facts = allFacts['cross-language-entities'];
1238
+ if (!facts)
1239
+ return [];
1240
+ const entities = [];
1241
+ for (const data of Object.values(facts)) {
1242
+ if (data?.entities)
1243
+ entities.push(...data.entities);
1244
+ }
1245
+ return entities;
1246
+ }
1247
+ /**
1248
+ * Build `calls` cross-references by resolving each entity's extracted callee
1249
+ * names against the corpus entity name index. Library calls (fetch, map, …)
1250
+ * resolve to nothing and are dropped — only real intra-corpus call edges become
1251
+ * references.
1252
+ */
1253
+ function clBuildReferences(entities) {
1254
+ const byName = new Map();
1255
+ for (const e of entities) {
1256
+ const key = e.name.toLowerCase();
1257
+ const list = byName.get(key) ?? [];
1258
+ list.push(e);
1259
+ byName.set(key, list);
1260
+ }
1261
+ const refs = [];
1262
+ for (const e of entities) {
1263
+ const callees = e.metadata?.callees ?? [];
1264
+ const eDir = e.file.split('/').slice(0, -1).join('/');
1265
+ for (const callee of callees) {
1266
+ const name = (callee.split('.').pop() ?? callee).toLowerCase();
1267
+ const others = (byName.get(name) ?? []).filter((t) => t.id !== e.id);
1268
+ if (others.length === 0)
1269
+ continue;
1270
+ // A bare call-expression name (`log`) can collide with many entities
1271
+ // across the corpus. Resolving it to *every* entity of that name
1272
+ // fabricates a dense graph (125k edges / 5.9k false cycles on a ~7.8k
1273
+ // node corpus) that corrupts cycle/hub/coupling detection. Narrow the
1274
+ // resolution to an unambiguous match — same file, then same directory,
1275
+ // then a unique global match — and drop the edge when the name is still
1276
+ // ambiguous: a missing edge is safer than a fabricated one for
1277
+ // structural analysis.
1278
+ const sameFile = others.filter((t) => t.file === e.file);
1279
+ const sameDir = sameFile.length === 0
1280
+ ? others.filter((t) => t.file.split('/').slice(0, -1).join('/') === eDir)
1281
+ : [];
1282
+ const uniqueGlobal = others.length === 1 ? others : [];
1283
+ const chosen = sameFile.length === 1
1284
+ ? sameFile
1285
+ : sameDir.length === 1
1286
+ ? sameDir
1287
+ : uniqueGlobal;
1288
+ for (const t of chosen) {
1289
+ refs.push({
1290
+ sourceId: e.id,
1291
+ targetId: t.id,
1292
+ type: 'calls',
1293
+ sourceLanguage: e.language,
1294
+ targetLanguage: t.language,
1295
+ confidence: 0.7,
1296
+ });
1297
+ }
1298
+ }
1299
+ }
1300
+ return refs;
1301
+ }
1302
+ // ── schema-validator reducer (Stage 4) ───────────────────────────────────────
1303
+ export function createSchemaValidatorReducer() {
1304
+ return {
1305
+ name: 'schema-validator',
1306
+ stage: 'derivedReducer',
1307
+ consumes: ['cross-language-entities'],
1308
+ getRuleIds: () => getRuleIdsFor('schema-validator'),
1309
+ async reduce(allFacts, context) {
1310
+ if (context.isScoped) {
1311
+ return {
1312
+ violations: [],
1313
+ facts: {},
1314
+ notRunReason: 'schema-validator requires a full-corpus audit (cross-language schema comparison is unavailable on scoped/diff runs)',
1315
+ };
1316
+ }
1317
+ const entities = clFlattenEntities(allFacts);
1318
+ if (entities.length === 0)
1319
+ return { violations: [], facts: {}, factsConsumed: 0 };
1320
+ try {
1321
+ const { SchemaValidator, extractSchemas, countCrossLanguagePairs } = await import('./analyzers/cross-language/SchemaValidator.js');
1322
+ const schemas = extractSchemas(entities);
1323
+ // A zero here would otherwise surface as `clean` in coverage — which
1324
+ // reads as "compared everything and found no mismatch" when the truth
1325
+ // is "there is nothing to compare". Distinguish the two: no schemas at
1326
+ // all, or schemas all in one language, is `notApplicable`, not `clean`.
1327
+ if (schemas.length === 0) {
1328
+ return { violations: [], facts: {}, notRunReason: 'no schema definitions found (no interfaces/structs to compare)' };
1329
+ }
1330
+ if (countCrossLanguagePairs(schemas) === 0) {
1331
+ return { violations: [], facts: {}, notRunReason: 'no cross-language pairs found (schema comparison requires ≥2 languages)' };
1332
+ }
1333
+ const validator = new SchemaValidator();
1334
+ const violations = await validator.validateSchemas(schemas);
1335
+ return { violations, facts: {}, factsConsumed: schemas.length };
1336
+ }
1337
+ catch (e) {
1338
+ console.error('[schema-validator reducer] error:', e.message);
1339
+ return { violations: [], facts: {}, factsConsumed: 0 };
1340
+ }
1341
+ },
1342
+ defaultConfig: {},
1343
+ description: 'Validates schema consistency across language boundaries (TS interfaces vs Go structs)',
1344
+ category: 'cross-language',
1345
+ };
1346
+ }
1347
+ // ── api-contract reducer (Stage 4) ───────────────────────────────────────────
1348
+ export function createAPIContractReducer() {
1349
+ return {
1350
+ name: 'api-contract',
1351
+ stage: 'derivedReducer',
1352
+ consumes: ['cross-language-entities'],
1353
+ getRuleIds: () => getRuleIdsFor('api-contract'),
1354
+ async reduce(allFacts, context) {
1355
+ if (context.isScoped) {
1356
+ return {
1357
+ violations: [],
1358
+ facts: {},
1359
+ notRunReason: 'api-contract requires a full-corpus audit (cross-language API comparison is unavailable on scoped/diff runs)',
1360
+ };
1361
+ }
1362
+ const entities = clFlattenEntities(allFacts);
1363
+ if (entities.length === 0)
1364
+ return { violations: [], facts: {}, factsConsumed: 0 };
1365
+ try {
1366
+ const { APIContractAnalyzer, extractEndpoints, extractAPICalls } = await import('./analyzers/cross-language/APIContractAnalyzer.js');
1367
+ const endpoints = extractEndpoints(entities);
1368
+ const calls = extractAPICalls(entities);
1369
+ const analyzer = new APIContractAnalyzer();
1370
+ const violations = await analyzer.analyzeContracts(endpoints, calls);
1371
+ return { violations, facts: {}, factsConsumed: endpoints.length + calls.length };
1372
+ }
1373
+ catch (e) {
1374
+ console.error('[api-contract reducer] error:', e.message);
1375
+ return { violations: [], facts: {}, factsConsumed: 0 };
1376
+ }
1377
+ },
1378
+ defaultConfig: {},
1379
+ description: 'Validates API contracts between frontend calls and backend endpoints across languages',
1380
+ category: 'cross-language',
1381
+ };
1382
+ }
1383
+ // ── dependency-graph reducer (Stage 4) ───────────────────────────────────────
1384
+ export function createDependencyGraphReducer() {
1385
+ return {
1386
+ name: 'dependency-graph',
1387
+ stage: 'derivedReducer',
1388
+ consumes: ['cross-language-entities'],
1389
+ getRuleIds: () => getRuleIdsFor('dependency-graph'),
1390
+ async reduce(allFacts, context) {
1391
+ if (context.isScoped) {
1392
+ return {
1393
+ violations: [],
1394
+ facts: {},
1395
+ notRunReason: 'dependency-graph requires a full-corpus audit (cycle/SCC/hub/orphan detection is unavailable on scoped/diff runs)',
1396
+ };
1397
+ }
1398
+ const entities = clFlattenEntities(allFacts);
1399
+ if (entities.length === 0)
1400
+ return { violations: [], facts: {}, factsConsumed: 0 };
1401
+ try {
1402
+ const { DependencyGraphBuilder } = await import('./analyzers/cross-language/DependencyGraphBuilder.js');
1403
+ const builder = new DependencyGraphBuilder({ includeTestFiles: false });
1404
+ const references = clBuildReferences(entities);
1405
+ const graph = await builder.buildGraph(entities, references);
1406
+ const health = await builder.analyzeDependencyHealth(graph);
1407
+ const idToEntity = new Map(entities.map((e) => [e.id, e]));
1408
+ const violations = [];
1409
+ for (const issue of health.issues) {
1410
+ const anchor = issue.affectedNodes.map((id) => idToEntity.get(id)).find(Boolean);
1411
+ violations.push({
1412
+ file: anchor?.file ?? '(unknown)',
1413
+ line: anchor?.startLine ?? 0,
1414
+ severity: issue.severity,
1415
+ message: issue.description,
1416
+ rule: issue.type,
1417
+ type: issue.type, // dependency-graph rules match on field: 'type'
1418
+ analyzer: 'dependency-graph',
1419
+ category: 'cross-language-dependency',
1420
+ details: issue.details,
1421
+ });
1422
+ }
1423
+ for (const s of health.suggestions) {
1424
+ violations.push({
1425
+ file: '(multiple)',
1426
+ line: 0,
1427
+ severity: s.priority === 'low' ? 'suggestion' : 'warning',
1428
+ message: s.description,
1429
+ rule: s.type,
1430
+ type: s.type,
1431
+ analyzer: 'dependency-graph',
1432
+ category: 'cross-language-dependency',
1433
+ details: s.affectedNodes ? { affectedNodes: s.affectedNodes } : undefined,
1434
+ });
1435
+ }
1436
+ return { violations, facts: {}, factsConsumed: entities.length };
1437
+ }
1438
+ catch (e) {
1439
+ console.error('[dependency-graph reducer] error:', e.message);
1440
+ return { violations: [], facts: {}, factsConsumed: 0 };
1441
+ }
1442
+ },
1443
+ defaultConfig: {},
1444
+ description: 'Builds a cross-language dependency graph and detects cycles, hubs, orphans, and tight coupling',
1445
+ category: 'cross-language',
1446
+ };
1447
+ }
692
1448
  // ── Invariants reducer (stage 3) ──────────────────────────────────────────────
693
1449
  /**
694
1450
  * Invariants reducer — enforces user-defined invariant rules from .codeauditor.json.
@@ -748,7 +1504,7 @@ export function createInvariantsReducer() {
748
1504
  }
749
1505
  }
750
1506
  }
751
- const result = await analyzeInvariants(files, { ...context.config, readSource: context.readSource, knownFiles, fileData }, { projectRoot: context.projectRoot, indexHandle: context.indexHandle });
1507
+ const result = await analyzeInvariants(files, { ...context.config, readSource: context.readSource, knownFiles, fileData, isScoped: context.isScoped }, { projectRoot: context.projectRoot, indexHandle: context.indexHandle });
752
1508
  return {
753
1509
  violations: result.violations ?? [],
754
1510
  facts: {},