mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-08-28 05:25:25 +00:00
* test(ingestion): characterize Laravel route → controller CALLS edges (RING4-2 #943) Pins the current processRoutesFromExtracted edge-emission behavior (which had no direct coverage) before migrating it off the legacy ResolutionContext.resolve tiered lookup. Locks edge target, reason, and confidence values. * refactor(ingestion): resolve Laravel route controllers via type registry (RING4-2 #943) Migrate processRoutesFromExtracted off the legacy ResolutionContext.resolve tiered lookup onto model.types.lookupClassByName (global class resolution) + model.symbols.lookupExactAll (same-file method lookup). Drops the TIER_CONFIDENCE dependency for a fixed ROUTE_EDGE_CONFIDENCE constant matching the prior global-tier confidence. Characterization tests (6) stay green — behavior preserved. * refactor(ingestion): delete ResolutionContext.resolve tiered lookup (RING4-2 #943) Removes the legacy tiered name resolution — resolve/resolveUncached, TieredCandidates, ResolutionTier, TIER_CONFIDENCE, walkBindingChain, the package-dir index, the per-file resolve cache, and tier-hit stats. The context is now a thin holder for the live SemanticModel plus the (now-dead) per-file import maps, which the follow-up prune removes. Deletes the dedicated resolution-context.test.ts and symbol-resolver.test.ts (both exercised the removed .resolve tiered lookup). Full unit suite green (the 3 analyze worker-pool tests are pre-existing load flakes — pass isolated). * refactor(ingestion): delete legacy import-map plumbing + wildcard synthesis (RING4-2 #943) The per-file importMap / namedImportMap / packageMap / moduleAliasMap that fed the retired tiered resolver are now dead — nothing reads them (IMPORTS edges come from scope-resolution's imports-to-edges bridge, independent of these maps). Removes: - wildcard-synthesis.ts (synthesized the dead namedImportMap/moduleAliasMap) - import-processor's resolution path (processImports/processImportsFromExtracted/ wireImplicitImports/buildImportResolutionContext), keeping only the live preprocessImportPath path-cleanup helper - the parse-impl orchestration that drove them The parse phase now threads its SemanticModel to scope-resolution directly (parseOutput.model) instead of wrapping it in the resolution context. Deletes the obsolete wildcard/import-processor unit tests; trims the dead processImports cases from sequential-language-availability (processParsing coverage kept). * refactor(ingestion): delete resolution context + named-binding plumbing (RING4-2 #943) Completes the legacy-resolution retirement. With the tiered resolver gone, the entire per-file import-extraction chain is dead — its only consumer was the deleted ResolutionContext.resolve, and scope-resolution emits IMPORTS edges from its own finalized ImportEdges: - delete model/resolution-context.ts (the legacy context); the parse phase now hands its SemanticModel to scope-resolution as parseOutput.model - delete the named-bindings/ extractors + the namedBindingExtractor provider hook (built the dead NamedImportMap) across all 8 providers + the worker - delete the orphaned implicitImportWirer hook + Swift implementation + providersWithImplicitWiring (scope-resolution owns implicit imports now) - drop the dead ExtractedImport type + worker/sequential import accumulation (result.imports / WorkerExtractedData.imports) - import-processor.ts and its preprocessImportPath helper are now unreferenced Deletes the obsolete named-bindings + preprocessImportPath unit tests. tsc clean; full unit suite green (3 analyze worker-pool tests are pre-existing load flakes); 1229 import/cross-file/resolver integration tests pass incl. the wildcard-import languages (Go/Ruby/C++/Swift) that previously used synthesis. * docs(ingestion): scrub stale references to deleted resolution-context machinery (RING4-2 #943) * docs(ingestion): reword route resolver comment to clear acceptance grep gate (#943) * fix(review): apply autofix feedback (RING4-2 #943) Code-review autofixes from the multi-agent pass: - delete orphaned dead code the deletion missed: swift.ts groupSwiftFilesByTarget + SwiftPackageConfig import (live copy is target-grouping.ts), import-resolvers EMPTY_INDEX export (no consumers after the importCtx reset was removed) - scrub stale comments referencing deleted symbols (processImports, preprocessImportPath, moduleAliasMap, NamedImportMap/PackageMap, wildcard-synthesis) and fix a broken comment fragment in parse-impl.ts - document the intentional global-resolution convergence for route controllers (the import-scoped tier was deleted with the resolver): confidence flattens 0.9→0.5 but resolved edges stay at the 0.5 process-trace/community gate; only the narrow imported-controller-with-unresolved-method guessed edge crosses it - add an overloaded-method characterization case pinning lookupExactAll[0] * style(ingestion): prettier-format parse-impl unwind + route characterization test (#943) * refactor(ingestion): address tri-review findings (RING4-2 #943) From the PR #2033 tri-review (Codex + CE lanes): - delete the now-dead importSemantics provider field + ImportSemantics type (wildcard-synthesis.ts was its sole consumer; zero readers remain) across language-provider.ts + 7 providers + DEFAULTS - correct the processRoutesFromExtracted JSDoc: the import-disambiguated controller skip is STRICTER than the legacy global-tier guard (the legacy import-scoped tier resolved aliased / same-short-name controllers and emitted the edge); document the aliased-import missed-edge case explicitly - add an aliased-controller characterization test pinning the documented global-resolution convergence (no edge for an aliased/unresolvable controller name) - scrub stale parse-impl.ts docstrings/comments that still listed the removed import-resolution / wildcard-synthesis / heritage passes Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(ingestion): capture routes-file use/FQN map for Laravel controller resolution (#943) Adds ExtractedRoute.controllerQualifiedName: the Laravel route extractor now builds the routes file's `use`-import alias map (local→normalized dot-joined FQN, via splitNamespaceUseDeclaration) and captures inline qualified ::class references, threading the disambiguating FQN through every route. Normalized via the shared normalizeQualifiedName so it matches the type registry's key shape (issue #1982). Foundation for qualified-first route→controller resolution (U2). * fix(ingestion): resolve Laravel route controllers qualified-first (#943) processRoutesFromExtracted now resolves the controller via model.types.lookupClassByQualifiedName(route.controllerQualifiedName) when the extractor disambiguated it (aliased use / same-short-name / inline FQN), falling back to the short-name lookupClassByName (which still skips on ambiguity). This restores the route→controller CALLS edges the PR #2033 tri-review (Codex F1 + ce-adversarial) found dropped, without re-adding the deleted per-file import map. Method resolution, guessed-id, and confidence are unchanged. JSDoc rewritten to qualified-first precedence; the aliased characterization test flips from no-edge to edge; adds duplicated-name-disambiguated + stale-FQN-fallback cases. * test(ingestion): end-to-end Laravel route→controller qualified resolution + PSR-4 disambiguation (#943) Adds an integration test that parses real namespaced PHP controllers + a routes file through the worker pipeline and asserts the route CALLS edges target the correct namespaced controller — the authoritative gate the unit tests can't be (hand-built models). It surfaced that PHP's statement-form `namespace X;` leaves the structure-phase qualifiedName as the SHORT name, so lookupClassByQualifiedName misses; resolveControllerByQualifiedName now adds a PSR-4 file-path disambiguation (FQN namespace tail ↔ file directory tail) to pick the right same-short-name controller. Forces the worker path (workerThresholdsForTest) since route extraction is worker-only. * style(ingestion): prettier-format Laravel route resolution changes (#943) * test(ingestion): regenerate php-captures golden for the new php-laravel-routes fixture (#943) * test(ingestion): move route fixture out of the php-* scope-capture corpus (#943) The laravel route-resolution fixture lived under lang-resolution/php-laravel-routes, which the php scope-capture golden + benchmark both glob (lang-resolution/php-*), drifting their fingerprints. The fixture is for route resolution, not php scope-capture parity, so rename it to lang-resolution/laravel-route-resolution to decouple it. Reverts the golden's php-laravel-routes entries; bench scope-capture --check passes (php back to baseline). --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
297 lines
11 KiB
TypeScript
297 lines
11 KiB
TypeScript
/**
|
||
* Characterization tests for `processRoutesFromExtracted` — the Laravel
|
||
* framework-route → controller-method `CALLS`-edge emitter in
|
||
* call-processor.ts.
|
||
*
|
||
* RING4-2 (#943) migrates this emitter off the legacy `ResolutionContext.resolve`
|
||
* tiered lookup and onto the scope-resolution registry / symbol table. These
|
||
* tests pin the *current* edge-emission behavior (which had no direct coverage)
|
||
* so the migration is provably behavior-preserving:
|
||
*
|
||
* - resolvable controller + same-file method → CALLS edge to the method node
|
||
* - resolvable controller + unknown method → CALLS edge to a *guessed* Method id
|
||
* - unknown controller → no edge
|
||
* - ambiguous global controller (>1 match) → no edge
|
||
* - one edge emitted per route
|
||
*
|
||
* Confidence values captured here (controller resolves at the `global` tier for
|
||
* routes-file → controller references, so 0.5; guessed-method edges are × 0.8)
|
||
* are the contract the migrated implementation must match.
|
||
*/
|
||
|
||
import { describe, it, expect } from 'vitest';
|
||
import { createKnowledgeGraph } from '../../src/core/graph/graph.js';
|
||
import { createSemanticModel } from '../../src/core/ingestion/model/index.js';
|
||
import { processRoutesFromExtracted } from '../../src/core/ingestion/call-processor.js';
|
||
import { generateId } from '../../src/lib/utils.js';
|
||
import type { ExtractedRoute } from '../../src/core/ingestion/route-extractors/laravel.js';
|
||
import type { KnowledgeGraph } from '../../src/core/graph/types.js';
|
||
|
||
const ROUTES_FILE = 'routes/web.php';
|
||
const CONTROLLER_FILE = 'app/Http/Controllers/OrderController.php';
|
||
|
||
function makeRoute(overrides: Partial<ExtractedRoute> = {}): ExtractedRoute {
|
||
return {
|
||
filePath: ROUTES_FILE,
|
||
httpMethod: 'get',
|
||
routePath: '/orders',
|
||
routeName: null,
|
||
controllerName: 'OrderController',
|
||
methodName: 'index',
|
||
middleware: [],
|
||
prefix: null,
|
||
lineNumber: 1,
|
||
...overrides,
|
||
};
|
||
}
|
||
|
||
/** A semantic model with a single OrderController class + the given methods
|
||
* registered in the controller's own file (so method resolution finds them
|
||
* via the same-file symbol-table lookup). */
|
||
function modelWithController(methods: string[]) {
|
||
const model = createSemanticModel();
|
||
model.symbols.add(CONTROLLER_FILE, 'OrderController', 'class:OrderController', 'Class');
|
||
for (const m of methods) {
|
||
model.symbols.add(CONTROLLER_FILE, m, `method:OrderController.${m}`, 'Method', {
|
||
ownerId: 'class:OrderController',
|
||
});
|
||
}
|
||
return model;
|
||
}
|
||
|
||
function routeCallsEdges(graph: KnowledgeGraph) {
|
||
return graph.relationships.filter((r) => r.type === 'CALLS' && r.reason === 'laravel-route');
|
||
}
|
||
|
||
describe('processRoutesFromExtracted — Laravel route → controller CALLS edges', () => {
|
||
it('resolvable controller + same-file method → one CALLS edge to the method node', async () => {
|
||
const graph = createKnowledgeGraph();
|
||
const model = modelWithController(['index']);
|
||
|
||
await processRoutesFromExtracted(graph, [makeRoute({ methodName: 'index' })], model);
|
||
|
||
const edges = routeCallsEdges(graph);
|
||
expect(edges).toHaveLength(1);
|
||
expect(edges[0].sourceId).toBe(generateId('File', ROUTES_FILE));
|
||
expect(edges[0].targetId).toBe('method:OrderController.index');
|
||
// controller resolved by global class name → ROUTE_EDGE_CONFIDENCE (0.5)
|
||
expect(edges[0].confidence).toBeCloseTo(0.5, 5);
|
||
});
|
||
|
||
it('resolvable controller + unknown method → CALLS edge to a guessed Method id at reduced confidence', async () => {
|
||
const graph = createKnowledgeGraph();
|
||
const model = modelWithController([]); // controller class only, no methods
|
||
|
||
await processRoutesFromExtracted(graph, [makeRoute({ methodName: 'ghost' })], model);
|
||
|
||
const edges = routeCallsEdges(graph);
|
||
expect(edges).toHaveLength(1);
|
||
expect(edges[0].sourceId).toBe(generateId('File', ROUTES_FILE));
|
||
expect(edges[0].targetId).toBe(generateId('Method', `${CONTROLLER_FILE}:ghost`));
|
||
// guessed-method edges are emitted at controller-confidence × 0.8
|
||
expect(edges[0].confidence).toBeCloseTo(0.5 * 0.8, 5);
|
||
});
|
||
|
||
it('unknown controller → no edge emitted', async () => {
|
||
const graph = createKnowledgeGraph();
|
||
const model = modelWithController(['index']);
|
||
|
||
await processRoutesFromExtracted(
|
||
graph,
|
||
[makeRoute({ controllerName: 'GhostController', methodName: 'index' })],
|
||
model,
|
||
);
|
||
|
||
expect(routeCallsEdges(graph)).toHaveLength(0);
|
||
});
|
||
|
||
it('ambiguous controller name (2+ global matches) → no edge emitted', async () => {
|
||
const graph = createKnowledgeGraph();
|
||
const model = createSemanticModel();
|
||
// Two distinct classes share the controller short-name in different files →
|
||
// lookupClassByName returns >1 candidate, which the emitter refuses.
|
||
model.symbols.add(
|
||
'app/A/OrderController.php',
|
||
'OrderController',
|
||
'class:A.OrderController',
|
||
'Class',
|
||
);
|
||
model.symbols.add(
|
||
'app/B/OrderController.php',
|
||
'OrderController',
|
||
'class:B.OrderController',
|
||
'Class',
|
||
);
|
||
|
||
await processRoutesFromExtracted(graph, [makeRoute({ methodName: 'index' })], model);
|
||
|
||
expect(routeCallsEdges(graph)).toHaveLength(0);
|
||
});
|
||
|
||
it('route missing controllerName or methodName → skipped', async () => {
|
||
const graph = createKnowledgeGraph();
|
||
const model = modelWithController(['index']);
|
||
|
||
await processRoutesFromExtracted(
|
||
graph,
|
||
[makeRoute({ controllerName: null }), makeRoute({ methodName: null })],
|
||
model,
|
||
);
|
||
|
||
expect(routeCallsEdges(graph)).toHaveLength(0);
|
||
});
|
||
|
||
it('multiple routes to the same controller → one edge per route, distinct targets', async () => {
|
||
const graph = createKnowledgeGraph();
|
||
const model = modelWithController(['index', 'store']);
|
||
|
||
await processRoutesFromExtracted(
|
||
graph,
|
||
[
|
||
makeRoute({ httpMethod: 'get', routePath: '/orders', methodName: 'index' }),
|
||
makeRoute({ httpMethod: 'post', routePath: '/orders', methodName: 'store' }),
|
||
],
|
||
model,
|
||
);
|
||
|
||
const edges = routeCallsEdges(graph);
|
||
expect(edges).toHaveLength(2);
|
||
expect(edges.map((e) => e.targetId).sort()).toEqual([
|
||
'method:OrderController.index',
|
||
'method:OrderController.store',
|
||
]);
|
||
});
|
||
|
||
it('overloaded controller method → edge targets the first-registered definition', async () => {
|
||
// Two same-name method definitions in the controller file (overloads).
|
||
// The emitter takes lookupExactAll(...)[0] — first-registered wins, parity
|
||
// with the legacy same-file tier which returned candidates[0]. Pins the
|
||
// selection policy so it can't silently drift.
|
||
const graph = createKnowledgeGraph();
|
||
const model = createSemanticModel();
|
||
model.symbols.add(CONTROLLER_FILE, 'OrderController', 'class:OrderController', 'Class');
|
||
model.symbols.add(CONTROLLER_FILE, 'index', 'method:OrderController.index#1', 'Method', {
|
||
ownerId: 'class:OrderController',
|
||
});
|
||
model.symbols.add(CONTROLLER_FILE, 'index', 'method:OrderController.index#2', 'Method', {
|
||
ownerId: 'class:OrderController',
|
||
});
|
||
|
||
await processRoutesFromExtracted(graph, [makeRoute({ methodName: 'index' })], model);
|
||
|
||
const edges = routeCallsEdges(graph);
|
||
expect(edges).toHaveLength(1);
|
||
expect(edges[0].targetId).toBe('method:OrderController.index#1');
|
||
});
|
||
|
||
it('aliased controller resolves via controllerQualifiedName → edge emitted', async () => {
|
||
// An aliased import `use App\\Http\\Controllers\\OrderController as Orders;`
|
||
// + `[Orders::class, 'index']` yields controllerName='Orders' but the extractor
|
||
// also threads controllerQualifiedName='App.Http.Controllers.OrderController'
|
||
// (the alias resolved to its FQN). The class is registered under that FQN, so
|
||
// lookupClassByQualifiedName resolves it → edge — restoring what the legacy
|
||
// import-scoped tier emitted (RING4-2 follow-up).
|
||
const graph = createKnowledgeGraph();
|
||
const model = createSemanticModel();
|
||
const FQN = 'App.Http.Controllers.OrderController';
|
||
model.symbols.add(CONTROLLER_FILE, 'OrderController', 'class:OrderController', 'Class', {
|
||
qualifiedName: FQN,
|
||
});
|
||
model.symbols.add(CONTROLLER_FILE, 'index', 'method:OrderController.index', 'Method', {
|
||
ownerId: 'class:OrderController',
|
||
});
|
||
|
||
await processRoutesFromExtracted(
|
||
graph,
|
||
[makeRoute({ controllerName: 'Orders', controllerQualifiedName: FQN, methodName: 'index' })],
|
||
model,
|
||
);
|
||
|
||
const edges = routeCallsEdges(graph);
|
||
expect(edges).toHaveLength(1);
|
||
expect(edges[0].targetId).toBe('method:OrderController.index');
|
||
expect(edges[0].confidence).toBeCloseTo(0.5, 5);
|
||
});
|
||
|
||
it('globally-duplicated short name disambiguated by controllerQualifiedName → edge to the specific controller', async () => {
|
||
// Two OrderControllers in different namespaces share the short name. The route
|
||
// carries the FQN of the one its `use` import selected, so the edge targets
|
||
// that specific class's method — not the other, and not a skip.
|
||
const graph = createKnowledgeGraph();
|
||
const model = createSemanticModel();
|
||
const ADMIN_FQN = 'App.Admin.OrderController';
|
||
const PUBLIC_FQN = 'App.Http.Controllers.OrderController';
|
||
model.symbols.add(
|
||
'app/Admin/OrderController.php',
|
||
'OrderController',
|
||
'class:Admin.OrderController',
|
||
'Class',
|
||
{
|
||
qualifiedName: ADMIN_FQN,
|
||
},
|
||
);
|
||
model.symbols.add(
|
||
'app/Admin/OrderController.php',
|
||
'index',
|
||
'method:Admin.OrderController.index',
|
||
'Method',
|
||
{
|
||
ownerId: 'class:Admin.OrderController',
|
||
},
|
||
);
|
||
model.symbols.add(
|
||
'app/Http/Controllers/OrderController.php',
|
||
'OrderController',
|
||
'class:Public.OrderController',
|
||
'Class',
|
||
{
|
||
qualifiedName: PUBLIC_FQN,
|
||
},
|
||
);
|
||
model.symbols.add(
|
||
'app/Http/Controllers/OrderController.php',
|
||
'index',
|
||
'method:Public.OrderController.index',
|
||
'Method',
|
||
{
|
||
ownerId: 'class:Public.OrderController',
|
||
},
|
||
);
|
||
|
||
await processRoutesFromExtracted(
|
||
graph,
|
||
[
|
||
makeRoute({
|
||
controllerName: 'OrderController',
|
||
controllerQualifiedName: ADMIN_FQN,
|
||
methodName: 'index',
|
||
}),
|
||
],
|
||
model,
|
||
);
|
||
|
||
const edges = routeCallsEdges(graph);
|
||
expect(edges).toHaveLength(1);
|
||
expect(edges[0].targetId).toBe('method:Admin.OrderController.index');
|
||
});
|
||
|
||
it('controllerQualifiedName set but no class matches → falls back to short-name resolution', async () => {
|
||
// A stale/unmatched FQN must not block the short-name fallback when that is unique.
|
||
const graph = createKnowledgeGraph();
|
||
const model = modelWithController(['index']); // 'OrderController' registered, no FQN
|
||
await processRoutesFromExtracted(
|
||
graph,
|
||
[
|
||
makeRoute({
|
||
controllerName: 'OrderController',
|
||
controllerQualifiedName: 'App.Nonexistent.OrderController',
|
||
methodName: 'index',
|
||
}),
|
||
],
|
||
model,
|
||
);
|
||
const edges = routeCallsEdges(graph);
|
||
expect(edges).toHaveLength(1);
|
||
expect(edges[0].targetId).toBe('method:OrderController.index');
|
||
});
|
||
});
|