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
6 changes: 3 additions & 3 deletions TRACKER.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
## Status

- **Current phase:** 5 — Context bundle & agent interface
- **Next step:** 5.1`resolveContext` orchestrator
- **Done:** 0.1–0.4, 1.1–1.6, 2.1–2.5, 3.1–3.5, 4.1–4.6
- **Next step:** 5.2Context-bundle contract
- **Done:** 0.1–0.4, 1.1–1.6, 2.1–2.5, 3.1–3.6, 4.1–4.6, 5.1
- **Gates passed:** Gate 0 (CI + red-path, #5/#6) · Gate 1 (precision 1.000, recall 0.895, zero poison) · Gate 2 (C1 instance attribution 1.000 · B1 4-level handler chains · C6 store writers↔readers · A9 portals — scorecard 137/0/0, precision & recall 1.000) · Gate 3 (B3 action effects · B4 routers · B6 cyclic journeys terminate · B7/B8 form & non-JSX events · G5 flag/role conditions — precision & recall 1.000) · Gate 4 (A4 rarity · A10 fuzzy/OCR · A1 structural · A6 subtree · E3 vision annotations · E2 aliases · G4 corrections — high-conf correct 1.000, ambiguity honesty 1.000, poison rate 0.000)

## What CodeRadar is
Expand Down Expand Up @@ -267,7 +267,7 @@ The heart of the project. C1 and B1 live here.

## Phase 5 — Context bundle & agent interface

### [ ] 5.1 `resolveContext` orchestrator
### [x] 5.1 `resolveContext` orchestrator
**Failure modes:** E1, E5, E6, F6(decline), E4(decline)
**Build:** `@coderadar/agent-sdk` package. `resolveContext(ticket: { text, screenshots?, links? })`:
- Entry-point classification: visual (screenshot present) / textual (UI terms in prose) / behavioral ("clicking X does nothing" → match on event/handler/journey vocabulary, E5) / out-of-domain (backend/infra/perf → `declined`, E6) / unsupported-input (video → structured decline, E4). Classification is rule-based + keyword lexicons; no LLM call inside the node (determinism, G8).
Expand Down
1 change: 1 addition & 0 deletions eval/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"typecheck": "tsc -p tsconfig.json --noEmit"
},
"dependencies": {
"@coderadar/agent-sdk": "workspace:*",
"@coderadar/core": "workspace:*",
"@coderadar/parser-react": "workspace:*",
"yaml": "^2.9.0"
Expand Down
6 changes: 6 additions & 0 deletions eval/src/golden.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,10 @@ export interface Scorecard {
ambiguityHonesty: number | null;
/** Fraction of `ok` answers that are confidently wrong (the poison rate). */
poisonRate: number | null;
/** Ticket entry-point classification accuracy (step 5.1). */
entryPointAccuracy: number | null;
/** Fraction of out-of-domain tickets correctly declined (step 5.1). */
oodRejection: number | null;
};
}

Expand All @@ -219,4 +223,6 @@ export interface Thresholds {
minHighConfidenceCorrect?: number;
minAmbiguityHonesty?: number;
maxPoisonRate?: number;
minEntryPointAccuracy?: number;
minOodRejection?: number;
}
40 changes: 40 additions & 0 deletions eval/src/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,44 @@ import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";

import { classifyTicket, type EntryPoint } from "@coderadar/agent-sdk";
import { CONFIDENCE_THRESHOLDS } from "@coderadar/core";
import { resolveHookEdges, scanReact } from "@coderadar/parser-react";
import { parse as parseYaml } from "yaml";

interface TicketCase {
id: string;
text: string;
screenshots?: number;
links?: string[];
expect: EntryPoint;
}

/** Classify the hand-written ticket suite → entry-point accuracy + OOD rejection (step 5.1). */
function runTickets(): { entryPointAccuracy: number | null; oodRejection: number | null } {
const ticketsPath = path.join(evalDir, "tickets", "tickets.json");
if (!fs.existsSync(ticketsPath)) return { entryPointAccuracy: null, oodRejection: null };
const tickets = JSON.parse(fs.readFileSync(ticketsPath, "utf-8")) as TicketCase[];
let correct = 0;
let ood = 0;
let oodRejected = 0;
const misses: string[] = [];
for (const ticket of tickets) {
const got = classifyTicket(ticket).entryPoint;
if (got === ticket.expect) correct += 1;
else misses.push(`${ticket.id}: expected ${ticket.expect}, got ${got}`);
if (ticket.expect === "out-of-domain") {
ood += 1;
if (got === "out-of-domain") oodRejected += 1;
}
}
if (misses.length > 0) console.log(`\n[tickets] misclassified: ${misses.join(" · ")}`);
return {
entryPointAccuracy: tickets.length > 0 ? round(correct / tickets.length) : null,
oodRejection: ood > 0 ? round(oodRejected / ood) : null,
};
}

import { runChecks } from "./checks.js";
import type { FixtureResult, Golden, Scorecard, Thresholds } from "./golden.js";

Expand Down Expand Up @@ -162,6 +196,7 @@ function buildScorecard(results: FixtureResult[]): Scorecard {
okAnswers.length > 0
? round(okAnswers.filter((o) => !o.correct).length / okAnswers.length)
: null,
...runTickets(),
},
};
}
Expand All @@ -185,6 +220,9 @@ function print(scorecard: Scorecard): void {
console.log(
`high-conf correct=${fmt(s.highConfidenceCorrect)} · ambiguity honesty=${fmt(s.ambiguityHonesty)} · poison rate=${fmt(s.poisonRate)}`,
);
console.log(
`ticket entry-point accuracy=${fmt(s.entryPointAccuracy)} · OOD rejection=${fmt(s.oodRejection)}`,
);
}

function gate(scorecard: Scorecard): void {
Expand All @@ -207,6 +245,8 @@ function gate(scorecard: Scorecard): void {
thresholds.minHighConfidenceCorrect,
);
checkFloor(violations, "ambiguityHonesty", s.ambiguityHonesty, thresholds.minAmbiguityHonesty);
checkFloor(violations, "entryPointAccuracy", s.entryPointAccuracy, thresholds.minEntryPointAccuracy);
checkFloor(violations, "oodRejection", s.oodRejection, thresholds.minOodRejection);
if (
thresholds.maxPoisonRate !== undefined &&
s.poisonRate !== null &&
Expand Down
4 changes: 3 additions & 1 deletion eval/thresholds.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,7 @@
"minLineageRecall": 0.95,
"minHighConfidenceCorrect": 1,
"minAmbiguityHonesty": 0.9,
"maxPoisonRate": 0.05
"maxPoisonRate": 0.05,
"minEntryPointAccuracy": 0.9,
"minOodRejection": 0.95
}
23 changes: 23 additions & 0 deletions eval/tickets/tickets.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
[
{ "id": "v1", "text": "Screenshot of the users table — the columns look wrong.", "screenshots": 1, "expect": "visual" },
{ "id": "v2", "text": "See attached; this is the invoice detail page after saving.", "screenshots": 1, "expect": "visual" },
{ "id": "v3", "text": "This dashboard card is misaligned. Grab from prod.", "screenshots": 1, "expect": "visual" },
{ "id": "v4", "text": "Attached a shot of the settings screen — the toggle is off.", "screenshots": 1, "expect": "visual" },
{ "id": "v5", "text": "Here is what the checkout page renders for me.", "screenshots": 1, "expect": "visual" },

{ "id": "t1", "text": "The 'All invoices' page shows stale totals after a refund.", "expect": "textual" },
{ "id": "t2", "text": "Fix the label on the Billing summary card — it says the wrong currency.", "expect": "textual" },
{ "id": "t3", "text": "The Team Members list is missing the avatar column.", "expect": "textual" },
{ "id": "t4", "text": "Rename 'Save draft' to 'Save' on the editor toolbar.", "expect": "textual" },

{ "id": "b1", "text": "Clicking the Save button does nothing on the profile page.", "expect": "behavioral" },
{ "id": "b2", "text": "After I submit the signup form, nothing happens — no error, no redirect.", "expect": "behavioral" },
{ "id": "b3", "text": "The Refresh button on the users page isn't working.", "expect": "behavioral" },

{ "id": "o1", "text": "The users API endpoint returns 503 under load; investigate the database connection pool.", "expect": "out-of-domain" },
{ "id": "o2", "text": "Kubernetes deployment is crash-looping after the last migration.", "expect": "out-of-domain" },
{ "id": "o3", "text": "Redis cache latency spiked and the cron worker queue is backed up.", "expect": "out-of-domain" },

{ "id": "u1", "text": "Screen recording attached showing the whole flow.", "links": ["https://files.example.com/bug-repro.mp4"], "expect": "unsupported" },
{ "id": "u2", "text": "I made a video of the checkout flow — see the recording.", "expect": "unsupported" }
]
32 changes: 32 additions & 0 deletions packages/agent-sdk/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"name": "@coderadar/agent-sdk",
"version": "0.2.0",
"private": true,
"description": "resolveContext orchestrator — classifies a ticket and runs the matching engine. Deterministic, no LLM in the node. Bundled into the published ui-lineage package.",
"license": "MIT",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"files": [
"dist"
],
"scripts": {
"build": "tsc -p tsconfig.json",
"typecheck": "tsc -p tsconfig.json --noEmit",
"test": "vitest run"
},
"dependencies": {
"@coderadar/core": "workspace:*"
},
"devDependencies": {
"@types/node": "^22.20.1",
"typescript": "^5.7.0",
"vitest": "^3.2.7"
}
}
97 changes: 97 additions & 0 deletions packages/agent-sdk/src/agent-sdk.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import type { ComponentNode, LineageGraph } from "@coderadar/core";
import { nodeId } from "@coderadar/core";
import { describe, expect, it } from "vitest";

import { classifyTicket } from "./classify.js";
import { extractTerms, resolveContext } from "./resolve.js";

const loc = (f: string) => ({ file: f, line: 1, endLine: 1 });
function component(name: string, texts: string[]): ComponentNode {
return {
id: nodeId("component", `${name}.tsx`, name),
kind: "component",
name,
loc: loc(`${name}.tsx`),
exportName: name,
props: [],
renderedText: texts.map((text) => ({ text, source: "jsx" as const })),
rendersComponents: [],
structure: {
table: 0,
columns: 0,
form: 0,
input: 0,
button: 0,
link: 0,
image: 0,
heading: 0,
list: 0,
repeated: 0,
},
};
}
const graph: LineageGraph = {
version: 2,
root: "/app",
generatedAt: "2026-01-01T00:00:00.000Z",
generator: "test",
nodes: [component("InvoiceList", ["All invoices"]), component("UserList", ["Team members"])],
edges: [],
};

describe("classifyTicket (TRACKER 5.1)", () => {
it("routes a screenshot to the visual path", () => {
expect(classifyTicket({ text: "looks wrong", screenshots: 1 }).entryPoint).toBe("visual");
});

it("declines backend/infra/perf tickets as out-of-domain (E6)", () => {
expect(classifyTicket({ text: "the Redis cache latency spiked" }).entryPoint).toBe(
"out-of-domain",
);
expect(classifyTicket({ text: "kubernetes deployment crash-loops" }).entryPoint).toBe(
"out-of-domain",
);
});

it("routes an interaction failure to the behavioral path (E5)", () => {
expect(classifyTicket({ text: "clicking Save does nothing" }).entryPoint).toBe("behavioral");
});

it("declines a video attachment as unsupported (E4)", () => {
expect(classifyTicket({ text: "repro", links: ["https://x/clip.mp4"] }).entryPoint).toBe(
"unsupported",
);
expect(classifyTicket({ text: "see the screen recording" }).entryPoint).toBe("unsupported");
});

it("treats UI prose as textual", () => {
expect(classifyTicket({ text: "the 'All invoices' page shows stale totals" }).entryPoint).toBe(
"textual",
);
});
});

describe("resolveContext (TRACKER 5.1)", () => {
it("declines an out-of-domain ticket with a structured reason", () => {
const result = resolveContext(graph, { text: "database migration failed on deploy" });
expect(result.entryPoint).toBe("out-of-domain");
expect(result.decline?.reason).toBe("out-of-scope");
expect(result.match).toBeUndefined();
});

it("declines a video ticket as unsupported input", () => {
const result = resolveContext(graph, { text: "x", links: ["https://x/a.mov"] });
expect(result.decline?.reason).toBe("unsupported-input");
});

it("matches a textual ticket against the graph", () => {
const result = resolveContext(graph, { text: "the 'All invoices' page is broken" });
expect(result.entryPoint).toBe("textual");
expect(result.match?.candidates[0]?.value.component.name).toBe("InvoiceList");
});

it("extracts quoted phrases as terms, else capitalized runs", () => {
expect(extractTerms('the "Save draft" button')).toEqual(["Save draft"]);
expect(extractTerms("the Team Members list")).toContain("Team Members");
});
});
46 changes: 46 additions & 0 deletions packages/agent-sdk/src/classify.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/**
* Rule-based ticket classification (TRACKER step 5.1). Deterministic, no LLM.
* Precedence: unsupported → visual → out-of-domain → behavioral → textual.
*/
import type { Classification, Ticket } from "./types.js";

/** Attachment URLs that are video/GIF, not still images (E4). */
const VIDEO_LINK = /\.(mp4|mov|webm|avi|mkv|gif)(\?|#|$)/i;
const VIDEO_MENTION = /\b(screen[-\s]?recording|screencast|video (attached|recording|of))\b/i;

/** Backend / infra / performance vocabulary — outside the UI codebase (E6). */
const OUT_OF_DOMAIN =
/\b(database|migration|sql|postgres|mysql|mongo|redis|kafka|rabbitmq|kubernetes|k8s|docker|deploy(ment|ments|s|ed|ing)?|infra(structure)?|terraform|cron|job queue|background job|worker|latency|throughput|memory leak|cpu usage|rate[-\s]?limit(ing|ed)?|webhook|ci\/cd|pipeline|nginx|load balancer|env(ironment)? variable|api gateway|microservice|502|503|504|gateway timeout|connection pool|index(ing)? (the )?table)\b/i;

/** Behavior/interaction phrasing — the entry point is an action, not a visual (E5). */
const BEHAVIORAL =
/\b(nothing happens|does(n'?t| not) (work|respond|do anything|fire|trigger|navigate|submit|save|load|open|close)|no (response|reaction|effect)|not working|unresponsive|isn'?t working|won'?t (submit|save|open|close|load|work)|after (i |you |we )?(click|press|tap|submit|hit)|when (i |you |we )?(click|press|tap|submit|hit)|clicking .* (does|has) )/i;

/** Classify a ticket into an entry point (E1/E5/E6/E4). */
export function classifyTicket(ticket: Ticket): Classification {
const text = ticket.text ?? "";
const links = ticket.links ?? [];

if (links.some((l) => VIDEO_LINK.test(l)) || VIDEO_MENTION.test(text)) {
return {
entryPoint: "unsupported",
reason: "video/GIF attachment — no still frame to match; ask for a screenshot",
};
}
if ((ticket.screenshots ?? 0) > 0) {
return { entryPoint: "visual", reason: "screenshot attached — match on its text and structure" };
}
if (OUT_OF_DOMAIN.test(text)) {
return {
entryPoint: "out-of-domain",
reason: "backend/infra/perf vocabulary — not a UI ticket",
};
}
if (BEHAVIORAL.test(text)) {
return {
entryPoint: "behavioral",
reason: "describes an interaction/failure, not a visual — match on event/journey vocabulary",
};
}
return { entryPoint: "textual", reason: "UI terms in prose — match on rendered text" };
}
3 changes: 3 additions & 0 deletions packages/agent-sdk/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export type { Classification, ContextResult, EntryPoint, Ticket } from "./types.js";
export { classifyTicket } from "./classify.js";
export { extractTerms, resolveContext } from "./resolve.js";
48 changes: 48 additions & 0 deletions packages/agent-sdk/src/resolve.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/**
* resolveContext (TRACKER step 5.1): classify a ticket, then run the matching
* engine with the signals available. Out-of-domain and unsupported tickets are
* declined with a structured reason rather than forced into a match.
*/
import { type LineageGraph, matchComponents } from "@coderadar/core";

import { classifyTicket } from "./classify.js";
import type { ContextResult, Ticket } from "./types.js";

export function resolveContext(graph: LineageGraph, ticket: Ticket): ContextResult {
const classification = classifyTicket(ticket);
const { entryPoint } = classification;

if (entryPoint === "out-of-domain") {
return {
entryPoint,
classification,
decline: {
reason: "out-of-scope",
message:
"This reads as a backend / infrastructure / performance ticket — outside the UI codebase CodeRadar maps. Route it to a human or a backend tool.",
},
};
}
if (entryPoint === "unsupported") {
return {
entryPoint,
classification,
decline: {
reason: "unsupported-input",
message: "Video/GIF attachments aren't supported — attach a still screenshot of the state.",
},
};
}

return { entryPoint, classification, match: matchComponents(graph, { terms: extractTerms(ticket.text) }) };
}

/** Pull candidate UI terms from ticket prose: quoted phrases, else capitalized runs. */
export function extractTerms(text: string): string[] {
const quoted = [...text.matchAll(/["'`]([^"'`]{2,})["'`]/g)].map((m) => m[1] ?? "");
const cleaned = quoted.filter((t) => t.trim().length > 1);
if (cleaned.length > 0) return cleaned;
const caps = [...text.matchAll(/\b([A-Z][a-z]+(?:\s+[A-Z][a-z0-9]+){0,3})\b/g)].map((m) => m[1] ?? "");
const distinctive = caps.filter((t) => t.length > 2);
return distinctive.length > 0 ? distinctive : [text];
}
Loading
Loading