Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions benchmarks/reports/report.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@

- Cases: 7
- Signal matches: 21/26
- Average reduction: 59.3%
- Average total MCP ratio: 40.7%
- Average reduction: 56.9%
- Average total MCP ratio: 43.1%

| Case | Raw KB | Brief KB | Evidence KB | Total MCP KB | Reduction | Tool Calls | Confidence | Signal Hit | File Hit | Code Hit |
|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|
| dipper-react-module-not-found | 2.1 | 1.4 | 0.0 | 1.4 | 33.2% | 1 | 0.90 | 2/3 | 1/1 | n/a |
| filament-tailwind-vite-build-failure | 4.5 | 1.9 | 0.0 | 1.9 | 57.3% | 1 | 0.90 | 3/3 | 1/1 | n/a |
| frappe-hrms-vite-pwa-build-failure | 5.1 | 2.0 | 0.0 | 2.0 | 60.3% | 1 | 0.80 | 4/4 | 1/1 | n/a |
| react-scan-next-build-failure | 5.0 | 1.4 | 0.0 | 1.4 | 73.0% | 1 | 0.90 | 0/4 | 2/2 | n/a |
| satellite-js-vite-build-failure | 13.1 | 3.4 | 0.0 | 3.4 | 73.7% | 1 | 0.90 | 4/4 | 1/1 | 1/1 |
| svelte-vite-bindable-build-failure | 1.1 | 0.6 | 0.0 | 0.6 | 47.7% | 1 | 0.80 | 4/4 | 1/1 | 1/1 |
| tanstack-vite-devtools-build-failure | 4.9 | 1.5 | 0.0 | 1.5 | 70.0% | 1 | 0.90 | 4/4 | 1/1 | n/a |
| dipper-react-module-not-found | 2.1 | 1.6 | 0.0 | 1.6 | 22.8% | 1 | 0.90 | 2/3 | 1/1 | n/a |
| filament-tailwind-vite-build-failure | 4.5 | 1.9 | 0.0 | 1.9 | 58.8% | 1 | 0.80 | 3/3 | 1/1 | n/a |
| frappe-hrms-vite-pwa-build-failure | 5.1 | 2.1 | 0.0 | 2.1 | 59.0% | 1 | 0.80 | 4/4 | 1/1 | n/a |
| react-scan-next-build-failure | 5.0 | 1.2 | 0.0 | 1.2 | 75.4% | 1 | 0.80 | 0/4 | 2/2 | n/a |
| satellite-js-vite-build-failure | 13.1 | 3.5 | 0.0 | 3.5 | 73.0% | 1 | 0.80 | 4/4 | 1/1 | 1/1 |
| svelte-vite-bindable-build-failure | 1.1 | 0.7 | 0.0 | 0.7 | 39.8% | 1 | 0.80 | 4/4 | 1/1 | 1/1 |
| tanstack-vite-devtools-build-failure | 4.9 | 1.5 | 0.0 | 1.5 | 69.6% | 1 | 0.80 | 4/4 | 1/1 | n/a |
89 changes: 55 additions & 34 deletions packages/core/src/analysis/aggregate.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,29 @@
import {
type FailureEvidence,
formatEvidenceSummary,
hasFailureEvidenceData,
} from '../diagnosis/evidence.js';
import { rankEvidence, reduceEvidence } from '../diagnosis/rank.js';
import type { CoreAnalysis, CoreAnalysisInput } from '../types/core.js';
import type { PluginResult } from '../types/plugin.js';
import { firstNonEmptyLine, uniqueNonEmptyStrings } from '../utils/text.js';

