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.
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +94 -0
- package/README.md +5 -5
- package/dist/analyzers/applicability.d.ts +13 -0
- package/dist/analyzers/applicability.d.ts.map +1 -1
- package/dist/analyzers/applicability.js +37 -0
- package/dist/analyzers/applicability.js.map +1 -1
- package/dist/analyzers/cross-language/APIContractAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/cross-language/APIContractAnalyzer.js +23 -2
- package/dist/analyzers/cross-language/APIContractAnalyzer.js.map +1 -1
- package/dist/analyzers/cross-language/DependencyGraphBuilder.d.ts +52 -9
- package/dist/analyzers/cross-language/DependencyGraphBuilder.d.ts.map +1 -1
- package/dist/analyzers/cross-language/DependencyGraphBuilder.js +228 -55
- package/dist/analyzers/cross-language/DependencyGraphBuilder.js.map +1 -1
- package/dist/analyzers/cross-language/SchemaValidator.d.ts +19 -12
- package/dist/analyzers/cross-language/SchemaValidator.d.ts.map +1 -1
- package/dist/analyzers/cross-language/SchemaValidator.js +78 -111
- package/dist/analyzers/cross-language/SchemaValidator.js.map +1 -1
- package/dist/analyzers/crossDomain/CrossDomainAnalyzer.d.ts +0 -6
- package/dist/analyzers/crossDomain/CrossDomainAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/crossDomain/CrossDomainAnalyzer.js +85 -73
- package/dist/analyzers/crossDomain/CrossDomainAnalyzer.js.map +1 -1
- package/dist/analyzers/documentationAnalyzer.js +5 -5
- package/dist/analyzers/documentationAnalyzer.js.map +1 -1
- package/dist/analyzers/invariantsAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/invariantsAnalyzer.js +1 -0
- package/dist/analyzers/invariantsAnalyzer.js.map +1 -1
- package/dist/analyzers/provenance.d.ts +7 -9
- package/dist/analyzers/provenance.d.ts.map +1 -1
- package/dist/analyzers/provenance.js +88 -124
- package/dist/analyzers/provenance.js.map +1 -1
- package/dist/analyzers/reactAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/reactAnalyzer.js +120 -21
- package/dist/analyzers/reactAnalyzer.js.map +1 -1
- package/dist/analyzers/ruleRegistry.d.ts +21 -0
- package/dist/analyzers/ruleRegistry.d.ts.map +1 -1
- package/dist/analyzers/ruleRegistry.js +61 -12
- package/dist/analyzers/ruleRegistry.js.map +1 -1
- package/dist/analyzers/universal/UniversalConventionsAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalConventionsAnalyzer.js +60 -24
- package/dist/analyzers/universal/UniversalConventionsAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalDRYAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalDRYAnalyzer.js +5 -2
- package/dist/analyzers/universal/UniversalDRYAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalDataAccessAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalDataAccessAnalyzer.js +173 -56
- package/dist/analyzers/universal/UniversalDataAccessAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalDocumentationAnalyzer.d.ts +2 -1
- package/dist/analyzers/universal/UniversalDocumentationAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalDocumentationAnalyzer.js +23 -12
- package/dist/analyzers/universal/UniversalDocumentationAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalSOLIDAnalyzer.d.ts +11 -1
- package/dist/analyzers/universal/UniversalSOLIDAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalSOLIDAnalyzer.js +60 -22
- package/dist/analyzers/universal/UniversalSOLIDAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalStylesAnalyzer.d.ts +2 -3
- package/dist/analyzers/universal/UniversalStylesAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalStylesAnalyzer.js +258 -87
- package/dist/analyzers/universal/UniversalStylesAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/schema/codeAnalysis.d.ts.map +1 -1
- package/dist/analyzers/universal/schema/codeAnalysis.js +7 -1
- package/dist/analyzers/universal/schema/codeAnalysis.js.map +1 -1
- package/dist/analyzers/universal/schema/jsonSchema.js +7 -11
- package/dist/analyzers/universal/schema/jsonSchema.js.map +1 -1
- package/dist/applyDataDirEnv.js +1 -1
- package/dist/applyDataDirEnv.js.map +1 -1
- package/dist/auditRouter.d.ts +41 -0
- package/dist/auditRouter.d.ts.map +1 -0
- package/dist/auditRouter.js +185 -0
- package/dist/auditRouter.js.map +1 -0
- package/dist/auditRunner.d.ts.map +1 -1
- package/dist/auditRunner.js +97 -58
- package/dist/auditRunner.js.map +1 -1
- package/dist/cli.js +247 -23
- package/dist/cli.js.map +1 -1
- package/dist/codeIndexDB.d.ts.map +1 -1
- package/dist/codeIndexDB.js +36 -2
- package/dist/codeIndexDB.js.map +1 -1
- package/dist/componentScanner.d.ts.map +1 -1
- package/dist/componentScanner.js +7 -2
- package/dist/componentScanner.js.map +1 -1
- package/dist/config/configLoader.d.ts +9 -0
- package/dist/config/configLoader.d.ts.map +1 -1
- package/dist/config/configLoader.js +30 -2
- package/dist/config/configLoader.js.map +1 -1
- package/dist/config/defaults.d.ts.map +1 -1
- package/dist/config/defaults.js +10 -3
- package/dist/config/defaults.js.map +1 -1
- package/dist/dataPaths.d.ts +8 -1
- package/dist/dataPaths.d.ts.map +1 -1
- package/dist/dataPaths.js +57 -5
- package/dist/dataPaths.js.map +1 -1
- package/dist/functionScanner.d.ts.map +1 -1
- package/dist/functionScanner.js +1 -17
- package/dist/functionScanner.js.map +1 -1
- package/dist/grammars/tree-sitter-scss.wasm +0 -0
- package/dist/invariants/ruleEngine.d.ts +8 -0
- package/dist/invariants/ruleEngine.d.ts.map +1 -1
- package/dist/invariants/ruleEngine.js +35 -16
- package/dist/invariants/ruleEngine.js.map +1 -1
- package/dist/languages/LanguageOrchestrator.d.ts +10 -11
- package/dist/languages/LanguageOrchestrator.d.ts.map +1 -1
- package/dist/languages/LanguageOrchestrator.js +135 -101
- package/dist/languages/LanguageOrchestrator.js.map +1 -1
- package/dist/languages/RuntimeManager.d.ts +28 -1
- package/dist/languages/RuntimeManager.d.ts.map +1 -1
- package/dist/languages/RuntimeManager.js +160 -49
- package/dist/languages/RuntimeManager.js.map +1 -1
- package/dist/languages/go/GoAdapter.d.ts +2 -0
- package/dist/languages/go/GoAdapter.d.ts.map +1 -1
- package/dist/languages/go/GoAdapter.js +15 -22
- package/dist/languages/go/GoAdapter.js.map +1 -1
- package/dist/languages/go/analyzer +0 -0
- package/dist/languages/go/analyzer-src/analyzer.go +510 -0
- package/dist/languages/go/analyzer-src/go.mod +3 -0
- package/dist/languages/go/analyzer-src/indexer.go +268 -0
- package/dist/languages/go/analyzer-src/parser.go +461 -0
- package/dist/languages/go/analyzer-src/solid.go +395 -0
- package/dist/languages/go/analyzer-src/testconventions.go +25 -0
- package/dist/languages/go/analyzer-src/types.go +148 -0
- package/dist/languages/go/go.mod +7 -0
- package/dist/languages/go/main.go +203 -0
- package/dist/languages/testConventions.d.ts +35 -0
- package/dist/languages/testConventions.d.ts.map +1 -0
- package/dist/languages/testConventions.js +51 -0
- package/dist/languages/testConventions.js.map +1 -0
- package/dist/languages/typescript/TreeSitterTypeScriptAdapter.d.ts +3 -0
- package/dist/languages/typescript/TreeSitterTypeScriptAdapter.d.ts.map +1 -1
- package/dist/languages/typescript/TreeSitterTypeScriptAdapter.js +25 -18
- package/dist/languages/typescript/TreeSitterTypeScriptAdapter.js.map +1 -1
- package/dist/mcp-tools/whitelistTools.d.ts +1 -1
- package/dist/mcp-tools/workflowGuide.js +3 -3
- package/dist/mcp-tools/workflowGuide.js.map +1 -1
- package/dist/mcp-tools-shared.d.ts +0 -3
- package/dist/mcp-tools-shared.d.ts.map +1 -1
- package/dist/mcp-tools-shared.js +24 -128
- package/dist/mcp-tools-shared.js.map +1 -1
- package/dist/mcp.js +18 -12
- package/dist/mcp.js.map +1 -1
- package/dist/mcpAuditJobs.d.ts.map +1 -1
- package/dist/mcpAuditJobs.js +13 -0
- package/dist/mcpAuditJobs.js.map +1 -1
- package/dist/mcpAutoIndex.js +1 -1
- package/dist/mcpAutoIndex.js.map +1 -1
- package/dist/nextFile.d.ts +21 -0
- package/dist/nextFile.d.ts.map +1 -0
- package/dist/nextFile.js +51 -0
- package/dist/nextFile.js.map +1 -0
- package/dist/nextFileIncremental.d.ts +141 -0
- package/dist/nextFileIncremental.d.ts.map +1 -0
- package/dist/nextFileIncremental.js +314 -0
- package/dist/nextFileIncremental.js.map +1 -0
- package/dist/pipeline.d.ts.map +1 -1
- package/dist/pipeline.js +22 -0
- package/dist/pipeline.js.map +1 -1
- package/dist/pipelineAdapters.d.ts +17 -0
- package/dist/pipelineAdapters.d.ts.map +1 -1
- package/dist/pipelineAdapters.js +800 -44
- package/dist/pipelineAdapters.js.map +1 -1
- package/dist/reporting/jsonReportGenerator.d.ts.map +1 -1
- package/dist/reporting/jsonReportGenerator.js +11 -30
- package/dist/reporting/jsonReportGenerator.js.map +1 -1
- package/dist/styles/styleIndexer.d.ts +7 -0
- package/dist/styles/styleIndexer.d.ts.map +1 -1
- package/dist/styles/styleIndexer.js +44 -61
- package/dist/styles/styleIndexer.js.map +1 -1
- package/dist/types/crossLanguage.d.ts +1 -0
- package/dist/types/crossLanguage.d.ts.map +1 -1
- package/dist/types.d.ts +27 -4
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils/fileDiscovery.d.ts +11 -2
- package/dist/utils/fileDiscovery.d.ts.map +1 -1
- package/dist/utils/fileDiscovery.js +23 -5
- package/dist/utils/fileDiscovery.js.map +1 -1
- package/dist/utils/reactDetection.d.ts.map +1 -1
- package/dist/utils/reactDetection.js +59 -44
- package/dist/utils/reactDetection.js.map +1 -1
- package/package.json +31 -27
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/README.md +6 -6
- package/plugin/hooks/hooks.json +4 -0
- package/plugin/scripts/hook-audit.sh +0 -0
- package/plugin/scripts/hook-self-audit.sh +107 -0
- package/plugin/skills/code-auditor/SKILL-RULE-KINDS.md +55 -7
- package/plugin/skills/code-auditor/SKILL.md +22 -23
package/dist/pipelineAdapters.js
CHANGED
|
@@ -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 (
|
|
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 (
|
|
493
|
-
violations.push(...checkRawElements(scanResults,
|
|
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
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
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
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: {},
|