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
28 changes: 20 additions & 8 deletions TRACKER.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@

## Status

- **Current phase:** 6Lifecycle, scale, hardening **COMPLETE** (Gate 6 passed, M6 reached). 6F field-hardening done bar the data-blocked 6F.6 detection half.
- **Next step:** publish 0.5.0 to npm (needs the user's creds — `npm publish`), then the tester re-validates via `ui-lineage-mcp`. After that, Phase 7 (backend parsers & federation) — detail the sketch after v1 feedback. 6F.6 detection half still blocked on a real failing test file from the tester.
- **Done:** 0.1–0.4, 1.1–1.6, 2.1–2.5, 3.1–3.6, 4.1–4.6, 5.1–5.7, 6F.1–6F.5, 6F.7–6F.10 + 6F.6 (defensive half), **6.1–6.5 (Phase 6 complete)** · **0.5.0 prepared** (all packages + generator/CLI/MCP version strings bumped, README changelog; `npm pack` verified; publish pending user's npm creds) (all packages bumped, changelog + version strings, `npm pack` → ui-lineage-0.4.1.tgz verified: both bins, no @coderadar leak; publish to npm pending). 0.4.0 tagged/merged earlier. **Self-validated on Grafana frontend** (6,461 files, 15,334 nodes / 18,367 edges in 72 s): 55 RTK-query data sources, 32 routes, 1,009 coverage edges, gibberish declines — all previously 0/1 in the field run.
- **Current phase:** 7Backend parsers & federation (v2 horizon). Phase 6 complete (Gate 6, M6). **0.5.0 published to npm.**
- **Next step:** publish 0.6.0 to npm (needs the user's creds — `npm publish`), then the tester validates the wider data-source coverage. After that, the backend-parser steps (7.4 Python, 7.5 Go, 7.6 federation) — larger, new-package work, still sketch-level; best held until that feedback confirms priority. 6F.6 detection half still blocked on a real failing test file from the tester.
- **Done:** 0.1–0.4, 1.1–1.6, 2.1–2.5, 3.1–3.6, 4.1–4.6, 5.1–5.7, 6F.1–6F.5, 6F.7–6F.10 + 6F.6 (defensive half), **6.1–6.5 (Phase 6 complete)**, **7.1 (GraphQL adapter), 7.2 (Next.js server data), 7.3 (push channels)** · **0.5.0 released & published to npm** (tag v0.5.0); **0.6.0 prepared** (7.1–7.3; all packages + generator/CLI/MCP version strings bumped, README changelog; `npm pack` verified; publish pending user's npm creds). **Self-validated on Grafana frontend** (6,461 files, 15,334 nodes / 18,367 edges in 72 s): 55 RTK-query data sources, 32 routes, 1,009 coverage edges, gibberish declines — all previously 0/1 in the field run.
- **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) · Gate 5 (F1 context bundle · F2 blast radius · F3 test coverage · F4 response schema · F5 git history · MCP server over stdio — scorecard 265/0/0, all honesty metrics 1.000; **M5 reached** — ticket in → budgeted context bundle out, over MCP)

## What CodeRadar is
Expand Down Expand Up @@ -705,11 +705,23 @@ formally stamped: 6F.6 test-coverage hardening (blocked on a real failing sample

## Phase 7 — Backend parsers & federation (v2 horizon)

Sketch level — detail before starting the phase, after v1 feedback.

- **7.1 GraphQL adapter** (C4): operations/fragments as data sources; codegen types feed 5.5.
- **7.2 Next.js server data** (C9): RSC async components, `getServerSideProps`, server actions as first-class data sources.
- **7.3 Push channels** (C8): WebSocket/SSE subscription nodes.
Sketch level for 7.2–7.6 — detail each before starting, after v1 feedback.

### [x] 7.1 GraphQL adapter
**Failure modes:** C4
**Build:** recognize GraphQL operations a component runs (Apollo/urql/graphql-request `useQuery`/`useMutation`/`useSubscription`/`useLazyQuery` with a `gql`/`graphql` tagged-template argument, inline or a resolved const) and emit them as data sources: operation NAME as identity, operation TYPE as method.
**Accept:** fixture with query/mutation/subscription operations (imported const + inline tag) → one `graphql` data source each with the right method and a `fetches-from` edge; react-query `useQuery`/`useMutation` (non-gql arg) still classify as react-query.
**Done:** new `graphql.ts` — `parseGraphqlOperation` reads the operation type/name (+ root selection fields as an anonymous-op fallback) from a gql document, ignoring `#` comments and handling shorthand `{ … }`; `graphqlOperationFromArg` resolves a hook's first argument to an operation (inline tagged template, or an identifier followed via `getDefinitionNodes()` so an imported/co-located gql const resolves cross-file). `detectDataSource` gains a GraphQL branch **before** the react-query branch (Apollo/urql reuse `useQuery`/`useMutation`, so it only claims the call when the argument is an actual gql document, else falls through). Emits `sourceKind: "graphql"`, `method` = query/mutation/subscription, `endpoint` = operation name (the value federation/attribution join on) — reusing the existing data-source node + `fetches-from` wiring, no schema change (`graphql` was already a `DataSourceKind`). New fixture `c4-graphql` (imported-const `useQuery`/`useMutation` + inline `useSubscription`); 9 unit tests incl. the react-query disambiguation on the c5 fixture. eval 326/0/0/0, determinism 1.000, all metrics 1.000; typecheck + lint clean. **Codegen response types (feeding 5.5) deferred** — needs a `.graphql`/codegen fixture; the operation→data-source spine is what unblocks federation.
### [x] 7.2 Next.js server data
**Failure modes:** C9
**Build:** attribute server-side data fetching to the page it feeds — async RSC server components (fetch in their own body) and pages-router `getServerSideProps`/`getStaticProps`/`getStaticPaths` (fetch in a separate exported function).
**Accept:** fixture with an async RSC page, a `getServerSideProps` page, and a `getStaticProps` page → each page attributes its server-side endpoint; the data functions produce no component/hook nodes.
**Done:** RSC async server components already worked — the async component is recognized and its inline `await fetch(...)` is caught by the body walk (locked with a test). The gap was pages-router data functions: `getServerSideProps`/`getStaticProps`/`getStaticPaths` are top-level exported functions, not components/hooks, so their fetches were never scanned. The main loop now collects those functions per file and, once the file's default-export page component is known, runs the data-source extraction on their bodies attributed to that page (`fetches-from`). New fixture `c9-nextjs-server-data` (async RSC + getServerSideProps + getStaticProps); 4 unit tests. eval 341/0/0/0, determinism 1.000, all metrics 1.000; parser-react 168 tests, typecheck + lint clean. **Server actions deferred** (`"use server"` mutations bound to `<form action>` — a mutation/event surface, fuzzier than the data-in path; revisit with feedback).
### [x] 7.3 Push channels
**Failure modes:** C8
**Build:** recognize server-push channels (`new WebSocket(url)`, `new EventSource(url)`) as data sources — long-lived connections, not request/response fetches — with a `fetches-from` edge from the component that opens them.
**Accept:** fixture with a WebSocket and an SSE channel → one `websocket` / one `sse` data source with the URL as endpoint and a `fetches-from` edge each; no stray sources from unrelated constructors.
**Done:** new `sse` `DataSourceKind` (schema regenerated, drift gate green); new `pushchannels.ts` — `detectPushChannel` matches a `new WebSocket(...)` / `new EventSource(...)` expression (by constructor name, allowing a `window.`/`self.` qualifier — a very low false-positive set) and returns a data source with the URL resolved through the existing `resolveEndpoint` (constant-folded, `:param`-normalized). `extractBodyFacts` gains a `NewExpression` pass that emits the node + `fetches-from` edge, mirroring the fetch path (skipped inside API wrappers). `websocket` for WebSocket, `sse` for EventSource, transport in `method` (`WS`/`SSE`). New fixture `c8-push-channels` (WebSocket `wss://…` + SSE `/api/…/stream`); 4 unit tests. eval 332/0/0/0, determinism 1.000, all metrics 1.000; parser-react 164 tests, typecheck + lint clean. **socket.io deferred** (`io()` needs import-aware detection to avoid false positives on the common `io` identifier).
- **7.4 Python backend parser** (C10, G6): FastAPI/Django route decorators → `ServesNode{ method, pattern }`; tree-sitter based, emits the same LineageGraph JSON.
- **7.5 Go backend parser**: mux/gin/echo handler registration → `ServesNode`.
- **7.6 Federation** (G6): endpoint-pattern join across graphs (`fetches-from: /api/users` ↔ `serves: /api/users`); multi-graph loader in agent-sdk; bundle lineage extends to the owning service + handler file.
Expand Down
16 changes: 16 additions & 0 deletions eval/fixtures/c4-graphql/app/InviteForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { useMutation } from "@apollo/client";

import { CREATE_USER } from "./queries";

// Apollo useMutation with an imported gql const → graphql data source
// "CreateUser" (method: mutation). Shares the useMutation name with react-query,
// but the gql argument makes it a GraphQL source, not a react-query one.
export function InviteForm() {
const [createUser] = useMutation(CREATE_USER);
return (
<form onSubmit={() => createUser({ variables: { input: {} } })}>
<h3>Invite teammate</h3>
<button type="submit">Send invite</button>
</form>
);
}
21 changes: 21 additions & 0 deletions eval/fixtures/c4-graphql/app/LiveTicker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { gql, useSubscription } from "@apollo/client";

// Inline gql tagged template (not a const) with a subscription operation →
// graphql data source "OnTick" (method: subscription).
export function LiveTicker() {
const { data } = useSubscription(
gql`
subscription OnTick {
tick {
at
}
}
`,
);
return (
<div>
<h3>Live updates</h3>
<span>{data?.tick?.at ?? "waiting"}</span>
</div>
);
}
14 changes: 14 additions & 0 deletions eval/fixtures/c4-graphql/app/UsersPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { useQuery } from "@apollo/client";

import { GET_USERS } from "./queries";

// Apollo useQuery with an imported gql const → graphql data source "GetUsers".
export function UsersPanel() {
const { data } = useQuery(GET_USERS);
return (
<section>
<h2>User directory</h2>
<ul>{data?.users?.map((u: { id: string; name: string }) => <li key={u.id}>{u.name}</li>)}</ul>
</section>
);
}
21 changes: 21 additions & 0 deletions eval/fixtures/c4-graphql/app/queries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { gql } from "@apollo/client";

// GraphQL operations defined as gql consts and imported by the components that
// run them — the common Apollo/urql pattern. The operation NAME is the data
// source identity; a component that runs it gets a fetches-from edge.
export const GET_USERS = gql`
query GetUsers {
users {
id
name
}
}
`;

export const CREATE_USER = gql`
mutation CreateUser($input: UserInput!) {
createUser(input: $input) {
id
}
}
`;
21 changes: 21 additions & 0 deletions eval/fixtures/c4-graphql/golden.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"failureMode": "C4",
"note": "GraphQL adapter (7.1): Apollo/urql operations become data sources keyed by operation NAME, with the operation TYPE (query/mutation/subscription) as the method. Covers an imported gql const via useQuery (GetUsers) and useMutation (CreateUser) — which share their hook names with react-query but are disambiguated by the gql document argument — and an inline gql tagged template via useSubscription (OnTick). Each running component gets a fetches-from edge to its operation.",
"expect": {
"components": [
{ "name": "UsersPanel", "instances": 0 },
{ "name": "InviteForm", "instances": 0 },
{ "name": "LiveTicker", "instances": 0 }
],
"attributions": [
{ "component": "UsersPanel", "endpoints": ["GetUsers"] },
{ "component": "InviteForm", "endpoints": ["CreateUser"] },
{ "component": "LiveTicker", "endpoints": ["OnTick"] }
],
"queries": [
{ "terms": ["User directory"], "status": "ok", "top": "UsersPanel" },
{ "terms": ["Invite teammate"], "status": "ok", "top": "InviteForm" },
{ "terms": ["Live updates"], "status": "ok", "top": "LiveTicker" }
]
}
}
17 changes: 17 additions & 0 deletions eval/fixtures/c8-push-channels/app/ChatPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { useEffect, useState } from "react";

// WebSocket push channel → websocket data source.
export function ChatPanel() {
const [messages, setMessages] = useState<string[]>([]);
useEffect(() => {
const socket = new WebSocket("wss://chat.example.com/socket");
socket.onmessage = (e) => setMessages((m) => [...m, e.data]);
return () => socket.close();
}, []);
return (
<section>
<h2>Team chat</h2>
<ul>{messages.map((m, i) => <li key={i}>{m}</li>)}</ul>
</section>
);
}
17 changes: 17 additions & 0 deletions eval/fixtures/c8-push-channels/app/NotificationsFeed.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { useEffect, useState } from "react";

// Server-Sent Events push channel → sse data source.
export function NotificationsFeed() {
const [count, setCount] = useState(0);
useEffect(() => {
const source = new EventSource("/api/notifications/stream");
source.onmessage = () => setCount((c) => c + 1);
return () => source.close();
}, []);
return (
<aside>
<h3>Live notifications</h3>
<span>{count} unread</span>
</aside>
);
}
18 changes: 18 additions & 0 deletions eval/fixtures/c8-push-channels/golden.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"failureMode": "C8",
"note": "Push channels (7.3): server-push data arrives over a long-lived connection, not a request/response fetch. `new WebSocket(url)` becomes a websocket data source and `new EventSource(url)` an sse data source, each with the URL as its endpoint and a fetches-from edge from the component that opens it — so a component fed by a live channel shows that channel in its lineage.",
"expect": {
"components": [
{ "name": "ChatPanel", "instances": 0 },
{ "name": "NotificationsFeed", "instances": 0 }
],
"attributions": [
{ "component": "ChatPanel", "endpoints": ["wss://chat.example.com/socket"] },
{ "component": "NotificationsFeed", "endpoints": ["/api/notifications/stream"] }
],
"queries": [
{ "terms": ["Team chat"], "status": "ok", "top": "ChatPanel" },
{ "terms": ["Live notifications"], "status": "ok", "top": "NotificationsFeed" }
]
}
}
15 changes: 15 additions & 0 deletions eval/fixtures/c9-nextjs-server-data/app/BlogPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Pages-router: getStaticProps fetches at build time; feeds the page.
export async function getStaticProps() {
const res = await fetch("/api/posts");
const posts = await res.json();
return { props: { posts } };
}

export default function BlogPage({ posts }: { posts: { id: string }[] }) {
return (
<main>
<h1>Blog index</h1>
<span>{posts.length} posts</span>
</main>
);
}
11 changes: 11 additions & 0 deletions eval/fixtures/c9-nextjs-server-data/app/DashboardPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// App-router RSC: an async server component that fetches in its own body.
export default async function DashboardPage() {
const res = await fetch("/api/dashboard/summary");
const data = await res.json();
return (
<main>
<h1>Dashboard totals</h1>
<p>{data.total}</p>
</main>
);
}
15 changes: 15 additions & 0 deletions eval/fixtures/c9-nextjs-server-data/app/ReportsPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Pages-router: getServerSideProps fetches server-side; the page renders props.
export async function getServerSideProps() {
const res = await fetch("/api/reports/latest");
const report = await res.json();
return { props: { report } };
}

export default function ReportsPage({ report }: { report: { title: string } }) {
return (
<main>
<h1>Report viewer</h1>
<span>{report.title}</span>
</main>
);
}
21 changes: 21 additions & 0 deletions eval/fixtures/c9-nextjs-server-data/golden.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"failureMode": "C9",
"note": "Next.js server data (7.2): server-side data fetching feeds a page even though the fetch is not in the component body. An async RSC server component fetches inline (already covered), while pages-router getServerSideProps/getStaticProps run separately from the page — their fetches are attributed to the file's default-export page component so its lineage shows the data it renders.",
"expect": {
"components": [
{ "name": "DashboardPage", "instances": 0 },
{ "name": "ReportsPage", "instances": 0 },
{ "name": "BlogPage", "instances": 0 }
],
"attributions": [
{ "component": "DashboardPage", "endpoints": ["/api/dashboard/summary"] },
{ "component": "ReportsPage", "endpoints": ["/api/reports/latest"] },
{ "component": "BlogPage", "endpoints": ["/api/posts"] }
],
"queries": [
{ "terms": ["Dashboard totals"], "status": "ok", "top": "DashboardPage" },
{ "terms": ["Report viewer"], "status": "ok", "top": "ReportsPage" },
{ "terms": ["Blog index"], "status": "ok", "top": "BlogPage" }
]
}
}
2 changes: 1 addition & 1 deletion eval/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@coderadar/eval",
"version": "0.5.0",
"version": "0.6.0",
"private": true,
"description": "CodeRadar eval harness — scans failure-mode fixtures, diffs against golden outputs, emits the scorecard that gates every phase.",
"license": "MIT",
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "coderadar",
"private": true,
"version": "0.5.0",
"version": "0.6.0",
"description": "Static analysis toolkit that maps UI components to their data sources (APIs, state, events) — enabling AI agents to trace any screenshot back to the code and data behind it.",
"license": "MIT",
"packageManager": "pnpm@11.2.2",
Expand Down
2 changes: 1 addition & 1 deletion packages/agent-sdk/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@coderadar/agent-sdk",
"version": "0.5.0",
"version": "0.6.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",
Expand Down
10 changes: 9 additions & 1 deletion packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,15 @@ Turn a screenshot into `{ terms, structure, annotations }` and match it — term

## What it understands

Endpoints (constants, templates, API wrappers, react-query/SWR), i18n text, cross-file instance trees & per-instance prop-flow, Redux/Zustand stores, portals/modals/toasts, React Router & Next.js routes, action effects (navigate/fetch/dispatch/setState), form libraries & non-JSX events (react-hook-form, `addEventListener`, hotkeys), and feature-flag/role conditions.
Endpoints (constants, templates, API wrappers, react-query/SWR/RTK Query), GraphQL operations, WebSocket/SSE push channels, Next.js server-side data (RSC async components, `getServerSideProps`/`getStaticProps`), i18n text, cross-file instance trees & per-instance prop-flow, Redux/Zustand stores, portals/modals/toasts, React Router & Next.js routes, action effects (navigate/fetch/dispatch/setState), form libraries & non-JSX events (react-hook-form, `addEventListener`, hotkeys), and feature-flag/role conditions.

## New in 0.6.0

Wider data-source coverage — the graph now sees the ways modern React apps actually fetch, beyond REST calls in the component body:

- **GraphQL** — Apollo/urql operations become data sources, keyed by operation name and typed by operation (`query` / `mutation` / `subscription`). Works with an inline `gql` tag or an imported/co-located `gql` const (resolved across files), and is disambiguated from react-query, which shares the `useQuery`/`useMutation` hook names.
- **Push channels** — `new WebSocket(url)` and `new EventSource(url)` become `websocket` / `sse` data sources, so a component fed by a live channel shows that channel in its lineage.
- **Next.js server data** — a page's server-side fetching is attributed to the page even when it isn't in the component body: pages-router `getServerSideProps` / `getStaticProps` / `getStaticPaths`, alongside app-router async RSC server components.

## New in 0.5.0

Expand Down
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ui-lineage",
"version": "0.5.0",
"version": "0.6.0",
"description": "Map UI components to their data sources and user journeys — trace any screenshot back to the code, APIs, state, and navigation behind it. Deterministic static analysis for React/TSX.",
"license": "MIT",
"type": "module",
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ program
.description(
"Map UI components to their data sources and user journeys — trace any screenshot back to the code, APIs, state, and navigation behind it.",
)
.version("0.5.0");
.version("0.6.0");

program
.command("scan")
Expand Down
Loading
Loading