function pickLeadResult(
function collectEvidence(
pluginResults: PluginResult<unknown, unknown>[],
): PluginResult<unknown, unknown> | undefined {
return (
pluginResults.find(
(result) => result.matched && result.plugin !== 'builtin-generic',
) ?? pluginResults.find((result) => result.matched)
);
): FailureEvidence[] {
return pluginResults
.filter((result) => result.matched && hasFailureEvidenceData(result.data))
.flatMap((result) =>
hasFailureEvidenceData(result.data) ? result.data.evidences : [],
);
}

function buildSummary(
input: CoreAnalysisInput,
leadResult: PluginResult<unknown, unknown> | undefined,
leadEvidence: FailureEvidence | undefined,
): string {
return (
leadResult?.summary ??
(leadEvidence ? formatEvidenceSummary(leadEvidence) : undefined) ??
firstNonEmptyLine(input.signals.snippet) ??
firstNonEmptyLine(input.capture.stderr) ??
firstNonEmptyLine(input.capture.stdout) ??
Expand All @@ -27,12 +33,12 @@ function buildSummary(

function buildLikelyCauses(
input: CoreAnalysisInput,
pluginResults: PluginResult<unknown, unknown>[],
rankedEvidence: FailureEvidence[],
): string[] {
const causes = pluginResults
.filter((result) => result.matched)
.map((result) => result.summary ?? '')
.filter(Boolean);
const causes = rankedEvidence
.filter((evidence) => evidence.source !== 'generic')
.slice(0, 3)
.map(formatEvidenceSummary);

if (input.signals.keywords.length > 0) {
causes.push(
Expand All @@ -51,42 +57,57 @@ function buildLikelyCauses(

function buildNextSteps(
input: CoreAnalysisInput,
pluginResults: PluginResult<unknown, unknown>[],
rankedEvidence: FailureEvidence[],
): string[] {
const pluginSuggestions = pluginResults.flatMap(
(result) => result.suggestions ?? [],
);

const leadEvidence = rankedEvidence[0];
const firstFile = leadEvidence?.file ?? input.signals.relatedFiles[0];
const genericSteps = [
input.signals.relatedFiles[0]
? `Inspect ${input.signals.relatedFiles[0]} first.`
firstFile
? `Inspect ${firstFile} first.`
: 'Inspect the first high-signal error line in stderr.',
input.signals.stackLines.length > 0
? 'Trace the top stack frame back to the application code path.'
: 'Re-run the command with more verbose logging if the failure is still ambiguous.',
];

return uniqueNonEmptyStrings([...pluginSuggestions, ...genericSteps]).slice(
0,
6,
);
if (leadEvidence?.source === 'dependency') {
genericSteps.push(
'Check dependency installation, package exports, module format, and workspace aliases before chasing framework stack frames.',
);
} else if (leadEvidence?.source === 'runtime') {
genericSteps.push(
'Start from the first application component frame rather than framework internals.',
);
} else if (leadEvidence?.source === 'compiler') {
genericSteps.push(
'Fix the earliest precise compiler diagnostic before chasing later cascade errors.',
);
} else if (input.signals.stackLines.length > 0) {
genericSteps.push(
'Trace the top stack frame back to the application code path.',
);
}

return uniqueNonEmptyStrings(genericSteps).slice(0, 4);
}

export function aggregateCoreAnalysis(
input: CoreAnalysisInput,
pluginResults: PluginResult<unknown, unknown>[],
): CoreAnalysis {
const leadResult = pickLeadResult(pluginResults);
const rankedEvidence = rankEvidence(
reduceEvidence(collectEvidence(pluginResults)),
);
const leadEvidence = rankedEvidence[0];

return {
host: input.capture.host,
summary: buildSummary(input, leadResult),
keySnippet: leadResult?.keySnippet ?? input.signals.snippet,
likelyCauses: buildLikelyCauses(input, pluginResults),
nextSteps: buildNextSteps(input, pluginResults),
summary: buildSummary(input, leadEvidence),
keySnippet: leadEvidence?.snippet ?? input.signals.snippet,
likelyCauses: buildLikelyCauses(input, rankedEvidence),
nextSteps: buildNextSteps(input, rankedEvidence),
relatedFiles: uniqueNonEmptyStrings([
...rankedEvidence.flatMap((evidence) =>
evidence.file ? [evidence.file] : [],
),
...input.signals.relatedFiles,
...pluginResults.flatMap((result) => result.relatedFiles ?? []),
]).slice(0, 10),
pluginResults,
};
Expand Down
80 changes: 80 additions & 0 deletions packages/core/src/diagnosis/evidence.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
export type FailureEvidenceSource =
| 'compiler'
| 'runtime'
| 'dependency'
| 'generic';

export interface FailureEvidence {
id: string;
ruleId: string;
source: FailureEvidenceSource;
category: string;
framework?: string;
message: string;
file?: string;
line?: number;
column?: number;
rawLine?: number;
confidence: number;
priority: number;
snippet?: string;
}

export interface FailureEvidencePluginData {
evidences: FailureEvidence[];
}

export function hasFailureEvidenceData(
data: unknown,
): data is FailureEvidencePluginData {
return (
typeof data === 'object' &&
data !== null &&
Array.isArray((data as { evidences?: unknown }).evidences)
);
}

export function formatEvidenceLocation(
evidence: Pick<FailureEvidence, 'file' | 'line' | 'column'>,
): string | undefined {
if (!evidence.file) {
return undefined;
}
if (evidence.line && evidence.column) {
return `${evidence.file}:${evidence.line}:${evidence.column}`;
}
if (evidence.line) {
return `${evidence.file}:${evidence.line}`;
}
return evidence.file;
}

export function formatEvidenceSummary(evidence: FailureEvidence): string {
const source = formatEvidenceLabel(evidence.framework ?? evidence.source);
const category = evidence.category.replaceAll('_', ' ');
const location = formatEvidenceLocation(evidence);
const prefix = location
? `${source} ${category} in ${location}`
: `${source} ${category}`;

return `${prefix}: ${evidence.message}`;
}

function formatEvidenceLabel(label: string): string {
switch (label.toLowerCase()) {
case 'typescript':
return 'TypeScript';
case 'react':
return 'React';
case 'vue':
return 'Vue';
case 'svelte':
return 'Svelte';
case 'next':
return 'Next';
case 'nuxt':
return 'Nuxt';
default:
return label;
}
}
111 changes: 111 additions & 0 deletions packages/core/src/diagnosis/rank.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import type { FailureEvidence } from './evidence.js';

function isApplicationFile(file: string | undefined): boolean {
return Boolean(file && !/node_modules|dist|build|\.next|\.nuxt/.test(file));
}

function hasPreciseLocation(evidence: FailureEvidence): boolean {
return Boolean(evidence.file && evidence.line);
}

function scoreEvidence(evidence: FailureEvidence): number {
let score = evidence.priority + evidence.confidence;

if (hasPreciseLocation(evidence)) {
score += 12;
} else if (evidence.file) {
score += 6;
}

if (isApplicationFile(evidence.file)) {
score += 8;
}

if (/\b(?:TS\d{3,5}|ERR_[A-Z_]+)\b/.test(evidence.message)) {
score += 8;
}

if (
/node_modules|webpack|rollup|vite|esbuild|tsx|tsc/i.test(
evidence.file ?? '',
)
) {
score -= 8;
}

if (evidence.category === 'build_wrapper') {
score -= 12;
}

if (evidence.source === 'generic') {
score -= 40;
}

return score;
}

function evidenceKey(evidence: FailureEvidence): string {
return [
evidence.ruleId,
evidence.file ?? '',
evidence.line ?? '',
evidence.column ?? '',
evidence.message.toLowerCase(),
].join('|');
}

function overlapKey(evidence: FailureEvidence): string {
return [
evidence.file ?? '',
evidence.line ?? '',
evidence.message.toLowerCase().slice(0, 120),
].join('|');
}

export function reduceEvidence(
evidences: FailureEvidence[],
): FailureEvidence[] {
const exact = new Map<string, FailureEvidence>();

for (const evidence of evidences) {
const key = evidenceKey(evidence);
const existing = exact.get(key);
if (!existing || scoreEvidence(evidence) > scoreEvidence(existing)) {
exact.set(key, evidence);
}
}

const overlapped = new Map<string, FailureEvidence>();
for (const evidence of exact.values()) {
const key = overlapKey(evidence);
const existing = overlapped.get(key);
if (!existing || scoreEvidence(evidence) > scoreEvidence(existing)) {
overlapped.set(key, evidence);
}
}

return [...overlapped.values()];
}

export function rankEvidence(evidences: FailureEvidence[]): FailureEvidence[] {
return [...evidences].sort((left, right) => {
const scoreDelta = scoreEvidence(right) - scoreEvidence(left);
if (scoreDelta !== 0) {
return scoreDelta;
}

const priorityDelta = right.priority - left.priority;
if (priorityDelta !== 0) {
return priorityDelta;
}

const rawLineDelta =
(left.rawLine ?? Number.MAX_SAFE_INTEGER) -
(right.rawLine ?? Number.MAX_SAFE_INTEGER);
if (rawLineDelta !== 0) {
return rawLineDelta;
}

return left.ruleId.localeCompare(right.ruleId);
});
}
9 changes: 9 additions & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,15 @@ export type {
export { aggregateCoreAnalysis } from './analysis/aggregate.js';
export { buildCoreAnalysisInput } from './analysis/build-input.js';
export { diagnoseCapture } from './analysis/diagnose.js';
export {
formatEvidenceLocation,
formatEvidenceSummary,
hasFailureEvidenceData,
type FailureEvidence,
type FailureEvidencePluginData,
type FailureEvidenceSource,
} from './diagnosis/evidence.js';
export { rankEvidence, reduceEvidence } from './diagnosis/rank.js';
export {
extractSignals,
normalizeLogs,
Expand Down
12 changes: 6 additions & 6 deletions packages/core/src/plugin/registry.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { dependencyResolutionPlugin } from '../plugins/dependency-resolution.js';
import { frontendCompilerPlugin } from '../plugins/frontend-compiler.js';
import { frontendRuntimePlugin } from '../plugins/frontend-runtime.js';
import { genericPlugin } from '../plugins/generic.js';
import { reactPlugin } from '../plugins/react.js';
import { typescriptPlugin } from '../plugins/typescript.js';
import { vuePlugin } from '../plugins/vue.js';
import type { Error2FixPlugin, PluginRegistry } from '../types/plugin.js';

export function createPluginRegistry(
Expand All @@ -19,9 +19,9 @@ export function registerPlugin(

export function getDefaultPluginRegistry(): PluginRegistry {
return createPluginRegistry([
typescriptPlugin,
reactPlugin,
vuePlugin,
frontendCompilerPlugin,
frontendRuntimePlugin,
dependencyResolutionPlugin,
genericPlugin,
]);
}
Loading