MCP tool results used Markdown ATX headings (##/###/####) for section headers — the status summary, each search hit, every file section in an exploration — which Markdown-rendering clients (e.g. the Claude Code VSCode extension) blow up to H1–H4 font size, filling the transcript with oversized lines (worst on search/explore, where the noise scales with result count). Swap them all for bold labels, which render at body size while keeping the same structure. CLI/TTY output (ContextBuilder) is unchanged — the issue notes it's fine. The format is parse-coupled, so kept in sync: - The explore truncation boundary and the offload chunker (reasoning/reasoner.ts) both key off the per-file header, now a unique `**`-prefixed marker emitted via a shared fileSectionHeader() helper. - Updated the offload strip regexes and switched the opt-in report-style prompt off ATX headings (same client, same rendering issue). - Updated test helpers (sectionFor, sourcedFiles, the callers section-boundary scan) that scanned the old markers. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
ace8d8a0d0
commit
3e1547bbe1
@@ -35,15 +35,16 @@ import CodeGraph from '../src/index';
|
||||
// (the steer-to-explore phrasing changed when the Read invitation was removed).
|
||||
const SKELETON_MARK = '· skeleton (signatures only';
|
||||
|
||||
/** Return the `#### <path> ...` section for a file basename, header through the
|
||||
* line before the next `###`/`####` header (or end of output). */
|
||||
/** Return the ``**`<path>`** ...`` section for a file basename, header through the
|
||||
* line before the next bold header (or end of output). Headers are bold labels,
|
||||
* not ATX headings (issue #778); file sections start with ``**` ``. */
|
||||
function sectionFor(text: string, basename: string): string {
|
||||
const lines = text.split('\n');
|
||||
const start = lines.findIndex((l) => l.startsWith('#### ') && l.includes(basename));
|
||||
const start = lines.findIndex((l) => l.startsWith('**`') && l.includes(basename));
|
||||
if (start < 0) return '';
|
||||
let end = lines.length;
|
||||
for (let i = start + 1; i < lines.length; i++) {
|
||||
if (lines[i].startsWith('### ') || lines[i].startsWith('#### ')) {
|
||||
if (lines[i].startsWith('**')) {
|
||||
end = i;
|
||||
break;
|
||||
}
|
||||
@@ -284,7 +285,7 @@ export class YamlCodec extends Codec {
|
||||
const text = result.content?.[0]?.text ?? '';
|
||||
|
||||
// Precondition: the spine must have formed, or nothing skeletonizes.
|
||||
expect(text).toContain('## Flow (call path among the symbols you queried)');
|
||||
expect(text).toContain('**Flow (call path among the symbols you queried)');
|
||||
|
||||
for (const [file, marker] of [
|
||||
['bridge-interceptor.ts', 'BRIDGE_BODY_MARKER'],
|
||||
@@ -345,7 +346,7 @@ export class YamlCodec extends Codec {
|
||||
it('spares an off-spine sibling when the agent NAMED a callable in it (RealCall fix)', async () => {
|
||||
const result = await handler.execute('codegraph_explore', { query: SPARE_QUERY, maxFiles: 15 });
|
||||
const text = result.content?.[0]?.text ?? '';
|
||||
expect(text).toContain('## Flow (call path among the symbols you queried)');
|
||||
expect(text).toContain('**Flow (call path among the symbols you queried)');
|
||||
|
||||
// auth-interceptor.ts is an off-spine Interceptor sibling — would skeletonize —
|
||||
// but the agent named its method `authenticate`, so it stays FULL.
|
||||
|
||||
@@ -184,7 +184,7 @@ describe('codegraph_explore — dynamic boundaries', () => {
|
||||
const res = await handler.execute('codegraph_explore', { query: 'routeSave onSave' });
|
||||
const text = res.content[0].text as string;
|
||||
|
||||
expect(text).toContain('## Dynamic boundaries');
|
||||
expect(text).toContain('**Dynamic boundaries');
|
||||
expect(text).toContain('computed member call');
|
||||
expect(text).toMatch(/router\.ts:6/); // the exact dispatch site
|
||||
expect(text).toContain('candidates for key `save`');
|
||||
@@ -212,7 +212,7 @@ describe('codegraph_explore — dynamic boundaries', () => {
|
||||
const res = await handler.execute('codegraph_explore', { query: 'route onSave' });
|
||||
const text = res.content[0].text as string;
|
||||
|
||||
expect(text).toContain('## Dynamic boundaries');
|
||||
expect(text).toContain('**Dynamic boundaries');
|
||||
expect(text).toContain('computed member call');
|
||||
expect(text).not.toContain('candidates for key'); // runtime key → no shortlist to claim
|
||||
});
|
||||
@@ -234,7 +234,7 @@ describe('codegraph_explore — dynamic boundaries', () => {
|
||||
// `processPayment` does not exist anywhere — only `route` resolves.
|
||||
const res = await handler.execute('codegraph_explore', { query: 'route processPayment' });
|
||||
const text = res.content[0].text as string;
|
||||
expect(text).toContain('## Dynamic boundaries');
|
||||
expect(text).toContain('**Dynamic boundaries');
|
||||
});
|
||||
|
||||
it('renders a direct synthesized emit→handler hop as a dynamic-dispatch link (#687 criterion 1)', async () => {
|
||||
@@ -267,11 +267,11 @@ describe('codegraph_explore — dynamic boundaries', () => {
|
||||
const res = await handler.execute('codegraph_explore', { query: 'completeCheckout settleInvoice' });
|
||||
const text = res.content[0].text as string;
|
||||
|
||||
expect(text).toContain('## Dynamic-dispatch links among your symbols');
|
||||
expect(text).toContain('**Dynamic-dispatch links among your symbols');
|
||||
expect(text).toMatch(/completeCheckout → settleInvoice/);
|
||||
expect(text).toContain('invoice.settled');
|
||||
// Connected via the synthesized edge — no boundary to announce.
|
||||
expect(text).not.toContain('## Dynamic boundaries');
|
||||
expect(text).not.toContain('**Dynamic boundaries');
|
||||
});
|
||||
|
||||
it('never adds the section to a fully connected flow', async () => {
|
||||
@@ -285,8 +285,8 @@ describe('codegraph_explore — dynamic boundaries', () => {
|
||||
|
||||
const res = await handler.execute('codegraph_explore', { query: 'stepOne stepThree' });
|
||||
const text = res.content[0].text as string;
|
||||
expect(text).toContain('## Flow');
|
||||
expect(text).not.toContain('## Dynamic boundaries');
|
||||
expect(text).toContain('**Flow');
|
||||
expect(text).not.toContain('**Dynamic boundaries');
|
||||
});
|
||||
|
||||
it('python getattr dispatch surfaces with a prefix-key candidate', async () => {
|
||||
@@ -305,7 +305,7 @@ describe('codegraph_explore — dynamic boundaries', () => {
|
||||
const res = await handler.execute('codegraph_explore', { query: 'process handle_save' });
|
||||
const text = res.content[0].text as string;
|
||||
|
||||
expect(text).toContain('## Dynamic boundaries');
|
||||
expect(text).toContain('**Dynamic boundaries');
|
||||
expect(text).toContain('getattr');
|
||||
expect(text).toContain('handle_save');
|
||||
});
|
||||
@@ -373,7 +373,7 @@ describe('codegraph_explore — interface dispatch', () => {
|
||||
const res = await handler.execute('codegraph_explore', { query: 'processRunExecutionData executeNode execute' });
|
||||
const text = res.content[0].text as string;
|
||||
|
||||
expect(text).toContain('## Interface dispatch (a named method has many implementations)');
|
||||
expect(text).toContain('**Interface dispatch (a named method has many implementations)');
|
||||
expect(text).toMatch(/`execute` → runtime dispatch to \*\*9\*\* types implementing `INodeType`/);
|
||||
// a couple of concrete targets, with file:line
|
||||
expect(text).toMatch(/\b\w+Node\.execute` \(/);
|
||||
@@ -392,8 +392,8 @@ describe('codegraph_explore — interface dispatch', () => {
|
||||
|
||||
const res = await handler.execute('codegraph_explore', { query: 'stepOne stepThree' });
|
||||
const text = res.content[0].text as string;
|
||||
expect(text).toContain('## Flow');
|
||||
expect(text).not.toContain('## Interface dispatch');
|
||||
expect(text).toContain('**Flow');
|
||||
expect(text).not.toContain('**Interface dispatch');
|
||||
});
|
||||
|
||||
it('stays SILENT when the interface family is below the polymorphism threshold (3 impls)', async () => {
|
||||
@@ -401,6 +401,6 @@ describe('codegraph_explore — interface dispatch', () => {
|
||||
|
||||
const res = await handler.execute('codegraph_explore', { query: 'processRunExecutionData executeNode execute' });
|
||||
const text = res.content[0].text as string;
|
||||
expect(text).not.toContain('## Interface dispatch');
|
||||
expect(text).not.toContain('**Interface dispatch');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -55,7 +55,7 @@ describe('codegraph_explore — blast radius', () => {
|
||||
const res = await handler.execute('codegraph_explore', { query: 'target' });
|
||||
const text = res.content[0].text;
|
||||
|
||||
expect(text).toContain('### Blast radius');
|
||||
expect(text).toContain('**Blast radius');
|
||||
expect(text).toContain('`target`');
|
||||
expect(text).toMatch(/caller/); // a caller count is reported
|
||||
// It names WHERE (the caller file) — not the caller's source body.
|
||||
|
||||
@@ -28,11 +28,12 @@ import * as os from 'os';
|
||||
import CodeGraph from '../src/index';
|
||||
import { ToolHandler } from '../src/mcp/tools';
|
||||
|
||||
/** Paths that explore rendered as full-body `#### <path> —` source sections. */
|
||||
/** Paths that explore rendered as full-body ``**`<path>`** —`` source sections.
|
||||
* Headers are bold labels, not ATX headings (issue #778). */
|
||||
function sourcedFiles(text: string): string[] {
|
||||
const out: string[] = [];
|
||||
for (const line of text.split('\n')) {
|
||||
const m = line.match(/^#### (.+?) —/);
|
||||
const m = line.match(/^\*\*`(.+?)`\*\* —/);
|
||||
if (m) out.push(m[1].trim());
|
||||
}
|
||||
return out;
|
||||
|
||||
@@ -206,8 +206,8 @@ describe('codegraph_explore output respects the adaptive budget', () => {
|
||||
const text = result.content?.[0]?.text ?? '';
|
||||
// Either there are relationships, or no edges were significant — both are fine.
|
||||
// We just want to confirm we did not accidentally gate it off.
|
||||
const hasRelationships = text.includes('### Relationships');
|
||||
const sourceFollowsHeader = text.indexOf('### Source Code') > 0;
|
||||
const hasRelationships = text.includes('**Relationships');
|
||||
const sourceFollowsHeader = text.indexOf('**Source Code') > 0;
|
||||
expect(hasRelationships || sourceFollowsHeader).toBe(true);
|
||||
});
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
* "### Relationships" section would have caught it, but that is disabled below 500 files.
|
||||
* Net: on a small RTK app the synthesized edge existed in the graph yet was invisible to
|
||||
* the agent. The fix feeds a `dynNamed` set (named non-callable endpoints that participate
|
||||
* in a heuristic edge) to the tier-independent "## Dynamic-dispatch links" scan. This test
|
||||
* in a heuristic edge) to the tier-independent "**Dynamic-dispatch links**" scan. This test
|
||||
* pins it on a deliberately tiny (<150-file) fixture so the Relationships gate is OFF and
|
||||
* the dynamic-dispatch-links path is the ONLY thing that can surface the hop.
|
||||
*/
|
||||
@@ -77,7 +77,7 @@ export const outerThunk = createAsyncThunk('app/outer', async (n: number, { disp
|
||||
|
||||
// The synthesized hop now surfaces (was invisible: both endpoints `constant` AND the
|
||||
// small-repo Relationships section is off).
|
||||
expect(text).toContain('## Dynamic-dispatch links among your symbols');
|
||||
expect(text).toContain('**Dynamic-dispatch links among your symbols');
|
||||
expect(text).toMatch(/outerThunk\s+→\s+innerThunk/);
|
||||
// It reads as a dynamic-dispatch bridge with its wiring site, not a bare `calls`.
|
||||
expect(text).toMatch(/dynamic: redux thunk @/);
|
||||
|
||||
@@ -175,7 +175,7 @@ describe('MCP staleness banner', () => {
|
||||
|
||||
const res = await handler.execute('codegraph_status', {});
|
||||
const text = res.content[0].text;
|
||||
expect(text).toContain('### Pending sync:');
|
||||
expect(text).toContain('**Pending sync:');
|
||||
expect(text).toContain('src/charlie-only.ts');
|
||||
// Status embeds the info first-class, so the auto-banner is suppressed.
|
||||
expect(text.startsWith('⚠️')).toBe(false);
|
||||
@@ -204,7 +204,7 @@ describe('MCP staleness banner', () => {
|
||||
|
||||
const res = await handler.execute('codegraph_status', {});
|
||||
const text = res.content[0].text;
|
||||
expect(text).toContain('### Auto-sync disabled:');
|
||||
expect(text).toContain('**Auto-sync disabled:');
|
||||
expect(text).toContain('OS watch/file limit exhausted');
|
||||
// status renders the notice inline, so the auto-banner is not also prepended.
|
||||
expect(text.startsWith('⚠️')).toBe(false);
|
||||
|
||||
@@ -99,7 +99,7 @@ describe('codegraph_node file-view (Read replacement)', () => {
|
||||
|
||||
it('symbolsOnly returns the structural map, not the source', async () => {
|
||||
const out = await text({ file: 'a.ts', symbolsOnly: true });
|
||||
expect(out).toContain('### Symbols');
|
||||
expect(out).toContain('**Symbols');
|
||||
expect(out).toContain('helper');
|
||||
expect(out).toContain('Widget');
|
||||
expect(out).not.toContain('return x + 1'); // bodies are NOT included in the map
|
||||
|
||||
@@ -231,16 +231,16 @@ describe('reasoning offload', () => {
|
||||
describe('stripAgentDirectives', () => {
|
||||
it('drops the agent-directed header but keeps source sections', () => {
|
||||
const ctx = [
|
||||
'## Exploration: how does X work',
|
||||
'**Exploration: how does X work**',
|
||||
'Found 12 symbols across 3 files.',
|
||||
'',
|
||||
'#### src/a.ts — foo(function)',
|
||||
'**`src/a.ts`** — foo(function)',
|
||||
'code body',
|
||||
].join('\n');
|
||||
const stripped = stripAgentDirectives(ctx);
|
||||
expect(stripped).not.toContain('## Exploration:');
|
||||
expect(stripped).not.toContain('**Exploration:');
|
||||
expect(stripped).not.toContain('Found 12 symbols');
|
||||
expect(stripped).toContain('#### src/a.ts');
|
||||
expect(stripped).toContain('**`src/a.ts`');
|
||||
expect(stripped).toContain('code body');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -99,7 +99,10 @@ describe('same-named symbols across apps (#764)', () => {
|
||||
expect(out).toContain('apps/billing/src/users/user.service.ts');
|
||||
// …and the billing section must list the billing controller, not admin's.
|
||||
const billingSection = out.slice(out.indexOf('apps/billing/src/users/user.service.ts'));
|
||||
const billingBody = billingSection.slice(0, billingSection.indexOf('###', 3) > 0 ? billingSection.indexOf('###', 3) : undefined);
|
||||
// The next definition heading is a line-start bold label (issue #778: ATX `###`
|
||||
// headings became `**…**`); billingSection starts mid-heading, so `\n**` finds it.
|
||||
const nextDef = billingSection.indexOf('\n**');
|
||||
const billingBody = billingSection.slice(0, nextDef > 0 ? nextDef : undefined);
|
||||
expect(billingBody).toContain('apps/billing/src/users/user.controller.ts');
|
||||
expect(billingBody).not.toContain('apps/admin/src/users/user.controller.ts');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user