mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-07 02:58:02 +00:00
feat(processes): select round-robin by terminal so the list is not one flow repeated
Ranking was `sort by length` alone, so the top of the list was one behaviour described many ways: eleven of the top fourteen processes on the reporting repo were four entry points crossed with three terminals of the SAME date-window utility cluster. Genuine call chains, but a reader learns one thing from fourteen entries, and the repo's own domain flows sat below them. Selection now round-robins across TERMINALS, deepest first. Depth still orders within a terminal and still leads the list; what changes is that no terminal takes a second slot until every other has had a first. Keying on the entry point was tried first and made it worse — many files declare a `main`, so each was a distinct entry that round-robin then awarded its own slot, and `Main -> AlignWindowEnd` went from one row to eight. The repetition was never in where a flow starts. Measured on that repo: distinct terminals in the top 20 went 3 -> 20, and its domain flows (`ReconcilePositions -> ...`) moved into the top 4%. Two things this deliberately does not claim. The reported cause — ranking rewarding fan-in, promoting chains ending in widely-called helpers — measured FALSE: those terminals have one caller each (`alignWindowStart` 1, `validateSymbol` 1). A fan-in discount was implemented against that hypothesis, measured, and reverted for moving nothing. And a business flow still cannot be a process in its own right: the walk only emits at a leaf, at max depth, or on a cycle, so a flow whose meaningful endpoint calls onward survives only as whatever leaf it bottoms out in. Both are recorded in the code so neither reads as settled. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
85800fc4e2
commit
e4bfff7f08
2 changed files with 143 additions and 4 deletions
|
|
@ -135,10 +135,63 @@ export const processProcesses = async (
|
|||
70,
|
||||
);
|
||||
|
||||
// Step 4: Limit to max processes (prioritize longer traces)
|
||||
const limitedTraces = endpointDeduped
|
||||
.sort((a, b) => b.length - a.length)
|
||||
.slice(0, cfg.maxProcesses);
|
||||
// Step 4: Limit to max processes — deepest first, but ROUND-ROBIN across
|
||||
// TERMINALS (R2-3).
|
||||
//
|
||||
// Ranking was `sort by length` alone, and the top of that list was one
|
||||
// behaviour described many ways: measured on the reporting repo, eleven of
|
||||
// the top fourteen processes were four entry points crossed with three
|
||||
// terminals of the same date-window utility cluster — `Handle ->
|
||||
// AlignWindowEnd`, `Main -> AlignWindowStart`, `ProcessSymbol ->
|
||||
// ResolveGridIntervalMs`. Genuine call chains, but a reader learns one thing
|
||||
// from fourteen entries.
|
||||
//
|
||||
// Keyed on the TERMINAL, not the entry point. Keying on the entry was tried
|
||||
// first and made it worse — many files declare a `main`, so each was a
|
||||
// distinct entry that round-robin then awarded its own slot, and
|
||||
// `Main -> AlignWindowEnd` went from one row to eight. The repetition was
|
||||
// never in where a flow starts; it is in where flows pile up.
|
||||
//
|
||||
// Depth still orders within a terminal and still leads the list, since
|
||||
// insertion order here is deepest-first. What changes is that no terminal
|
||||
// takes a second slot until every other has had a first. Measured: distinct
|
||||
// terminals in the top 20 went 3 -> 20, and the repo's own domain flows
|
||||
// (`ReconcilePositions -> ...`) moved into the top 4%.
|
||||
//
|
||||
// Two things this does NOT do, recorded so neither reads as settled. The
|
||||
// reported cause — ranking rewarding fan-in, promoting chains ending in
|
||||
// widely-called helpers — measured FALSE: those terminals have one caller
|
||||
// each (`alignWindowStart` 1, `validateSymbol` 1). A fan-in discount was
|
||||
// implemented against that hypothesis and moved nothing. And a business flow
|
||||
// still cannot be a process in its own right: `traceFromEntryPoint` only
|
||||
// emits at a leaf, at max depth, or on a cycle, so a flow whose meaningful
|
||||
// endpoint calls onward is never a candidate — it survives here only as
|
||||
// whatever leaf it happens to bottom out in. Fixing that means teaching the
|
||||
// walk what a sink is (I/O, route handler, external call), which is a change
|
||||
// to what a process IS rather than to how processes are ranked.
|
||||
const tracesByTerminal = new Map<string, string[][]>();
|
||||
for (const trace of [...endpointDeduped].sort((a, b) => b.length - a.length)) {
|
||||
const terminalId = trace[trace.length - 1];
|
||||
if (terminalId === undefined) continue;
|
||||
const existing = tracesByTerminal.get(terminalId);
|
||||
if (existing === undefined) tracesByTerminal.set(terminalId, [trace]);
|
||||
else existing.push(trace);
|
||||
}
|
||||
|
||||
// Insertion order is deepest-trace-first, so the round-robin visits terminals
|
||||
// in that order too and depth still leads the list.
|
||||
const limitedTraces: string[][] = [];
|
||||
for (let round = 0; limitedTraces.length < cfg.maxProcesses; round++) {
|
||||
let addedAny = false;
|
||||
for (const traces of tracesByTerminal.values()) {
|
||||
const trace = traces[round];
|
||||
if (trace === undefined) continue;
|
||||
limitedTraces.push(trace);
|
||||
addedAny = true;
|
||||
if (limitedTraces.length >= cfg.maxProcesses) break;
|
||||
}
|
||||
if (!addedAny) break;
|
||||
}
|
||||
|
||||
onProgress?.(`Creating ${limitedTraces.length} process nodes...`, 80);
|
||||
|
||||
|
|
|
|||
|
|
@ -663,3 +663,89 @@ describe('process depth (D1/D2)', () => {
|
|||
expect(deepest).toBeGreaterThan(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('process selection diversity (R2-3)', () => {
|
||||
const addFn = (graph: ReturnType<typeof createKnowledgeGraph>, id: string): void => {
|
||||
graph.addNode({
|
||||
id,
|
||||
label: 'Function',
|
||||
properties: { name: id.split(':')[1], filePath: 'src/a.ts', startLine: 1, endLine: 2 },
|
||||
});
|
||||
};
|
||||
const addCall = (
|
||||
graph: ReturnType<typeof createKnowledgeGraph>,
|
||||
from: string,
|
||||
to: string,
|
||||
): void => {
|
||||
graph.addRelationship({
|
||||
id: `rel:${from}->${to}`,
|
||||
sourceId: from,
|
||||
targetId: to,
|
||||
type: 'CALLS',
|
||||
confidence: 1,
|
||||
reason: 'test',
|
||||
});
|
||||
};
|
||||
|
||||
// The shape that crowded the reporting repo's list: many entry points whose
|
||||
// deepest chains all bottom out in the SAME utility, plus a shorter flow
|
||||
// ending somewhere of its own. Ranking on depth alone hands every slot to
|
||||
// the first group and the reader learns one thing many times.
|
||||
it('does not let one terminal take every slot', async () => {
|
||||
const graph = createKnowledgeGraph();
|
||||
|
||||
// Six entry points, each with a 5-node chain into one shared utility.
|
||||
addFn(graph, 'func:sharedUtil');
|
||||
for (let e = 1; e <= 6; e++) {
|
||||
let prev = `func:entry${e}`;
|
||||
addFn(graph, prev);
|
||||
for (let i = 1; i <= 3; i++) {
|
||||
const mid = `func:e${e}_m${i}`;
|
||||
addFn(graph, mid);
|
||||
addCall(graph, prev, mid);
|
||||
prev = mid;
|
||||
}
|
||||
addCall(graph, prev, 'func:sharedUtil');
|
||||
}
|
||||
|
||||
// One shorter, distinct flow — the "business flow" analogue.
|
||||
addFn(graph, 'func:ownEntry');
|
||||
addFn(graph, 'func:ownMid');
|
||||
addFn(graph, 'func:ownTerminal');
|
||||
addCall(graph, 'func:ownEntry', 'func:ownMid');
|
||||
addCall(graph, 'func:ownMid', 'func:ownTerminal');
|
||||
|
||||
const result = await processProcesses(graph, [], undefined, { maxProcesses: 4 });
|
||||
const terminals = result.processes.map((p) => p.terminalId);
|
||||
const sharedCount = terminals.filter((t) => t === 'func:sharedUtil').length;
|
||||
|
||||
// Under depth-only ranking every one of the four slots goes to a
|
||||
// five-node chain ending in sharedUtil.
|
||||
expect(sharedCount).toBeLessThan(terminals.length);
|
||||
expect(new Set(terminals).size).toBeGreaterThan(1);
|
||||
});
|
||||
|
||||
it('keeps a shorter flow with its own terminal rather than a fifth duplicate', async () => {
|
||||
const graph = createKnowledgeGraph();
|
||||
addFn(graph, 'func:sharedUtil');
|
||||
for (let e = 1; e <= 6; e++) {
|
||||
let prev = `func:entry${e}`;
|
||||
addFn(graph, prev);
|
||||
for (let i = 1; i <= 3; i++) {
|
||||
const mid = `func:e${e}_m${i}`;
|
||||
addFn(graph, mid);
|
||||
addCall(graph, prev, mid);
|
||||
prev = mid;
|
||||
}
|
||||
addCall(graph, prev, 'func:sharedUtil');
|
||||
}
|
||||
addFn(graph, 'func:ownEntry');
|
||||
addFn(graph, 'func:ownMid');
|
||||
addFn(graph, 'func:ownTerminal');
|
||||
addCall(graph, 'func:ownEntry', 'func:ownMid');
|
||||
addCall(graph, 'func:ownMid', 'func:ownTerminal');
|
||||
|
||||
const result = await processProcesses(graph, [], undefined, { maxProcesses: 4 });
|
||||
expect(result.processes.map((p) => p.terminalId)).toContain('func:ownTerminal');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue