From c1dc6723f4fe8552b11681cbc4cdd54f6de1a2ee Mon Sep 17 00:00:00 2001 From: Deep Kumar Singh Kushwah Date: Wed, 15 Jul 2026 11:36:56 +0530 Subject: [PATCH] feat(agent-sdk): resolveContext orchestrator + ticket classification (E1/E5/E6/E4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New @coderadar/agent-sdk package (TRACKER step 5.1), bundled into ui-lineage: - classifyTicket(ticket) — rule-based, no LLM (deterministic, G8). Routes to visual (screenshot) / textual (UI prose) / behavioral ("clicking X does nothing", E5) / out-of-domain (backend/infra/perf → decline, E6) / unsupported (video/GIF → decline, E4). Precedence: unsupported → visual → OOD → behavioral → textual. - resolveContext(graph, ticket) — classify, then run the matcher on terms pulled from the prose (quoted phrases, else capitalized runs); OOD/unsupported get a structured decline instead of a forced match. Eval: hand-written ticket suite (eval/tickets/tickets.json, 17 cases across all entry points); harness reports and gates entry-point accuracy (>=0.90) and OOD rejection (>=0.95) — both 1.000. CLI `resolve ` command; lib re-exports resolveContext/classifyTicket. 41 core + 9 agent-sdk + 3 vision + 105 parser tests pass; eval green. Co-Authored-By: Claude Opus 4.8 --- TRACKER.md | 6 +- eval/package.json | 1 + eval/src/golden.ts | 6 ++ eval/src/run.ts | 40 ++++++++++ eval/thresholds.json | 4 +- eval/tickets/tickets.json | 23 ++++++ packages/agent-sdk/package.json | 32 ++++++++ packages/agent-sdk/src/agent-sdk.test.ts | 97 ++++++++++++++++++++++++ packages/agent-sdk/src/classify.ts | 46 +++++++++++ packages/agent-sdk/src/index.ts | 3 + packages/agent-sdk/src/resolve.ts | 48 ++++++++++++ packages/agent-sdk/src/types.ts | 35 +++++++++ packages/agent-sdk/tsconfig.json | 8 ++ packages/cli/package.json | 1 + packages/cli/src/index.ts | 27 +++++++ packages/cli/src/lib.ts | 7 ++ pnpm-lock.yaml | 22 ++++++ 17 files changed, 402 insertions(+), 4 deletions(-) create mode 100644 eval/tickets/tickets.json create mode 100644 packages/agent-sdk/package.json create mode 100644 packages/agent-sdk/src/agent-sdk.test.ts create mode 100644 packages/agent-sdk/src/classify.ts create mode 100644 packages/agent-sdk/src/index.ts create mode 100644 packages/agent-sdk/src/resolve.ts create mode 100644 packages/agent-sdk/src/types.ts create mode 100644 packages/agent-sdk/tsconfig.json diff --git a/TRACKER.md b/TRACKER.md index eeee9a7..4c63f6b 100644 --- a/TRACKER.md +++ b/TRACKER.md @@ -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.2 — Context-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 @@ -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). diff --git a/eval/package.json b/eval/package.json index ac10c9d..409148e 100644 --- a/eval/package.json +++ b/eval/package.json @@ -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" diff --git a/eval/src/golden.ts b/eval/src/golden.ts index 8b050ca..e223a6b 100644 --- a/eval/src/golden.ts +++ b/eval/src/golden.ts @@ -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; }; } @@ -219,4 +223,6 @@ export interface Thresholds { minHighConfidenceCorrect?: number; minAmbiguityHonesty?: number; maxPoisonRate?: number; + minEntryPointAccuracy?: number; + minOodRejection?: number; } diff --git a/eval/src/run.ts b/eval/src/run.ts index 811dc75..748043d 100644 --- a/eval/src/run.ts +++ b/eval/src/run.ts @@ -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"; @@ -162,6 +196,7 @@ function buildScorecard(results: FixtureResult[]): Scorecard { okAnswers.length > 0 ? round(okAnswers.filter((o) => !o.correct).length / okAnswers.length) : null, + ...runTickets(), }, }; } @@ -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 { @@ -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 && diff --git a/eval/thresholds.json b/eval/thresholds.json index ded3b1d..2fba2f1 100644 --- a/eval/thresholds.json +++ b/eval/thresholds.json @@ -6,5 +6,7 @@ "minLineageRecall": 0.95, "minHighConfidenceCorrect": 1, "minAmbiguityHonesty": 0.9, - "maxPoisonRate": 0.05 + "maxPoisonRate": 0.05, + "minEntryPointAccuracy": 0.9, + "minOodRejection": 0.95 } diff --git a/eval/tickets/tickets.json b/eval/tickets/tickets.json new file mode 100644 index 0000000..cb1a82d --- /dev/null +++ b/eval/tickets/tickets.json @@ -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" } +] diff --git a/packages/agent-sdk/package.json b/packages/agent-sdk/package.json new file mode 100644 index 0000000..af8d1d1 --- /dev/null +++ b/packages/agent-sdk/package.json @@ -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" + } +} diff --git a/packages/agent-sdk/src/agent-sdk.test.ts b/packages/agent-sdk/src/agent-sdk.test.ts new file mode 100644 index 0000000..9b8ea3e --- /dev/null +++ b/packages/agent-sdk/src/agent-sdk.test.ts @@ -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"); + }); +}); diff --git a/packages/agent-sdk/src/classify.ts b/packages/agent-sdk/src/classify.ts new file mode 100644 index 0000000..30416de --- /dev/null +++ b/packages/agent-sdk/src/classify.ts @@ -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" }; +} diff --git a/packages/agent-sdk/src/index.ts b/packages/agent-sdk/src/index.ts new file mode 100644 index 0000000..7cac608 --- /dev/null +++ b/packages/agent-sdk/src/index.ts @@ -0,0 +1,3 @@ +export type { Classification, ContextResult, EntryPoint, Ticket } from "./types.js"; +export { classifyTicket } from "./classify.js"; +export { extractTerms, resolveContext } from "./resolve.js"; diff --git a/packages/agent-sdk/src/resolve.ts b/packages/agent-sdk/src/resolve.ts new file mode 100644 index 0000000..cd866b0 --- /dev/null +++ b/packages/agent-sdk/src/resolve.ts @@ -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]; +} diff --git a/packages/agent-sdk/src/types.ts b/packages/agent-sdk/src/types.ts new file mode 100644 index 0000000..27d6cce --- /dev/null +++ b/packages/agent-sdk/src/types.ts @@ -0,0 +1,35 @@ +/** + * The agent-facing entry point (TRACKER step 5.1). A ticket comes in; a + * classified, matched context result comes out — or an honest decline. + */ +import type { ComponentMatch, QueryResult } from "@coderadar/core"; + +export interface Ticket { + /** The ticket prose. */ + text: string; + /** Number of screenshots attached (presence routes to the visual path). */ + screenshots?: number; + /** Attachment/reference URLs; a video/GIF here makes the input unsupported (E4). */ + links?: string[]; +} + +/** + * How CodeRadar should read the ticket (E1/E5/E6/E4). Classification is + * rule-based + keyword lexicons — no LLM call, so the node is deterministic (G8). + */ +export type EntryPoint = "visual" | "textual" | "behavioral" | "out-of-domain" | "unsupported"; + +export interface Classification { + entryPoint: EntryPoint; + /** Human/agent-readable reason for the routing decision. */ + reason: string; +} + +export interface ContextResult { + entryPoint: EntryPoint; + classification: Classification; + /** The component match — present for visual / textual / behavioral tickets. */ + match?: QueryResult; + /** A structured decline — present for out-of-domain / unsupported tickets. */ + decline?: { reason: "out-of-scope" | "unsupported-input"; message: string }; +} diff --git a/packages/agent-sdk/tsconfig.json b/packages/agent-sdk/tsconfig.json new file mode 100644 index 0000000..5285d28 --- /dev/null +++ b/packages/agent-sdk/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "dist" + }, + "include": ["src"] +} diff --git a/packages/cli/package.json b/packages/cli/package.json index 1d34e0a..d64232f 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -50,6 +50,7 @@ "yaml": "^2.9.0" }, "devDependencies": { + "@coderadar/agent-sdk": "workspace:*", "@coderadar/core": "workspace:*", "@coderadar/parser-react": "workspace:*", "@coderadar/vision": "workspace:*", diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 25a7047..2529e86 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -16,6 +16,7 @@ import { saveGraph, traceLineage, } from "@coderadar/core"; +import { resolveContext } from "@coderadar/agent-sdk"; import { resolveHookEdges, scanReact } from "@coderadar/parser-react"; import { Command } from "commander"; import { parse as parseYaml } from "yaml"; @@ -178,6 +179,32 @@ program for (const path of paths) printJourneyPath(path); }); +program + .command("resolve") + .description("Resolve a ticket: classify its entry point, then match it to component(s)") + .argument("", "the ticket text") + .option("-g, --graph ", "graph file", "ui-lineage.graph.json") + .option("-s, --screenshot", "the ticket has a screenshot attached") + .action((text: string, opts: { graph: string; screenshot?: boolean }) => { + const graph = loadGraph(opts.graph); + const result = resolveContext(graph, { + text, + ...(opts.screenshot ? { screenshots: 1 } : {}), + }); + console.log(`entry point: ${result.entryPoint} — ${result.classification.reason}`); + if (result.decline !== undefined) { + console.log(`declined (${result.decline.reason}): ${result.decline.message}`); + return; + } + const match = result.match; + if (match === undefined || match.status === "declined") { + console.log(`no component matched (${match?.declineReason ?? "no result"}).`); + return; + } + if (match.status === "ambiguous") console.log(`ambiguous — ${match.disambiguation}\n`); + for (const candidate of match.candidates.slice(0, 5)) printMatchCandidate(candidate); + }); + program .command("correct") .description("Record that some on-screen text means a component — feeds future `find` results") diff --git a/packages/cli/src/lib.ts b/packages/cli/src/lib.ts index c9309f1..858d631 100644 --- a/packages/cli/src/lib.ts +++ b/packages/cli/src/lib.ts @@ -9,3 +9,10 @@ */ export * from "@coderadar/core"; export { resolveHookEdges, scanReact, type ScanOptions } from "@coderadar/parser-react"; +export { + classifyTicket, + type ContextResult, + type EntryPoint, + resolveContext, + type Ticket, +} from "@coderadar/agent-sdk"; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 11876e5..ac15674 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,6 +10,9 @@ importers: eval: dependencies: + '@coderadar/agent-sdk': + specifier: workspace:* + version: link:../packages/agent-sdk '@coderadar/core': specifier: workspace:* version: link:../packages/core @@ -27,6 +30,22 @@ importers: specifier: ^5.7.0 version: 5.9.3 + packages/agent-sdk: + dependencies: + '@coderadar/core': + specifier: workspace:* + version: link:../core + devDependencies: + '@types/node': + specifier: ^22.20.1 + version: 22.20.1 + typescript: + specifier: ^5.7.0 + version: 5.9.3 + vitest: + specifier: ^3.2.7 + version: 3.2.7(@types/node@22.20.1)(yaml@2.9.0) + packages/cli: dependencies: commander: @@ -39,6 +58,9 @@ importers: specifier: ^2.9.0 version: 2.9.0 devDependencies: + '@coderadar/agent-sdk': + specifier: workspace:* + version: link:../agent-sdk '@coderadar/core': specifier: workspace:* version: link:../core