From 8c9d58b47f71c2b69ecc934079fbe05af4ea4464 Mon Sep 17 00:00:00 2001 From: Vladimir Babin Date: Tue, 22 Sep 2026 19:53:45 +0300 Subject: [PATCH 1/2] fix(graph): admit impact grounding records before callers and source --- CHANGELOG.md | 1 + src/graph/cli-agent.ts | 21 +++++++++++---------- test/graph-cli-agent.test.ts | 35 +++++++++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e4a888b..08cd734d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ All notable changes to this project will be documented in this file. - Project Hub Overview now opens with a compact Context card above the atlas instead of a header button. It links to Context when the Wiki index is fresh and to Health otherwise, so a stale or unavailable index no longer leads to a page that cannot load. ### Fixed +- `mex impact` no longer drops grounding records first when the output budget runs out. They are admitted right after the target's definitions, ahead of transitive callers and source ranges, so a target with many callers still returns its knowledge links at the default budget; the response is still marked `truncated` when callers are cut (#225). - `mex graph status` no longer prints placeholder zeros as measurements when immutable inspection is skipped. While a stranded `graph.db-wal`, an unreadable sidecar, a containment failure or a failed invariant audit blocks the read, the text output now says `Last successful index: not inspected`, `Sources: not inspected` and `Parse health: not inspected` instead of `never` and `0 ok`, and `--json` carries an additive `inspected: false` so a consumer can tell an uninspected store from one that genuinely parsed nothing. The same applies when a `GRAPH_SNAPSHOT_CONTENT_MISMATCH` stops the source comparison before the index timestamps are read, and to the `mex check` fallback status when the loader itself throws. `parseHealth` and `changes` keep their shape (#204). - Agent population failures retain the real copyable manual prompt for retry or manual continuation. Integration pointer notes are visible as non-blocking guidance. - Setup and Overview share the computer's contact preference so completing or skipping the invitation does not immediately trigger another request. diff --git a/src/graph/cli-agent.ts b/src/graph/cli-agent.ts index 40f6051b..1c277392 100644 --- a/src/graph/cli-agent.ts +++ b/src/graph/cli-agent.ts @@ -132,6 +132,17 @@ export function runImpact( } } const ordered = [...impacted.values()].sort((a, b) => a.depth - b.depth || a.node.id.localeCompare(b.node.id)); + + // Grounding is keyed by node, so an excluded node must not reappear + // through it. Nodes omitted only by the returned-node cap still count. + const affectedIds = [...new Set([...roots.map((node) => node.id), ...impacted.keys()])] + .filter((id) => !isDriftedFile(session, session.graph.getNode(id)?.filePath)); + const groundingRecords: Rec[] = []; + for (const grounding of groundedFiles(session.db, affectedIds)) { + const record: Rec = { type: "grounding", node: grounding.node_id, file: grounding.scaffold_file }; + if (ledger.tryAdd(record)) groundingRecords.push(record); else truncated = true; + } + for (const entry of ordered) { if (emittedNodes.length >= opts.maxNodes) { truncated = true; break; } const fact = factFor(session, entry.node.id, opts.detail, opts.fingerprint); @@ -147,16 +158,6 @@ export function runImpact( const sourceRecords = planSource(session, ledger, emittedNodes, rootDir, opts); - // Grounding is keyed by node, so an excluded node must not reappear - // through it. Nodes omitted only by the returned-node cap still count. - const affectedIds = [...new Set([...roots.map((node) => node.id), ...impacted.keys()])] - .filter((id) => !isDriftedFile(session, session.graph.getNode(id)?.filePath)); - const groundingRecords: Rec[] = []; - for (const grounding of groundedFiles(session.db, affectedIds)) { - const record: Rec = { type: "grounding", node: grounding.node_id, file: grounding.scaffold_file }; - if (ledger.tryAdd(record)) groundingRecords.push(record); else truncated = true; - } - emitAll(write, meta, [ ...configDriftRecords(session), ...headRecords, ...factRecords, ...sourceRecords, ...groundingRecords, diff --git a/test/graph-cli-agent.test.ts b/test/graph-cli-agent.test.ts index 2a6918f4..f94d915d 100644 --- a/test/graph-cli-agent.test.ts +++ b/test/graph-cli-agent.test.ts @@ -78,6 +78,41 @@ describe("agent graph commands", () => { expect(fixture.close).toHaveBeenCalledOnce(); }); + it("impact keeps grounding records when callers exhaust the default budget", () => { + const leaf = node("function:leaf", "leaf"); + const callers = Array.from({ length: 60 }, (_, i) => node(`function:caller${i}`, `caller${i}`, `src/caller${i}.ts`, 4)); + const nodes = [leaf, ...callers]; + const graph: GraphEngine = { + build: vi.fn(), sync: vi.fn(), close: vi.fn(), + getNode: (id) => nodes.find((entry) => entry.id === id) ?? null, + searchNodes: (query) => nodes.filter((entry) => entry.name.toLowerCase() === query.toLowerCase()), + getCallers: (id) => id === leaf.id ? callers : [], + getCallees: () => [], + getIncoming: () => [], + getOutgoing: () => [], + getIndexedFiles: () => [], + }; + const groundings = Array.from({ length: 6 }, (_, i) => ({ + scaffold_file: `.mex/context/architecture-${i}.md`, node_id: i < 3 ? leaf.id : callers[i].id, + })); + const db = { + prepare: (sql: string) => ({ + run: vi.fn(), get: vi.fn(), iterate: vi.fn(), + all: () => sql.includes("FROM nodes") ? [] : groundings, + }), + exec: vi.fn(), pragma: vi.fn(), transaction: (fn: () => T) => fn(), close: vi.fn(), open: true, + }; + const output: string[] = []; + runImpact("leaf", "/repo", { open: () => ({ graph, db, close: vi.fn() }), write: (line) => output.push(line) }, { maxNodes: 100 }); + const rows = output.map((line) => JSON.parse(line)); + expect(rows.at(-1)).toMatchObject({ type: "summary", truncated: true }); + expect(rows.filter((row) => row.type === "caller").length).toBeLessThan(callers.length); + expect(rows.filter((row) => row.type === "grounding")).toEqual( + groundings.map((g) => ({ type: "grounding", node: g.node_id, file: g.scaffold_file })), + ); + expect(rows.findIndex((row) => row.type === "grounding")).toBeLessThan(rows.findIndex((row) => row.type === "summary")); + }); + it("impact accepts a file and reports each node it defines", () => { const fixture = deps(); runImpact("src/a.ts", "/repo", fixture.deps); From b470372c5813f9add8872e63b9915058fcb2f4ea Mon Sep 17 00:00:00 2001 From: Yashasvi Date: Thu, 24 Sep 2026 09:15:54 +0530 Subject: [PATCH 2/2] test(graph): cover impact at a budget where even grounding cannot fit Pin the other half of #225: at the smallest accepted output budget the impact response carries no grounding records, stays within budget, and is still marked truncated. Record why grounding is admitted before callers and source, and name the accepted cost in the CHANGELOG. --- CHANGELOG.md | 2 +- src/graph/cli-agent.ts | 3 +++ test/graph-cli-agent.test.ts | 16 ++++++++++++++++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 08cd734d..985430a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,7 +24,7 @@ All notable changes to this project will be documented in this file. - Project Hub Overview now opens with a compact Context card above the atlas instead of a header button. It links to Context when the Wiki index is fresh and to Health otherwise, so a stale or unavailable index no longer leads to a page that cannot load. ### Fixed -- `mex impact` no longer drops grounding records first when the output budget runs out. They are admitted right after the target's definitions, ahead of transitive callers and source ranges, so a target with many callers still returns its knowledge links at the default budget; the response is still marked `truncated` when callers are cut (#225). +- `mex impact` no longer drops grounding records first when the output budget runs out. They are admitted right after the target's definitions, ahead of transitive callers and source ranges, so a target with many callers still returns its knowledge links at the default budget; the response is still marked `truncated` when callers are cut. The accepted cost: on a target with many groundings, a few caller or source records that used to fit now give way to them (#225). - `mex graph status` no longer prints placeholder zeros as measurements when immutable inspection is skipped. While a stranded `graph.db-wal`, an unreadable sidecar, a containment failure or a failed invariant audit blocks the read, the text output now says `Last successful index: not inspected`, `Sources: not inspected` and `Parse health: not inspected` instead of `never` and `0 ok`, and `--json` carries an additive `inspected: false` so a consumer can tell an uninspected store from one that genuinely parsed nothing. The same applies when a `GRAPH_SNAPSHOT_CONTENT_MISMATCH` stops the source comparison before the index timestamps are read, and to the `mex check` fallback status when the loader itself throws. `parseHealth` and `changes` keep their shape (#204). - Agent population failures retain the real copyable manual prompt for retry or manual continuation. Integration pointer notes are visible as non-blocking guidance. - Setup and Overview share the computer's contact preference so completing or skipping the invitation does not immediately trigger another request. diff --git a/src/graph/cli-agent.ts b/src/graph/cli-agent.ts index 1c277392..c56d4005 100644 --- a/src/graph/cli-agent.ts +++ b/src/graph/cli-agent.ts @@ -137,6 +137,9 @@ export function runImpact( // through it. Nodes omitted only by the returned-node cap still count. const affectedIds = [...new Set([...roots.map((node) => node.id), ...impacted.keys()])] .filter((id) => !isDriftedFile(session, session.graph.getNode(id)?.filePath)); + // Knowledge links are admitted before callers and source (#225): they are + // tiny and no other command returns them, so budget pressure cuts the + // callers other commands can reproduce instead. Output order is unchanged. const groundingRecords: Rec[] = []; for (const grounding of groundedFiles(session.db, affectedIds)) { const record: Rec = { type: "grounding", node: grounding.node_id, file: grounding.scaffold_file }; diff --git a/test/graph-cli-agent.test.ts b/test/graph-cli-agent.test.ts index f94d915d..9a26e95b 100644 --- a/test/graph-cli-agent.test.ts +++ b/test/graph-cli-agent.test.ts @@ -113,6 +113,22 @@ describe("agent graph commands", () => { expect(rows.findIndex((row) => row.type === "grounding")).toBeLessThan(rows.findIndex((row) => row.type === "summary")); }); + it("impact stays well-formed and truncated when even grounding cannot fit", () => { + // The smallest accepted budget holds the protocol framing and nothing else. + const probe = deps(); + runImpact("leaf", "/repo", probe.deps, { maxOutputTokens: 1 }); + const floor = Number(/use at least (\d+)/.exec(JSON.parse(probe.output[0]!).message)?.[1]); + expect(floor).toBeGreaterThan(1); + + const fixture = deps(); + runImpact("leaf", "/repo", fixture.deps, { maxOutputTokens: floor }); + const rows = fixture.output.map((line) => JSON.parse(line)); + expect(rows[0]).toMatchObject({ type: "meta" }); + expect(rows.at(-1)).toMatchObject({ type: "summary", truncated: true }); + expect(rows.at(-1).estimatedOutputTokens).toBeLessThanOrEqual(floor); + expect(rows.filter((row) => row.type === "grounding")).toEqual([]); + }); + it("impact accepts a file and reports each node it defines", () => { const fixture = deps(); runImpact("src/a.ts", "/repo", fixture.deps);